#include "PluginListModel.h" #include "MOPlugin/Settings.h" #include "PluginListDropInfo.h" #include #include #include #include #include #include namespace BSPluginList { PluginListModel::PluginListModel(TESData::PluginList* plugins) : m_Plugins{plugins} {} QModelIndex PluginListModel::index(int row, int column, [[maybe_unused]] const QModelIndex& parent) const { if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { return QModelIndex(); } return createIndex(row, column, row); } QModelIndex PluginListModel::parent([[maybe_unused]] const QModelIndex& index) const { return QModelIndex(); } Qt::ItemFlags PluginListModel::flags(const QModelIndex& index) const { const int id = index.row(); Qt::ItemFlags result = QAbstractItemModel::flags(index); if (index.isValid()) { const auto plugin = m_Plugins->getPlugin(id); if (plugin && (!plugin->forceLoaded() && !plugin->forceDisabled())) { if (index.column() == COL_PRIORITY) result |= Qt::ItemIsEditable; result |= Qt::ItemIsUserCheckable; } result |= Qt::ItemIsDragEnabled; result &= ~Qt::ItemIsDropEnabled; } else { result |= Qt::ItemIsDropEnabled; } return result; } static QVariantList conflictListData(const TESData::PluginList* pluginList, const TESData::FileInfo* plugin, const QSet& (TESData::FileInfo::*getConflicts)() const) { if (!plugin || !plugin->enabled()) { return QVariantList(); } QVariantList list; for (const int otherId : (plugin->*getConflicts)()) { const auto other = pluginList->getPlugin(otherId); if (other && other->enabled()) { list.append(otherId); } } return list; } QVariant PluginListModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::DisplayRole: case Qt::EditRole: return displayData(index); case Qt::CheckStateRole: if (index.column() == 0) { return checkstateData(index); } break; case Qt::ForegroundRole: return foregroundData(index); case Qt::BackgroundRole: return backgroundData(index); case Qt::FontRole: return fontData(index); case Qt::TextAlignmentRole: return alignmentData(index); case Qt::ToolTipRole: return tooltipData(index); case GroupingRole: { const auto id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return plugin ? plugin->group() : QVariant(); } case IndexRole: return index.row(); case InfoRole: { const auto id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return QVariant::fromValue(plugin); } case ConflictsIconRole: return conflictData(index); case FlagsIconRole: return iconData(index); case OriginRole: { const int id = index.row(); return m_Plugins->getOriginName(id); } case OverridingRole: { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return conflictListData(m_Plugins, plugin, &TESData::FileInfo::getPluginOverriding); } case OverriddenRole: { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return conflictListData(m_Plugins, plugin, &TESData::FileInfo::getPluginOverridden); } case OverwritingAuxRole: { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return conflictListData(m_Plugins, plugin, &TESData::FileInfo::getPluginOverwritingArchive); } case OverwrittenAuxRole: { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return conflictListData(m_Plugins, plugin, &TESData::FileInfo::getPluginOverwrittenArchive); } } return QVariant(); } QVariant PluginListModel::displayData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); if (!plugin) { return QVariant(); } switch (index.column()) { case COL_NAME: return plugin->name(); case COL_PRIORITY: return plugin->priority(); case COL_MODINDEX: return plugin->index(); case COL_FORMVERSION: return plugin->formVersion() != 0 ? QString::number(plugin->formVersion()) : QString(); case COL_HEADERVERSION: return QString::number(plugin->headerVersion()); case COL_AUTHOR: return plugin->author(); case COL_DESCRIPTION: return plugin->description(); default: return QVariant(); } } QVariant PluginListModel::checkstateData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); if (!plugin) { return QVariant(); } if (plugin->isAlwaysEnabled()) { // HACK: PluginListStyledItemDelegate draws the checkbox separately return QVariant(); } else if (plugin->forceDisabled()) { return QVariant(); } else { return plugin->enabled() ? Qt::Checked : Qt::Unchecked; } } QVariant PluginListModel::foregroundData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); if (!plugin) { return QVariant(); } if (plugin->hasNoRecords()) { if (index.column() == COL_NAME) { return QBrush(Qt::gray); } } if (plugin->forceDisabled()) { if (index.column() == COL_NAME) { return QBrush(Qt::darkRed); } } return QVariant(); } QVariant PluginListModel::backgroundData([[maybe_unused]] const QModelIndex& index) const { return QVariant(); } QVariant PluginListModel::fontData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); QFont result; if (index.column() == COL_NAME) { if (plugin && plugin->hasNoRecords()) { result.setItalic(true); } } return result; } QVariant PluginListModel::alignmentData(const QModelIndex& index) const { if (index.column() == 0) { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); } else { return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } } static QString truncateString(const QString& text, int length = 1024) { QString new_text = text; if (new_text.length() > length) { new_text.truncate(length); new_text += "..."; } return new_text; } static QString makeLootTooltip(const MOTools::Loot::Plugin&) { // LOOT integration removed for Fluorine port — tooltip never built. return {}; } QVariant PluginListModel::tooltipData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); const auto lootInfo = m_Plugins->getLootReport(plugin->name()); if (!plugin) { return QVariant(); } switch (index.column()) { case COL_NAME: { QString toolTip; toolTip += "" + tr("Origin") + ": " + m_Plugins->getOriginName(id); if (plugin->forceLoaded()) { toolTip += "
" + tr("This plugin can't be disabled or moved (enforced by the game).") + ""; } else if (plugin->forceEnabled()) { toolTip += "
" + tr("This plugin can't be disabled (enforced by the game).") + ""; } if (plugin->formVersion() != 0) { // Oblivion-style plugin headers don't have a form version toolTip += "
" + tr("Form Version") + ": " + QString::number(plugin->formVersion()); } toolTip += "
" + tr("Header Version") + ": " + QString::number(plugin->headerVersion()); if (!plugin->author().isEmpty()) { toolTip += "
" + tr("Author") + ": " + truncateString(plugin->author()); } if (plugin->description().size() > 0) { toolTip += "
" + tr("Description") + ": " + truncateString(plugin->description()); } if (plugin->enabled() && plugin->missingMasters().size() > 0) { toolTip += "
" + tr("Missing Masters") + ": " + "" + truncateString(QStringList(plugin->missingMasters().begin(), plugin->missingMasters().end()) .join(", ")) + ""; } QStringList enabledMasters; std::ranges::remove_copy_if(plugin->masters(), std::back_inserter(enabledMasters), [&](auto&& master) { return plugin->missingMasters().contains(master); }); if (!enabledMasters.empty()) { toolTip += "
" + tr("Enabled Masters") + ": " + truncateString(enabledMasters.join(", ")); } if (!plugin->archives().empty()) { QString archiveString = plugin->archives().size() < 6 ? truncateString( QStringList(plugin->archives().begin(), plugin->archives().end()) .join(", ")) : ""; toolTip += "
" + tr("Loads Archives") + ": " + archiveString; } if (plugin->hasIni()) { toolTip += "
" + tr("Loads INI settings") + ": " + QFileInfo(plugin->name()).baseName() + ".ini"; } if (plugin->hasNoRecords()) { toolTip += "

