mirror of
https://github.com/ModOrganizer2/modorganizer.git
synced 2026-07-27 13:58:24 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41249036e5 | ||
|
|
d69289e081 | ||
|
|
0adf8a9f1b | ||
|
|
05d03eec68 | ||
|
|
592fa73104 | ||
|
|
64ba6cae1e | ||
|
|
f71accc1ad | ||
|
|
44332c778d | ||
|
|
6a1d57fe0c | ||
|
|
777229f28f | ||
|
|
2eeaa0e31a | ||
|
|
9d3281fd58 | ||
|
|
1a468d66db | ||
|
|
19fe01b60d | ||
|
|
b6fac68b32 | ||
|
|
50d883079e | ||
|
|
e379610805 | ||
|
|
6dc736ca37 | ||
|
|
7791ea0bd5 | ||
|
|
e34a9e7315 | ||
|
|
4d546ea262 | ||
|
|
2ca7c1c9ea | ||
|
|
4b0ba5c579 | ||
|
|
7a10a74766 | ||
|
|
7db0841a22 | ||
|
|
2c082d6a3b | ||
|
|
a965a9dc41 | ||
|
|
82cc488a18 | ||
|
|
4adfeac9b7 | ||
|
|
c7a90f9506 | ||
|
|
80645bacc1 | ||
|
|
0c7265be4f | ||
|
|
6c4e237d4b |
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Game support Request
|
||||
about: Request support for a new game
|
||||
title: Add support for game [GAME NAME]
|
||||
labels: 'additional games support, Feature Request, area: mo2 game plugins'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**This template is useful to add basic support using https://github.com/ModOrganizer2/modorganizer-basic_games.**
|
||||
|
||||
**If you are vaguely familiar with programming you can try following the instructions on that link to get something working yourself. If you have trouble with that please fill in this template.**
|
||||
|
||||
**Please provide the following information for the game (remove the optional lines you do not use):**
|
||||
|
||||
- **Name:** Name of the game
|
||||
- **Short name:** Name of the game in Nexus URL (e.g. skyrimse for Skyrim SE).
|
||||
- **Nexus ID [optional]:** ID of the game on Nexus (you can usually find this in Nexus URL).
|
||||
- **Executable:** Name of the main executable for the game (relative to the game folder).
|
||||
- **Launcher [optional]:** Name of the game launcher (relative to the game folder).
|
||||
- **Data path:** Path to the data folder of the game (relative to the game folder).
|
||||
- **Documents path:** Path to folder containing INI files, etc., for the game (usually under "My Games", or the game folder itself).
|
||||
- **Saves directory [optional]:** Path to the folder containing save games (this default to the path above).
|
||||
- **Save extension [optional]:** Extension of the saves
|
||||
- **Steam ID [optional]:** ID of the game on Steam (if the game is on Steam).
|
||||
|
||||
**Provide additional details regarding the game that could be required for the plugin.**
|
||||
@@ -395,6 +395,11 @@
|
||||
<string>Nubbie (Swedish)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string notr="true">Hakan "Nyks45" Albayrak (Turkish)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>...and all other contributors!</string>
|
||||
|
||||
+2
-2
@@ -118,9 +118,9 @@ void DataTab::updateTree()
|
||||
void DataTab::ensureFullyLoaded()
|
||||
{
|
||||
if (!m_filetree->fullyLoaded()) {
|
||||
m_filter.proxyModel()->setRecursiveFilteringEnabled(false);
|
||||
m_filter.setFilteringEnabled(false);
|
||||
m_filetree->ensureFullyLoaded();
|
||||
m_filter.proxyModel()->setRecursiveFilteringEnabled(true);
|
||||
m_filter.setFilteringEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
#ifndef ENV_ENVFS_H
|
||||
#define ENV_ENVFS_H
|
||||
|
||||
#include "thread_utils.h"
|
||||
#include <thread>
|
||||
|
||||
namespace env
|
||||
@@ -125,7 +126,7 @@ private:
|
||||
ThreadInfo()
|
||||
: busy(true), ready(false), stop(false)
|
||||
{
|
||||
thread = std::thread([&]{ run(); });
|
||||
thread = MOShared::startSafeThread([&]{ run(); });
|
||||
}
|
||||
|
||||
~ThreadInfo()
|
||||
|
||||
+14
-2
@@ -802,11 +802,11 @@ void FileTree::addCommonMenus(QMenu& menu)
|
||||
.addTo(menu);
|
||||
|
||||
MenuItem(tr("Ex&pand All"))
|
||||
.callback([&]{ m_tree->expandAll(); })
|
||||
.callback([&]{ expandAll(); })
|
||||
.addTo(menu);
|
||||
|
||||
MenuItem(tr("&Collapse All"))
|
||||
.callback([&]{ m_tree->collapseAll(); })
|
||||
.callback([&]{ collapseAll(); })
|
||||
.addTo(menu);
|
||||
}
|
||||
|
||||
@@ -820,3 +820,15 @@ QModelIndex FileTree::proxiedIndex(const QModelIndex& index)
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
void FileTree::collapseAll()
|
||||
{
|
||||
m_tree->collapseAll();
|
||||
}
|
||||
|
||||
void FileTree::expandAll()
|
||||
{
|
||||
m_model->aboutToExpandAll();
|
||||
m_tree->expandAll();
|
||||
m_model->expandedAll();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ public:
|
||||
bool fullyLoaded() const;
|
||||
void ensureFullyLoaded();
|
||||
|
||||
void expandAll();
|
||||
void collapseAll();
|
||||
|
||||
void open(FileTreeItem* item=nullptr);
|
||||
void openHooked(FileTreeItem* item=nullptr);
|
||||
void preview(FileTreeItem* item=nullptr);
|
||||
|
||||
+119
-56
@@ -14,9 +14,7 @@ constexpr bool AlwaysSortDirectoriesFirst = true;
|
||||
|
||||
const QString& directoryFileType()
|
||||
{
|
||||
static QString name;
|
||||
|
||||
if (name.isEmpty()) {
|
||||
static const QString name = [] {
|
||||
const DWORD flags = SHGFI_TYPENAME;
|
||||
SHFILEINFOW sfi = {};
|
||||
|
||||
@@ -30,15 +28,88 @@ const QString& directoryFileType()
|
||||
"SHGetFileInfoW failed for folder file type, {}",
|
||||
formatSystemMessage(e));
|
||||
|
||||
name = "File folder";
|
||||
return QString("File folder");
|
||||
} else {
|
||||
name = QString::fromWCharArray(sfi.szTypeName);
|
||||
return QString::fromWCharArray(sfi.szTypeName);
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
const QString& cachedFileTypeNoExtension()
|
||||
{
|
||||
static const QString name = [] {
|
||||
const DWORD flags = SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES;
|
||||
SHFILEINFOW sfi = {};
|
||||
|
||||
// dummy filename with no extension
|
||||
const auto r = SHGetFileInfoW(L"file", 0, &sfi, sizeof(sfi), flags);
|
||||
|
||||
if (!r) {
|
||||
const auto e = GetLastError();
|
||||
|
||||
log::error(
|
||||
"SHGetFileInfoW failed for file without extension, {}",
|
||||
formatSystemMessage(e));
|
||||
|
||||
return QString("File");
|
||||
} else {
|
||||
return QString::fromWCharArray(sfi.szTypeName);
|
||||
}
|
||||
}();
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
const QString& cachedFileType(const std::wstring& file, bool isOnFilesystem)
|
||||
{
|
||||
static std::map<std::wstring, QString, std::less<>> map;
|
||||
static std::mutex mutex;
|
||||
|
||||
const auto dot = file.find_last_of(L'.');
|
||||
if (dot == std::wstring::npos) {
|
||||
return cachedFileTypeNoExtension();
|
||||
}
|
||||
|
||||
std::scoped_lock lock(mutex);
|
||||
const auto sv = std::wstring_view(file.c_str() + dot, file.size() - dot);
|
||||
|
||||
auto itor = map.find(sv);
|
||||
if (itor != map.end()) {
|
||||
return itor->second;
|
||||
}
|
||||
|
||||
|
||||
DWORD flags = SHGFI_TYPENAME;
|
||||
|
||||
if (!isOnFilesystem) {
|
||||
// files from archives are not on the filesystem; this flag forces
|
||||
// SHGetFileInfoW() to only work with the filename
|
||||
flags |= SHGFI_USEFILEATTRIBUTES;
|
||||
}
|
||||
|
||||
SHFILEINFOW sfi = {};
|
||||
const auto r = SHGetFileInfoW(file.c_str(), 0, &sfi, sizeof(sfi), flags);
|
||||
|
||||
QString s;
|
||||
|
||||
if (!r) {
|
||||
const auto e = GetLastError();
|
||||
|
||||
log::error(
|
||||
"SHGetFileInfoW failed for '{}', {}",
|
||||
file, formatSystemMessage(e));
|
||||
|
||||
s = cachedFileTypeNoExtension();
|
||||
} else {
|
||||
s = QString::fromWCharArray(sfi.szTypeName);
|
||||
}
|
||||
|
||||
return map.emplace(sv, s).first->second;
|
||||
}
|
||||
|
||||
|
||||
|
||||
FileTreeItem::FileTreeItem(
|
||||
FileTreeModel* model, FileTreeItem* parent,
|
||||
@@ -176,51 +247,60 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
void FileTreeItem::sort()
|
||||
void FileTreeItem::queueSort()
|
||||
{
|
||||
if (!m_children.empty()) {
|
||||
m_model->sortItem(*this, true);
|
||||
m_model->queueSortItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeItem::makeSortingStale()
|
||||
{
|
||||
m_sortingStale = true;
|
||||
|
||||
for (auto& c : m_children) {
|
||||
c->makeSortingStale();
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreeItem::sort(int column, Qt::SortOrder order, bool force)
|
||||
{
|
||||
if (!force && !m_expanded) {
|
||||
if (!m_expanded) {
|
||||
m_sortingStale = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_sortingStale) {
|
||||
if (m_sortingStale || force) {
|
||||
//log::debug("sorting is stale for {}, sorting now", debugName());
|
||||
m_sortingStale = false;
|
||||
|
||||
std::sort(m_children.begin(), m_children.end(), [&](auto&& a, auto&& b) {
|
||||
int r = 0;
|
||||
|
||||
if (a->isDirectory() && !b->isDirectory()) {
|
||||
if constexpr (AlwaysSortDirectoriesFirst) {
|
||||
return true;
|
||||
} else {
|
||||
r = -1;
|
||||
}
|
||||
} else if (!a->isDirectory() && b->isDirectory()) {
|
||||
if constexpr (AlwaysSortDirectoriesFirst) {
|
||||
return false;
|
||||
} else {
|
||||
r = 1;
|
||||
}
|
||||
} else {
|
||||
r = FileTreeItem::Sorter::compare(column, a.get(), b.get());
|
||||
}
|
||||
|
||||
if (order == Qt::AscendingOrder) {
|
||||
return (r < 0);
|
||||
} else {
|
||||
return (r > 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::sort(m_children.begin(), m_children.end(), [&](auto&& a, auto&& b) {
|
||||
int r = 0;
|
||||
|
||||
if (a->isDirectory() && !b->isDirectory()) {
|
||||
if constexpr (AlwaysSortDirectoriesFirst) {
|
||||
return true;
|
||||
} else {
|
||||
r = -1;
|
||||
}
|
||||
} else if (!a->isDirectory() && b->isDirectory()) {
|
||||
if constexpr (AlwaysSortDirectoriesFirst) {
|
||||
return false;
|
||||
} else {
|
||||
r = 1;
|
||||
}
|
||||
} else {
|
||||
r = FileTreeItem::Sorter::compare(column, a.get(), b.get());
|
||||
}
|
||||
|
||||
if (order == Qt::AscendingOrder) {
|
||||
return (r < 0);
|
||||
} else {
|
||||
return (r > 0);
|
||||
}
|
||||
});
|
||||
|
||||
for (auto& child : m_children) {
|
||||
child->sort(column, order, force);
|
||||
}
|
||||
@@ -321,28 +401,11 @@ void FileTreeItem::getFileType() const
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD flags = SHGFI_TYPENAME;
|
||||
|
||||
if (isFromArchive()) {
|
||||
// files from archives are not on the filesystem; this flag forces
|
||||
// SHGetFileInfoW() to only work with the filename
|
||||
flags |= SHGFI_USEFILEATTRIBUTES;
|
||||
}
|
||||
|
||||
SHFILEINFOW sfi = {};
|
||||
const auto r = SHGetFileInfoW(
|
||||
m_wsRealPath.c_str(), 0, &sfi, sizeof(sfi), flags);
|
||||
|
||||
if (!r) {
|
||||
const auto e = GetLastError();
|
||||
|
||||
log::error(
|
||||
"SHGetFileInfoW failed for '{}', {}",
|
||||
m_realPath, formatSystemMessage(e));
|
||||
|
||||
const auto& t = cachedFileType(m_wsRealPath, !isFromArchive());
|
||||
if (t.isEmpty()) {
|
||||
m_fileType.fail();
|
||||
} else {
|
||||
m_fileType.set(QString::fromWCharArray(sfi.szTypeName));
|
||||
m_fileType.set(t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -93,6 +93,7 @@ public:
|
||||
}
|
||||
|
||||
void sort(int column, Qt::SortOrder order, bool force);
|
||||
void makeSortingStale();
|
||||
|
||||
FileTreeItem* parent()
|
||||
{
|
||||
@@ -223,7 +224,7 @@ public:
|
||||
m_expanded = b;
|
||||
|
||||
if (m_expanded && m_sortingStale) {
|
||||
sort();
|
||||
queueSort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +315,7 @@ private:
|
||||
std::wstring dataRelativeParentPath, bool isDirectory, std::wstring file);
|
||||
|
||||
void getFileType() const;
|
||||
void sort();
|
||||
void queueSort();
|
||||
};
|
||||
|
||||
#endif // MODORGANIZER_FILETREEITEM_INCLUDED
|
||||
|
||||
+50
-17
@@ -196,13 +196,13 @@ void* makeInternalPointer(FileTreeItem* item)
|
||||
FileTreeModel::FileTreeModel(OrganizerCore& core, QObject* parent) :
|
||||
QAbstractItemModel(parent), m_core(core), m_enabled(true),
|
||||
m_root(FileTreeItem::createDirectory(this, nullptr, L"", L"")),
|
||||
m_flags(NoFlags), m_fullyLoaded(false)
|
||||
m_flags(NoFlags), m_fullyLoaded(false), m_sortingEnabled(true)
|
||||
{
|
||||
m_root->setExpanded(true);
|
||||
m_sortTimer.setSingleShot(true);
|
||||
|
||||
connect(&m_removeTimer, &QTimer::timeout, [&]{ removeItems(); });
|
||||
connect(&m_sortTimer, &QTimer::timeout, [&]{ sortItems(); });
|
||||
|
||||
connect(&m_iconPendingTimer, &QTimer::timeout, [&]{ updatePendingIcons(); });
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ void FileTreeModel::refresh()
|
||||
|
||||
m_fullyLoaded = false;
|
||||
update(*m_root, *m_core.directoryStructure(), L"", false);
|
||||
sortItem(*m_root, false);
|
||||
}
|
||||
|
||||
void FileTreeModel::clear()
|
||||
@@ -226,7 +227,7 @@ void FileTreeModel::clear()
|
||||
void FileTreeModel::recursiveFetchMore(const QModelIndex& m)
|
||||
{
|
||||
if (canFetchMore(m)) {
|
||||
doFetchMore(m, false);
|
||||
doFetchMore(m, false, false);
|
||||
}
|
||||
|
||||
for (int i=0; i<rowCount(m); ++i) {
|
||||
@@ -239,6 +240,7 @@ void FileTreeModel::ensureFullyLoaded()
|
||||
if (!m_fullyLoaded) {
|
||||
TimeThis tt("FileTreeModel:: fully loading for search");
|
||||
recursiveFetchMore(QModelIndex());
|
||||
sortItem(*m_root, false);
|
||||
m_fullyLoaded = true;
|
||||
}
|
||||
}
|
||||
@@ -253,6 +255,17 @@ void FileTreeModel::setEnabled(bool b)
|
||||
m_enabled = b;
|
||||
}
|
||||
|
||||
void FileTreeModel::aboutToExpandAll()
|
||||
{
|
||||
m_sortingEnabled = false;
|
||||
}
|
||||
|
||||
void FileTreeModel::expandedAll()
|
||||
{
|
||||
m_sortingEnabled = true;
|
||||
sortItem(*m_root, false);
|
||||
}
|
||||
|
||||
const FileTreeModel::SortInfo& FileTreeModel::sortInfo() const
|
||||
{
|
||||
return m_sort;
|
||||
@@ -338,10 +351,11 @@ bool FileTreeModel::canFetchMore(const QModelIndex& parent) const
|
||||
|
||||
void FileTreeModel::fetchMore(const QModelIndex& parent)
|
||||
{
|
||||
doFetchMore(parent, true);
|
||||
doFetchMore(parent, true, true);
|
||||
}
|
||||
|
||||
void FileTreeModel::doFetchMore(const QModelIndex& parent, bool forFetch)
|
||||
void FileTreeModel::doFetchMore(
|
||||
const QModelIndex& parent, bool forFetch, bool doSort)
|
||||
{
|
||||
FileTreeItem* item = itemFromIndex(parent);
|
||||
if (!item) {
|
||||
@@ -360,6 +374,10 @@ void FileTreeModel::doFetchMore(const QModelIndex& parent, bool forFetch)
|
||||
|
||||
const auto parentPath = item->dataRelativeParentPath();
|
||||
update(*item, *parentEntry, parentPath.toStdWString(), forFetch);
|
||||
|
||||
if (!forFetch && doSort) {
|
||||
sortItem(*item, false);
|
||||
}
|
||||
}
|
||||
|
||||
QVariant FileTreeModel::data(const QModelIndex& index, int role) const
|
||||
@@ -485,7 +503,7 @@ void FileTreeModel::sort(int column, Qt::SortOrder order)
|
||||
m_sort.column = column;
|
||||
m_sort.order = order;
|
||||
|
||||
sortItem(*m_root, false);
|
||||
sortItem(*m_root, true);
|
||||
}
|
||||
|
||||
FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
|
||||
@@ -558,11 +576,15 @@ void FileTreeModel::update(
|
||||
}
|
||||
|
||||
if (added) {
|
||||
parentItem.makeSortingStale();
|
||||
|
||||
// see comment at the top of this file
|
||||
if (forFetching)
|
||||
queueSortItem(&parentItem);
|
||||
else
|
||||
sortItem(parentItem, true);
|
||||
if (forFetching) {
|
||||
// don't pass a specific item, this will start a timer and re-sort the
|
||||
// whole tree, which is faster than potentially queuing every single
|
||||
// node if the whole tree is expanded
|
||||
queueSortItem(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,21 +895,32 @@ void FileTreeModel::removeItems()
|
||||
|
||||
void FileTreeModel::queueSortItem(FileTreeItem* item)
|
||||
{
|
||||
m_sortItems.push_back(item);
|
||||
if (!m_sortingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
m_sortItems.push_back(item);
|
||||
}
|
||||
|
||||
m_sortTimer.start(1);
|
||||
}
|
||||
|
||||
void FileTreeModel::sortItems()
|
||||
{
|
||||
// see comment at the top of this file
|
||||
trace(log::debug("sort item timer: sorting {} items", m_sortItems.size()));
|
||||
|
||||
auto copy = std::move(m_sortItems);
|
||||
m_sortItems.clear();
|
||||
m_sortTimer.stop();
|
||||
if (m_sortItems.empty()) {
|
||||
sortItem(*m_root, false);
|
||||
} else {
|
||||
log::debug("sort item timer: sorting {} items", m_sortItems.size());
|
||||
|
||||
for (auto&& f : copy) {
|
||||
sortItem(*f, true);
|
||||
auto items = std::move(m_sortItems);
|
||||
m_sortItems.clear();
|
||||
|
||||
for (auto* item : items) {
|
||||
sortItem(*item, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -61,6 +61,10 @@ public:
|
||||
bool enabled() const;
|
||||
void setEnabled(bool b);
|
||||
|
||||
void aboutToExpandAll();
|
||||
void expandedAll();
|
||||
|
||||
|
||||
const SortInfo& sortInfo() const;
|
||||
|
||||
QModelIndex index(int row, int col, const QModelIndex& parent={}) const override;
|
||||
@@ -77,6 +81,7 @@ public:
|
||||
|
||||
FileTreeItem* itemFromIndex(const QModelIndex& index) const;
|
||||
void sortItem(FileTreeItem& item, bool force);
|
||||
void queueSortItem(FileTreeItem* item);
|
||||
|
||||
private:
|
||||
class Range;
|
||||
@@ -92,11 +97,12 @@ private:
|
||||
mutable QTimer m_iconPendingTimer;
|
||||
SortInfo m_sort;
|
||||
bool m_fullyLoaded;
|
||||
bool m_sortingEnabled;
|
||||
|
||||
// see top of filetreemodel.cpp
|
||||
std::vector<FileTreeItem*> m_removeItems;
|
||||
QTimer m_removeTimer;
|
||||
std::vector<FileTreeItem*> m_sortItems;
|
||||
QTimer m_removeTimer;
|
||||
QTimer m_sortTimer;
|
||||
|
||||
|
||||
@@ -113,12 +119,11 @@ private:
|
||||
FileTreeItem& parentItem, const MOShared::DirectoryEntry& parentEntry,
|
||||
const std::wstring& parentPath, bool forFetching);
|
||||
|
||||
void doFetchMore(const QModelIndex& parent, bool forFetch);
|
||||
void doFetchMore(const QModelIndex& parent, bool forFetch, bool doSort);
|
||||
|
||||
void queueRemoveItem(FileTreeItem* item);
|
||||
void removeItems();
|
||||
|
||||
void queueSortItem(FileTreeItem* item);
|
||||
void sortItems();
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#include "iconfetcher.h"
|
||||
#include "thread_utils.h"
|
||||
#include "shared/util.h"
|
||||
|
||||
void IconFetcher::Waiter::wait()
|
||||
@@ -25,7 +26,7 @@ IconFetcher::IconFetcher()
|
||||
m_quickCache.file = getPixmapIcon(QFileIconProvider::File);
|
||||
m_quickCache.directory = getPixmapIcon(QFileIconProvider::Folder);
|
||||
|
||||
m_thread = std::thread([&]{ threadFun(); });
|
||||
m_thread = MOShared::startSafeThread([&]{ threadFun(); });
|
||||
}
|
||||
|
||||
IconFetcher::~IconFetcher()
|
||||
|
||||
+33
-4
@@ -127,9 +127,10 @@ bool bootstrap()
|
||||
return true;
|
||||
}
|
||||
|
||||
LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr;
|
||||
thread_local LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr;
|
||||
thread_local std::terminate_handler prevTerminateHandler = nullptr;
|
||||
|
||||
static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs)
|
||||
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs)
|
||||
{
|
||||
const std::wstring& dumpPath = OrganizerCore::crashDumpsPath();
|
||||
int dumpRes =
|
||||
@@ -139,12 +140,39 @@ static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *except
|
||||
else
|
||||
log::error("ModOrganizer has crashed, CreateMiniDump failed ({}, error {}).", dumpRes, GetLastError());
|
||||
|
||||
if (prevUnhandledExceptionFilter)
|
||||
if (prevUnhandledExceptionFilter && exceptionPtrs)
|
||||
return prevUnhandledExceptionFilter(exceptionPtrs);
|
||||
else
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
void terminateHandler() noexcept
|
||||
{
|
||||
__try
|
||||
{
|
||||
// force an exception to get a valid stack trace for this thread
|
||||
*(int*)0 = 42;
|
||||
}
|
||||
__except
|
||||
(
|
||||
MyUnhandledExceptionFilter(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
if (prevTerminateHandler) {
|
||||
prevTerminateHandler();
|
||||
} else {
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
void setUnhandledExceptionHandler()
|
||||
{
|
||||
prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
|
||||
prevTerminateHandler = std::set_terminate(terminateHandler);
|
||||
}
|
||||
|
||||
// Parses the first parseArgCount arguments of the current process command line and returns
|
||||
// them in parsedArgs, the rest of the command line is returned untouched.
|
||||
LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstring>& parsedArgs)
|
||||
@@ -171,6 +199,7 @@ LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector<std::wstrin
|
||||
return cmd;
|
||||
}
|
||||
|
||||
|
||||
static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) {
|
||||
PROCESS_INFORMATION pi{ 0 };
|
||||
STARTUPINFO si{ 0 };
|
||||
@@ -947,7 +976,7 @@ int main(int argc, char *argv[])
|
||||
application.setProperty("dataPath", dataPath);
|
||||
|
||||
// initialize dump collection only after "dataPath" since the crashes are stored under it
|
||||
prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
|
||||
setUnhandledExceptionHandler();
|
||||
|
||||
const auto logFile =
|
||||
qApp->property("dataPath").toString() + "/logs/mo_interface.log";
|
||||
|
||||
+42
-34
@@ -3309,6 +3309,29 @@ void MainWindow::visitWebPage_clicked()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::visitNexusOrWebPage(const QModelIndex& idx)
|
||||
{
|
||||
int row_idx = idx.data(Qt::UserRole + 1).toInt();
|
||||
|
||||
ModInfo::Ptr info = ModInfo::getByIndex(row_idx);
|
||||
if (!info) {
|
||||
log::error("mod {} not found", row_idx);
|
||||
return;
|
||||
}
|
||||
|
||||
int modID = info->getNexusID();
|
||||
QString gameName = info->getGameName();
|
||||
const auto url = info->parseCustomURL();
|
||||
|
||||
if (modID > 0) {
|
||||
linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
|
||||
} else if (url.isValid()) {
|
||||
linkClicked(url.toString());
|
||||
} else {
|
||||
log::error("mod '{}' has no valid link", info->name());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::visitNexusOrWebPage_clicked() {
|
||||
QItemSelectionModel* selection = ui->modList->selectionModel();
|
||||
if (selection->hasSelection() && selection->selectedRows().count() > 1) {
|
||||
@@ -3320,43 +3343,14 @@ void MainWindow::visitNexusOrWebPage_clicked() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int row_idx;
|
||||
ModInfo::Ptr info;
|
||||
QString gameName;
|
||||
|
||||
for (QModelIndex idx : selection->selectedRows()) {
|
||||
row_idx = idx.data(Qt::UserRole + 1).toInt();
|
||||
info = ModInfo::getByIndex(row_idx);
|
||||
int modID = info->getNexusID();
|
||||
gameName = info->getGameName();
|
||||
const auto url = info->parseCustomURL();
|
||||
if (modID > 0) {
|
||||
linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
|
||||
}
|
||||
else if (url.isValid()) {
|
||||
linkClicked(url.toString());
|
||||
}
|
||||
else {
|
||||
log::error("mod '{}' has no valid link", info->name());
|
||||
}
|
||||
visitNexusOrWebPage(idx);
|
||||
}
|
||||
}
|
||||
else {
|
||||
int modID = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole).toInt();
|
||||
QString gameName = m_OrganizerCore.modList()->data(m_OrganizerCore.modList()->index(m_ContextRow, 0), Qt::UserRole + 4).toString();
|
||||
if (modID > 0) {
|
||||
linkClicked(NexusInterface::instance(&m_PluginContainer)->getModURL(modID, gameName));
|
||||
}
|
||||
else {
|
||||
ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow);
|
||||
const auto url = info->parseCustomURL();
|
||||
if (url.isValid()) {
|
||||
linkClicked(url.toString());
|
||||
}
|
||||
else {
|
||||
MessageDialog::showMessage(tr("No valid Web Page for this mod"), this);
|
||||
}
|
||||
}
|
||||
QModelIndex idx = m_OrganizerCore.modList()->index(m_ContextRow, 0);
|
||||
visitNexusOrWebPage(idx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3865,7 +3859,10 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index)
|
||||
if (modifiers.testFlag(Qt::ControlModifier)) {
|
||||
try {
|
||||
m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
|
||||
openExplorer_clicked();
|
||||
|
||||
ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow);
|
||||
shell::Explore(modInfo->absolutePath());
|
||||
|
||||
// workaround to cancel the editor that might have opened because of
|
||||
// selection-click
|
||||
ui->modList->closePersistentEditor(index);
|
||||
@@ -3877,7 +3874,8 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index)
|
||||
else if (modifiers.testFlag(Qt::ShiftModifier)) {
|
||||
try {
|
||||
m_ContextRow = m_ModListSortProxy->mapToSource(index).row();
|
||||
visitNexusOrWebPage_clicked();
|
||||
QModelIndex idx = m_OrganizerCore.modList()->index(m_ContextRow, 0);
|
||||
visitNexusOrWebPage(idx);
|
||||
ui->modList->closePersistentEditor(index);
|
||||
}
|
||||
catch (const std::exception & e) {
|
||||
@@ -6220,6 +6218,16 @@ void MainWindow::on_showHiddenBox_toggled(bool checked)
|
||||
|
||||
void MainWindow::on_bossButton_clicked()
|
||||
{
|
||||
const auto r = QMessageBox::question(
|
||||
this, tr("Sorting plugins"),
|
||||
tr("Are you sure you want to sort your plugins list?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (r != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
m_OrganizerCore.savePluginList();
|
||||
|
||||
setEnabled(false);
|
||||
|
||||
+2
-1
@@ -251,7 +251,7 @@ private:
|
||||
void fixCategories();
|
||||
|
||||
bool extractProgress(QProgressDialog &extractProgress, int percentage, std::string fileName);
|
||||
|
||||
|
||||
// Performs checks, sets the m_NumberOfProblems and signals checkForProblemsDone().
|
||||
void checkForProblemsImpl();
|
||||
|
||||
@@ -498,6 +498,7 @@ private slots:
|
||||
ModListSortProxy::FilterMode mode, ModListSortProxy::SeparatorsMode sep);
|
||||
|
||||
void displayModInformation(const QString &modName, ModInfoTabIDs tabID);
|
||||
void visitNexusOrWebPage(const QModelIndex& idx);
|
||||
|
||||
void modRenamed(const QString &oldName, const QString &newName);
|
||||
void modRemoved(const QString &fileName);
|
||||
|
||||
@@ -96,6 +96,7 @@ ConflictListModel::ConflictListModel(QTreeView* tree, std::vector<Column> column
|
||||
|
||||
void ConflictListModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_items.clear();
|
||||
endResetModel();
|
||||
}
|
||||
@@ -129,16 +130,39 @@ int ConflictListModel::columnCount(const QModelIndex&) const
|
||||
return static_cast<int>(m_columns.size());
|
||||
}
|
||||
|
||||
const ConflictItem* ConflictListModel::itemFromIndex(
|
||||
const QModelIndex& index) const
|
||||
{
|
||||
const auto row = index.row();
|
||||
if (row < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto i = static_cast<std::size_t>(row);
|
||||
if (i >= m_items.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return &m_items[i];
|
||||
}
|
||||
|
||||
QModelIndex ConflictListModel::indexFromItem(
|
||||
const ConflictItem* item, int col)
|
||||
{
|
||||
for (std::size_t i=0; i<m_items.size(); ++i) {
|
||||
if (&m_items[i] == item) {
|
||||
return createIndex(static_cast<int>(i), col);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariant ConflictListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole || role == Qt::FontRole) {
|
||||
const auto row = index.row();
|
||||
if (row < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto i = static_cast<std::size_t>(row);
|
||||
if (i >= m_items.size()) {
|
||||
const ConflictItem* item = itemFromIndex(index);
|
||||
if (!item) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -152,12 +176,10 @@ QVariant ConflictListModel::data(const QModelIndex& index, int role) const
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& item = m_items[i];
|
||||
|
||||
if (role == Qt::DisplayRole) {
|
||||
return (item.*m_columns[c].getText)();
|
||||
return (item->*m_columns[c].getText)();
|
||||
} else if (role == Qt::FontRole) {
|
||||
if (item.isArchive()) {
|
||||
if (item->isArchive()) {
|
||||
QFont f = m_tree->font();
|
||||
f.setItalic(true);
|
||||
return f;
|
||||
@@ -191,7 +213,31 @@ void ConflictListModel::sort(int colIndex, Qt::SortOrder order)
|
||||
m_sortColumn = colIndex;
|
||||
m_sortOrder = order;
|
||||
|
||||
emit layoutAboutToBeChanged({}, QAbstractItemModel::VerticalSortHint);
|
||||
|
||||
const auto oldList = persistentIndexList();
|
||||
std::vector<std::pair<const ConflictItem*, int>> oldItems;
|
||||
|
||||
const auto itemCount = oldList.size();
|
||||
oldItems.reserve(static_cast<std::size_t>(itemCount));
|
||||
|
||||
for (int i=0; i<itemCount; ++i) {
|
||||
const QModelIndex& index = oldList[i];
|
||||
oldItems.push_back({itemFromIndex(index), index.column()});
|
||||
}
|
||||
|
||||
doSort();
|
||||
|
||||
QModelIndexList newList;
|
||||
newList.reserve(itemCount);
|
||||
|
||||
for (int i=0; i<itemCount; ++i) {
|
||||
const auto& pair = oldItems[static_cast<std::size_t>(i)];
|
||||
newList.append(indexFromItem(pair.first, pair.second));
|
||||
}
|
||||
|
||||
changePersistentIndexList(oldList, newList);
|
||||
|
||||
emit layoutChanged({}, QAbstractItemModel::VerticalSortHint);
|
||||
}
|
||||
|
||||
@@ -202,8 +248,10 @@ void ConflictListModel::add(ConflictItem item)
|
||||
|
||||
void ConflictListModel::finished()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
doSort();
|
||||
|
||||
sort(m_sortColumn, m_sortOrder);
|
||||
}
|
||||
|
||||
const ConflictItem* ConflictListModel::getItem(std::size_t row) const
|
||||
|
||||
@@ -78,6 +78,9 @@ private:
|
||||
int m_sortColumn;
|
||||
Qt::SortOrder m_sortOrder;
|
||||
|
||||
const ConflictItem* itemFromIndex(const QModelIndex& index) const;
|
||||
QModelIndex indexFromItem(const ConflictItem* item, int col);
|
||||
|
||||
void doSort();
|
||||
};
|
||||
|
||||
|
||||
+232
-227
File diff suppressed because it is too large
Load Diff
@@ -221,7 +221,7 @@ void OrganizerCore::updateExecutablesList()
|
||||
void OrganizerCore::updateModInfoFromDisc() {
|
||||
ModInfo::updateFromDisc(
|
||||
m_Settings.paths().mods(), &m_DirectoryStructure,
|
||||
m_PluginContainer, m_Settings.interface().displayForeign(),
|
||||
m_PluginContainer, m_Settings.interface().displayForeign(),
|
||||
m_Settings.refreshThreadCount(), managedGame());
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ void OrganizerCore::downloadRequestedNXM(const QString &url)
|
||||
}
|
||||
}
|
||||
|
||||
void OrganizerCore::userInterfaceInitialized()
|
||||
void OrganizerCore::userInterfaceInitialized()
|
||||
{
|
||||
m_UserInterfaceInitialized(m_UserInterface->mainWindow());
|
||||
}
|
||||
@@ -1139,7 +1139,7 @@ void OrganizerCore::refreshModList(bool saveChanges)
|
||||
|
||||
ModInfo::updateFromDisc(
|
||||
m_Settings.paths().mods(), &m_DirectoryStructure,
|
||||
m_PluginContainer, m_Settings.interface().displayForeign(),
|
||||
m_PluginContainer, m_Settings.interface().displayForeign(),
|
||||
m_Settings.refreshThreadCount(), managedGame());
|
||||
|
||||
m_CurrentProfile->refreshModStatus();
|
||||
@@ -1473,7 +1473,7 @@ void OrganizerCore::directory_refreshed()
|
||||
m_StructureDeleter.join();
|
||||
}
|
||||
|
||||
m_StructureDeleter = std::thread([=]{
|
||||
m_StructureDeleter = MOShared::startSafeThread([=]{
|
||||
log::debug("structure deleter thread start");
|
||||
delete newStructure;
|
||||
log::debug("structure deleter thread done");
|
||||
|
||||
@@ -65,7 +65,7 @@ private:
|
||||
OrganizerCore *m_Proxied;
|
||||
PluginContainer *m_PluginContainer;
|
||||
|
||||
const QString &m_PluginName;
|
||||
QString m_PluginName;
|
||||
|
||||
};
|
||||
|
||||
|
||||
+22
-3
@@ -1,12 +1,31 @@
|
||||
#ifndef MO2_THREAD_UTILS_H
|
||||
#define MO2_THREAD_UTILS_H
|
||||
|
||||
#include <log.h>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
// in main.cpp
|
||||
void setUnhandledExceptionHandler();
|
||||
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs);
|
||||
|
||||
|
||||
namespace MOShared {
|
||||
|
||||
// starts an std::thread with an unhandled exception handler for core dumps
|
||||
// and a top-level catch
|
||||
//
|
||||
template <class F>
|
||||
std::thread startSafeThread(F&& f)
|
||||
{
|
||||
return std::thread([f=std::forward<F>(f)] {
|
||||
setUnhandledExceptionHandler();
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class that can be used to perform thread-safe memoization.
|
||||
*
|
||||
@@ -26,7 +45,7 @@ struct MemoizedLocked {
|
||||
template <class Callable>
|
||||
MemoizedLocked(Callable &&callable, T value = {}) :
|
||||
m_Fn{ std::forward<Callable>(callable) }, m_Value{ std::move(value) } { }
|
||||
|
||||
|
||||
template <class... Args>
|
||||
T& value(Args&&... args) const {
|
||||
if (m_NeedUpdating) {
|
||||
@@ -66,7 +85,7 @@ private:
|
||||
*
|
||||
*/
|
||||
template <class It, class Callable>
|
||||
void parallelMap(It begin, It end, Callable callable, std::size_t nThreads)
|
||||
void parallelMap(It begin, It end, Callable callable, std::size_t nThreads)
|
||||
{
|
||||
std::mutex m;
|
||||
std::vector<std::thread> threads(nThreads);
|
||||
@@ -75,7 +94,7 @@ void parallelMap(It begin, It end, Callable callable, std::size_t nThreads)
|
||||
// - The mutex is only used to fetch/increment the iterator.
|
||||
// - The callable is copied in each thread to avoid conflicts.
|
||||
for (auto &thread: threads) {
|
||||
thread = std::thread([&m, &begin, end, callable]() {
|
||||
thread = startSafeThread([&m, &begin, end, callable]() {
|
||||
while (true) {
|
||||
decltype(begin) it;
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user