diff --git a/CHANGELOG.md b/CHANGELOG.md index 181116ca9d..3f07def9ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Bug #7084: Resurrecting an actor doesn't take into account base record changes Bug #7088: Deleting last save game of last character doesn't clear character name/details Bug #7092: BSA archives from higher priority directories don't take priority + Bug #7103: Multiple paths pointing to the same plugin but with different cases lead to automatically removed config entries Bug #7122: Teleportation to underwater should cancel active water walking effect Bug #7131: MyGUI log spam when post processing HUD is open Bug #7134: Saves with an invalid last generated RefNum can be loaded @@ -121,6 +122,7 @@ Bug #7712: Casting doesn't support spells and enchantments with no effects Bug #7723: Assaulting vampires and werewolves shouldn't be a crime Bug #7724: Guards don't help vs werewolves + Bug #7733: Launcher shows incorrect data paths when there's two plugins with the same name Bug #7742: Governing attribute training limit should use the modified attribute Bug #7758: Water walking is not taken into account to compute path cost on the water Bug #7761: Rain and ambient loop sounds are mutually exclusive diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index dc2c07d9bd..114221ce92 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -125,27 +125,6 @@ namespace Launcher { return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024); } - - std::optional findFirstPath(const QStringList& directories, const QString& fileName) - { - for (const QString& directoryPath : directories) - { - const QString filePath = QDir(directoryPath).absoluteFilePath(fileName); - if (QFile::exists(filePath)) - return filePath; - } - return std::nullopt; - } - - QStringList findAllFilePaths(const QStringList& directories, const QStringList& fileNames) - { - QStringList result; - result.reserve(fileNames.size()); - for (const QString& fileName : fileNames) - if (const auto filepath = findFirstPath(directories, fileName)) - result.append(*filepath); - return result; - } } } @@ -366,8 +345,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) row++; } - mSelector->setProfileContent( - findAllFilePaths(directories, mLauncherSettings.getContentListFiles(contentModelName))); + mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName)); } void Launcher::DataFilesPage::saveSettings(const QString& profile) diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 9b7bb11f09..0aab06ac90 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -2,12 +2,15 @@ #include "esmfile.hpp" #include +#include #include #include +#include #include #include #include +#include #include #include @@ -127,7 +130,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index continue; noGameFiles = false; - if (isChecked(depFile->filePath())) + if (mCheckedFiles.contains(depFile)) { gamefileChecked = true; break; @@ -214,7 +217,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int if (file == mGameFile) return QVariant(); - return mCheckStates[file->filePath()]; + return mCheckedFiles.contains(file) ? Qt::Checked : Qt::Unchecked; } case Qt::UserRole: @@ -228,7 +231,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int } case Qt::UserRole + 1: - return isChecked(file->filePath()); + return mCheckedFiles.contains(file); } return QVariant(); } @@ -276,12 +279,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const { int checkValue = value.toInt(); bool setState = false; - if ((checkValue == Qt::Checked) && !isChecked(file->filePath())) + if (checkValue == Qt::Checked && !mCheckedFiles.contains(file)) { setState = true; success = true; } - else if ((checkValue == Qt::Checked) && isChecked(file->filePath())) + else if (checkValue == Qt::Checked && mCheckedFiles.contains(file)) setState = true; else if (checkValue == Qt::Unchecked) setState = true; @@ -314,34 +317,12 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const 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; + return false; } 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; + return false; } Qt::DropActions ContentSelectorModel::ContentModel::supportedDropActions() const @@ -357,13 +338,14 @@ QStringList ContentSelectorModel::ContentModel::mimeTypes() const QMimeData* ContentSelectorModel::ContentModel::mimeData(const QModelIndexList& indexes) const { QByteArray encodedData; + QDataStream stream(&encodedData, QIODevice::WriteOnly); for (const QModelIndex& index : indexes) { if (!index.isValid()) continue; - encodedData.append(item(index.row())->encodedData()); + stream << index.row(); } QMimeData* mimeData = new QMimeData(); @@ -395,26 +377,31 @@ bool ContentSelectorModel::ContentModel::dropMimeData( QByteArray encodedData = data->data(mMimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); + std::vector toMove; 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); + int sourceRow; + stream >> sourceRow; + toMove.emplace_back(mFiles.at(sourceRow)); } + int minRow = mFiles.size(); + int maxRow = 0; + for (EsmFile* file : toMove) + { + int from = mFiles.indexOf(file); + int to = beginRow; + if (from < beginRow) + to--; + else if (from > beginRow) + beginRow++; + minRow = std::min(minRow, std::min(to, from)); + maxRow = std::max(maxRow, std::max(to, from)); + mFiles.move(from, to); + } + + dataChanged(index(minRow, 0), index(maxRow, 0)); + // at this point we know that drag and drop has finished. + checkForLoadOrderErrors(); return true; } @@ -447,26 +434,37 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf { 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; + EsmFile* file = const_cast(item(info.fileName())); + bool add = file == nullptr; + std::unique_ptr newFile; + if (add) + { + newFile = std::make_unique(path2); + file = newFile.get(); + } + else + { + // We've found the same file in a higher priority dir, update our existing entry + file->setFileName(path2); + file->setGameFiles({}); + } + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) { - EsmFile* file = new EsmFile(path2); file->setDate(info.lastModified()); file->setFilePath(info.absoluteFilePath()); - addFile(file); + if (add) + addFile(newFile.release()); 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()); @@ -522,14 +520,14 @@ void ContentSelectorModel::ContentModel::addFiles(const QString& path, bool newf } // Put the file in the table - addFile(file); + if (add) + addFile(newFile.release()); setNew(file->fileName(), newfiles); } catch (std::runtime_error& e) { // An error occurred while reading the .esp qWarning() << "Error reading addon file: " << e.what(); - continue; } } } @@ -554,6 +552,7 @@ void ContentSelectorModel::ContentModel::clearFiles() if (filesCount > 0) { beginRemoveRows(QModelIndex(), 0, filesCount - 1); + qDeleteAll(mFiles); mFiles.clear(); endRemoveRows(); } @@ -616,14 +615,6 @@ void ContentSelectorModel::ContentModel::sortFiles() 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); @@ -684,7 +675,7 @@ void ContentSelectorModel::ContentModel::checkForLoadOrderErrors() { for (int row = 0; row < mFiles.count(); ++row) { - EsmFile* file = item(row); + EsmFile* file = mFiles.at(row); bool isRowInError = checkForLoadOrderErrors(file, row).count() != 0; if (isRowInError) { @@ -711,7 +702,7 @@ QList ContentSelectorModel::ContentModel:: } else { - if (!isChecked(dependentFile->filePath())) + if (!mCheckedFiles.contains(dependentFile)) { errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName)); } @@ -761,19 +752,18 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (!file) return false; - Qt::CheckState state = Qt::Unchecked; - if (checkState) - state = Qt::Checked; + mCheckedFiles.insert(file); + else + mCheckedFiles.erase(file); - 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) + if (checkState) { for (const QString& upstreamName : file->gameFiles()) { @@ -782,14 +772,13 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (!upstreamFile) continue; - if (!isChecked(upstreamFile->filePath())) - mCheckStates[upstreamFile->filePath()] = Qt::Checked; + mCheckedFiles.insert(upstreamFile); 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) + else { for (const EsmFile* downstreamFile : mFiles) { @@ -798,8 +787,7 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString& filepath, if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive)) { - if (mCheckStates.contains(downstreamFile->filePath())) - mCheckStates[downstreamFile->filePath()] = Qt::Unchecked; + mCheckedFiles.erase(downstreamFile); emit dataChanged(indexFromItem(downstreamFile), indexFromItem(downstreamFile)); } @@ -817,7 +805,7 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke // 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())) + if (mCheckedFiles.contains(file)) list << file; return list; @@ -826,6 +814,6 @@ ContentSelectorModel::ContentFileList ContentSelectorModel::ContentModel::checke void ContentSelectorModel::ContentModel::uncheckAll() { emit layoutAboutToBeChanged(); - mCheckStates.clear(); + mCheckedFiles.clear(); emit layoutChanged(); } diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index d56f8f9a3b..1ba3090a32 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -7,6 +7,8 @@ #include #include +#include + namespace ContentSelectorModel { class EsmFile; @@ -57,7 +59,6 @@ namespace ContentSelectorModel void setCurrentGameFile(const EsmFile* file); bool isEnabled(const QModelIndex& index) const; - bool isChecked(const QString& filepath) const; bool setCheckState(const QString& filepath, bool isChecked); bool isNew(const QString& filepath) const; void setNew(const QString& filepath, bool isChecked); @@ -85,7 +86,7 @@ namespace ContentSelectorModel const EsmFile* mGameFile; ContentFileList mFiles; QStringList mArchives; - QHash mCheckStates; + std::set mCheckedFiles; QHash mNewFiles; QSet mPluginsWithLoadOrderError; QString mEncoding; diff --git a/components/contentselector/model/esmfile.cpp b/components/contentselector/model/esmfile.cpp index 39a33e710e..e4280baef7 100644 --- a/components/contentselector/model/esmfile.cpp +++ b/components/contentselector/model/esmfile.cpp @@ -1,10 +1,5 @@ #include "esmfile.hpp" -#include -#include - -int ContentSelectorModel::EsmFile::sPropertyCount = 7; - ContentSelectorModel::EsmFile::EsmFile(const QString& fileName, ModelItem* parent) : ModelItem(parent) , mFileName(fileName) @@ -46,17 +41,6 @@ void ContentSelectorModel::EsmFile::setDescription(const QString& description) mDescription = description; } -QByteArray ContentSelectorModel::EsmFile::encodedData() const -{ - QByteArray encodedData; - QDataStream stream(&encodedData, QIODevice::WriteOnly); - - stream << mFileName << mAuthor << mVersion << mModified.toString(Qt::ISODate) << mPath << mDescription - << mGameFiles; - - return encodedData; -} - bool ContentSelectorModel::EsmFile::isGameFile() const { return (mGameFiles.size() == 0) diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index a65c778294..606cc3d319 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -30,15 +30,11 @@ namespace ContentSelectorModel }; EsmFile(const QString& fileName = QString(), ModelItem* parent = nullptr); - // EsmFile(const EsmFile &); - - ~EsmFile() {} void setFileProperty(const FileProperty prop, const QString& value); void setFileName(const QString& fileName); void setAuthor(const QString& author); - void setSize(const int size); void setDate(const QDateTime& modified); void setFormat(const QString& format); void setFilePath(const QString& path); @@ -68,10 +64,6 @@ namespace ContentSelectorModel } bool isGameFile() const; - QByteArray encodedData() const; - - public: - static int sPropertyCount; private: QString mTooltipTemlate = tr( diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 3f75b82487..00c32e272d 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -108,6 +108,7 @@ void ContentSelectorView::ContentSelector::buildAddonView() connect(ui->addonView, &QTableView::activated, this, &ContentSelector::slotAddonTableItemActivated); connect(mContentModel, &ContentSelectorModel::ContentModel::dataChanged, this, &ContentSelector::signalAddonDataChanged); + connect(mContentModel, &ContentSelectorModel::ContentModel::dataChanged, this, &ContentSelector::slotRowsMoved); buildContextMenu(); } @@ -331,3 +332,8 @@ void ContentSelectorView::ContentSelector::slotSearchFilterTextChanged(const QSt { ui->addonView->setDragEnabled(newText.isEmpty()); } + +void ContentSelectorView::ContentSelector::slotRowsMoved() +{ + ui->addonView->selectionModel()->clearSelection(); +} \ No newline at end of file diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 48377acb86..2b739645ba 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -85,6 +85,7 @@ namespace ContentSelectorView void slotUncheckMultiSelectedItems(); void slotCopySelectedItemsPaths(); void slotSearchFilterTextChanged(const QString& newText); + void slotRowsMoved(); }; }