1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-02-22 12:39:59 +00:00

Merge branch 'launcher-datadirs' into 'master'

Make launcher handle data dirs #2858 and BSA

See merge request OpenMW/openmw!192
This commit is contained in:
psi29a 2022-04-27 17:31:52 +00:00
commit 66a96bfa5e
14 changed files with 662 additions and 54 deletions

View File

@ -119,6 +119,7 @@
Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record 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 #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 #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 #3245: Grid and angle snapping for the OpenMW-CS
Feature #3616: Allow Zoom levels on the World Map Feature #3616: Allow Zoom levels on the World Map
Feature #4297: Implement APPLIED_ONCE flag for magic effects Feature #4297: Implement APPLIED_ONCE flag for magic effects

View File

@ -44,6 +44,7 @@ set(LAUNCHER_UI
${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui
${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui ${CMAKE_SOURCE_DIR}/files/ui/settingspage.ui
${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui ${CMAKE_SOURCE_DIR}/files/ui/advancedpage.ui
${CMAKE_SOURCE_DIR}/files/ui/directorypicker.ui
) )
source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER}) source_group(launcher FILES ${LAUNCHER} ${LAUNCHER_HEADER})

View File

@ -7,6 +7,9 @@
#include <QMessageBox> #include <QMessageBox>
#include <QMenu> #include <QMenu>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QFileDialog>
#include <QTreeView>
#include <qnamespace.h>
#include <thread> #include <thread>
#include <mutex> #include <mutex>
#include <algorithm> #include <algorithm>
@ -20,12 +23,34 @@
#include <components/config/gamesettings.hpp> #include <components/config/gamesettings.hpp>
#include <components/config/launchersettings.hpp> #include <components/config/launchersettings.hpp>
#include <components/bsa/compressedbsafile.hpp>
#include <components/navmeshtool/protocol.hpp> #include <components/navmeshtool/protocol.hpp>
#include <components/vfs/bsaarchive.hpp>
#include "utils/textinputdialog.hpp" #include "utils/textinputdialog.hpp"
#include "utils/profilescombobox.hpp"
#include "ui_directorypicker.h"
const char *Launcher::DataFilesPage::mDefaultContentListName = "Default"; 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 Launcher
{ {
namespace namespace
@ -114,6 +139,14 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
this, SLOT(updateNewProfileOkButton(QString))); this, SLOT(updateNewProfileOkButton(QString)));
connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), connect(mCloneProfileDialog->lineEdit(), SIGNAL(textChanged(QString)),
this, SLOT(updateCloneProfileOkButton(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(); buildView();
loadSettings(); loadSettings();
@ -134,7 +167,6 @@ void Launcher::DataFilesPage::buildView()
ui.newProfileButton->setToolTip ("Create a new Content List"); ui.newProfileButton->setToolTip ("Create a new Content List");
ui.cloneProfileButton->setToolTip ("Clone the current Content List"); ui.cloneProfileButton->setToolTip ("Clone the current Content List");
ui.deleteProfileButton->setToolTip ("Delete an existing Content List"); ui.deleteProfileButton->setToolTip ("Delete an existing Content List");
refreshButton->setToolTip("Refresh Data Files");
//combo box //combo box
ui.profilesComboBox->addItem(mDefaultContentListName); ui.profilesComboBox->addItem(mDefaultContentListName);
@ -188,20 +220,94 @@ bool Launcher::DataFilesPage::loadSettings()
void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
{ {
QStringList paths = mGameSettings.getDataDirs(); mSelector->clearFiles();
ui.archiveListWidget->clear();
ui.directoryListWidget->clear();
QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName);
if (directories.isEmpty())
directories = mGameSettings.getDataDirs();
mDataLocal = mGameSettings.getDataLocal(); mDataLocal = mGameSettings.getDataLocal();
if (!mDataLocal.isEmpty()) 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);
for (const QString &path : paths) // add directories, archives and content files
mSelector->addFiles(path); 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));
}
// 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(); 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)); mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator));
} }
@ -232,6 +338,9 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile)
if (profileName.isEmpty()) if (profileName.isEmpty())
profileName = ui.profilesComboBox->currentText(); profileName = ui.profilesComboBox->currentText();
//retrieve the data paths
auto dirList = selectedDirectoriesPaths();
//retrieve the files selected for the profile //retrieve the files selected for the profile
ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
@ -243,11 +352,36 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile)
{ {
fileNames.append(item->fileName()); fileNames.append(item->fileName());
} }
mLauncherSettings.setContentList(profileName, fileNames); mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames);
mGameSettings.setContentList(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 //retrieve the files selected for the profile
ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
@ -255,16 +389,9 @@ QStringList Launcher::DataFilesPage::selectedFilePaths()
for (const ContentSelectorModel::EsmFile *item : items) for (const ContentSelectorModel::EsmFile *item : items)
{ {
QFile file(item->filePath()); QFile file(item->filePath());
if(file.exists()) if(file.exists())
{
filePaths.append(item->filePath()); filePaths.append(item->filePath());
} }
else
{
slotRefreshButtonClicked();
}
}
return filePaths; return filePaths;
} }
@ -307,8 +434,18 @@ void Launcher::DataFilesPage::setProfile (const QString &previous, const QString
ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current)); ui.profilesComboBox->setCurrentProfile (ui.profilesComboBox->findText (current));
mNewDataDirs.clear();
mKnownArchives.clear();
populateFileViews(current); 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(); checkForDefaultProfile();
} }
@ -397,7 +534,7 @@ void Launcher::DataFilesPage::on_cloneProfileAction_triggered()
if (profile.isEmpty()) if (profile.isEmpty())
return; return;
mLauncherSettings.setContentList(profile, selectedFilePaths()); mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), selectedFilePaths());
addProfile(profile, true); addProfile(profile, true);
} }
@ -435,6 +572,155 @@ void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString &text)
mCloneProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1); 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() void Launcher::DataFilesPage::checkForDefaultProfile()
{ {
//don't allow deleting "Default" profile //don't allow deleting "Default" profile

View File

@ -43,12 +43,6 @@ namespace Launcher
void saveSettings(const QString &profile = ""); void saveSettings(const QString &profile = "");
bool loadSettings(); bool loadSettings();
/**
* Returns the file paths of all selected content files
* @return the file paths of all selected content files
*/
QStringList selectedFilePaths();
signals: signals:
void signalProfileChanged (int index); void signalProfileChanged (int index);
void signalLoadedCellsChanged(QStringList selectedFiles); void signalLoadedCellsChanged(QStringList selectedFiles);
@ -66,6 +60,11 @@ namespace Launcher
void updateNewProfileOkButton(const QString &text); void updateNewProfileOkButton(const QString &text);
void updateCloneProfileOkButton(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_newProfileAction_triggered();
void on_cloneProfileAction_triggered(); void on_cloneProfileAction_triggered();
@ -103,10 +102,14 @@ namespace Launcher
QString mPreviousProfile; QString mPreviousProfile;
QStringList previousSelectedFiles; QStringList previousSelectedFiles;
QString mDataLocal; QString mDataLocal;
QStringList mKnownArchives;
QStringList mNewDataDirs;
Process::ProcessInvoker* mNavMeshToolInvoker; Process::ProcessInvoker* mNavMeshToolInvoker;
NavMeshToolProgress mNavMeshToolProgress; NavMeshToolProgress mNavMeshToolProgress;
void addArchive(const QString& name, Qt::CheckState selected, int row = -1);
void addArchivesFromDir(const QString& dir);
void buildView(); void buildView();
void setProfile (int index, bool savePrevious); void setProfile (int index, bool savePrevious);
void setProfile (const QString &previous, const QString &current, bool savePrevious); void setProfile (const QString &previous, const QString &current, bool savePrevious);
@ -118,6 +121,15 @@ namespace Launcher
void reloadCells(QStringList selectedFiles); void reloadCells(QStringList selectedFiles);
void refreshDataFilesView (); void refreshDataFilesView ();
void updateNavMeshProgress(int minDataSize); 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 class PathIterator
{ {

View File

@ -7,7 +7,9 @@
#include <components/files/configurationmanager.hpp> #include <components/files/configurationmanager.hpp>
const char Config::GameSettings::sArchiveKey[] = "fallback-archive";
const char Config::GameSettings::sContentKey[] = "content"; const char Config::GameSettings::sContentKey[] = "content";
const char Config::GameSettings::sDirectoryKey[] = "data";
Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg) Config::GameSettings::GameSettings(Files::ConfigurationManager &cfg)
: mCfgMgr(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 QStringList Config::GameSettings::values(const QString &key, const QStringList &defaultValues) const
{ {
if (!mSettings.values(key).isEmpty()) if (!mSettings.values(key).isEmpty())
@ -475,13 +485,29 @@ bool Config::GameSettings::hasMaster()
return result; return result;
} }
void Config::GameSettings::setContentList(const QStringList& fileNames) void Config::GameSettings::setContentList(const QStringList& dirNames, const QStringList& archiveNames, const QStringList& fileNames)
{ {
remove(sContentKey); auto const reset = [this](const char* key, const QStringList& list)
for (const QString& fileName : fileNames)
{ {
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 QStringList Config::GameSettings::getContentList() const

View File

@ -53,7 +53,8 @@ namespace Config
mUserSettings.remove(key); 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 removeDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.removeAll(dir); }
inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); } inline void addDataDir(const QString &dir) { if(!dir.isEmpty()) mDataDirs.append(dir); }
@ -70,7 +71,8 @@ namespace Config
bool writeFile(QTextStream &stream); bool writeFile(QTextStream &stream);
bool writeFileWithComments(QFile &file); 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; QStringList getContentList() const;
void clear(); void clear();
@ -85,7 +87,9 @@ namespace Config
QStringList mDataDirs; QStringList mDataDirs;
QString mDataLocal; QString mDataLocal;
static const char sArchiveKey[];
static const char sContentKey[]; static const char sContentKey[];
static const char sDirectoryKey[];
static bool isOrderedLine(const QString& line) ; static bool isOrderedLine(const QString& line) ;
}; };

View File

@ -7,9 +7,15 @@
#include <QDebug> #include <QDebug>
#include <boost/filesystem/operations.hpp>
#include <components/files/configurationmanager.hpp>
const char Config::LauncherSettings::sCurrentContentListKey[] = "Profiles/currentprofile"; const char Config::LauncherSettings::sCurrentContentListKey[] = "Profiles/currentprofile";
const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg"; const char Config::LauncherSettings::sLauncherConfigFileName[] = "launcher.cfg";
const char Config::LauncherSettings::sContentListsSectionPrefix[] = "Profiles/"; 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"; const char Config::LauncherSettings::sContentListSuffix[] = "/content";
QStringList Config::LauncherSettings::subKeys(const QString &key) QStringList Config::LauncherSettings::subKeys(const QString &key)
@ -86,6 +92,16 @@ QStringList Config::LauncherSettings::getContentLists()
return subKeys(QString(sContentListsSectionPrefix)); 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) QString Config::LauncherSettings::makeContentListKey(const QString& contentListName)
{ {
return QString(sContentListsSectionPrefix) + contentListName + QString(sContentListSuffix); return QString(sContentListsSectionPrefix) + contentListName + QString(sContentListSuffix);
@ -94,18 +110,28 @@ QString Config::LauncherSettings::makeContentListKey(const QString& contentListN
void Config::LauncherSettings::setContentList(const GameSettings& gameSettings) void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
{ {
// obtain content list from game settings (if present) // obtain content list from game settings (if present)
QStringList dirs(gameSettings.getDataDirs());
const QStringList archives(gameSettings.getArchiveList());
const QStringList files(gameSettings.getContentList()); const QStringList files(gameSettings.getContentList());
// if openmw.cfg has no content, exit so we don't create an empty content list. // 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; 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 // if any existing profile in launcher matches the content list, make that profile the default
for (const QString &listName : getContentLists()) 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); setCurrentContentListName(listName);
return; return;
@ -115,11 +141,13 @@ void Config::LauncherSettings::setContentList(const GameSettings& gameSettings)
// otherwise, add content list // otherwise, add content list
QString newContentListName(makeNewContentListName()); QString newContentListName(makeNewContentListName());
setCurrentContentListName(newContentListName); setCurrentContentListName(newContentListName);
setContentList(newContentListName, files); setContentList(newContentListName, dirs, archives, files);
} }
void Config::LauncherSettings::removeContentList(const QString &contentListName) void Config::LauncherSettings::removeContentList(const QString &contentListName)
{ {
remove(makeDirectoryListKey(contentListName));
remove(makeArchiveListKey(contentListName));
remove(makeContentListKey(contentListName)); remove(makeContentListKey(contentListName));
} }
@ -129,14 +157,18 @@ void Config::LauncherSettings::setCurrentContentListName(const QString &contentL
setValue(QString(sCurrentContentListKey), contentListName); 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); auto const assign = [this](const QString key, const QStringList& list)
QString key = makeContentListKey(contentListName);
for (const QString& fileName : fileNames)
{ {
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 QString Config::LauncherSettings::getCurrentContentListName() const
@ -144,6 +176,17 @@ QString Config::LauncherSettings::getCurrentContentListName() const
return value(QString(sCurrentContentListKey)); 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 QStringList Config::LauncherSettings::getContentListFiles(const QString& contentListName) const
{ {
// QMap returns multiple rows in LIFO order, so need to reverse // QMap returns multiple rows in LIFO order, so need to reverse

View File

@ -18,7 +18,7 @@ namespace Config
void setContentList(const GameSettings& gameSettings); void setContentList(const GameSettings& gameSettings);
/// Create a Content List (or replace if it already exists) /// 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); void removeContentList(const QString &contentListName);
@ -26,6 +26,8 @@ namespace Config
QString getCurrentContentListName() const; QString getCurrentContentListName() const;
QStringList getDataDirectoryList(const QString& contentListName) const;
QStringList getArchiveList(const QString& contentListName) const;
QStringList getContentListFiles(const QString& contentListName) const; QStringList getContentListFiles(const QString& contentListName) const;
/// \return new list that is reversed order of input /// \return new list that is reversed order of input
@ -35,6 +37,12 @@ namespace Config
private: 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 /// \return key to use to get/set the files in the specified Content List
static QString makeContentListKey(const QString& contentListName); static QString makeContentListKey(const QString& contentListName);
@ -51,6 +59,8 @@ namespace Config
/// section of launcher.cfg holding the Content Lists /// section of launcher.cfg holding the Content Lists
static const char sContentListsSectionPrefix[]; static const char sContentListsSectionPrefix[];
static const char sDirectoryListSuffix[];
static const char sArchiveListSuffix[];
static const char sContentListSuffix[]; static const char sContentListSuffix[];
}; };
} }

View File

@ -160,6 +160,15 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex &index, int
return isLoadOrderError(file) ? mWarningIcon : QVariant(); return isLoadOrderError(file) ? mWarningIcon : QVariant();
} }
case Qt::BackgroundRole:
{
if (isNew(file->fileName()))
{
return QVariant(QColor(Qt::green));
}
return QVariant();
}
case Qt::EditRole: case Qt::EditRole:
case Qt::DisplayRole: case Qt::DisplayRole:
{ {
@ -413,7 +422,7 @@ void ContentSelectorModel::ContentModel::addFile(EsmFile *file)
emit dataChanged (idx, idx); emit dataChanged (idx, idx);
} }
void ContentSelectorModel::ContentModel::addFiles(const QString &path) void ContentSelectorModel::ContentModel::addFiles(const QString &path, bool newfiles)
{ {
QDir dir(path); QDir dir(path);
QStringList filters; QStringList filters;
@ -471,6 +480,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path)
// Put the file in the table // Put the file in the table
addFile(file); addFile(file);
setNew(file->fileName(), newfiles);
} catch(std::runtime_error &e) { } catch(std::runtime_error &e) {
// An error occurred while reading the .esp // 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() void ContentSelectorModel::ContentModel::clearFiles()
{ {
const int filesCount = mFiles.count(); const int filesCount = mFiles.count();
@ -553,6 +573,28 @@ bool ContentSelectorModel::ContentModel::isEnabled (const QModelIndex& index) co
return (flags(index) & Qt::ItemIsEnabled); 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 bool ContentSelectorModel::ContentModel::isLoadOrderError(const EsmFile *file) const
{ {
return mPluginsWithLoadOrderError.contains(file->filePath()); return mPluginsWithLoadOrderError.contains(file->filePath());

View File

@ -43,8 +43,9 @@ namespace ContentSelectorModel
QMimeData *mimeData(const QModelIndexList &indexes) const override; QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) 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(); void sortFiles();
bool containsDataFiles(const QString &path);
void clearFiles(); void clearFiles();
QModelIndex indexFromItem(const EsmFile *item) const; QModelIndex indexFromItem(const EsmFile *item) const;
@ -56,6 +57,8 @@ namespace ContentSelectorModel
bool isEnabled (const QModelIndex& index) const; bool isEnabled (const QModelIndex& index) const;
bool isChecked(const QString &filepath) const; bool isChecked(const QString &filepath) const;
bool setCheckState(const QString &filepath, bool isChecked); 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); void setContentList(const QStringList &fileList);
ContentFileList checkedItems() const; ContentFileList checkedItems() const;
void uncheckAll(); void uncheckAll();
@ -79,7 +82,9 @@ namespace ContentSelectorModel
QString toolTip(const EsmFile *file) const; QString toolTip(const EsmFile *file) const;
ContentFileList mFiles; ContentFileList mFiles;
QStringList mArchives;
QHash<QString, Qt::CheckState> mCheckStates; QHash<QString, Qt::CheckState> mCheckStates;
QHash<QString, bool> mNewFiles;
QSet<QString> mPluginsWithLoadOrderError; QSet<QString> mPluginsWithLoadOrderError;
QString mEncoding; QString mEncoding;
QIcon mWarningIcon; QIcon mWarningIcon;

View File

@ -153,9 +153,9 @@ ContentSelectorModel::ContentFileList
return mContentModel->checkedItems(); 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 // add any game files to the combo box
for (const QString& gameFileName : mContentModel->gameFiles()) for (const QString& gameFileName : mContentModel->gameFiles())
@ -178,6 +178,11 @@ void ContentSelectorView::ContentSelector::sortFiles()
mContentModel->sortFiles(); mContentModel->sortFiles();
} }
bool ContentSelectorView::ContentSelector::containsDataFiles(const QString &path)
{
return mContentModel->containsDataFiles(path);
}
void ContentSelectorView::ContentSelector::clearFiles() void ContentSelectorView::ContentSelector::clearFiles()
{ {
mContentModel->clearFiles(); mContentModel->clearFiles();

View File

@ -27,8 +27,9 @@ namespace ContentSelectorView
QString currentFile() const; QString currentFile() const;
void addFiles(const QString &path); void addFiles(const QString &path, bool newfiles = false);
void sortFiles(); void sortFiles();
bool containsDataFiles(const QString &path);
void clearFiles(); void clearFiles();
void setProfileContent (const QStringList &fileList); void setProfileContent (const QStringList &fileList);

View File

@ -17,16 +17,141 @@
<item> <item>
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>2</number>
</property> </property>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="dirTab">
<attribute name="title"> <attribute name="title">
<string>Data Files</string> <string>Data Directories</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QGridLayout" name="dirTabLayout">
<item> <item row="0" column="0" rowspan="26">
<widget class="QListWidget" name="directoryListWidget">
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="directoryAddSubdirsButton">
<property name="toolTip">
<string>Scan directories for likely data directories and append them at the end of the list.</string>
</property>
<property name="text">
<string>Append</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="directoryInsertButton">
<property name="toolTip">
<string>Scan directories for likely data directories and insert them above the selected position</string>
</property>
<property name="text">
<string>Insert Above</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="directoryUpButton">
<property name="toolTip">
<string>Move selected directory one position up</string>
</property>
<property name="text">
<string>Move Up</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="directoryDownButton">
<property name="toolTip">
<string>Move selected directory one position down</string>
</property>
<property name="text">
<string>Move Down</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="directoryRemoveButton">
<property name="toolTip">
<string>Remove selected directory</string>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item row="27" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;note: directories that are not part of current Content List are &lt;/span&gt;&lt;span style=&quot; font-style:italic; background-color:#00ff00;&quot;&gt;highlighted&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="archiveTab">
<attribute name="title">
<string>Archive Files</string>
</attribute>
<layout class="QGridLayout" name="archiveTabLayout">
<item row="0" column="0" rowspan="26">
<widget class="QListWidget" name="archiveListWidget">
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="archiveUpButton">
<property name="toolTip">
<string>Move selected archive one position up</string>
</property>
<property name="text">
<string>Move Up</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="archiveDownButton">
<property name="toolTip">
<string>Move selected archive one position down</string>
</property>
<property name="text">
<string>Move Down</string>
</property>
</widget>
</item>
<item row="27" column="0" colspan="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;note: archives that are not part of current Content List are &lt;/span&gt;&lt;span style=&quot; font-style:italic; background-color:#00ff00;&quot;&gt;highlighted&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="dataTab">
<attribute name="title">
<string>Content Files</string>
</attribute>
<layout class="QGridLayout" name="dataTabLayout">
<item row="0" column="0">
<widget class="QWidget" name="contentSelectorWidget" native="true"/> <widget class="QWidget" name="contentSelectorWidget" native="true"/>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;note: content files that are not part of current Content List are &lt;/span&gt;&lt;span style=&quot; font-style:italic; background-color:#00ff00;&quot;&gt;highlighted&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab_2"> <widget class="QWidget" name="tab_2">

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectSubdirs</class>
<widget class="QDialog" name="SelectSubdirs">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
<string>Select directories you wish to add</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QDialogButtonBox" name="confirmButton">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QListWidget" name="dirListWidget"/>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>confirmButton</sender>
<signal>accepted()</signal>
<receiver>SelectSubdirs</receiver>
<slot>accept()</slot>
</connection>
<connection>
<sender>confirmButton</sender>
<signal>rejected()</signal>
<receiver>SelectSubdirs</receiver>
<slot>reject()</slot>
</connection>
</connections>
</ui>