|
|
|
#include "contentmodel.hpp"
|
|
|
|
#include "esmfile.hpp"
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
#include <stdexcept>
|
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
#include <QDebug>
|
|
|
|
#include <QDir>
|
|
|
|
|
|
|
|
#include <components/esm/format.hpp>
|
|
|
|
#include <components/esm3/esmreader.hpp>
|
|
|
|
#include <components/esm4/reader.hpp>
|
|
|
|
#include <components/files/openfile.hpp>
|
|
|
|
#include <components/files/qtconversion.hpp>
|
|
|
|
|
|
|
|
ContentSelectorModel::ContentModel::ContentModel(QObject* parent, QIcon& warningIcon, bool showOMWScripts)
|
|
|
|
: QAbstractTableModel(parent)
|
|
|
|
, mWarningIcon(warningIcon)
|
|
|
|
, mShowOMWScripts(showOMWScripts)
|
|
|
|
, mMimeType("application/omwcontent")
|
|
|
|
, mMimeTypes(QStringList() << mMimeType)
|
|
|
|
, mColumnCount(1)
|
|
|
|
, mDropActions(Qt::MoveAction)
|
|
|
|
{
|
|
|
|
setEncoding("win1252");
|
|
|
|
uncheckAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
ContentSelectorModel::ContentModel::~ContentModel()
|
|
|
|
{
|
|
|
|
qDeleteAll(mFiles);
|
|
|
|
mFiles.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::setEncoding(const QString& encoding)
|
|
|
|
{
|
|
|
|
mEncoding = encoding;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ContentSelectorModel::ContentModel::columnCount(const QModelIndex& parent) const
|
|
|
|
{
|
|
|
|
if (parent.isValid())
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return mColumnCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ContentSelectorModel::ContentModel::rowCount(const QModelIndex& parent) const
|
|
|
|
{
|
|
|
|
if (parent.isValid())
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return mFiles.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
const ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(int row) const
|
|
|
|
{
|
|
|
|
if (row >= 0 && row < mFiles.size())
|
|
|
|
return mFiles.at(row);
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(int row)
|
|
|
|
{
|
|
|
|
if (row >= 0 && row < mFiles.count())
|
|
|
|
return mFiles.at(row);
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
const ContentSelectorModel::EsmFile* ContentSelectorModel::ContentModel::item(const QString& name) const
|
|
|
|
{
|
|
|
|
EsmFile::FileProperty fp = EsmFile::FileProperty_FileName;
|
|
|
|
|
|
|
|
if (name.contains('/'))
|
|
|
|
fp = EsmFile::FileProperty_FilePath;
|
|
|
|
|
|
|
|
for (const EsmFile* file : mFiles)
|
|
|
|
{
|
|
|
|
if (name.compare(file->fileProperty(fp).toString(), Qt::CaseInsensitive) == 0)
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
QModelIndex ContentSelectorModel::ContentModel::indexFromItem(const EsmFile* item) const
|
|
|
|
{
|
|
|
|
// workaround: non-const pointer cast for calls from outside contentmodel/contentselector
|
|
|
|
EsmFile* non_const_file_ptr = const_cast<EsmFile*>(item);
|
|
|
|
|
|
|
|
if (item)
|
|
|
|
return index(mFiles.indexOf(non_const_file_ptr), 0);
|
|
|
|
|
|
|
|
return QModelIndex();
|
|
|
|
}
|
|
|
|
|
|
|
|
Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index) const
|
|
|
|
{
|
|
|
|
if (!index.isValid())
|
|
|
|
return Qt::ItemIsDropEnabled;
|
|
|
|
|
|
|
|
const EsmFile* file = item(index.row());
|
|
|
|
|
|
|
|
if (!file)
|
|
|
|
return Qt::NoItemFlags;
|
|
|
|
|
|
|
|
// game files can always be checked
|
|
|
|
if (file == mGameFile)
|
|
|
|
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
|
|
|
|
|
|
|
|
Qt::ItemFlags returnFlags;
|
|
|
|
|
|
|
|
// addon can be checked if its gamefile is
|
|
|
|
// ... special case, addon with no dependency can be used with any gamefile.
|
|
|
|
bool gamefileChecked = false;
|
|
|
|
bool noGameFiles = true;
|
|
|
|
for (const QString& fileName : file->gameFiles())
|
|
|
|
{
|
|
|
|
for (QListIterator<EsmFile*> dependencyIter(mFiles); dependencyIter.hasNext(); dependencyIter.next())
|
|
|
|
{
|
|
|
|
// compare filenames only. Multiple instances
|
|
|
|
// of the filename (with different paths) is not relevant here.
|
|
|
|
EsmFile* depFile = dependencyIter.peekNext();
|
|
|
|
if (!depFile->isGameFile() || depFile->fileName().compare(fileName, Qt::CaseInsensitive) != 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
noGameFiles = false;
|
|
|
|
if (isChecked(depFile->filePath()))
|
|
|
|
{
|
|
|
|
gamefileChecked = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (gamefileChecked || noGameFiles)
|
|
|
|
{
|
|
|
|
returnFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnFlags;
|
|
|
|
}
|
|
|
|
|
|
|
|
QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int role) const
|
|
|
|
{
|
|
|
|
if (!index.isValid())
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
if (index.row() >= mFiles.size())
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
const EsmFile* file = item(index.row());
|
|
|
|
|
|
|
|
if (!file)
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
const int column = index.column();
|
|
|
|
|
|
|
|
switch (role)
|
|
|
|
{
|
|
|
|
case Qt::DecorationRole:
|
|
|
|
{
|
|
|
|
return isLoadOrderError(file) ? mWarningIcon : QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::BackgroundRole:
|
|
|
|
{
|
|
|
|
if (isNew(file->fileName()))
|
|
|
|
{
|
|
|
|
return QVariant(QColor(Qt::green));
|
|
|
|
}
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::ForegroundRole:
|
|
|
|
{
|
|
|
|
if (isNew(file->fileName()))
|
|
|
|
{
|
|
|
|
return QVariant(QColor(Qt::black));
|
|
|
|
}
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::EditRole:
|
|
|
|
case Qt::DisplayRole:
|
|
|
|
{
|
|
|
|
if (column >= 0 && column <= EsmFile::FileProperty_GameFile)
|
|
|
|
return file->fileProperty(static_cast<EsmFile::FileProperty>(column));
|
|
|
|
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::TextAlignmentRole:
|
|
|
|
{
|
|
|
|
switch (column)
|
|
|
|
{
|
|
|
|
case 0:
|
|
|
|
case 1:
|
|
|
|
return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
case 2:
|
|
|
|
case 3:
|
|
|
|
return QVariant(Qt::AlignRight | Qt::AlignVCenter);
|
|
|
|
default:
|
|
|
|
return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::ToolTipRole:
|
|
|
|
{
|
|
|
|
if (column != 0)
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
return toolTip(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::CheckStateRole:
|
|
|
|
{
|
|
|
|
if (file == mGameFile)
|
|
|
|
return QVariant();
|
|
|
|
|
|
|
|
return mCheckStates[file->filePath()];
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::UserRole:
|
|
|
|
{
|
|
|
|
if (file == mGameFile)
|
|
|
|
return ContentType_GameFile;
|
|
|
|
else if (flags(index))
|
|
|
|
return ContentType_Addon;
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Qt::UserRole + 1:
|
|
|
|
return isChecked(file->filePath());
|
|
|
|
}
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
|
|
|
{
|
|
|
|
if (!index.isValid())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
EsmFile* file = item(index.row());
|
|
|
|
QString fileName = file->fileName();
|
|
|
|
bool success = false;
|
|
|
|
|
|
|
|
switch (role)
|
|
|
|
{
|
|
|
|
case Qt::EditRole:
|
|
|
|
{
|
|
|
|
QStringList list = value.toStringList();
|
|
|
|
|
|
|
|
for (int i = 0; i < EsmFile::FileProperty_GameFile; i++)
|
|
|
|
file->setFileProperty(static_cast<EsmFile::FileProperty>(i), list.at(i));
|
|
|
|
|
|
|
|
for (int i = EsmFile::FileProperty_GameFile; i < list.size(); i++)
|
|
|
|
file->setFileProperty(EsmFile::FileProperty_GameFile, list.at(i));
|
|
|
|
|
|
|
|
emit dataChanged(index, index);
|
|
|
|
|
|
|
|
success = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Qt::UserRole + 1:
|
|
|
|
{
|
|
|
|
success = (flags(index) & Qt::ItemIsEnabled);
|
|
|
|
|
|
|
|
if (success)
|
|
|
|
{
|
|
|
|
success = setCheckState(file->filePath(), value.toBool());
|
|
|
|
emit dataChanged(index, index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Qt::CheckStateRole:
|
|
|
|
{
|
|
|
|
int checkValue = value.toInt();
|
|
|
|
bool setState = false;
|
|
|
|
if ((checkValue == Qt::Checked) && !isChecked(file->filePath()))
|
|
|
|
{
|
|
|
|
setState = true;
|
|
|
|
success = true;
|
|
|
|
}
|
|
|
|
else if ((checkValue == Qt::Checked) && isChecked(file->filePath()))
|
|
|
|
setState = true;
|
|
|
|
else if (checkValue == Qt::Unchecked)
|
|
|
|
setState = true;
|
|
|
|
|
|
|
|
if (setState)
|
|
|
|
{
|
|
|
|
setCheckState(file->filePath(), success);
|
|
|
|
emit dataChanged(index, index);
|
|
|
|
checkForLoadOrderErrors();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return success;
|
|
|
|
|
|
|
|
for (EsmFile* file2 : mFiles)
|
|
|
|
{
|
|
|
|
if (file2->gameFiles().contains(fileName, Qt::CaseInsensitive))
|
|
|
|
{
|
|
|
|
QModelIndex idx = indexFromItem(file2);
|
|
|
|
emit dataChanged(idx, idx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
success = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::insertRows(int position, int rows, const QModelIndex& parent)
|
|
|
|
{
|
|
|
|
if (parent.isValid())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
beginInsertRows(parent, position, position + rows - 1);
|
|
|
|
{
|
|
|
|
for (int row = 0; row < rows; ++row)
|
|
|
|
mFiles.insert(position, new EsmFile);
|
|
|
|
}
|
|
|
|
endInsertRows();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::removeRows(int position, int rows, const QModelIndex& parent)
|
|
|
|
{
|
|
|
|
if (parent.isValid())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
beginRemoveRows(parent, position, position + rows - 1);
|
|
|
|
{
|
|
|
|
for (int row = 0; row < rows; ++row)
|
|
|
|
delete mFiles.takeAt(position);
|
|
|
|
}
|
|
|
|
endRemoveRows();
|
|
|
|
|
|
|
|
// at this point we know that drag and drop has finished.
|
|
|
|
checkForLoadOrderErrors();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Qt::DropActions ContentSelectorModel::ContentModel::supportedDropActions() const
|
|
|
|
{
|
|
|
|
return mDropActions;
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList ContentSelectorModel::ContentModel::mimeTypes() const
|
|
|
|
{
|
|
|
|
return mMimeTypes;
|
|
|
|
}
|
|
|
|
|
|
|
|
QMimeData* ContentSelectorModel::ContentModel::mimeData(const QModelIndexList& indexes) const
|
|
|
|
{
|
|
|
|
QByteArray encodedData;
|
|
|
|
|
|
|
|
for (const QModelIndex& index : indexes)
|
|
|
|
{
|
|
|
|
if (!index.isValid())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
encodedData.append(item(index.row())->encodedData());
|
|
|
|
}
|
|
|
|
|
|
|
|
QMimeData* mimeData = new QMimeData();
|
|
|
|
mimeData->setData(mMimeType, encodedData);
|
|
|
|
|
|
|
|
return mimeData;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::dropMimeData(
|
|
|
|
const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent)
|
|
|
|
{
|
|
|
|
if (action == Qt::IgnoreAction)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if (column > 0)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (!data->hasFormat(mMimeType))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
int beginRow = rowCount();
|
|
|
|
|
|
|
|
if (row != -1)
|
|
|
|
beginRow = row;
|
|
|
|
|
|
|
|
else if (parent.isValid())
|
|
|
|
beginRow = parent.row();
|
|
|
|
|
|
|
|
QByteArray encodedData = data->data(mMimeType);
|
|
|
|
QDataStream stream(&encodedData, QIODevice::ReadOnly);
|
|
|
|
|
|
|
|
while (!stream.atEnd())
|
|
|
|
{
|
|
|
|
|
|
|
|
QString value;
|
|
|
|
QStringList values;
|
|
|
|
QStringList gamefiles;
|
|
|
|
|
|
|
|
for (int i = 0; i < EsmFile::FileProperty_GameFile; ++i)
|
|
|
|
{
|
|
|
|
stream >> value;
|
|
|
|
values << value;
|
|
|
|
}
|
|
|
|
|
|
|
|
stream >> gamefiles;
|
|
|
|
|
|
|
|
insertRows(beginRow, 1);
|
|
|
|
|
|
|
|
QModelIndex idx = index(beginRow++, 0, QModelIndex());
|
|
|
|
setData(idx, QStringList() << values << gamefiles, Qt::EditRole);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::addFile(EsmFile* file)
|
|
|
|
{
|
|
|
|
beginInsertRows(QModelIndex(), mFiles.count(), mFiles.count());
|
|
|
|
mFiles.append(file);
|
|
|
|
endInsertRows();
|
|
|
|
|
|
|
|
QModelIndex idx = index(mFiles.size() - 2, 0, QModelIndex());
|
|
|
|
|
|
|
|
emit dataChanged(idx, idx);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newfiles)
|
|
|
|
{
|
|
|
|
QDir dir(path);
|
|
|
|
QStringList filters;
|
|
|
|
filters << "*.esp"
|
|
|
|
<< "*.esm"
|
|
|
|
<< "*.omwgame"
|
|
|
|
<< "*.omwaddon";
|
|
|
|
if (mShowOMWScripts)
|
|
|
|
filters << "*.omwscripts";
|
|
|
|
dir.setNameFilters(filters);
|
|
|
|
dir.setSorting(QDir::Name);
|
|
|
|
|
|
|
|
for (const QString& path2 : dir.entryList())
|
|
|
|
{
|
|
|
|
QFileInfo info(dir.absoluteFilePath(path2));
|
|
|
|
|
|
|
|
if (item(info.fileName()))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Enabled by default in system openmw.cfg; shouldn't be shown in content list.
|
|
|
|
if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive))
|
|
|
|
{
|
|
|
|
EsmFile* file = new EsmFile(path2);
|
|
|
|
file->setDate(info.lastModified());
|
|
|
|
file->setFilePath(info.absoluteFilePath());
|
|
|
|
addFile(file);
|
|
|
|
setNew(file->fileName(), newfiles);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
EsmFile* file = new EsmFile(path2);
|
|
|
|
file->setDate(info.lastModified());
|
|
|
|
file->setFilePath(info.absoluteFilePath());
|
|
|
|
std::filesystem::path filepath = Files::pathFromQString(info.absoluteFilePath());
|
|
|
|
|
|
|
|
auto stream = Files::openBinaryInputFileStream(filepath);
|
|
|
|
if (!stream->is_open())
|
|
|
|
{
|
|
|
|
qWarning() << "Failed to open addon file " << info.fileName() << ": "
|
|
|
|
<< std::generic_category().message(errno).c_str();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const ESM::Format format = ESM::readFormat(*stream);
|
|
|
|
stream->seekg(0);
|
|
|
|
switch (format)
|
|
|
|
{
|
|
|
|
case ESM::Format::Tes3:
|
|
|
|
{
|
|
|
|
ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding.toStdString()));
|
|
|
|
ESM::ESMReader fileReader;
|
|
|
|
fileReader.setEncoder(&encoder);
|
|
|
|
fileReader.open(std::move(stream), filepath);
|
|
|
|
file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str()));
|
|
|
|
file->setFormat(fileReader.getFormatVersion());
|
|
|
|
file->setDescription(QString::fromUtf8(fileReader.getDesc().c_str()));
|
|
|
|
for (const auto& master : fileReader.getGameFiles())
|
|
|
|
file->addGameFile(QString::fromUtf8(master.name.c_str()));
|
|
|
|
|
|
|
|
// HACK
|
|
|
|
// Load order constraint of Bloodmoon.esm needing Tribunal.esm is missing
|
|
|
|
// from the file supplied by Bethesda, so we have to add it ourselves
|
|
|
|
if (file->fileName().compare("Bloodmoon.esm", Qt::CaseInsensitive) == 0)
|
|
|
|
file->addGameFile(QString::fromUtf8("Tribunal.esm"));
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ESM::Format::Tes4:
|
|
|
|
{
|
|
|
|
ToUTF8::StatelessUtf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding.toStdString()));
|
|
|
|
ESM4::Reader reader(std::move(stream), filepath, nullptr, &encoder, true);
|
|
|
|
file->setAuthor(QString::fromUtf8(reader.getAuthor().c_str()));
|
|
|
|
file->setFormat(reader.esmVersion());
|
|
|
|
file->setDescription(QString::fromUtf8(reader.getDesc().c_str()));
|
|
|
|
for (const auto& master : reader.getGameFiles())
|
|
|
|
file->addGameFile(QString::fromUtf8(master.name.c_str()));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
{
|
|
|
|
qWarning() << "Error reading addon file " << info.fileName() << ": unsupported ESM format "
|
|
|
|
<< ESM::NAME(format).toString().c_str();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Put the file in the table
|
|
|
|
addFile(file);
|
|
|
|
setNew(file->fileName(), newfiles);
|
|
|
|
}
|
|
|
|
catch (std::runtime_error& e)
|
|
|
|
{
|
|
|
|
// An error occurred while reading the .esp
|
|
|
|
qWarning() << "Error reading addon file: " << e.what();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
if (filesCount > 0)
|
|
|
|
{
|
|
|
|
beginRemoveRows(QModelIndex(), 0, filesCount - 1);
|
|
|
|
mFiles.clear();
|
|
|
|
endRemoveRows();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList ContentSelectorModel::ContentModel::gameFiles() const
|
|
|
|
{
|
|
|
|
QStringList gameFiles;
|
|
|
|
for (const ContentSelectorModel::EsmFile* file : mFiles)
|
|
|
|
{
|
|
|
|
if (file->isGameFile())
|
|
|
|
{
|
|
|
|
gameFiles.append(file->fileName());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return gameFiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::setCurrentGameFile(const EsmFile* file)
|
|
|
|
{
|
|
|
|
QModelIndex oldIndex = indexFromItem(mGameFile);
|
|
|
|
QModelIndex index = indexFromItem(file);
|
|
|
|
mGameFile = file;
|
|
|
|
emit dataChanged(oldIndex, oldIndex);
|
|
|
|
emit dataChanged(index, index);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::sortFiles()
|
|
|
|
{
|
|
|
|
emit layoutAboutToBeChanged();
|
|
|
|
// Dependency sort
|
|
|
|
std::unordered_set<const EsmFile*> moved;
|
|
|
|
for (int i = mFiles.size() - 1; i > 0;)
|
|
|
|
{
|
|
|
|
const auto file = mFiles.at(i);
|
|
|
|
if (moved.find(file) == moved.end())
|
|
|
|
{
|
|
|
|
int index = -1;
|
|
|
|
for (int j = 0; j < i; ++j)
|
|
|
|
{
|
|
|
|
const QStringList& gameFiles = mFiles.at(j)->gameFiles();
|
|
|
|
// All addon files are implicitly dependent on the game file
|
|
|
|
// so that they don't accidentally become the game file
|
|
|
|
if (gameFiles.contains(file->fileName(), Qt::CaseInsensitive) || file == mGameFile)
|
|
|
|
{
|
|
|
|
index = j;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (index >= 0)
|
|
|
|
{
|
|
|
|
mFiles.move(i, index);
|
|
|
|
moved.insert(file);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
--i;
|
|
|
|
moved.clear();
|
|
|
|
}
|
|
|
|
emit layoutChanged();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const
|
|
|
|
{
|
|
|
|
const auto it = mCheckStates.find(filepath);
|
|
|
|
if (it == mCheckStates.end())
|
|
|
|
return false;
|
|
|
|
return it.value() == Qt::Checked;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::isEnabled(const QModelIndex& index) const
|
|
|
|
{
|
|
|
|
return (flags(index) & Qt::ItemIsEnabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::isNew(const QString& filepath) const
|
|
|
|
{
|
|
|
|
const auto it = mNewFiles.find(filepath);
|
|
|
|
if (it == mNewFiles.end())
|
|
|
|
return false;
|
|
|
|
return it.value();
|
|
|
|
}
|
|
|
|
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::setContentList(const QStringList& fileList)
|
|
|
|
{
|
|
|
|
mPluginsWithLoadOrderError.clear();
|
|
|
|
int previousPosition = -1;
|
|
|
|
for (const QString& filepath : fileList)
|
|
|
|
{
|
|
|
|
if (setCheckState(filepath, true))
|
|
|
|
{
|
|
|
|
// as necessary, move plug-ins in visible list to match sequence of supplied filelist
|
|
|
|
const EsmFile* file = item(filepath);
|
|
|
|
int filePosition = indexFromItem(file).row();
|
|
|
|
if (filePosition < previousPosition)
|
|
|
|
{
|
|
|
|
mFiles.move(filePosition, previousPosition);
|
|
|
|
emit dataChanged(index(filePosition, 0, QModelIndex()), index(previousPosition, 0, QModelIndex()));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
previousPosition = filePosition;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
checkForLoadOrderErrors();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::checkForLoadOrderErrors()
|
|
|
|
{
|
|
|
|
for (int row = 0; row < mFiles.count(); ++row)
|
|
|
|
{
|
|
|
|
EsmFile* file = item(row);
|
|
|
|
bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0;
|
|
|
|
if (isRowInError)
|
|
|
|
{
|
|
|
|
mPluginsWithLoadOrderError.insert(file->filePath());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
mPluginsWithLoadOrderError.remove(file->filePath());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QList<ContentSelectorModel::LoadOrderError> ContentSelectorModel::ContentModel::checkForLoadOrderErrors(
|
|
|
|
const EsmFile* file, int row) const
|
|
|
|
{
|
|
|
|
QList<LoadOrderError> errors = QList<LoadOrderError>();
|
|
|
|
for (const QString& dependentfileName : file->gameFiles())
|
|
|
|
{
|
|
|
|
const EsmFile* dependentFile = item(dependentfileName);
|
|
|
|
|
|
|
|
if (!dependentFile)
|
|
|
|
{
|
|
|
|
errors.append(LoadOrderError(LoadOrderError::ErrorCode_MissingDependency, dependentfileName));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (!isChecked(dependentFile->filePath()))
|
|
|
|
{
|
|
|
|
errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName));
|
|
|
|
}
|
|
|
|
if (row < indexFromItem(dependentFile).row())
|
|
|
|
{
|
|
|
|
errors.append(LoadOrderError(LoadOrderError::ErrorCode_LoadOrder, dependentfileName));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return errors;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ContentSelectorModel::ContentModel::toolTip(const EsmFile* file) const
|
|
|
|
{
|
|
|
|
if (isLoadOrderError(file))
|
|
|
|
{
|
|
|
|
QString text("<b>");
|
|
|
|
int index = indexFromItem(item(file->filePath())).row();
|
|
|
|
for (const LoadOrderError& error : checkForLoadOrderErrors(file, index))
|
|
|
|
{
|
|
|
|
text += "<p>";
|
|
|
|
text += error.toolTip();
|
|
|
|
text += "</p>";
|
|
|
|
}
|
|
|
|
text += ("</b>");
|
|
|
|
text += file->toolTip();
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return file->toolTip();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::refreshModel()
|
|
|
|
{
|
|
|
|
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, bool checkState)
|
|
|
|
{
|
|
|
|
if (filepath.isEmpty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const EsmFile* file = item(filepath);
|
|
|
|
|
|
|
|
if (!file)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
Qt::CheckState state = Qt::Unchecked;
|
|
|
|
|
|
|
|
if (checkState)
|
|
|
|
state = Qt::Checked;
|
|
|
|
|
|
|
|
mCheckStates[filepath] = state;
|
|
|
|
emit dataChanged(indexFromItem(item(filepath)), indexFromItem(item(filepath)));
|
|
|
|
|
|
|
|
if (file->isGameFile())
|
|
|
|
refreshModel();
|
|
|
|
|
|
|
|
// if we're checking an item, ensure all "upstream" files (dependencies) are checked as well.
|
|
|
|
if (state == Qt::Checked)
|
|
|
|
{
|
|
|
|
for (const QString& upstreamName : file->gameFiles())
|
|
|
|
{
|
|
|
|
const EsmFile* upstreamFile = item(upstreamName);
|
|
|
|
|
|
|
|
if (!upstreamFile)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (!isChecked(upstreamFile->filePath()))
|
|
|
|
mCheckStates[upstreamFile->filePath()] = Qt::Checked;
|
|
|
|
|
|
|
|
emit dataChanged(indexFromItem(upstreamFile), indexFromItem(upstreamFile));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// otherwise, if we're unchecking an item (or the file is a game file) ensure all downstream files are unchecked.
|
|
|
|
if (state == Qt::Unchecked)
|
|
|
|
{
|
|
|
|
for (const EsmFile* downstreamFile : mFiles)
|
|
|
|
{
|
|
|
|
QFileInfo fileInfo(filepath);
|
|
|
|
QString filename = fileInfo.fileName();
|
|
|
|
|
|
|
|
if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive))
|
|
|
|
{
|
|
|
|
if (mCheckStates.contains(downstreamFile->filePath()))
|
|
|
|
mCheckStates[downstreamFile->filePath()] = Qt::Unchecked;
|
|
|
|
|
|
|
|
emit dataChanged(indexFromItem(downstreamFile), indexFromItem(downstreamFile));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checkedItems() const
|
|
|
|
{
|
|
|
|
ContentFileList list;
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
// First search for game files and next addons,
|
|
|
|
// so we get more or less correct game files vs addons order.
|
|
|
|
for (EsmFile* file : mFiles)
|
|
|
|
if (isChecked(file->filePath()))
|
|
|
|
list << file;
|
|
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ContentSelectorModel::ContentModel::uncheckAll()
|
|
|
|
{
|
|
|
|
emit layoutAboutToBeChanged();
|
|
|
|
mCheckStates.clear();
|
|
|
|
emit layoutChanged();
|
|
|
|
}
|