Merge branch 'launcher-datadirs' into 'master'

Make launcher handle data dirs #2858 and BSA

See merge request OpenMW/openmw!192
pull/3226/head
psi29a 3 years ago
commit 66a96bfa5e

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

@ -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);
for (const QString &path : paths)
mSelector->addFiles(path);
// 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();
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::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()
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)
{
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

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