Compare commits

...
61 Commits
Author SHA1 Message Date
Chris Bessent 2a5dd6c7cf Bump to version 2.4.2 2021-05-11 06:25:20 -07:00
Chris Bessent ce2450885c Bump version to 2.4.2rc1.1 2021-05-01 19:38:14 -07:00
Chris Bessent d78cb58593 Update translation file 2021-05-01 17:53:37 -07:00
Chris Bessent 2c54d128b4 Add some more Microsoft Store errors 2021-05-01 17:10:27 -07:00
Chris Bessent f71288db16 Bump to 2.4.2 RC 1 2021-05-01 15:23:16 -07:00
Chris Bessent 8bac17c2c5 Merge pull request #1508 from LostDragonist/gamepass
Add log for Microsoft Store / Gamepass installs
2021-05-01 15:21:45 -07:00
Chris Bessent 4509911a8e Add log for Microsoft Store / Gamepass installs 2021-05-01 15:18:15 -07:00
Chris Bessent f098491306 Merge pull request #1503 from LostDragonist/filename
Sanitize download file names
2021-05-01 14:38:17 -07:00
Chris Bessent bcaceb9637 Merge pull request #1504 from LostDragonist/skipcwd
Ignore "." and ".." in archives
2021-05-01 14:38:10 -07:00
Chris Bessent b7e8e69f47 Merge pull request #1507 from LostDragonist/tutorials
Fix up tutorials
2021-05-01 14:37:59 -07:00
Chris Bessent a8bcbf6555 Fix up tutorials 2021-04-30 08:41:29 -07:00
Chris Bessent aafd523736 Merge pull request #1505 from LostDragonist/foresto
Add foresto as a contributor
2021-04-29 02:02:19 -07:00
Chris Bessent 4a9da205cd Merge pull request #1485 from foresto/patch-1
Reduce log level of harmless failures
2021-04-29 02:02:05 -07:00
Chris Bessent 68e7820154 Add foresto as a contributor 2021-04-29 02:01:38 -07:00
Chris Bessent 3c6190d713 Merge pull request #1470 from isanae/filtered-nav-crash
Crash for next/previous in mod info dialog
2021-04-29 01:59:05 -07:00
Chris Bessent 1197b562f6 Merge pull request #1496 from LostDragonist/nexus_links
Change the "Visit Nexus" button to support alternate sources
2021-04-29 01:57:13 -07:00
Chris Bessent 7df5329ea0 Merge pull request #1497 from LostDragonist/profile_fix
Fix selecting the profile in the dialog
2021-04-29 01:56:52 -07:00
Chris Bessent dc3dcd9500 Fix fixDirectoryName includes 2021-04-29 01:50:23 -07:00
Chris Bessent cc8fb9bd23 Ignore "." and ".." in archives 2021-04-29 01:29:38 -07:00
Chris Bessent 9638a797d1 Sanitize download file names
Also switch instance name sanitizer to MOBase version
2021-04-29 00:50:11 -07:00
Chris Bessent 8308dfcbff Fix selecting the profile in the dialog
Previously, when clicking the "select" button in the profiles
dialog (when the dialog was launched in the menu or toolbar),
everything would be refreshed without checking the selected
profile in the dialog.

Now, the selected profile is set with some signal blocking to
help prevent redundant refreshes from occurring.
2021-04-25 05:09:58 -07:00
Chris Bessent dfbaed0ade Add a separator after the primary game and sort the secondary games 2021-04-25 04:45:35 -07:00
Chris Bessent e475893ff4 Change the "Visit Nexus" button to support alternate sources
If a game plugin supports more than one Nexus site for downloads,
the "Visit Nexus" button will be turned into a drop-down that
lets you select each site.
2021-04-25 04:13:12 -07:00
Al 40a4bf1e3a Update issue templates 2021-04-08 16:51:11 +02:00
Forest 3ee2598533 Reduce log level of harmless failures
These two messages (along with their glaring red X icons) are cluttering up the log on linux/wine systems, misleading people who are trying to diagnose real problems. Let's reduce their log priority to make it clear that they're harmless.

