From b88d32ff5b6d0da496d4b5a6df4d8ed4f49701a7 Mon Sep 17 00:00:00 2001 From: fredzio Date: Sun, 26 Apr 2020 15:31:39 +0200 Subject: [PATCH] Add 3 tabs in the "Data Files" page 1 with the data directories 2 with the BSA archives 3 with the content selector When user select a directory to be added, first we walk the directory hierarchy to make a list of all potential data= entries. If we find none, the selected directory is added. If more than one data directory is found, user is presented with a directory list to check which one(s) are to be added. Directories containing one or more content file are marked with an icon. data= and fallback-archive= lines are handled like content= lines: - they are part of the profile in launcher.cfg, prefixed by the profile name - they are updated in openmw.cfg when profile is selected / created Directories can be moved in the list by drag and drop or by buttons. Insertion is possible anywhere in the list. Global data path and data local are shown but are greyed out, as they are always included. No attempt is made to ensure that the user choice are valid (dependencies, overwrite of content). After a profile is loaded, any added content is highlighted in green. --- CHANGELOG.md | 1 + apps/launcher/CMakeLists.txt | 1 + apps/launcher/datafilespage.cpp | 326 ++++++++++++++++-- apps/launcher/datafilespage.hpp | 24 +- components/config/gamesettings.cpp | 36 +- components/config/gamesettings.hpp | 8 +- components/config/launchersettings.cpp | 61 +++- components/config/launchersettings.hpp | 14 +- .../contentselector/model/contentmodel.cpp | 44 ++- .../contentselector/model/contentmodel.hpp | 7 +- .../contentselector/view/contentselector.cpp | 9 +- .../contentselector/view/contentselector.hpp | 3 +- files/ui/datafilespage.ui | 135 +++++++- files/ui/directorypicker.ui | 47 +++ 14 files changed, 662 insertions(+), 54 deletions(-) create mode 100644 files/ui/directorypicker.ui diff --git a/CHANGELOG.md b/CHANGELOG.md index bfcce6018d..6f71991ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2766: Warn user if their version of Morrowind is not the latest. Feature #2780: A way to see current OpenMW version in the console + Feature #2858: Add a tab to the launcher for handling datafolders Feature #3245: Grid and angle snapping for the OpenMW-CS Feature #3616: Allow Zoom levels on the World Map Feature #4297: Implement APPLIED_ONCE flag for magic effects diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 7e8bd67e94..e3256519c3 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -44,6 +44,7 @@ set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui + ${CMAKE_SOURCE_DIR}/files/ui/directorypicker.ui ) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 58a20c7853..9268c8e142 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -20,12 +23,34 @@ #include #include +#include #include +#include #include "utils/textinputdialog.hpp" +#include "utils/profilescombobox.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.absolutePath()); + + for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + contentSubdirs(subdir.absoluteFilePath(), dirs); + } +} + namespace Launcher { namespace @@ -114,6 +139,14 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: this, SLOT(updateNewProfileOkButton(QString))); connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateCloneProfileOkButton(QString))); + connect(ui.directoryAddSubdirsButton, &QPushButton::released, this, [=]() { this->addSubdirectories(true); }); + connect(ui.directoryInsertButton, &QPushButton::released, this, [=]() { this->addSubdirectories(false); }); + connect(ui.directoryUpButton, &QPushButton::released, this, [=]() { this->moveDirectory(-1); }); + connect(ui.directoryDownButton, &QPushButton::released, this, [=]() { this->moveDirectory(1); }); + connect(ui.directoryRemoveButton, &QPushButton::released, this, [=]() { this->removeDirectory(); }); + connect(ui.archiveUpButton, &QPushButton::released, this, [=]() { this->moveArchive(-1); }); + connect(ui.archiveDownButton, &QPushButton::released, this, [=]() { this->moveArchive(1); }); + connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [=]() { this->sortDirectories(); }); buildView(); loadSettings(); @@ -128,13 +161,12 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: void Launcher::DataFilesPage::buildView() { - QToolButton * refreshButton = mSelector->refreshButton(); + 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"); - refreshButton->setToolTip("Refresh Data Files"); //combo box ui.profilesComboBox->addItem(mDefaultContentListName); @@ -188,20 +220,94 @@ bool Launcher::DataFilesPage::loadSettings() void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) { - QStringList paths = mGameSettings.getDataDirs(); + mSelector->clearFiles(); + ui.archiveListWidget->clear(); + ui.directoryListWidget->clear(); - mDataLocal = mGameSettings.getDataLocal(); + QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName); + if (directories.isEmpty()) + directories = mGameSettings.getDataDirs(); + mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) - paths.insert(0, mDataLocal); + directories.insert(0, mDataLocal); - mSelector->clearFiles(); + const auto globalDataDir = QString(mGameSettings.getGlobalDataDir().c_str()); + if (!globalDataDir.isEmpty()) + directories.insert(0, globalDataDir); + + // add directories, archives and content files + directories.removeDuplicates(); + for (const auto& currentDir : directories) + { + // add new achives files presents in current directory + addArchivesFromDir(currentDir); + + // Display new content with green background + QColor background; + QString tooltip; + if (mNewDataDirs.contains(currentDir)) + { + tooltip += "Will be added to the current profile\n"; + background = Qt::green; + } + else + background = Qt::white; + + // add content files presents in current directory + mSelector->addFiles(currentDir, mNewDataDirs.contains(currentDir)); + + // add current directory to list + ui.directoryListWidget->addItem(currentDir); + auto row = ui.directoryListWidget->count() - 1; + auto* item = ui.directoryListWidget->item(row); + item->setBackground(background); + + // deactivate data-local and global data directory: they are always included + if (currentDir == mDataLocal || currentDir == globalDataDir) + { + auto flags = item->flags(); + item->setFlags(flags & ~(Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled|Qt::ItemIsEnabled)); + } - for (const QString &path : paths) - mSelector->addFiles(path); + // 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(background); + auto emptyIcon = QIcon(pixmap); + item->setIcon(emptyIcon); + } + item->setToolTip(tooltip); + } mSelector->sortFiles(); - PathIterator pathIterator(paths); + 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)); } @@ -232,6 +338,9 @@ void Launcher::DataFilesPage::saveSettings(const QString &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(); @@ -243,11 +352,36 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) { fileNames.append(item->fileName()); } - mLauncherSettings.setContentList(profileName, fileNames); - mGameSettings.setContentList(fileNames); + mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames); + mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames); } -QStringList Launcher::DataFilesPage::selectedFilePaths() +QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const +{ + QStringList dirList; + for (int i = 0; i < ui.directoryListWidget->count(); ++i) + { + if (ui.directoryListWidget->item(i)->background() != Qt::gray) + 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(); @@ -255,15 +389,8 @@ QStringList Launcher::DataFilesPage::selectedFilePaths() for (const ContentSelectorModel::EsmFile *item : items) { QFile file(item->filePath()); - if(file.exists()) - { filePaths.append(item->filePath()); - } - else - { - slotRefreshButtonClicked(); - } } return filePaths; } @@ -307,8 +434,18 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString 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()); + item->setBackground(Qt::white); + } + checkForDefaultProfile(); } @@ -397,7 +534,7 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered() if (profile.isEmpty()) return; - mLauncherSettings.setContentList(profile, selectedFilePaths()); + mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths()); addProfile(profile, true); } @@ -435,6 +572,155 @@ void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString &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 fileDialog.selectedFiles()[0]; + +} + +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); +} + +void Launcher::DataFilesPage::addArchivesFromDir(const QString& path) +{ + QDir dir(path, "*.bsa"); + + for (const auto& fileinfo : dir.entryInfoList()) + { + const auto absPath = fileinfo.absoluteFilePath(); + if (Bsa::CompressedBSAFile::detectVersion(absPath.toStdString()) == 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 diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index e004ca7542..0a235209f3 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -43,12 +43,6 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); - /** - * Returns the file paths of all selected content files - * @return the file paths of all selected content files - */ - QStringList selectedFilePaths(); - signals: void signalProfileChanged (int index); void signalLoadedCellsChanged(QStringList selectedFiles); @@ -66,6 +60,11 @@ namespace Launcher void updateNewProfileOkButton(const QString &text); void updateCloneProfileOkButton(const QString &text); + void addSubdirectories(bool append); + void sortDirectories(); + void removeDirectory(); + void moveArchive(int step); + void moveDirectory(int step); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); @@ -103,10 +102,14 @@ namespace Launcher QString mPreviousProfile; QStringList previousSelectedFiles; QString mDataLocal; + QStringList mKnownArchives; + QStringList mNewDataDirs; Process::ProcessInvoker* mNavMeshToolInvoker; NavMeshToolProgress mNavMeshToolProgress; + void addArchive(const QString& name, Qt::CheckState selected, int row = -1); + void addArchivesFromDir(const QString& dir); void buildView(); void setProfile (int index, bool savePrevious); void setProfile (const QString &previous, const QString ¤t, bool savePrevious); @@ -118,6 +121,15 @@ namespace Launcher void reloadCells(QStringList selectedFiles); void refreshDataFilesView (); void updateNavMeshProgress(int minDataSize); + QString selectDirectory(); + + /** + * Returns the file paths of all selected content files + * @return the file paths of all selected content files + */ + QStringList selectedFilePaths() const; + QStringList selectedArchivePaths(bool all=false) const; + QStringList selectedDirectoriesPaths() const; class PathIterator { diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 2b4bce5faf..6253d53f45 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -7,7 +7,9 @@ #include +const char Config::GameSettings::sArchiveKey[] = "fallback-archive"; const char Config::GameSettings::sContentKey[] = "content"; +const char Config::GameSettings::sDirectoryKey[] = "data"; Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) : mCfgMgr(cfg) @@ -63,6 +65,14 @@ void Config::GameSettings::validatePaths() } } +std::string Config::GameSettings::getGlobalDataDir() const +{ + // global data dir may not exists if OpenMW is not installed (ie if run from build directory) + if (boost::filesystem::exists(mCfgMgr.getGlobalDataPath())) + return boost::filesystem::canonical(mCfgMgr.getGlobalDataPath()).string(); + return {}; +} + QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) const { if (!mSettings.values(key).isEmpty()) @@ -475,13 +485,29 @@ bool Config::GameSettings::hasMaster() return result; } -void Config::GameSettings::setContentList(const QStringList& fileNames) +void Config::GameSettings::setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames) { - remove(sContentKey); - for (const QString& fileName : fileNames) + auto const reset = [this](const char* key, const QStringList& list) { - setMultiValue(sContentKey, fileName); - } + remove(key); + for (auto const& item : list) + setMultiValue(key, item); + }; + + reset(sDirectoryKey, dirNames); + reset(sArchiveKey, archiveNames); + reset(sContentKey, fileNames); +} + +QStringList Config::GameSettings::getDataDirs() const +{ + return Config::LauncherSettings::reverse(mDataDirs); +} + +QStringList Config::GameSettings::getArchiveList() const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return Config::LauncherSettings::reverse(values(sArchiveKey)); } QStringList Config::GameSettings::getContentList() const diff --git a/components/config/gamesettings.hpp b/components/config/gamesettings.hpp index 263a151c93..d4191523ad 100644 --- a/components/config/gamesettings.hpp +++ b/components/config/gamesettings.hpp @@ -53,7 +53,8 @@ namespace Config mUserSettings.remove(key); } - inline QStringList getDataDirs() const { return mDataDirs; } + QStringList getDataDirs() const; + std::string getGlobalDataDir() const; inline void removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } @@ -70,7 +71,8 @@ namespace Config bool writeFile(QTextStream &stream); bool writeFileWithComments(QFile &file); - void setContentList(const QStringList& fileNames); + QStringList getArchiveList() const; + void setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); QStringList getContentList() const; void clear(); @@ -85,7 +87,9 @@ namespace Config QStringList mDataDirs; QString mDataLocal; + static const char sArchiveKey[]; static const char sContentKey[]; + static const char sDirectoryKey[]; static bool isOrderedLine(const QString& line) ; }; diff --git a/components/config/launchersettings.cpp b/components/config/launchersettings.cpp index 025bc43827..f0d9093993 100644 --- a/components/config/launchersettings.cpp +++ b/components/config/launchersettings.cpp @@ -7,9 +7,15 @@ #include +#include + +#include + const char Config::LauncherSettings::sCurrentContentListKey[] = "Profiles/currentprofile"; const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg"; const char Config::LauncherSettings::sContentListsSectionPrefix[] = "Profiles/"; +const char Config::LauncherSettings::sDirectoryListSuffix[] = "/data"; +const char Config::LauncherSettings::sArchiveListSuffix[] = "/fallback-archive"; const char Config::LauncherSettings::sContentListSuffix[] = "/content"; QStringList Config::LauncherSettings::subKeys(const QString &key) @@ -86,6 +92,16 @@ QStringList Config::LauncherSettings::getContentLists() return subKeys(QString(sContentListsSectionPrefix)); } +QString Config::LauncherSettings::makeDirectoryListKey(const QString& contentListName) +{ + return QString(sContentListsSectionPrefix) + contentListName + QString(sDirectoryListSuffix); +} + +QString Config::LauncherSettings::makeArchiveListKey(const QString& contentListName) +{ + return QString(sContentListsSectionPrefix) + contentListName + QString(sArchiveListSuffix); +} + QString Config::LauncherSettings::makeContentListKey(const QString& contentListName) { return QString(sContentListsSectionPrefix) + contentListName + QString(sContentListSuffix); @@ -94,18 +110,28 @@ QString Config::LauncherSettings::makeContentListKey(const QString& contentListN void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) { // obtain content list from game settings (if present) + QStringList dirs(gameSettings.getDataDirs()); + const QStringList archives(gameSettings.getArchiveList()); const QStringList files(gameSettings.getContentList()); // if openmw.cfg has no content, exit so we don't create an empty content list. - if (files.isEmpty()) + if (dirs.isEmpty() || files.isEmpty()) { return; } + // global and local data directories are not part of any profile + const auto globalDataDir = QString(gameSettings.getGlobalDataDir().c_str()); + const auto dataLocal = gameSettings.getDataLocal(); + dirs.removeAll(globalDataDir); + dirs.removeAll(dataLocal); + // if any existing profile in launcher matches the content list, make that profile the default for (const QString &listName : getContentLists()) { - if (isEqual(files, getContentListFiles(listName))) + if (isEqual(files, getContentListFiles(listName)) && + isEqual(archives, getArchiveList(listName)) && + isEqual(dirs, getDataDirectoryList(listName))) { setCurrentContentListName(listName); return; @@ -115,11 +141,13 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) // otherwise, add content list QString newContentListName(makeNewContentListName()); setCurrentContentListName(newContentListName); - setContentList(newContentListName, files); + setContentList(newContentListName, dirs, archives, files); } void Config::LauncherSettings::removeContentList(const QString &contentListName) { + remove(makeDirectoryListKey(contentListName)); + remove(makeArchiveListKey(contentListName)); remove(makeContentListKey(contentListName)); } @@ -129,14 +157,18 @@ void Config::LauncherSettings::setCurrentContentListName(const QString &contentL setValue(QString(sCurrentContentListKey), contentListName); } -void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& fileNames) +void Config::LauncherSettings::setContentList(const QString& contentListName, const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames) { - removeContentList(contentListName); - QString key = makeContentListKey(contentListName); - for (const QString& fileName : fileNames) + auto const assign = [this](const QString key, const QStringList& list) { - setMultiValue(key, fileName); - } + for (auto const& item : list) + setMultiValue(key, item); + }; + + removeContentList(contentListName); + assign(makeDirectoryListKey(contentListName), dirNames); + assign(makeArchiveListKey(contentListName), archiveNames); + assign(makeContentListKey(contentListName), fileNames); } QString Config::LauncherSettings::getCurrentContentListName() const @@ -144,6 +176,17 @@ QString Config::LauncherSettings::getCurrentContentListName() const return value(QString(sCurrentContentListKey)); } +QStringList Config::LauncherSettings::getDataDirectoryList(const QString& contentListName) const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return reverse(getSettings().values(makeDirectoryListKey(contentListName))); +} + +QStringList Config::LauncherSettings::getArchiveList(const QString& contentListName) const +{ + // QMap returns multiple rows in LIFO order, so need to reverse + return reverse(getSettings().values(makeArchiveListKey(contentListName))); +} QStringList Config::LauncherSettings::getContentListFiles(const QString& contentListName) const { // QMap returns multiple rows in LIFO order, so need to reverse diff --git a/components/config/launchersettings.hpp b/components/config/launchersettings.hpp index da492c85ce..06632423a8 100644 --- a/components/config/launchersettings.hpp +++ b/components/config/launchersettings.hpp @@ -18,7 +18,7 @@ namespace Config void setContentList(const GameSettings& gameSettings); /// Create a Content List (or replace if it already exists) - void setContentList(const QString& contentListName, const QStringList& fileNames); + void setContentList(const QString& contentListName, const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames); void removeContentList(const QString &contentListName); @@ -26,15 +26,23 @@ namespace Config QString getCurrentContentListName() const; + QStringList getDataDirectoryList(const QString& contentListName) const; + QStringList getArchiveList(const QString& contentListName) const; QStringList getContentListFiles(const QString& contentListName) const; /// \return new list that is reversed order of input static QStringList reverse(const QStringList& toReverse); static const char sLauncherConfigFileName[]; - + private: + /// \return key to use to get/set the files in the specified data Directory List + static QString makeDirectoryListKey(const QString& contentListName); + + /// \return key to use to get/set the files in the specified Archive List + static QString makeArchiveListKey(const QString& contentListName); + /// \return key to use to get/set the files in the specified Content List static QString makeContentListKey(const QString& contentListName); @@ -51,6 +59,8 @@ namespace Config /// section of launcher.cfg holding the Content Lists static const char sContentListsSectionPrefix[]; + static const char sDirectoryListSuffix[]; + static const char sArchiveListSuffix[]; static const char sContentListSuffix[]; }; } diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index f7cedc83a4..f4760aaea6 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -160,6 +160,15 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int return isLoadOrderError(file) ? mWarningIcon : QVariant(); } + case Qt::BackgroundRole: + { + if (isNew(file->fileName())) + { + return QVariant(QColor(Qt::green)); + } + return QVariant(); + } + case Qt::EditRole: case Qt::DisplayRole: { @@ -413,7 +422,7 @@ void ContentSelectorModel::ContentModel::addFile(EsmFile *file) emit dataChanged (idx, idx); } -void ContentSelectorModel::ContentModel::addFiles(const QString &path) +void ContentSelectorModel::ContentModel::addFiles(const QString &path, bool newfiles) { QDir dir(path); QStringList filters; @@ -471,6 +480,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) // Put the file in the table addFile(file); + setNew(file->fileName(), newfiles); } catch(std::runtime_error &e) { // An error occurred while reading the .esp @@ -481,6 +491,16 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) } } +bool ContentSelectorModel::ContentModel::containsDataFiles(const QString &path) +{ + QDir dir(path); + QStringList filters; + filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + dir.setNameFilters(filters); + + return dir.entryList().count() != 0; +} + void ContentSelectorModel::ContentModel::clearFiles() { const int filesCount = mFiles.count(); @@ -553,6 +573,28 @@ bool ContentSelectorModel::ContentModel::isEnabled (const QModelIndex& index) co return (flags(index) & Qt::ItemIsEnabled); } +bool ContentSelectorModel::ContentModel::isNew(const QString& filepath) const +{ + if (mNewFiles.contains(filepath)) + return mNewFiles[filepath]; + + return false; +} + +void ContentSelectorModel::ContentModel::setNew(const QString &filepath, bool isNew) +{ + if (filepath.isEmpty()) + return; + + const EsmFile *file = item(filepath); + + if (!file) + return; + + mNewFiles[filepath] = isNew; +} + + bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile *file) const { return mPluginsWithLoadOrderError.contains(file->filePath()); diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 4bbe73b427..9a3dddb1c7 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -43,8 +43,9 @@ namespace ContentSelectorModel QMimeData *mimeData(const QModelIndexList &indexes) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; - void addFiles(const QString &path); + void addFiles(const QString &path, bool newfiles); void sortFiles(); + bool containsDataFiles(const QString &path); void clearFiles(); QModelIndex indexFromItem(const EsmFile *item) const; @@ -56,6 +57,8 @@ namespace ContentSelectorModel bool isEnabled (const QModelIndex& index) const; bool isChecked(const QString &filepath) const; bool setCheckState(const QString &filepath, bool isChecked); + bool isNew(const QString &filepath) const; + void setNew(const QString &filepath, bool isChecked); void setContentList(const QStringList &fileList); ContentFileList checkedItems() const; void uncheckAll(); @@ -79,7 +82,9 @@ namespace ContentSelectorModel QString toolTip(const EsmFile *file) const; ContentFileList mFiles; + QStringList mArchives; QHash mCheckStates; + QHash mNewFiles; QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index ef925148ab..441caa3b23 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -153,9 +153,9 @@ ContentSelectorModel::ContentFileList return mContentModel->checkedItems(); } -void ContentSelectorView::ContentSelector::addFiles(const QString &path) +void ContentSelectorView::ContentSelector::addFiles(const QString &path, bool newfiles) { - mContentModel->addFiles(path); + mContentModel->addFiles(path, newfiles); // add any game files to the combo box for (const QString& gameFileName : mContentModel->gameFiles()) @@ -178,6 +178,11 @@ void ContentSelectorView::ContentSelector::sortFiles() mContentModel->sortFiles(); } +bool ContentSelectorView::ContentSelector::containsDataFiles(const QString &path) +{ + return mContentModel->containsDataFiles(path); +} + void ContentSelectorView::ContentSelector::clearFiles() { mContentModel->clearFiles(); diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index b40675bedc..75ca3e17b4 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -27,8 +27,9 @@ namespace ContentSelectorView QString currentFile() const; - void addFiles(const QString &path); + void addFiles(const QString &path, bool newfiles = false); void sortFiles(); + bool containsDataFiles(const QString &path); void clearFiles(); void setProfileContent (const QStringList &fileList); diff --git a/files/ui/datafilespage.ui b/files/ui/datafilespage.ui index e942cd652b..dfcf02fced 100644 --- a/files/ui/datafilespage.ui +++ b/files/ui/datafilespage.ui @@ -17,16 +17,141 @@ - 0 + 2 - + - Data Files + Data Directories - - + + + + + QAbstractItemView::InternalMove + + + + + + + Scan directories for likely data directories and append them at the end of the list. + + + Append + + + + + + + Scan directories for likely data directories and insert them above the selected position + + + Insert Above + + + + + + + Move selected directory one position up + + + Move Up + + + + + + + Move selected directory one position down + + + Move Down + + + + + + + Remove selected directory + + + Remove + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-style:italic;">note: directories that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + + + + + + + + Archive Files + + + + + + QAbstractItemView::InternalMove + + + + + + + Move selected archive one position up + + + Move Up + + + + + + + Move selected archive one position down + + + Move Down + + + + + + + <html><head/><body><p><span style=" font-style:italic;">note: archives that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + + + + + + + + Content Files + + + + + + + <html><head/><body><p><span style=" font-style:italic;">note: content files that are not part of current Content List are </span><span style=" font-style:italic; background-color:#00ff00;">highlighted</span></p></body></html> + + + diff --git a/files/ui/directorypicker.ui b/files/ui/directorypicker.ui new file mode 100644 index 0000000000..6350bcd2d3 --- /dev/null +++ b/files/ui/directorypicker.ui @@ -0,0 +1,47 @@ + + + SelectSubdirs + + + + 0 + 0 + 800 + 500 + + + + Select directories you wish to add + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + confirmButton + accepted() + SelectSubdirs + accept() + + + confirmButton + rejected() + SelectSubdirs + reject() + + +