#include "contentmodel.hpp" #include "esmfile.hpp" #include #include #include #include #include #include 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(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->isGameFile()) 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 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(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->isGameFile()) return QVariant(); return mCheckStates[file->filePath()]; } case Qt::UserRole: { if (file->isGameFile()) 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(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); continue; } try { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding.toStdString())); fileReader.setEncoder(&encoder); fileReader.open(Files::pathFromQString(dir.absoluteFilePath(path2))); EsmFile* file = new EsmFile(path2); for (std::vector::const_iterator itemIter = fileReader.getGameFiles().begin(); itemIter != fileReader.getGameFiles().end(); ++itemIter) file->addGameFile(QString::fromUtf8(itemIter->name.c_str())); file->setAuthor(QString::fromUtf8(fileReader.getAuthor().c_str())); file->setDate(info.lastModified()); file->setFormat(fileReader.getFormatVersion()); file->setFilePath(info.absoluteFilePath()); file->setDescription(QString::fromUtf8(fileReader.getDesc().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")); } // 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::sortFiles() { emit layoutAboutToBeChanged(); // Dependency sort std::unordered_set 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(); if (gameFiles.contains(file->fileName(), Qt::CaseInsensitive) || (!mFiles.at(j)->isGameFile() && gameFiles.isEmpty() && file->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files { 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::ContentModel::checkForLoadOrderErrors( const EsmFile* file, int row) const { QList errors = QList(); 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(""); int index = indexFromItem(item(file->filePath())).row(); for (const LoadOrderError& error : checkForLoadOrderErrors(file, index)) { text += "

"; text += error.toolTip(); text += "

"; } text += ("
"); 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(); }