As mentioned here:
https://github.com/ModOrganizer2/modorganizer/issues/372#issuecomment-575231673
2021-04-05 23:10:54 -07:00
isanae 234ee923fe model() can return null for invalid indexes
happens when looking at the mod info for a mod filtered out of the mod list
2021-03-20 14:51:44 -04:00
Mikaël Capelle 88ee41ae85 Bump to 2.4.1. 2021-03-20 17:03:36 +01:00
Mikaël Capelle 8a29bc5110 Bump to 2.4.1 RC2. 2021-03-14 20:08:58 +01:00
Mikaël Capelle 77dbd7ff31 Merge pull request #1465 from Holt59/fix-saves-tab-header
Stretch last section in the Saves tab.
2021-03-14 20:08:30 +01:00
Mikaël Capelle 7fab050adb Stretch last section in the Saves tab. 2021-03-14 20:07:09 +01:00
Mikaël Capelle b547045203 Bump to 2.4.1rc1.1. 2021-03-14 20:00:00 +01:00
Mikaël Capelle fcd2e1473f Merge pull request #1462 from Holt59/fix-stylesheets-qscrollarea
Remove border on QScrollArea from stylesheets.
2021-03-13 16:06:18 +01:00
Mikaël Capelle 287dd6d805 Remove border on QScrollArea from stylesheets. 2021-03-13 16:05:43 +01:00
Mikaël Capelle 717c2ca9cf Bump to 2.4.1rc1 and update translations. 2021-03-12 20:51:46 +01:00
Mikaël Capelle abd1a11768 Merge pull request #1449 from isanae/uilocker-disabled
Disabled uilocker for child dialogs
2021-03-03 18:52:55 +01:00
Mikaël Capelle 34636301ea Merge pull request #1447 from LostDragonist/settings_resize
Add scrollbars to some settings tabs to reduce minimum size
2021-03-03 18:52:46 +01:00
Mikaël Capelle f298ea0ae3 Merge pull request #1443 from Holt59/fix-savegame-name
Display savegame name and filepath in saves tab.
2021-03-03 18:52:28 +01:00
Chris Bessent eee1b4b3f1 Put stylesheet stuff into .ui file instead 2021-03-02 21:20:23 -07:00
isanae 00120605fb don't disable dialogs that are children of other dialogs 2021-03-02 14:41:23 -05:00
Mikaël Capelle 32088b47f1 Merge pull request #1446 from LostDragonist/disabled_plugins
Allow plugins to disable themselves by default
2021-03-02 19:27:24 +01:00
Chris Bessent 726b381ea7 Add scrollbars to some settings tabs to reduce minimum size
The previous minimum size was around 450 x 560px.  At a DPI scaling
of 200%, this means 900 x 1120px which extends off the typical 1080p
screen.  Why is someone using 200% scaling on a 1080p screen?  I have
no clue.

