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.
pull/3226/head
fredzio 5 years ago
parent 6d794cf9ca
commit b88d32ff5b

@ -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

@ -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})

@ -7,6 +7,9 @@
#include <QMessageBox>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QFileDialog>
#include <QTreeView>
#include <qnamespace.h>
#include <thread>
#include <mutex>
#include <algorithm>
@ -20,12 +23,34 @@
#include <components/config/gamesettings.hpp>
#include <components/config/launchersettings.hpp>
#include <components/bsa/compressedbsafile.hpp>
#include <components/navmeshtool/protocol.hpp>
#include <components/vfs/bsaarchive.hpp>
#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();
@ -134,7 +167,6 @@ void Launcher::DataFilesPage::buildView()
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::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::selectedFilePaths()
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,16 +389,9 @@ 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

@ -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 &current, 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
{

@ -7,7 +7,9 @@
#include <components/files/configurationmanager.hpp>
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)
{
remove(key);
for (auto const& item : list)
setMultiValue(key, item);
};
reset(sDirectoryKey, dirNames);
reset(sArchiveKey, archiveNames);
reset(sContentKey, fileNames);
}
QStringList Config::GameSettings::getDataDirs() const
{
setMultiValue(sContentKey, fileName);
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

@ -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) ;
};

@ -7,9 +7,15 @@
#include <QDebug>
#include <boost/filesystem/operations.hpp>
#include <components/files/configurationmanager.hpp>
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

@ -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,6 +26,8 @@ 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
@ -35,6 +37,12 @@ namespace Config
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[];
};
}

@ -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());

@ -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<QString, Qt::CheckState> mCheckStates;
QHash<QString, bool> mNewFiles;
QSet<QString> mPluginsWithLoadOrderError;
QString mEncoding;
QIcon mWarningIcon;

@ -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();

@ -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);

@ -17,16 +17,141 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
<number>2</number>
</property>
<widget class="QWidget" name="tab">
<widget class="QWidget" name="dirTab">
<attribute name="title">
<string>Data Files</string>
<string>Data Directories</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QGridLayout" name="dirTabLayout">
<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"/>
</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>
</widget>
<widget class="QWidget" name="tab_2">

@ -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>
Loading…
Cancel
Save