/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "pluginlist.h" #include "settings.h" #include "scopeguard.h" #include "modinfo.h" #include "modlist.h" #include "viewmarkingscrollbar.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace MOShared; static bool ByName(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) { return LHS.name.toUpper() < RHS.name.toUpper(); } static bool ByPriority(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) { if (LHS.isMaster && !RHS.isMaster) { return true; } else if (!LHS.isMaster && RHS.isMaster) { return false; } else { return LHS.priority < RHS.priority; } } static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) { return QFileInfo(LHS.fullPath).lastModified() < QFileInfo(RHS.fullPath).lastModified(); } static QString TruncateString(const QString& text) { QString new_text = text; if (new_text.length() > 1024) { new_text.truncate(1024); new_text += "..."; } return new_text; } PluginList::PluginList(QObject *parent) : QAbstractItemModel(parent) , m_FontMetrics(QFont()) { connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes())); m_LastCheck.start(); } PluginList::~PluginList() { m_Refreshed.disconnect_all_slots(); m_PluginMoved.disconnect_all_slots(); m_PluginStateChanged.disconnect_all_slots(); } QString PluginList::getColumnName(int column) { switch (column) { case COL_NAME: return tr("Name"); case COL_PRIORITY: return tr("Priority"); case COL_MODINDEX: return tr("Mod Index"); case COL_FLAGS: return tr("Flags"); default: return tr("unknown"); } } QString PluginList::getColumnToolTip(int column) { switch (column) { case COL_NAME: return tr("Name of the plugin"); case COL_FLAGS: return tr("Emblems to highlight things that might require attention."); case COL_PRIORITY: return tr("Load priority of plugins. The higher, the more \"important\" it is and thus " "overwrites data from plugins with lower priority."); case COL_MODINDEX: return tr("Determines the formids of objects originating from this mods."); default: return tr("unknown"); } } void PluginList::highlightPlugins(const QItemSelectionModel *selection, const MOShared::DirectoryEntry &directoryEntry, const Profile &profile) { for (auto &esp : m_ESPs) { esp.modSelected = false; } for (QModelIndex idx : selection->selectedRows(ModList::COL_PRIORITY)) { int modIndex = idx.data(Qt::UserRole + 1).toInt(); if (modIndex == UINT_MAX) continue; ModInfo::Ptr selectedMod = ModInfo::getByIndex(modIndex); if (!selectedMod.isNull() && profile.modEnabled(modIndex)) { QDir dir(selectedMod->absolutePath()); QStringList plugins = dir.entryList(QStringList() << "*.esp" << "*.esm" << "*.esl"); MOShared::FilesOrigin origin = directoryEntry.getOriginByName(selectedMod->internalName().toStdWString()); if (plugins.size() > 0) { for (auto plugin : plugins) { MOShared::FileEntry::Ptr file = directoryEntry.findFile(plugin.toStdWString()); if (file && file->getOrigin() != origin.getID()) { const std::vector>> alternatives = file->getAlternatives(); if (std::find_if(alternatives.begin(), alternatives.end(), [&](const std::pair>& element) { return element.first == origin.getID(); }) == alternatives.end()) continue; } std::map::iterator iter = m_ESPsByName.find(plugin.toLower()); if (iter != m_ESPsByName.end()) { m_ESPs[iter->second].modSelected = true; } } } } } emit dataChanged(this->index(0, 0), this->index(static_cast(m_ESPs.size()) - 1, this->columnCount() - 1)); } void PluginList::refresh(const QString &profileName , const DirectoryEntry &baseDirectory , const QString &lockedOrderFile , bool force) { if (force) { m_ESPs.clear(); m_ESPsByName.clear(); m_ESPsByPriority.clear(); } ChangeBracket layoutChange(this); QStringList primaryPlugins = m_GamePlugin->primaryPlugins(); bool lightPluginsAreSupported = m_GamePlugin->feature()->lightPluginsAreSupported(); m_CurrentProfile = profileName; QStringList availablePlugins; std::vector files = baseDirectory.getFiles(); for (FileEntry::Ptr current : files) { if (current.get() == nullptr) { continue; } QString filename = ToQString(current->getName()); QString extension = filename.right(3).toLower(); if ((extension == "esp") || (extension == "esm") || (extension == "esl")) { availablePlugins.append(filename.toLower()); if (m_ESPsByName.find(filename.toLower()) != m_ESPsByName.end()) { continue; } bool forceEnabled = Settings::instance().game().forceEnableCoreFiles() && primaryPlugins.contains(filename, Qt::CaseInsensitive); //(std::find(primaryPlugins.begin(), primaryPlugins.end(), filename.toLower()) != primaryPlugins.end()); bool archive = false; try { FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive)); //name without extension QString baseName = QFileInfo(filename).baseName(); QString iniPath = baseName + ".ini"; bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr; std::set loadedArchives; QString candidateName; for (FileEntry::Ptr archiveCandidate : files) { candidateName = ToQString(archiveCandidate->getName()); if (candidateName.startsWith(baseName, Qt::CaseInsensitive) && (candidateName.endsWith(".bsa", Qt::CaseInsensitive) || candidateName.endsWith(".ba2", Qt::CaseInsensitive))) { loadedArchives.insert(candidateName); } } QString originName = ToQString(origin.getName()); unsigned int modIndex = ModInfo::getIndex(originName); if (modIndex != UINT_MAX) { ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); originName = modInfo->name(); } m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives, lightPluginsAreSupported)); m_ESPs.rbegin()->priority = -1; } catch (const std::exception &e) { reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what())); } } } for (const auto &espName : m_ESPsByName) { if (!availablePlugins.contains(espName.first)) { m_ESPs[espName.second].name = ""; } } m_ESPs.erase(std::remove_if(m_ESPs.begin(), m_ESPs.end(), [](const ESPInfo &info) -> bool { return info.name.isEmpty(); }), m_ESPs.end()); fixPriorities(); // functions in GamePlugins will use the IPluginList interface of this, so // indices need to work. priority will be off however updateIndices(); GamePlugins *gamePlugins = m_GamePlugin->feature(); gamePlugins->readPluginLists(this); testMasters(); updateIndices(); readLockedOrderFrom(lockedOrderFile); layoutChange.finish(); refreshLoadOrder(); emit dataChanged(this->index(0, 0), this->index(static_cast(m_ESPs.size()), columnCount())); m_Refreshed(); } void PluginList::fixPriorities() { std::vector> espPrios; for (int i = 0; i < m_ESPs.size(); ++i) { int prio = m_ESPs[i].priority; if (prio == -1) { prio = INT_MAX; } espPrios.push_back(std::make_pair(prio, i)); } std::sort(espPrios.begin(), espPrios.end(), [](const std::pair &lhs, const std::pair &rhs) { return lhs.first < rhs.first; }); for (int i = 0; i < espPrios.size(); ++i) { m_ESPs[espPrios[i].second].priority = i; } } void PluginList::enableESP(const QString &name, bool enable) { std::map::iterator iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { m_ESPs[iter->second].enabled = enable | m_ESPs[iter->second].forceEnabled; emit writePluginsList(); } else { reportError(tr("Plugin not found: %1").arg(qUtf8Printable(name))); } } int PluginList::findPluginByPriority(int priority) { for (int i = 0; i < m_ESPs.size(); i++ ) { if (m_ESPs[i].priority == priority) { return i; } } log::error("No plugin with priority {}", priority); return -1; } void PluginList::enableSelected(const QItemSelectionModel *selectionModel) { if (selectionModel->hasSelection()) { bool dirty = false; for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { int rowIndex = findPluginByPriority(row.data().toInt()); if (!m_ESPs[rowIndex].enabled) { m_ESPs[rowIndex].enabled = true; dirty = true; } } if (dirty) emit writePluginsList(); } } void PluginList::disableSelected(const QItemSelectionModel *selectionModel) { if (selectionModel->hasSelection()) { bool dirty = false; for (auto row : selectionModel->selectedRows(COL_PRIORITY)) { int rowIndex = findPluginByPriority(row.data().toInt()); if (!m_ESPs[rowIndex].forceEnabled && m_ESPs[rowIndex].enabled) { m_ESPs[rowIndex].enabled = false; dirty = true; } } if (dirty) emit writePluginsList(); } } void PluginList::enableAll() { if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really enable all plugins?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { for (ESPInfo &info : m_ESPs) { info.enabled = true; } emit writePluginsList(); } } void PluginList::disableAll() { if (QMessageBox::question(nullptr, tr("Confirm"), tr("Really disable all plugins?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { for (ESPInfo &info : m_ESPs) { if (!info.forceEnabled) { info.enabled = false; } } emit writePluginsList(); } } void PluginList::sendToPriority(const QItemSelectionModel *selectionModel, int newPriority) { if (selectionModel->hasSelection()) { std::vector pluginsToMove; for (auto row: selectionModel->selectedRows(COL_PRIORITY)) { int rowIndex = findPluginByPriority(row.data().toInt()); if (!m_ESPs[rowIndex].forceEnabled) { pluginsToMove.push_back(rowIndex); } } if (pluginsToMove.size()) { changePluginPriority(pluginsToMove, newPriority); } } } bool PluginList::isEnabled(const QString &name) { std::map::iterator iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { return m_ESPs[iter->second].enabled; } else { return false; } } void PluginList::clearInformation(const QString &name) { std::map::iterator iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { m_AdditionalInfo[name.toLower()].messages.clear(); } } void PluginList::clearAdditionalInformation() { m_AdditionalInfo.clear(); } void PluginList::addInformation(const QString &name, const QString &message) { std::map::iterator iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { m_AdditionalInfo[name.toLower()].messages.append(message); } else { log::warn("failed to associate message for \"{}\"", name); } } void PluginList::addLootReport(const QString& name, Loot::Plugin plugin) { auto iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { m_AdditionalInfo[name.toLower()].loot = std::move(plugin); } else { log::warn("failed to associate loot report for \"{}\"", name); } } bool PluginList::isEnabled(int index) { return m_ESPs.at(index).enabled; } void PluginList::readLockedOrderFrom(const QString &fileName) { m_LockedOrder.clear(); QFile file(fileName); if (!file.exists()) { // no locked load order, that's ok return; } file.open(QIODevice::ReadOnly); int lineNumber = 0; while (!file.atEnd()) { ++lineNumber; QByteArray line = file.readLine(); if ((line.size() > 0) && (line.at(0) != '#')) { QList fields = line.split('|'); if (fields.count() == 2) { int priority = fields.at(1).trimmed().toInt(); QString name = QString::fromUtf8(fields.at(0)); // Avoid locking a force-enabled plugin if (!m_ESPs[m_ESPsByName.at(name)].forceEnabled) { // Is this an open and unclaimed priority? if (m_ESPs[m_ESPsByPriority.at(priority)].forceEnabled || std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair &a) { return a.second == priority; }) != m_LockedOrder.end()) { // Attempt to find a priority but step over force-enabled plugins and already-set locks int calcPriority = priority; do { ++calcPriority; } while (calcPriority < m_ESPsByPriority.size() || (m_ESPs[m_ESPsByPriority.at(calcPriority)].forceEnabled && std::find_if(m_LockedOrder.begin(), m_LockedOrder.end(), [&](const std::pair &a) { return a.second == calcPriority; }) != m_LockedOrder.end())); // If we have a match, we can reassign the priority... if (calcPriority < m_ESPsByPriority.size()) m_LockedOrder[name] = calcPriority; } else { m_LockedOrder[name] = priority; } } } else { log::error("locked order file: invalid line #{} '{}'", lineNumber, QString::fromUtf8(line)); reportError(tr("The file containing locked plugin indices is broken")); break; } } } file.close(); } void PluginList::writeLockedOrder(const QString &fileName) const { SafeWriteFile file(fileName); file->resize(0); file->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8()); for (auto iter = m_LockedOrder.begin(); iter != m_LockedOrder.end(); ++iter) { file->write(QString("%1|%2\r\n").arg(iter->first).arg(iter->second).toUtf8()); } file.commit(); } void PluginList::saveTo(const QString &lockedOrderFileName , const QString& deleterFileName , bool hideUnchecked) const { GamePlugins *gamePlugins = m_GamePlugin->feature(); gamePlugins->writePluginLists(this); writeLockedOrder(lockedOrderFileName); if (hideUnchecked) { SafeWriteFile deleterFile(deleterFileName); deleterFile->write(QString("# This file was automatically generated by Mod Organizer.\r\n").toUtf8()); for (size_t i = 0; i < m_ESPs.size(); ++i) { int priority = m_ESPsByPriority[i]; if (!m_ESPs[priority].enabled) { deleterFile->write(m_ESPs[priority].name.toUtf8()); deleterFile->write("\r\n"); } } deleterFile.commitIfDifferent(m_LastSaveHash[deleterFileName]); } else if (QFile::exists(deleterFileName)) { shellDelete(QStringList() << deleterFileName); } } bool PluginList::saveLoadOrder(DirectoryEntry &directoryStructure) { if (m_GamePlugin->loadOrderMechanism() != IPluginGame::LoadOrderMechanism::FileTime) { // nothing to do return true; } log::debug("setting file times on esps"); for (ESPInfo &esp : m_ESPs) { std::wstring espName = ToWString(esp.name); const FileEntry::Ptr fileEntry = directoryStructure.findFile(espName); if (fileEntry.get() != nullptr) { QString fileName; bool archive = false; int originid = fileEntry->getOrigin(archive); fileName = QString("%1\\%2") .arg(QDir::toNativeSeparators(ToQString(directoryStructure.getOriginByID(originid).getPath()))) .arg(esp.name); HANDLE file = ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { if (::GetLastError() == ERROR_SHARING_VIOLATION) { // file is locked, probably the game is running return false; } else { throw windows_error(QObject::tr("failed to access %1").arg(fileName).toUtf8().constData()); } } ULONGLONG temp = 0; temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL; FILETIME newWriteTime; newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF); newWriteTime.dwHighDateTime = (DWORD)(temp >> 32); esp.time = newWriteTime; fileEntry->setFileTime(newWriteTime); if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) { throw windows_error(QObject::tr("failed to set file time %1").arg(fileName).toUtf8().constData()); } CloseHandle(file); } } return true; } int PluginList::enabledCount() const { int enabled = 0; for (const auto &info : m_ESPs) { if (info.enabled) { ++enabled; } } return enabled; } QString PluginList::getIndexPriority(int index) const { return m_ESPs[index].index; } bool PluginList::isESPLocked(int index) const { return m_LockedOrder.find(m_ESPs.at(index).name.toLower()) != m_LockedOrder.end(); } void PluginList::lockESPIndex(int index, bool lock) { if (lock) { if (!m_ESPs.at(index).forceEnabled) m_LockedOrder[getName(index).toLower()] = m_ESPs.at(index).loadOrder; else return; } else { auto iter = m_LockedOrder.find(getName(index).toLower()); if (iter != m_LockedOrder.end()) { m_LockedOrder.erase(iter); } } emit writePluginsList(); } void PluginList::syncLoadOrder() { int loadOrder = 0; for (unsigned int i = 0; i < m_ESPs.size(); ++i) { int index = m_ESPsByPriority[i]; if (m_ESPs[index].enabled) { m_ESPs[index].loadOrder = loadOrder++; } else { m_ESPs[index].loadOrder = -1; } } } void PluginList::refreshLoadOrder() { ChangeBracket layoutChange(this); syncLoadOrder(); // set priorities according to locked load order std::map lockedLoadOrder; std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(), [&lockedLoadOrder] (const std::pair &ele) { lockedLoadOrder[ele.second] = ele.first; }); int targetPrio = 0; bool savePluginsList = false; // this is guaranteed to iterate from lowest key (load order) to highest for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) { auto nameIter = m_ESPsByName.find(iter->second.toLower()); if (nameIter != m_ESPsByName.end()) { // locked esp exists // find the location to insert at while ((targetPrio < static_cast(m_ESPs.size() - 1)) && (m_ESPs[m_ESPsByPriority[targetPrio]].loadOrder < iter->first)) { ++targetPrio; } if (static_cast(targetPrio) >= m_ESPs.size()) { continue; } int temp = targetPrio; int index = nameIter->second; if (m_ESPs[index].priority != temp) { setPluginPriority(index, temp); m_ESPs[index].loadOrder = iter->first; syncLoadOrder(); savePluginsList = true; } } } if (savePluginsList) { emit writePluginsList(); } } void PluginList::disconnectSlots() { m_PluginMoved.disconnect_all_slots(); m_Refreshed.disconnect_all_slots(); m_PluginStateChanged.disconnect_all_slots(); } int PluginList::timeElapsedSinceLastChecked() const { return m_LastCheck.elapsed(); } QStringList PluginList::pluginNames() const { QStringList result; for (const ESPInfo &info : m_ESPs) { result.append(info.name); } return result; } IPluginList::PluginStates PluginList::state(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return IPluginList::STATE_MISSING; } else { return m_ESPs[iter->second].enabled ? IPluginList::STATE_ACTIVE : IPluginList::STATE_INACTIVE; } } void PluginList::setState(const QString &name, PluginStates state) { auto iter = m_ESPsByName.find(name.toLower()); if (iter != m_ESPsByName.end()) { m_ESPs[iter->second].enabled = (state == IPluginList::STATE_ACTIVE) || m_ESPs[iter->second].forceEnabled; } else { log::warn("Plugin not found: {}", name); } } void PluginList::setLoadOrder(const QStringList &pluginList) { for (ESPInfo &info : m_ESPs) { info.priority = -1; } int maxPriority = 0; for (const QString &plugin : pluginList) { auto iter = m_ESPsByName.find(plugin.toLower()); if (iter !=m_ESPsByName.end()) { m_ESPs[iter->second].priority = maxPriority++; } } // use old priorities for (ESPInfo &info : m_ESPs) { if (info.priority == -1) { info.priority = maxPriority++; } } updateIndices(); } int PluginList::priority(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return -1; } else { return m_ESPs[iter->second].priority; } } int PluginList::loadOrder(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return -1; } else { return m_ESPs[iter->second].loadOrder; } } bool PluginList::isMaster(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return false; } else { return m_ESPs[iter->second].isMaster; } } bool PluginList::isLight(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return false; } else { return m_ESPs[iter->second].isLight; } } bool PluginList::isLightFlagged(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return false; } else { return m_ESPs[iter->second].isLightFlagged; } } QStringList PluginList::masters(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return QStringList(); } else { QStringList result; for (const QString &master : m_ESPs[iter->second].masters) { result.append(master); } return result; } } QString PluginList::origin(const QString &name) const { auto iter = m_ESPsByName.find(name.toLower()); if (iter == m_ESPsByName.end()) { return QString(); } else { return m_ESPs[iter->second].originName; } } bool PluginList::onPluginStateChanged(const std::function &func) { auto conn = m_PluginStateChanged.connect(func); return conn.connected(); } bool PluginList::onRefreshed(const std::function &callback) { auto conn = m_Refreshed.connect(callback); return conn.connected(); } bool PluginList::onPluginMoved(const std::function &func) { auto conn = m_PluginMoved.connect(func); return conn.connected(); } void PluginList::updateIndices() { m_ESPsByName.clear(); m_ESPsByPriority.clear(); m_ESPsByPriority.resize(m_ESPs.size()); for (unsigned int i = 0; i < m_ESPs.size(); ++i) { if (m_ESPs[i].priority < 0) { continue; } if (m_ESPs[i].priority >= static_cast(m_ESPs.size())) { log::error("invalid plugin priority: {}", m_ESPs[i].priority); continue; } m_ESPsByName[m_ESPs[i].name.toLower()] = i; m_ESPsByPriority.at(static_cast(m_ESPs[i].priority)) = i; } generatePluginIndexes(); } void PluginList::generatePluginIndexes() { int numESLs = 0; int numSkipped = 0; bool lightPluginsSupported = m_GamePlugin->feature()->lightPluginsAreSupported(); for (int l = 0; l < m_ESPs.size(); ++l) { int i = m_ESPsByPriority.at(l); if (!m_ESPs[i].enabled) { m_ESPs[i].index = QString(); ++numSkipped; continue; } if (lightPluginsSupported && (m_ESPs[i].isLight || m_ESPs[i].isLightFlagged)) { int ESLpos = 254 + ((numESLs + 1) / 4096); m_ESPs[i].index = QString("%1:%2").arg(ESLpos, 2, 16, QChar('0')).arg((numESLs) % 4096, 3, 16, QChar('0')).toUpper(); ++numESLs; } else { m_ESPs[i].index = QString("%1").arg(l - numESLs - numSkipped, 2, 16, QChar('0')).toUpper(); } } emit esplist_changed(); } int PluginList::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return static_cast(m_ESPs.size()); } else { return 0; } } int PluginList::columnCount(const QModelIndex &) const { return COL_LASTCOLUMN + 1; } void PluginList::testMasters() { std::set enabledMasters; for (const auto& iter: m_ESPs) { if (iter.enabled) { enabledMasters.insert(iter.name.toLower()); } } for (auto& iter: m_ESPs) { iter.masterUnset.clear(); if (iter.enabled) { for (const auto& master: iter.masters) { if (enabledMasters.find(master.toLower()) == enabledMasters.end()) { iter.masterUnset.insert(master); } } } } } QVariant PluginList::data(const QModelIndex &modelIndex, int role) const { int index = modelIndex.row(); if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) { return displayData(modelIndex); } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) { return checkstateData(modelIndex); } else if (role == Qt::ForegroundRole) { return foregroundData(modelIndex); } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { return backgroundData(modelIndex); } else if (role == Qt::FontRole) { return fontData(modelIndex); } else if (role == Qt::TextAlignmentRole) { return alignmentData(modelIndex); } else if (role == Qt::ToolTipRole) { return tooltipData(modelIndex); } else if (role == Qt::UserRole + 1) { return iconData(modelIndex); } return QVariant(); } QVariant PluginList::displayData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); switch (modelIndex.column()) { case COL_NAME: return m_ESPs[index].name; case COL_PRIORITY: return m_ESPs[index].priority; case COL_MODINDEX: return m_ESPs[index].index; default: return {}; } } QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); if (m_ESPs[index].forceEnabled) { return {}; } return m_ESPs[index].enabled ? Qt::Checked : Qt::Unchecked; } QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); if ((modelIndex.column() == COL_NAME) && m_ESPs[index].forceEnabled) { return QBrush(Qt::gray); } return {}; } QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); if (m_ESPs[index].modSelected) { return Settings::instance().colors().pluginListContained(); } return {}; } QVariant PluginList::fontData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); QFont result; if (m_ESPs[index].isMaster) { result.setItalic(true); result.setWeight(QFont::Bold); } else if (m_ESPs[index].isLight || m_ESPs[index].isLightFlagged) { result.setItalic(true); } return result; } QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); if (modelIndex.column() == 0) { return QVariant(Qt::AlignLeft | Qt::AlignVCenter); } else { return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } } QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const { const int index = modelIndex.row(); const auto& esp = m_ESPs[index]; QString toolTip; toolTip += "" + tr("Origin") + ": " + esp.originName; if (esp.forceEnabled) { toolTip += "
" + tr("This plugin can't be disabled (enforced by the game).") + ""; } else { if (!esp.author.isEmpty()) { toolTip += "
" + tr("Author") + ": " + TruncateString(esp.author); } if (esp.description.size() > 0) { toolTip += "
" + tr("Description") + ": " + TruncateString(esp.description); } if (esp.masterUnset.size() > 0) { toolTip += "
" + tr("Missing Masters") + ": " + "" + TruncateString(SetJoin(esp.masterUnset, ", ")) + ""; } std::set enabledMasters; std::set_difference(esp.masters.begin(), esp.masters.end(), esp.masterUnset.begin(), esp.masterUnset.end(), std::inserter(enabledMasters, enabledMasters.end())); if (!enabledMasters.empty()) { toolTip += "
" + tr("Enabled Masters") + ": " + TruncateString(SetJoin(enabledMasters, ", ")); } if (!esp.archives.empty()) { toolTip += "
" + tr("Loads Archives") + ": " + TruncateString(SetJoin(esp.archives, ", ")) + "
" + 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. (This flag only checks for Archives from the same mod as " "the plugin)"); } if (esp.hasIni) { toolTip += "
" + tr("Loads INI settings") + ": " "
" + 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 (esp.isLightFlagged && !esp.isLight) { toolTip += "

" + tr( "This ESP is flagged as an ESL. It will adhere to the ESP load " "order but the records will be loaded in ESL space."); } } // additional info auto itor = m_AdditionalInfo.find(esp.name.toLower()); if (itor != m_AdditionalInfo.end()) { if (!itor->second.messages.isEmpty()) { toolTip += "
    "; for (auto&& message : itor->second.messages) { toolTip += "
  • " + message + "
  • "; } toolTip += "
"; } // loot toolTip += makeLootTooltip(itor->second.loot); } return toolTip; } QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const { QString s; for (auto&& f : loot.incompatibilities) { s += "
  • " + tr("Incompatible with %1") .arg(f.displayName.isEmpty() ? f.name : f.displayName) + "
  • "; } for (auto&& m : loot.missingMasters) { s += "
  • " + tr("Depends on missing %1").arg(m) + "
  • "; } for (auto&& m : loot.messages) { s += "
  • "; switch (m.type) { case log::Warning: s += tr("Warning") + ": "; break; case log::Error: s += tr("Error") + ": "; break; case log::Info: // fall-through case log::Debug: default: // nothing break; } s += m.text + "
  • "; } for (auto&& d : loot.dirty) { s += "
  • " + d.toString(false) + "
  • "; } for (auto&& c : loot.clean) { s += "
  • " + c.toString(true) + "
  • "; } if (!s.isEmpty()) { s = "
    " "
      " + s + "
    "; } return s; } QVariant PluginList::iconData(const QModelIndex &modelIndex) const { int index = modelIndex.row(); QVariantList result; const auto& esp = m_ESPs[index]; const QString nameLower = esp.name.toLower(); auto infoItor = m_AdditionalInfo.find(nameLower); const AdditionalInfo* info = nullptr; if (infoItor != m_AdditionalInfo.end()) { info = &infoItor->second; } if (isProblematic(esp, info)) { result.append(":/MO/gui/warning"); } if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) { result.append(":/MO/gui/locked"); } if (hasInfo(esp, info)) { result.append(":/MO/gui/information"); } if (esp.hasIni) { result.append(":/MO/gui/attachment"); } if (!esp.archives.empty()) { result.append(":/MO/gui/archive_conflict_neutral"); } if (esp.isLightFlagged && !esp.isLight) { result.append(":/MO/gui/awaiting"); } if (info && !info->loot.dirty.empty()) { result.append(":/MO/gui/edit_clear"); } return result; } bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const { if (esp.masterUnset.size() > 0) { return true; } if (info) { if (!info->loot.incompatibilities.empty()) { return true; } if (!info->loot.missingMasters.empty()) { return true; } } return false; } bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const { if (info) { if (!info->messages.empty()) { return true; } if (!info->loot.messages.empty()) { return true; } } return false; } bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role) { QString modName = modIndex.data().toString(); IPluginList::PluginStates oldState = state(modName); bool result = false; if (role == Qt::CheckStateRole) { m_ESPs[modIndex.row()].enabled = value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].forceEnabled; m_LastCheck.restart(); emit dataChanged(modIndex, modIndex); refreshLoadOrder(); emit writePluginsList(); result = true; } else if (role == Qt::EditRole) { if (modIndex.column() == COL_PRIORITY) { bool ok = false; int newPriority = value.toInt(&ok); if (ok) { setPluginPriority(modIndex.row(), newPriority); result = true; } refreshLoadOrder(); } } IPluginList::PluginStates newState = state(modName); if (oldState != newState) { try { m_PluginStateChanged(modName, newState); testMasters(); emit dataChanged( this->index(0, 0), this->index(static_cast(m_ESPs.size()), columnCount())); } catch (const std::exception &e) { log::error("failed to invoke state changed notification: {}", e.what()); } catch (...) { log::error("failed to invoke state changed notification: unknown exception"); } } return result; } QVariant PluginList::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { return getColumnName(section); } else if (role == Qt::ToolTipRole) { return getColumnToolTip(section); } } return QAbstractItemModel::headerData(section, orientation, role); } Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const { int index = modelIndex.row(); Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); if (modelIndex.isValid()) { if (!m_ESPs[index].forceEnabled) { result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } if (modelIndex.column() == COL_PRIORITY) { result |= Qt::ItemIsEditable; } result &= ~Qt::ItemIsDropEnabled; } else { result |= Qt::ItemIsDropEnabled; } return result; } void PluginList::setPluginPriority(int row, int &newPriority) { int newPriorityTemp = newPriority; // enforce valid range if (newPriorityTemp < 0) newPriorityTemp = 0; else if (newPriorityTemp >= static_cast(m_ESPsByPriority.size())) newPriorityTemp = static_cast(m_ESPsByPriority.size()) - 1; if (!m_ESPs[row].isMaster && !m_ESPs[row].isLight) { // don't allow esps to be moved above esms while ((newPriorityTemp < static_cast(m_ESPsByPriority.size() - 1)) && (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster || m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight)) { ++newPriorityTemp; } } else { // don't allow esms to be moved below esps while ((newPriorityTemp > 0) && !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isMaster && !m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).isLight) { --newPriorityTemp; } // also don't allow "regular" esms to be moved above primary plugins while ((newPriorityTemp < static_cast(m_ESPsByPriority.size() - 1)) && (m_ESPs.at(m_ESPsByPriority.at(newPriorityTemp)).forceEnabled)) { ++newPriorityTemp; } } try { int oldPriority = m_ESPs.at(row).priority; if (newPriorityTemp > oldPriority) { // priority is higher than the old, so the gap we left is in lower priorities for (int i = oldPriority + 1; i <= newPriorityTemp; ++i) { --m_ESPs.at(m_ESPsByPriority.at(i)).priority; } emit dataChanged(index(oldPriority + 1, 0), index(newPriorityTemp, columnCount())); } else { for (int i = newPriorityTemp; i < oldPriority; ++i) { ++m_ESPs.at(m_ESPsByPriority.at(i)).priority; } emit dataChanged(index(newPriorityTemp, 0), index(oldPriority - 1, columnCount())); ++newPriority; } m_ESPs.at(row).priority = newPriorityTemp; emit dataChanged(index(row, 0), index(row, columnCount())); m_PluginMoved(m_ESPs[row].name, oldPriority, newPriorityTemp); } catch (const std::out_of_range&) { reportError(tr("failed to restore load order for %1").arg(m_ESPs[row].name)); } updateIndices(); } void PluginList::changePluginPriority(std::vector rows, int newPriority) { ChangeBracket layoutChange(this); const std::vector &esp = m_ESPs; int minPriority = INT_MAX; int maxPriority = INT_MIN; // don't try to move plugins before force-enabled plugins for (std::vector::const_iterator iter = m_ESPs.begin(); iter != m_ESPs.end(); ++iter) { if (iter->forceEnabled) { newPriority = std::max(newPriority, iter->priority+1); } maxPriority = std::max(maxPriority, iter->priority+1); minPriority = std::min(minPriority, iter->priority); } // limit the new priority to existing priorities newPriority = std::min(newPriority, maxPriority); newPriority = std::max(newPriority, minPriority); // sort the moving plugins by ascending priorities std::sort(rows.begin(), rows.end(), [&esp](const int &LHS, const int &RHS) { return esp[LHS].priority < esp[RHS].priority; }); // if at least on plugin is increasing in priority, the target index is // that of the row BELOW the dropped location, otherwise it's the one above for (std::vector::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) { if (m_ESPs[*iter].priority < newPriority) { --newPriority; break; } } for (std::vector::const_iterator iter = rows.begin(); iter != rows.end(); ++iter) { setPluginPriority(*iter, newPriority); } layoutChange.finish(); refreshLoadOrder(); emit writePluginsList(); } bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) { if (action == Qt::IgnoreAction) { return true; } QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); QDataStream stream(&encoded, QIODevice::ReadOnly); std::vector sourceRows; while (!stream.atEnd()) { int sourceRow, col; QMap roleDataMap; stream >> sourceRow >> col >> roleDataMap; if (col == 0) { // only add each row once sourceRows.push_back(sourceRow); } } if (row == -1) { row = parent.row(); } int newPriority; if ((row < 0) || (row >= static_cast(m_ESPs.size()))) { newPriority = static_cast(m_ESPs.size()); } else { newPriority = m_ESPs[row].priority; } changePluginPriority(sourceRows, newPriority); return false; } QModelIndex PluginList::index(int row, int column, const QModelIndex&) const { if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { return QModelIndex(); } return createIndex(row, column, row); } QModelIndex PluginList::parent(const QModelIndex&) const { return QModelIndex(); } bool PluginList::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QAbstractItemView *itemView = qobject_cast(obj); if (itemView == nullptr) { return QAbstractItemModel::eventFilter(obj, event); } QKeyEvent *keyEvent = static_cast(event); // ctrl+up and ctrl+down -> increase or decrease priority of selected plugins if ((keyEvent->modifiers() == Qt::ControlModifier) && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { QItemSelectionModel *selectionModel = itemView->selectionModel(); const QSortFilterProxyModel *proxyModel = qobject_cast(selectionModel->model()); if (proxyModel != nullptr) { int diff = -1; if (((keyEvent->key() == Qt::Key_Up) && (proxyModel->sortOrder() == Qt::DescendingOrder)) || ((keyEvent->key() == Qt::Key_Down) && (proxyModel->sortOrder() == Qt::AscendingOrder))) { diff = 1; } QModelIndexList rows = selectionModel->selectedRows(); // remove elements that aren't supposed to be movable QMutableListIterator iter(rows); while (iter.hasNext()) { if ((iter.next().flags() & Qt::ItemIsDragEnabled) == 0) { iter.remove(); } } if (keyEvent->key() == Qt::Key_Down) { for (int i = 0; i < rows.size() / 2; ++i) { rows.swapItemsAt(i, rows.size() - i - 1); } } for (QModelIndex idx : rows) { idx = proxyModel->mapToSource(idx); int newPriority = m_ESPs[idx.row()].priority + diff; if ((newPriority >= 0) && (newPriority < rowCount())) { setPluginPriority(idx.row(), newPriority); } } refreshLoadOrder(); } return true; } else if (keyEvent->key() == Qt::Key_Space) { QItemSelectionModel *selectionModel = itemView->selectionModel(); const QSortFilterProxyModel *proxyModel = qobject_cast(selectionModel->model()); QList indices; for (QModelIndex idx : selectionModel->selectedRows()) { indices.append(idx); } QModelIndex minRow, maxRow; for (QModelIndex idx : indices) { if (proxyModel != nullptr) { idx = proxyModel->mapToSource(idx); } if (!minRow.isValid() || (idx.row() < minRow.row())) { minRow = idx; } if (!maxRow.isValid() || (idx.row() > maxRow.row())) { maxRow = idx; } int oldState = idx.data(Qt::CheckStateRole).toInt(); setData(idx, oldState == Qt::Unchecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); } emit dataChanged(minRow, maxRow); return true; } } return QAbstractItemModel::eventFilter(obj, event); } PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, bool hasIni, std::set archives, bool lightPluginsAreSupported) : name(name), fullPath(fullPath), enabled(enabled), forceEnabled(enabled), priority(0), loadOrder(-1), originName(originName), hasIni(hasIni), archives(archives), modSelected(false) { try { ESP::File file(ToWString(fullPath)); isMaster = file.isMaster(); auto extension = name.right(3).toLower(); isLight = lightPluginsAreSupported && (extension == "esl"); isLightFlagged = lightPluginsAreSupported && file.isLight(); author = QString::fromLatin1(file.author().c_str()); description = QString::fromLatin1(file.description().c_str()); for (auto&& m : file.masters()) { masters.insert(QString::fromStdString(m)); } } catch (const std::exception &e) { log::error("failed to parse plugin file {}: {}", fullPath, e.what()); isMaster = false; isLight = false; isLightFlagged = false; } } void PluginList::managedGameChanged(const IPluginGame *gamePlugin) { m_GamePlugin = gamePlugin; }