Now, the minimum size is around 450 x 410px for 900 x 820 px at 200%
scaling.  Which works for anything short of a 1376 x 768 screen at
200% scaling which I just don't really care about.
2021-03-02 04:07:46 -07:00
Chris Bessent 071550f7fb Allow plugins to disable themselves by default 2021-03-02 02:41:16 -07:00
Mikaël Capelle f8a50edd0f Merge pull request #1444 from LostDragonist/lockedorder
Fix load order when using locked plugins
2021-03-01 19:21:24 +01:00
Mikaël Capelle 08c7113ecb Merge pull request #1439 from Holt59/fix-downloadlist
Fix download list sorting.
2021-03-01 19:21:15 +01:00
Mikaël Capelle d436fe8cb3 Merge pull request #1442 from Holt59/fix-createempty-mod
Fix priority for 'Create Empty Mod'.
2021-03-01 19:21:06 +01:00
Chris Bessent d5183ebe6b Fix load order when using locked plugins 2021-02-27 18:50:45 -07:00
Mikaël Capelle c72326ee4b Display savegame name and filepath in saves tab. 2021-02-27 12:42:37 +01:00
Mikaël Capelle 75e90f2f3f Fix priority for 'Create Empty Mod'. 2021-02-27 10:36:17 +01:00
Mikaël Capelle be012715f3 Fix download list sorting. 2021-02-26 21:32:39 +01:00
Mikaël Capelle 2b86a14c86 Merge pull request #1437 from Holt59/fix-sendtoseparator
Fix 'Send to separator... ' in descending priority.
2021-02-26 21:12:04 +01:00
Mikaël Capelle c96c01f40e Fix 'Send to separator... ' in descending priority. 2021-02-24 20:07:55 +01:00
Mikaël Capelle d6edc5d6b7 Merge pull request #1433 from Holt59/fix-bycategory-sorting
Fix sorting by priority when grouping by priority.
2021-02-24 18:35:18 +01:00
isanae 4040dc12b7 Merge pull request #1436 from isanae/tutorial-fix
Fix tutorial stuck on mod info dialog
2021-02-24 05:46:48 -05:00
isanae d4fc7c3de1 emit modInfoDisplayed, it's required by the tutorial 2021-02-24 04:57:54 -05:00
Mikaël Capelle 27c4861907 Fix sorting by priority when grouping by priority. 2021-02-23 19:16:18 +01:00
Mikaël Capelle 8decb9df68 Merge pull request #1431 from Holt59/fix-update-filter
Switch back to invalidating filter when receiving mod updates.
2021-02-23 17:23:08 +01:00
Mikaël Capelle 8365f26d83 Merge pull request #1424 from isanae/dl-share
Network shares for envfs
2021-02-23 17:19:53 +01:00
Mikaël Capelle 0ca21d9a5e Merge pull request #1429 from Holt59/fix-stylesheets
Fix stylesheets for 2.4.
2021-02-23 17:19:43 +01:00
Mikaël Capelle f60d0e5ec6 Fix stylesheets for 2.4. 2021-02-23 09:38:52 +01:00
isanae d22d77d921 handle network shares for NtOpenFile()
changed error messages to use formatNtMessage(), which handles NTSTATUS errors
2021-02-22 18:28:07 -05:00
Mikaël Capelle 010a5d80d6 Switch back to invalidating filter when receiving mod updates. 2021-02-21 11:12:16 +01:00
50 changed files with 2210 additions and 1853 deletions
@@ -2,7 +2,7 @@
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'
labels: 'Feature Request, additional games support, area: mo2 game plugins'
assignees: ''
---
@@ -18,7 +18,7 @@ assignees: ''
- **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).
- **Data path:** Path to the data folder of the game (relative to the game folder). Please note that the Virtual Files System often does not work for top level dlls or exe files.
- **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
+5
View File
@@ -469,6 +469,11 @@
<string notr="true">Drew Warwick</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">foresto</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">GamerPoet</string>
+13 -6
View File
@@ -158,14 +158,14 @@ protected:
currentName = std::get<0>(p)[0];
}
// If the name is different, we need to create a directory from what we have
// If the name is different, we need to create a directory from what we have
// accumulated:
if (currentName != std::get<0>(p)[0]) {
// We may or may not have an index here, it depends on the type of archive (some archives list
// intermediate non-empty folders, some don't):
entries.push_back(std::make_shared<ArchiveFileTreeImpl>(parent, currentName, currentIndex, std::move(currentFiles)));
currentFiles.clear(); // Back to a valid state.
// Reset the index:
@@ -200,7 +200,7 @@ protected:
if (currentName != "") {
entries.push_back(std::make_shared<ArchiveFileTreeImpl>(parent, currentName, currentIndex, std::move(currentFiles)));
}
// Let the parent class sort the entries:
return false;
}
@@ -214,7 +214,7 @@ private:
mutable std::vector<File> m_Files;
};
std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archive)
std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archive)
{
auto const& data = archive.getFileList();
@@ -222,9 +222,16 @@ std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archiv
files.reserve(data.size());
for (size_t i = 0; i < data.size(); ++i) {
// Ignore "." and ".." as they're useless and muck things up
if (data[i]->getArchiveFilePath().compare(L".") == 0 ||
data[i]->getArchiveFilePath().compare(L"..") == 0)
{
continue;
}
files.push_back(std::make_tuple(
QString::fromStdWString(data[i]->getArchiveFilePath()).replace("\\", "/").split("/", Qt::SkipEmptyParts),
data[i]->isDirectory(),
QString::fromStdWString(data[i]->getArchiveFilePath()).replace("\\", "/").split("/", Qt::SkipEmptyParts),
data[i]->isDirectory(),
(int) i));
}
+49 -4
View File
@@ -8,6 +8,7 @@
#include <iplugingame.h>
#include <report.h>
#include <utility.h>
#include "filesystemutilities.h"
namespace cid
{
@@ -292,7 +293,7 @@ void GamePage::select(IPluginGame* game, const QString& dir)
Game* checked = findGame(game);
if (checked) {
if (!checked->installed) {
if (!checked->installed || (detectMicrosoftStore(checked->dir) && !confirmMicrosoftStore(checked->dir, checked->game))) {
if (dir.isEmpty()) {
// the selected game has no installation directory and none was given,
// ask the user
@@ -304,6 +305,9 @@ void GamePage::select(IPluginGame* game, const QString& dir)
if (path.isEmpty()) {
// cancelled
checked = nullptr;
} else if (detectMicrosoftStore(path) && !confirmMicrosoftStore(path, game)) {
// cancelled
checked = nullptr;
} else {
// check whether a plugin supports the given directory; this can
// return the same plugin, a different one, or null
@@ -353,6 +357,13 @@ void GamePage::selectCustom()
return;
}
// Microsoft store games are not supported
if (detectMicrosoftStore(path) && !confirmMicrosoftStore(path, nullptr)) {
// reselect the previous button
selectButton(m_selection);
return;
}
// try to find a plugin that likes this directory
for (auto& g : m_games) {
if (g->game->looksValid(path)) {
@@ -593,6 +604,11 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
if (detectMicrosoftStore(path) && confirmMicrosoftStore(path, g->game)) {
// okay
return g;
}
// the selected game can't use that folder, find another one
IPluginGame* otherGame = nullptr;
@@ -639,6 +655,35 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
bool GamePage::detectMicrosoftStore(const QString& path)
{
return path.contains("/ModifiableWindowsApps/") ||
path.contains("/WindowsApps/");
}
bool GamePage::confirmMicrosoftStore(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
.title(QObject::tr("Microsoft Store game"))
.main(QObject::tr("Microsoft Store game"))
.content(QObject::tr(
"The folder %1 seems to be a Microsoft Store game install. Games"
" installed through the Microsoft Store are not supported by Mod Organizer"
" and will not work properly.")
.arg(path))
.button({
game ? QObject::tr("Use this folder for %1").arg(game->gameName())
: QObject::tr("Use this folder"),
QObject::tr("I know what I'm doing"),
QMessageBox::Ignore})
.button({
QObject::tr("Cancel"),
QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Ignore);
}
bool GamePage::confirmUnknown(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
@@ -856,7 +901,7 @@ QString NamePage::selectedInstanceName() const
}
const auto text = ui->instanceName->text().trimmed();
return InstanceManager::singleton().sanitizeInstanceName(text);
return MOBase::sanitizeFileName(text);
}
void NamePage::onChanged()
@@ -883,7 +928,7 @@ bool NamePage::checkName(QString parentDir, QString name)
if (name.isEmpty()) {
empty = true;
} else {
if (InstanceManager::singleton().validInstanceName(name)) {
if (MOBase::validFileName(name)) {
exists = QDir(parentDir).exists(name);
} else {
invalid = true;
@@ -1108,7 +1153,7 @@ bool PathsPage::checkPath(
} else {
const QDir d(path);
if (m.validInstanceName(d.dirName())) {
if (MOBase::validFileName(d.dirName())) {
if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) {
// the default data path for a portable instance is the application
// directory, so it's not an error if it exists
+9
View File
@@ -354,6 +354,15 @@ private:
MOBase::IPluginGame* confirmOtherGame(
const QString& path,
MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame);
// detects if the given path likely contains a Microsoft Store game
//
bool detectMicrosoftStore(const QString& path);
// tells the user that the path probably contains a Microsoft Store game that
// is not supported, returns true if the user decides to accept anyway.
//
bool confirmMicrosoftStore(const QString& path, MOBase::IPluginGame* game);
};
+2 -2
View File
@@ -226,7 +226,7 @@ bool DownloadList::lessThanPredicate(const QModelIndex &left, const QModelIndex
if ((leftIndex < m_manager.numTotalDownloads())
&& (rightIndex < m_manager.numTotalDownloads())) {
if (left.column() == DownloadList::COL_NAME) {
return m_manager.getFileName(left.row()).compare(m_manager.getFileName(right.row()), Qt::CaseInsensitive) < 0;
return left.data(Qt::DisplayRole).toString().compare(right.data(Qt::DisplayRole).toString(), Qt::CaseInsensitive) < 0;
} else if (left.column() == DownloadList::COL_MODNAME) {
QString leftName, rightName;
@@ -275,7 +275,7 @@ bool DownloadList::lessThanPredicate(const QModelIndex &left, const QModelIndex
if (leftState == rightState)
return m_manager.getFileTime(left.row()) < m_manager.getFileTime(right.row());
else
return leftState > rightState;
return leftState < rightState;
} else if (left.column() == DownloadList::COL_SIZE) {
return m_manager.getFileSize(left.row()) < m_manager.getFileSize(right.row());
} else if (left.column() == DownloadList::COL_FILETIME) {
+3 -2
View File
@@ -33,6 +33,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "shared/util.h"
#include <utility.h>
#include <report.h>
#include "filesystemutilities.h"
#include <QTimer>
#include <QFileInfo>
@@ -1456,7 +1457,7 @@ void DownloadManager::markUninstalled(QString fileName)
QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const
{
QString fullPath = m_OutputDirectory + "/" + baseName;
QString fullPath = m_OutputDirectory + "/" + MOBase::sanitizeFileName(baseName);
if (QFile::exists(fullPath) && rename) {
int i = 1;
while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) {
@@ -1476,7 +1477,7 @@ QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply)
std::cmatch result;
if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) {
return QString::fromUtf8(result.str(1).c_str());
return MOBase::sanitizeFileName(QString::fromUtf8(result.str(1).c_str()));
}
}
+23 -5
View File
@@ -232,8 +232,8 @@ void forEachEntryImpl(
if (status < 0) {
log::error(
"NtOpenFile() failed for '{}', {}",
toString(poa), formatSystemMessage(status));
"failed to open directory '{}': {}",
toString(poa), formatNtMessage(status));
return;
}
@@ -264,8 +264,9 @@ void forEachEntryImpl(
break;
} else if (status < 0) {
log::error(
"NtQueryDirectoryFile() failed for '{}', {}",
toString(poa), formatSystemMessage(status));
"failed to read directory '{}': {}",
toString(poa), formatNtMessage(status));
break;
}
@@ -321,6 +322,23 @@ void forEachEntryImpl(
}
}
std::wstring makeNtPath(const std::wstring& path)
{
constexpr const wchar_t* nt_prefix = L"\\??\\";
constexpr const wchar_t* nt_unc_prefix = L"\\??\\UNC\\";
constexpr const wchar_t* share_prefix = L"\\\\";
if (path.starts_with(nt_prefix)) {
// already an nt path
return path;
} else if (path.starts_with(share_prefix)) {
// network shared need \??\UNC\ as a prefix
return nt_unc_prefix + path.substr(2);
} else {
// prepend the \??\ prefix
return nt_prefix + path;
}
}
void DirectoryWalker::forEachEntry(
const std::wstring& path, void* cx,
@@ -335,7 +353,7 @@ void DirectoryWalker::forEachEntry(
NtClose = (NtClose_type)::GetProcAddress(m.get(), "NtClose");
}
const std::wstring ntpath = std::wstring(L"\\??\\") + path;
const std::wstring ntpath = makeNtPath(path);
UNICODE_STRING ObjectName = {};
ObjectName.Buffer = const_cast<wchar_t*>(ntpath.c_str());
+2 -2
View File
@@ -130,7 +130,7 @@ Module::FileInfo Module::getFileInfo() const
return {};
}
log::error(
log::debug(
"GetFileVersionInfoSizeW() failed on '{}', {}",
m_path, formatSystemMessage(e));
@@ -268,7 +268,7 @@ QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
if (h.get() == INVALID_HANDLE_VALUE) {
const auto e = GetLastError();
log::error(
log::debug(
"can't open file '{}' for timestamp, {}",
m_path, formatSystemMessage(e));
+1 -1
View File
@@ -21,7 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "installationmanager.h"
#include "utility.h"
#include "filesystemutilities.h"
#include "report.h"
#include "categories.h"
#include "questionboxmemory.h"
+2 -30
View File
@@ -32,6 +32,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <iplugingame.h>
#include <utility.h>
#include <log.h>
#include "filesystemutilities.h"
#include <QCoreApplication>
#include <QDir>
@@ -721,7 +722,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
QString InstanceManager::makeUniqueName(const QString& instanceName) const
{
const QString sanitized = sanitizeInstanceName(instanceName);
const QString sanitized = MOBase::sanitizeFileName(instanceName);
// trying "name (N)"
QString name = sanitized;
@@ -742,35 +743,6 @@ bool InstanceManager::instanceExists(const QString& instanceName) const
return root.exists(instanceName);
}
QString InstanceManager::sanitizeInstanceName(const QString &name) const
{
QString new_name = name;
// Restrict the allowed characters
new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]"));
// Don't end in spaces and periods
new_name = new_name.remove(QRegExp("\\.*$"));
new_name = new_name.remove(QRegExp(" *$"));
// Recurse until stuff stops changing
if (new_name != name) {
return sanitizeInstanceName(new_name);
}
return new_name;
}
bool InstanceManager::validInstanceName(const QString& instanceName) const
{
if (instanceName.isEmpty()) {
return false;
}
return (instanceName == sanitizeInstanceName(instanceName));
}
std::unique_ptr<Instance> selectInstance()
{
auto& m = InstanceManager::singleton();
-10
View File
@@ -305,10 +305,6 @@ public:
//
std::vector<QString> globalInstancePaths() const;
// returns `name` modified so that it is a valid instance name
//
QString sanitizeInstanceName(const QString &name) const;
// sanitizes the given instance name and either
// 1) returns it if there is no instance with this name
// 2) tries to add " (N)" at the end until it works
@@ -321,12 +317,6 @@ public:
//
bool instanceExists(const QString& instanceName) const;
// returns whether the given instance name would be a valid name; this does
// not check whether the instance already exists, it's basiscally just a check
// against what sanitizeInstanceName() returns
//
bool validInstanceName(const QString& instanceName) const;
// returns the absolute path of a global instance with the given name; this
// does not check if the name is valid or if exists
//
+4 -3
View File
@@ -10,6 +10,7 @@
#include <utility.h>
#include <report.h>
#include <iplugingame.h>
#include "filesystemutilities.h"
using namespace MOBase;
@@ -109,10 +110,10 @@ QString getInstanceName(
if (text->text().isEmpty()) {
error->setText("");
} else if (!m.validInstanceName(text->text())) {
} else if (!MOBase::validFileName(text->text())) {
error->setText(QObject::tr("The instance name must be a valid folder name."));
} else {
const auto name = m.sanitizeInstanceName(text->text());
const auto name = MOBase::sanitizeFileName(text->text());
if ((name != oldName) && m.instanceExists(text->text())) {
error->setText(QObject::tr("An instance with this name already exists."));
@@ -136,7 +137,7 @@ QString getInstanceName(
return {};
}
return m.sanitizeInstanceName(text->text());
return MOBase::sanitizeFileName(text->text());
}
+74 -23
View File
@@ -351,6 +351,7 @@ MainWindow::MainWindow(Settings &settings
}
settings.geometry().restoreState(ui->downloadView->header());
settings.geometry().restoreState(ui->savegameList->header());
ui->splitter->setStretchFactor(0, 3);
ui->splitter->setStretchFactor(1, 2);
@@ -512,6 +513,8 @@ void MainWindow::setupModList()
connect(&ui->modList->actions(), &ModListViewActions::overwriteCleared, [=]() { scheduleCheckForProblems(); });
connect(&ui->modList->actions(), &ModListViewActions::originModified, this, &MainWindow::originModified);
connect(&ui->modList->actions(), &ModListViewActions::modInfoDisplayed, this, &MainWindow::modInfoDisplayed);
connect(m_OrganizerCore.modList(), &ModList::modPrioritiesChanged, [&]() { m_ArchiveListWriter.write(); });
}
@@ -1437,12 +1440,36 @@ void MainWindow::registerModPage(IPluginModPage *modPage)
ui->actionModPage->menu()->addAction(action);
}
bool MainWindow::registerNexusPage(const QString& gameName)
{
// Get the plugin
IPluginGame* plugin = m_OrganizerCore.getGame(gameName);
if (plugin == nullptr)
return false;
// Create an action
QAction* action = new QAction(
plugin->gameIcon(),
QObject::tr("Visit %1 on Nexus").arg(plugin->gameName()),
this);
// Bind the action
connect(action, &QAction::triggered, this, [this, gameName]() {
shell::Open(QUrl(NexusInterface::instance().getGameURL(gameName)));
}, Qt::QueuedConnection);
// Add the action
ui->actionModPage->menu()->addAction(action);
return true;
}
void MainWindow::updateModPageMenu()
{
// Clear the menu:
ui->actionModPage->menu()->clear();
ui->actionModPage->menu()->addAction(ui->actionNexus);
// Determine the loaded mod page plugins
std::vector<IPluginModPage*> modPagePlugins = m_PluginContainer.plugins<IPluginModPage>();
// Sort the plugins by display name
@@ -1463,14 +1490,30 @@ void MainWindow::updateModPageMenu()
registerModPage(modPagePlugin);
}
// Add the primary game (with a separator)
registerNexusPage(m_OrganizerCore.managedGame()->gameShortName());
ui->actionModPage->menu()->addSeparator();
// Add the secondary games (sorted)
bool secondaryGameAdded = false;
QStringList secondaryGames = m_OrganizerCore.managedGame()->validShortNames();
secondaryGames.sort(Qt::CaseInsensitive);
for (auto gameName : secondaryGames)
{
if (registerNexusPage(gameName)) {
secondaryGameAdded = true;
}
}
// No mod page plugin and the menu was visible:
if (modPagePlugins.empty()) {
bool keepOriginalAction = modPagePlugins.size() == 0 && !secondaryGameAdded;
if (keepOriginalAction) {
ui->toolBar->insertAction(ui->actionAdd_Profile, ui->actionNexus);
}
else {
ui->toolBar->removeAction(ui->actionNexus);
}
ui->actionModPage->setVisible(!modPagePlugins.empty());
ui->actionModPage->setVisible(!keepOriginalAction);
}
void MainWindow::startExeAction()
@@ -2003,6 +2046,7 @@ void MainWindow::storeSettings()
s.geometry().saveState(ui->espList->header());
s.geometry().saveState(ui->downloadView->header());
s.geometry().saveState(ui->savegameList->header());
s.widgets().saveIndex(ui->executablesListBox);
s.widgets().saveIndex(ui->tabWidget);
@@ -2144,6 +2188,15 @@ void MainWindow::on_actionAdd_Profile_triggered()
profilesDialog.exec();
m_SavesTab->refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
if (profilesDialog.selectedProfile())
{
// Change profile while blocking signals to prevent extra signals being sent
// Doesn't matter much as refreshProfiles() is being called after this
ui->profileBox->blockSignals(true);
ui->profileBox->setCurrentText(profilesDialog.selectedProfile().value());
ui->profileBox->blockSignals(false);
}
if (refreshProfiles() && !profilesDialog.failed()) {
break;
}
@@ -2819,41 +2872,37 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVa
}
QVariantList resultList = resultData.toList();
QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> *watcher = new QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>>();
QObject::connect(watcher, &QFutureWatcher<std::set<QSharedPointer<ModInfo>>>::finished, this, &MainWindow::finishUpdateInfo);
QFuture<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> future = QtConcurrent::run([=]() -> std::pair<QString, std::set<QSharedPointer<ModInfo>>> {
return std::make_pair(gameNameReal, ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true));
auto* watcher = new QFutureWatcher<NxmUpdateInfoData>();
QObject::connect(watcher, &QFutureWatcher<NxmUpdateInfoData>::finished, [this, watcher]() {
finishUpdateInfo(watcher->result());
watcher->deleteLater();
});
auto future = QtConcurrent::run([=]() {
return NxmUpdateInfoData{ gameNameReal, ModInfo::filteredMods(gameNameReal, resultList, userData.toBool(), true) };
});
watcher->setFuture(future);
ui->modList->invalidateFilter();
}
void MainWindow::finishUpdateInfo()
void MainWindow::finishUpdateInfo(const NxmUpdateInfoData& data)
{
QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> *watcher = static_cast<QFutureWatcher<std::pair<QString, std::set<QSharedPointer<ModInfo>>>> *>(sender());
QString game = watcher->result().first;
auto finalMods = watcher->result().second;
if (finalMods.empty()) {
log::info("{}", tr("None of your %1 mods appear to have had recent file updates.").arg(game));
if (data.finalMods.empty()) {
log::info("{}", tr("None of your %1 mods appear to have had recent file updates.").arg(data.game));
}
std::set<std::pair<QString, int>> organizedGames;
for (auto mod : finalMods) {
for (auto& mod : data.finalMods) {
if (mod->canBeUpdated()) {
organizedGames.insert(std::make_pair<QString, int>(mod->gameName().toLower(), mod->nexusId()));
}
m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name()));
}
if (!finalMods.empty() && organizedGames.empty())
if (!data.finalMods.empty() && organizedGames.empty())
log::warn("{}", tr("All of your mods have been checked recently. We restrict update checks to help preserve your available API requests."));
for (auto game : organizedGames)
for (const auto& game : organizedGames) {
NexusInterface::instance().requestUpdates(game.second, this, QVariant(), game.first, QString());
disconnect(sender());
delete sender();
}
}
void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
@@ -2928,13 +2977,15 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD
if (foundUpdate) {
// Just get the standard data updates for endorsements and descriptions
mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc());
m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name()));
} else {
// Scrape mod data here so we can use the mod version if no file update was located
requiresInfo = true;
}
}
// invalidate the filter to display mods with an update
ui->modList->invalidateFilter();
if (requiresInfo)
NexusInterface::instance().requestModInfo(gameNameReal, modID, this, QVariant(), QString());
}
+13 -2
View File
@@ -143,6 +143,9 @@ public slots:
void refresherProgress(const DirectoryRefreshProgress* p);
signals:
// emitted after the information dialog has been closed, used by tutorials
//
void modInfoDisplayed();
/**
* @brief emitted when the selected style changes
@@ -184,6 +187,7 @@ private:
void setToolbarButtonStyle(Qt::ToolButtonStyle s);
void registerModPage(MOBase::IPluginModPage* modPage);
bool registerNexusPage(const QString& gameName);
void registerPluginTool(MOBase::IPluginTool* tool, QString name = QString(), QMenu* menu = nullptr);
void updateToolbarMenu();
@@ -224,6 +228,13 @@ private:
void toggleMO2EndorseState();
void toggleUpdateAction();
// update info
struct NxmUpdateInfoData {
QString game;
std::set<ModInfo::Ptr> finalMods;
};
void finishUpdateInfo(const NxmUpdateInfoData& data);
private:
static const char *PATTERN_BACKUP_GLOB;
@@ -331,10 +342,10 @@ private slots:
void modInstalled(const QString &modName);
void finishUpdateInfo();
// update info
void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID);
void nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int);
void nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int requestID);
void nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
void nxmModInfoAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
void nxmEndorsementToggled(QString, int, QVariant, QVariant resultData, int);
+39 -2
View File
@@ -1199,7 +1199,7 @@ p, li { white-space: pre-wrap; }
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="savegameList">
<widget class="QTreeWidget" name="savegameList">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
@@ -1215,6 +1215,9 @@ p, li { white-space: pre-wrap; }
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:8pt;&quot;&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;If you click &amp;quot;Fix Mods...&amp;quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustIgnored</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
@@ -1224,9 +1227,43 @@ p, li { white-space: pre-wrap; }
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="uniformItemSizes">
<property name="indentation">
<number>0</number>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="headerCascadingSectionResizes">
<bool>false</bool>
</attribute>
<attribute name="headerStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Name</string>
</property>
<property name="textAlignment">
<set>AlignLeading|AlignVCenter</set>
</property>
</column>
<column>
<property name="text">
<string>File</string>
</property>
<property name="textAlignment">
<set>AlignLeading|AlignVCenter</set>
</property>
</column>
</widget>
</item>
</layout>
+1 -1
View File
@@ -34,7 +34,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "shared/filesorigin.h"
#include "shared/appconfig.h"
#include <utility.h>
#include "filesystemutilities.h"
#include <report.h>
#include <QFileInfo>
+6
View File
@@ -108,6 +108,12 @@ bool ModListSortProxy::lessThan(const QModelIndex &left,
if (sortColumn() != ModList::COL_PRIORITY) {
return QSortFilterProxyModel::lessThan(left, right);
}
else if (qobject_cast<QtGroupingProxy*>(sourceModel())) {
// if the underlying proxy is a QtGroupingProxy we need to rely on
// Qt::DisplayRole because the other roles are not correctly handled
// by that kind of proxy
return left.data(Qt::DisplayRole).toInt() < right.data(Qt::DisplayRole).toInt();
}
}
bool lOk, rOk;
+13 -1
View File
@@ -5,7 +5,7 @@
#include <widgetutility.h>
#include <utility.h>
#include "filesystemutilities.h"
#include <report.h>
#include "ui_mainwindow.h"
@@ -299,6 +299,11 @@ std::optional<unsigned int> ModListView::prevMod(unsigned int modIndex) const
return {};
}
void ModListView::invalidateFilter()
{
m_sortProxy->invalidate();
}
void ModListView::setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria)
{
m_sortProxy->setCriteria(criteria);
@@ -342,6 +347,9 @@ QModelIndexList ModListView::indexViewToModel(const QModelIndexList& index) cons
QModelIndex ModListView::nextIndex(const QModelIndex& index) const
{
auto* model = index.model();
if (!model) {
return {};
}
if (model->rowCount(index) > 0) {
return model->index(0, index.column(), index);
@@ -367,6 +375,10 @@ QModelIndex ModListView::prevIndex(const QModelIndex& index) const
}
auto* model = index.model();
if (!model) {
return {};
}
auto prev = model->index((index.row() - 1) % model->rowCount(index.parent()), index.column(), index.parent());
if (model->rowCount(prev) > 0) {
+4
View File
@@ -112,6 +112,10 @@ signals:
public slots:
// invalidate (refresh) the filter (similar to a layout changed event)
//
void invalidateFilter();
// set the filter criteria/options for mods
//
void setFilterCriteria(const std::vector<ModListSortProxy::Criteria>& criteria);

Some files were not shown because too many files have changed in this diff Show More