" + tr("This is a dummy plugin. It contains no records and is " "typically used to load a paired archive file."); } return toolTip; } case COL_CONFLICTS: { const uint conflictFlags = data(index, ConflictsIconRole).toUInt(); using enum TESData::FileInfo::EConflictFlag; QString toolTip; if ((conflictFlags & CONFLICT_MIXED) == CONFLICT_MIXED) { toolTip += tr("Overrides & has overridden records"); } else if (conflictFlags & CONFLICT_OVERRIDE) { toolTip += tr("Overrides records"); } else if (conflictFlags & CONFLICT_OVERRIDDEN) { toolTip += tr("Has overridden records"); } if ((conflictFlags & CONFLICT_MIXED) && (conflictFlags & CONFLICT_ARCHIVE_MIXED)) { toolTip += "
"; } if ((conflictFlags & CONFLICT_ARCHIVE_MIXED) == CONFLICT_ARCHIVE_MIXED) { toolTip += tr("Overwrites & has overwritten archive files"); } else if (conflictFlags & CONFLICT_ARCHIVE_OVERWRITE) { toolTip += tr("Overwrites another archive file"); } else if (conflictFlags & CONFLICT_ARCHIVE_OVERWRITTEN) { toolTip += tr("Overwritten by another archive file"); } return toolTip; } case COL_FLAGS: { // HACK: insert some HTML to enable multiline tooltips QString toolTip = ""; const QString spacing = "

