diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbd47beba..4561574175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,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() + + +