"; if (plugin->enabled() && plugin->missingMasters().size() > 0) { toolTip += "" + tr("Missing Masters") + ": " + "" + truncateString(QStringList(plugin->missingMasters().begin(), plugin->missingMasters().end()) .join(", ")) + "" + spacing; } if (plugin->hasIni()) { toolTip += tr("There is an ini file connected to this plugin. Its settings will " "be added to your game settings, overwriting in case of conflicts.") + "

"; } if (!plugin->archives().empty()) { toolTip += tr("There are Archives connected to this plugin. Their assets will be " "added to your game, overwriting in case of conflicts following the " "plugin order. Loose files will always overwrite assets from " "Archives.") + spacing; } if (plugin->isMasterFile()) { toolTip += tr("This file is flagged as a master plugin (ESM). It will load " "before any non-ESM " "files in the load order.") + spacing; } if (plugin->isSmallFile()) { toolTip += tr("This file is flagged as a light plugin (ESL). It will adhere to its " "position in " "the load order but the records will be loaded in ESL space (FE/FF). You " "can have up to 4096 light plugins in addition to other plugin types.") + spacing; } else if (plugin->isMediumFile()) { toolTip += tr("This file is flagged as a medium plugin (ESH). It will adhere to " "its position in the load order but the records will be loaded in " "ESH space (FD). You can have 256 medium plugins in addition to " "other plugin types.") + spacing; } if (plugin->isBlueprintFile()) { toolTip += tr("This plugin has the blueprint flag. This forces it to load after " "every other non-blueprint plugin. Blueprint plugins will adhere " "to standard load order rules with other blueprint plugins.") + spacing; } if (plugin->isLightFlagged() && plugin->isMediumFlagged()) { toolTip += tr("WARNING: This plugin is both light and medium flagged. This could " "indicate that the file was saved improperly and may have " "mismatched record references. Use it at your own risk.") + spacing; } if (plugin->forceDisabled()) { toolTip += tr("This game does not currently permit custom plugin " "loading. There may be manual workarounds."); } if (toolTip.endsWith(spacing)) { toolTip.chop(spacing.length()); } if (lootInfo) { const auto lootToolTip = makeLootTooltip(*lootInfo); if (toolTip.length() > 7 && !lootToolTip.isEmpty()) { toolTip += "
"; } toolTip += lootToolTip; } return toolTip; } default: return QVariant(); } } QVariant PluginListModel::conflictData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); return plugin->conflictState(); } static bool isProblematic(const TESData::FileInfo* plugin, const MOTools::Loot::Plugin* lootInfo) { if (plugin && plugin->enabled() && plugin->hasMissingMasters()) { return true; } if (lootInfo && Settings::instance()->lootShowProblems()) { if (!lootInfo->incompatibilities.empty()) { return true; } if (!lootInfo->missingMasters.empty()) { return true; } } return false; } QVariant PluginListModel::iconData(const QModelIndex& index) const { const int id = index.row(); const auto plugin = m_Plugins->getPlugin(id); const auto lootInfo = m_Plugins->getLootReport(plugin->name()); if (!plugin) { return QVariant(); } using enum TESData::FileInfo::EFlag; uint flag = 0; if (isProblematic(plugin, lootInfo)) { flag |= FLAG_PROBLEMATIC; } if (lootInfo && !lootInfo->messages.empty() && Settings::instance()->lootShowMessages()) { flag |= FLAG_INFORMATION; } if (plugin->hasIni()) { flag |= FLAG_INI; } if (!plugin->archives().empty()) { flag |= FLAG_BSA; } if (plugin->isMasterFile()) { flag |= FLAG_MASTER; } if (plugin->isMediumFile()) { flag |= FLAG_MEDIUM; } if (plugin->isSmallFile()) { flag |= FLAG_LIGHT; } if (plugin->isBlueprintFile()) { flag |= FLAG_BLUEPRINT; } if (lootInfo && !lootInfo->dirty.empty() && Settings::instance()->lootShowDirty()) { flag |= FLAG_CLEAN; } return flag; } QVariant PluginListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { switch (section) { case COL_NAME: return tr("Name"); case COL_CONFLICTS: return tr("Conflicts"); case COL_FLAGS: return tr("Flags"); case COL_PRIORITY: return tr("Priority"); case COL_MODINDEX: return tr("Mod Index"); case COL_FORMVERSION: return tr("Form Version"); case COL_HEADERVERSION: return tr("Header Version"); case COL_AUTHOR: return tr("Author"); case COL_DESCRIPTION: return tr("Description"); default: return tr("unknown"); } } } return QAbstractItemModel::headerData(section, orientation, role); } int PluginListModel::rowCount([[maybe_unused]] const QModelIndex& parent) const { return m_Plugins->pluginCount(); } int PluginListModel::columnCount([[maybe_unused]] const QModelIndex& parent) const { return COL_COUNT; } bool PluginListModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (role == Qt::CheckStateRole) { const int id = index.row(); m_Plugins->setEnabled(id, value.toInt() == Qt::Checked); emit dataChanged(this->index(0, 0), this->index(rowCount() - 1, COL_MODINDEX), {Qt::EditRole, Qt::CheckStateRole}); emit pluginStatesChanged({index}); return true; } else if (role == Qt::EditRole) { if (index.column() == COL_PRIORITY) { bool ok; const int newPriority = value.toInt(&ok); if (ok) { int destination = newPriority; if (newPriority > index.data(Qt::EditRole).toInt()) { ++destination; } m_Plugins->moveToPriority({index.row()}, destination); emit dataChanged(this->index(0, 0), this->index(rowCount() - 1, columnCount() - 1), {Qt::EditRole, GroupingRole}); emit pluginOrderChanged(); return true; } } } return false; } Qt::DropActions PluginListModel::supportedDropActions() const { return Qt::MoveAction; } bool PluginListModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, [[maybe_unused]] int column, const QModelIndex& parent) const { if (action == Qt::IgnoreAction) { return true; } if (action != Qt::MoveAction) { return false; } PluginListDropInfo dropInfo{data, row, parent, m_Plugins}; return m_Plugins->canMoveToPriority(dropInfo.sourceRows(), dropInfo.destination()); } bool PluginListModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, [[maybe_unused]] int column, const QModelIndex& parent) { if (action == Qt::IgnoreAction) { return true; } if (action != Qt::MoveAction) { return false; } PluginListDropInfo dropInfo{data, row, parent, m_Plugins}; m_Plugins->moveToPriority(dropInfo.sourceRows(), dropInfo.destination()); emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {Qt::DisplayRole, GroupingRole}); emit pluginOrderChanged(); return true; } QStringList PluginListModel::groups(std::function pred) const { boost::container::flat_set groupSet; QStringList groups; QString lastGroup; for (int priority = 0, count = m_Plugins->pluginCount(); priority < count; ++priority) { const auto plugin = m_Plugins->getPluginByPriority(priority); if (pred && !pred(plugin)) { continue; } const auto& group = plugin ? plugin->group() : QString(); if (group.isEmpty() || group == lastGroup) { continue; } auto [it, inserted] = groupSet.insert(group); if (inserted) { groups.append(group); } lastGroup = group; } return groups; } QStringList PluginListModel::masterGroups() const { return groups([](auto&& plugin) { return !plugin->forceLoaded() && plugin->isMasterFile(); }); } QStringList PluginListModel::regularGroups() const { return groups([](auto&& plugin) { return !plugin->forceLoaded() && !plugin->isMasterFile(); }); } void PluginListModel::refresh() { emit beginResetModel(); m_Plugins->refresh(); emit endResetModel(); } void PluginListModel::invalidate() { emit beginResetModel(); m_Plugins->refresh(true); emit endResetModel(); } void PluginListModel::invalidateConflicts() { for (int i = 0, count = m_Plugins->pluginCount(); i < count; ++i) { const auto plugin = m_Plugins->getPlugin(i); plugin->invalidateConflicts(); } emit dataChanged(index(0, COL_CONFLICTS), index(rowCount() - 1, COL_CONFLICTS), {PluginListModel::ConflictsIconRole}); } void PluginListModel::movePlugin(const QString& name, [[maybe_unused]] int oldPriority, int newPriority) { m_Plugins->setPriority(name, newPriority); emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {Qt::DisplayRole, GroupingRole}); emit pluginOrderChanged(); } void PluginListModel::changePluginStates( const std::map& infos) { QModelIndexList indices; for (auto& [name, state] : infos) { m_Plugins->setState(name, state); const auto idx = m_Plugins->getIndex(name); if (idx != -1) { indices.append(index(idx, 0)); } } emit dataChanged(index(0, 0), index(rowCount() - 1, COL_MODINDEX), {Qt::DisplayRole, Qt::CheckStateRole}); emit pluginStatesChanged(indices); } void PluginListModel::setEnabledAll(bool enabled) { QModelIndexList indices; indices.reserve(rowCount()); std::generate_n(std::back_inserter(indices), rowCount(), [this, i = 0]() mutable { return index(i++, 0); }); setEnabled(indices, enabled); } void PluginListModel::setEnabled(const QModelIndexList& indices, bool enabled) { if (indices.empty()) { return; } std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->setEnabled(std::move(ids), enabled); emit pluginStatesChanged(indices); } void PluginListModel::sendToPriority(const QModelIndexList& indices, int priority, bool disjoint) { if (indices.empty()) { return; } std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->moveToPriority(std::move(ids), priority, disjoint); emit dataChanged(index(0, COL_PRIORITY), index(rowCount() - 1, COL_MODINDEX), {Qt::DisplayRole}); emit pluginOrderChanged(); } void PluginListModel::shiftPluginsPriority(const QModelIndexList& indices, int offset) { if (indices.empty()) { return; } std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->shiftPriority(std::move(ids), offset); emit dataChanged(index(0, COL_PRIORITY), index(rowCount() - 1, COL_MODINDEX), {Qt::DisplayRole}); emit pluginOrderChanged(); } void PluginListModel::toggleState(const QModelIndexList& indices) { if (indices.empty()) { return; } std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->toggleState(std::move(ids)); emit pluginStatesChanged(indices); } void PluginListModel::setGroup(const QModelIndexList& indices, const QString& group) { if (indices.empty()) { return; } std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->setGroup(std::move(ids), group); emit dataChanged(this->index(0, 0), this->index(rowCount() - 1, COL_MODINDEX), {GroupingRole}); } void PluginListModel::sendToGroup(const QModelIndexList& indices, const QString& group, bool isESM) { int destination = -1; for (int priority = 0, count = m_Plugins->pluginCount(); priority < count; ++priority) { const auto plugin = m_Plugins->getPluginByPriority(priority); if (plugin && plugin->isMasterFile() == isESM && plugin->group() == group) { destination = priority + 1; } } if (destination == -1) return; std::vector ids; ids.reserve(indices.size()); std::ranges::transform(indices, std::back_inserter(ids), [](const QModelIndex& idx) { return idx.row(); }); m_Plugins->setGroup(ids, group); m_Plugins->moveToPriority(std::move(ids), destination); emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {Qt::DisplayRole, GroupingRole}); emit pluginOrderChanged(); } } // namespace BSPluginList