From a6153f631d2334273ce28b1ff43b03cd0da0e9ec Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 18 Jun 2025 14:40:56 +1000 Subject: [PATCH 01/43] Add Character Token system and column to Group Manager with image caching - Introduced a new CHARACTER_TOKEN column to the Group Manager table. - Integrated TokenManager for resolving and loading character token images. - Implemented fallback logic and QPixmapCache in TokenManager to reduce redundant image loading. - Added GroupDelegate logic to render icons, with default size set to 32x32 pixels. - Suppressed repeated debug output now that caching is effective. - Layout and header adjusted to support the new Icon column without disrupting existing columns. Foundation laid for future enhancements, including token toggle and dynamic resizing. --- src/CMakeLists.txt | 2 + src/group/CGroupChar.cpp | 10 +++ src/group/CGroupChar.h | 15 ++++ src/group/groupwidget.cpp | 33 ++++++-- src/group/groupwidget.h | 5 ++ src/group/tokenmanager.cpp | 157 +++++++++++++++++++++++++++++++++++++ src/group/tokenmanager.h | 26 ++++++ 7 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 src/group/tokenmanager.cpp create mode 100644 src/group/tokenmanager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 585e85b69..1bcbe2295 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -218,6 +218,8 @@ set(mmapper_SRCS group/mmapper2character.h group/mmapper2group.cpp group/mmapper2group.h + group/tokenmanager.cpp + group/tokenmanager.h logger/autologger.cpp logger/autologger.h mainwindow/DescriptionWidget.cpp diff --git a/src/group/CGroupChar.cpp b/src/group/CGroupChar.cpp index 832adcc59..c22e15451 100644 --- a/src/group/CGroupChar.cpp +++ b/src/group/CGroupChar.cpp @@ -326,3 +326,13 @@ bool CGroupChar::setScore(const QString &textHP, const QString &textMana, const #undef X_SCORE return updated; } + +QString CGroupChar::getDisplayName() const +{ + if (getLabel().isEmpty() + || getName().getStdStringViewUtf8() == getLabel().getStdStringViewUtf8()) { + return getName().toQString(); + } else { + return QString("%1 (%2)").arg(getName().toQString(), getLabel().toQString()); + } +} diff --git a/src/group/CGroupChar.h b/src/group/CGroupChar.h index c93b72ad2..1fa50f77f 100644 --- a/src/group/CGroupChar.h +++ b/src/group/CGroupChar.h @@ -61,6 +61,9 @@ class NODISCARD CGroupChar final : public std::enable_shared_from_this #include +<<<<<<< HEAD +======= +static constexpr const int GROUP_COLUMN_COUNT = 10; +static_assert(GROUP_COLUMN_COUNT == static_cast(GroupModel::ColumnTypeEnum::ROOM_NAME) + 1, + "# of columns"); + +>>>>>>> dabeb884 (Add Character Token system and column to Group Manager with image caching) static constexpr const char *GROUP_MIME_TYPE = "application/vnd.mm_groupchar.row"; namespace { // anonymous @@ -467,8 +474,17 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, // Map column to data switch (role) { + case Qt::DecorationRole: + if (column == ColumnTypeEnum::CHARACTER_TOKEN && m_tokenManager) { + QString key = character.getDisplayName(); // Use display name + qDebug() << "GroupModel: Requesting token for display name:" << key; + return QIcon(m_tokenManager->getToken(key)); + } + return QVariant(); case Qt::DisplayRole: switch (column) { + case ColumnTypeEnum::CHARACTER_TOKEN: + return QVariant(); case ColumnTypeEnum::NAME: if (character.getLabel().isEmpty() || character.getName().getStdStringViewUtf8() @@ -541,6 +557,8 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, }; switch (column) { + case ColumnTypeEnum::CHARACTER_TOKEN: + return QVariant(); // or appropriate fallback case ColumnTypeEnum::HP_PERCENT: return getRatioTooltip(character.getHits(), character.getMaxHits()); case ColumnTypeEnum::MANA_PERCENT: @@ -584,12 +602,14 @@ QVariant GroupModel::data(const QModelIndex &index, int role) const return QVariant(); } - if (index.row() >= 0 && index.row() < static_cast(m_characters.size())) { - const SharedGroupChar &character = m_characters.at(static_cast(index.row())); - return dataForCharacter(character, static_cast(index.column()), role); + if (index.row() < 0 || index.row() >= static_cast(m_characters.size())) { + return QVariant(); } - return QVariant(); + const SharedGroupChar &character = m_characters.at(static_cast(index.row())); + const ColumnTypeEnum column = static_cast(index.column()); + + return dataForCharacter(character, column, role); } QVariant GroupModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -711,6 +731,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget } else { m_model.setCharacters({}); } + m_model.setTokenManager(&tokenManager); auto *layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignTop); @@ -738,8 +759,8 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget layout->addWidget(m_table); // Minimize row height - m_table->verticalHeader()->setDefaultSectionSize( - m_table->verticalHeader()->minimumSectionSize()); + m_table->verticalHeader()->setDefaultSectionSize(32); + m_table->setIconSize(QSize(32, 32)); m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index ed76fc73f..6ba6f2837 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -5,6 +5,7 @@ #include "CGroupChar.h" #include "mmapper2character.h" +#include "tokenmanager.h" #include #include @@ -73,6 +74,7 @@ class NODISCARD_QOBJECT GroupDelegate final : public QStyledItemDelegate }; #define XFOREACH_COLUMNTYPE(X) \ + X(CHARACTER_TOKEN, character_token, CharacterToken, "Icon") \ X(NAME, name, Name, "Name") \ X(HP_PERCENT, hp_percent, HpPercent, "HP") \ X(MANA_PERCENT, mana_percent, ManaPercent, "Mana") \ @@ -103,6 +105,7 @@ class NODISCARD_QOBJECT GroupModel final : public QAbstractTableModel private: GroupVector m_characters; bool m_mapLoaded = false; + TokenManager *m_tokenManager = nullptr; public: explicit GroupModel(QObject *parent = nullptr); @@ -117,6 +120,7 @@ class NODISCARD_QOBJECT GroupModel final : public QAbstractTableModel void insertCharacter(const SharedGroupChar &newCharacter); void removeCharacterById(GroupId charId); void updateCharacter(const SharedGroupChar &updatedCharacter); + void setTokenManager(TokenManager *manager) { m_tokenManager = manager; } void resetModel(); private: @@ -154,6 +158,7 @@ class NODISCARD_QOBJECT GroupWidget final : public QWidget MapData *m_map = nullptr; GroupProxyModel *m_proxyModel = nullptr; GroupModel m_model; + TokenManager tokenManager; void updateColumnVisibility(); diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp new file mode 100644 index 000000000..a9e013db3 --- /dev/null +++ b/src/group/tokenmanager.cpp @@ -0,0 +1,157 @@ +#include "tokenmanager.h" +#include "../configuration/configuration.h" + +#include +#include +#include +#include +#include +#include + +QString TokenManager::normalizeKey(const QString &rawKey) +{ + QString key = rawKey.trimmed().toLower(); + key.replace(QRegularExpression("[^a-z0-9_]+"), "_"); // Replace spaces and punctuation + return key; +} + +TokenManager::TokenManager() +{ + scanDirectories(); +} + +void TokenManager::scanDirectories() +{ + m_availableFiles.clear(); + m_watcher.removePaths(m_watcher.files()); + m_watcher.removePaths(m_watcher.directories()); + + const QString tokensDir = getConfig().canvas.resourcesDirectory + "/tokens"; + + QDir dir(tokensDir); + if (!dir.exists()) { + qWarning() << "TokenManager: 'tokens' directory not found at:" << tokensDir; + return; + } + + m_watcher.addPath(tokensDir); + + QList supportedFormats = QImageReader::supportedImageFormats(); + QSet formats(supportedFormats.begin(), supportedFormats.end()); + + QDirIterator it(tokensDir, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) + { + QString path = it.next(); + QFileInfo info(path); + QString suffix = info.suffix().toLower(); + + if (formats.contains(suffix.toUtf8())) + { + QString key = info.baseName(); + if (!m_availableFiles.contains(key)) + { + m_availableFiles.insert(key, path); + qDebug() << "TokenManager: Found token image" << key << "at" << path; + m_watcher.addPath(path); + } + } + } +} + +QPixmap TokenManager::getToken(const QString &key) +{ + //qDebug() << "TokenManager: Received raw key:" << key; + + // Normalize the key + QString resolvedKey = normalizeKey(key); + //qDebug() << "TokenManager: Normalized key:" << resolvedKey; + + if (resolvedKey.isEmpty()) { + qWarning() << "TokenManager: Received empty key — defaulting to 'blank_character'"; + resolvedKey = "blank_character"; + } + + //qDebug() << "TokenManager: Requested key:" << resolvedKey; + + // ✅ Step 1: Check path cache first + if (m_tokenPathCache.contains(resolvedKey)) { + QString path = m_tokenPathCache[resolvedKey]; + QPixmap cached; + if (QPixmapCache::find(path, &cached)) { + qDebug() << "TokenManager: Using cached pixmap (via cache) for" << path; + return cached; + } + QPixmap pix; + if (pix.load(path)) { + QPixmapCache::insert(path, pix); + return pix; + } + qWarning() << "TokenManager: Cached path was invalid:" << path; + // Fall through to re-resolve path + } + + // Case-insensitive match against available keys + QString matchedKey; + for (const QString &k : m_availableFiles.keys()) { + if (k.compare(resolvedKey, Qt::CaseInsensitive) == 0) { + matchedKey = k; + break; + } + } + + qDebug() << "TokenManager: Available token keys:"; + for (const auto &availableKey : m_availableFiles.keys()) { + qDebug() << " - " << availableKey; + } + + if (!matchedKey.isEmpty()) + { + const QString &path = m_availableFiles.value(matchedKey); + qDebug() << "TokenManager: Found path for key:" << matchedKey << "->" << path; + + // ✅ Step 2: Cache resolved path + m_tokenPathCache[resolvedKey] = path; + + QPixmap cached; + if (QPixmapCache::find(path, &cached)) { + qDebug() << "TokenManager: Using cached pixmap for" << path; + return cached; + } + + QPixmap pix; + if (pix.load(path)) + { + qDebug() << "TokenManager: Loaded and caching image for" << matchedKey; + QPixmapCache::insert(path, pix); + return pix; + } + else + { + qWarning() << "TokenManager: Failed to load image from path:" << path; + } + } + else + { + qWarning() << "TokenManager: No match found for key:" << resolvedKey; + } + + // Fallback: user-defined blank_character.png in tokens folder + QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tokens/blank_character.png"; + if (QFile::exists(userFallback)) { + qDebug() << "TokenManager: Using fallback image from modding folder:" << userFallback; + m_tokenPathCache[resolvedKey] = userFallback; // ✅ Cache fallback + return QPixmap(userFallback); + } + + // Final fallback: built-in resource image + QString finalFallback = ":/pixmaps/char-room-sel.png"; + qDebug() << "TokenManager: Using final fallback image"; + m_tokenPathCache[resolvedKey] = finalFallback; // ✅ Cache fallback + return QPixmap(finalFallback); +} + +const QMap &TokenManager::availableFiles() const +{ + return m_availableFiles; +} diff --git a/src/group/tokenmanager.h b/src/group/tokenmanager.h new file mode 100644 index 000000000..b73db60d9 --- /dev/null +++ b/src/group/tokenmanager.h @@ -0,0 +1,26 @@ +#ifndef TOKENMANAGER_H +#define TOKENMANAGER_H + +#include +#include +#include +#include +#include + +class TokenManager +{ +public: + TokenManager(); + + QPixmap getToken(const QString &key); + const QMap &availableFiles() const; + +private: + void scanDirectories(); + + QMap m_availableFiles; + QFileSystemWatcher m_watcher; + static QString normalizeKey(const QString &rawKey); +mutable QMap m_tokenPathCache; +}; +#endif From 91c483c023b7625a7b8d532770c5bc010fe35434 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 24 Jun 2025 14:25:20 +1000 Subject: [PATCH 02/43] GroupManager: add showTokens preference to toggle icon column --- src/configuration/configuration.cpp | 2 ++ src/configuration/configuration.h | 1 + src/group/groupwidget.cpp | 33 ++--------------------------- src/preferences/grouppage.cpp | 5 +++++ 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 1541e3de1..c7a7ecc39 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -689,6 +689,7 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcColorOverride = conf.value(KEY_GROUP_NPC_COLOR_OVERRIDE, false).toBool(); npcHide = conf.value(KEY_GROUP_NPC_HIDE, false).toBool(); npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); + showTokens = conf.value("showTokens", showTokens).toBool(); } void Configuration::MumeClockSettings::read(const QSettings &conf) @@ -855,6 +856,7 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_NPC_COLOR_OVERRIDE, npcColorOverride); conf.setValue(KEY_GROUP_NPC_HIDE, npcHide); conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom); + conf.setValue("showTokens", showTokens); } void Configuration::MumeClockSettings::write(QSettings &conf) const diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 58d1d1e9f..d507893a0 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -291,6 +291,7 @@ class NODISCARD Configuration final bool npcColorOverride = false; bool npcSortBottom = false; bool npcHide = false; + bool showTokens = true; private: SUBGROUP(); diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index c4ad348a6..38700483d 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -30,13 +30,6 @@ #include #include -<<<<<<< HEAD -======= -static constexpr const int GROUP_COLUMN_COUNT = 10; -static_assert(GROUP_COLUMN_COUNT == static_cast(GroupModel::ColumnTypeEnum::ROOM_NAME) + 1, - "# of columns"); - ->>>>>>> dabeb884 (Add Character Token system and column to Group Manager with image caching) static constexpr const char *GROUP_MIME_TYPE = "application/vnd.mm_groupchar.row"; namespace { // anonymous @@ -862,30 +855,8 @@ void GroupWidget::updateColumnVisibility() const bool hide_mana = !one_character_had_mana(); m_table->setColumnHidden(static_cast(ColumnTypeEnum::MANA), hide_mana); m_table->setColumnHidden(static_cast(ColumnTypeEnum::MANA_PERCENT), hide_mana); -} - -void GroupWidget::slot_onCharacterAdded(SharedGroupChar character) -{ - assert(character); - m_model.insertCharacter(character); - updateColumnVisibility(); -} -void GroupWidget::slot_onCharacterRemoved(const GroupId characterId) -{ - assert(characterId != INVALID_GROUPID); - m_model.removeCharacterById(characterId); - updateColumnVisibility(); + const bool hide_tokens = !getConfig().groupManager.showTokens; + m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); } -void GroupWidget::slot_onCharacterUpdated(SharedGroupChar character) -{ - assert(character); - m_model.updateCharacter(character); -} - -void GroupWidget::slot_onGroupReset(const GroupVector &newCharacterList) -{ - m_model.setCharacters(newCharacterList); - updateColumnVisibility(); -} diff --git a/src/preferences/grouppage.cpp b/src/preferences/grouppage.cpp index f9f3a988a..5152ac364 100644 --- a/src/preferences/grouppage.cpp +++ b/src/preferences/grouppage.cpp @@ -36,6 +36,10 @@ GroupPage::GroupPage(QWidget *const parent) setConfig().groupManager.npcHide = checked; emit sig_groupSettingsChanged(); }); + connect(ui->showTokensCheckbox, &QCheckBox::stateChanged, this, [this](int checked) { + setConfig().groupManager.showTokens = checked; + emit sig_groupSettingsChanged(); + }); slot_loadConfig(); } @@ -60,6 +64,7 @@ void GroupPage::slot_loadConfig() ui->npcSortBottomCheckbox->setChecked(settings.npcSortBottom); ui->npcHideCheckbox->setChecked(settings.npcHide); + ui->showTokensCheckbox->setChecked(settings.showTokens); } void GroupPage::slot_chooseColor() From 10959520b5154e69a598668122e554f0bb8e0d4b Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 24 Jun 2025 15:16:15 +1000 Subject: [PATCH 03/43] GroupManager: add tokenIconSize preference (16???64 px live-resize) --- src/configuration/configuration.cpp | 8 +++-- src/configuration/configuration.h | 3 +- src/group/groupwidget.cpp | 19 ++++++++-- src/preferences/grouppage.cpp | 10 ++++++ src/preferences/grouppage.ui | 55 +++++++++++++++++++++++++---- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index c7a7ecc39..ac9060c79 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -240,6 +240,8 @@ ConstString KEY_GROUP_NPC_COLOR = "npc color"; ConstString KEY_GROUP_NPC_COLOR_OVERRIDE = "npc color override"; ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom"; ConstString KEY_GROUP_NPC_HIDE = "npc hide"; +ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; +ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size"; ConstString KEY_AUTO_LOG = "Auto log"; ConstString KEY_AUTO_LOG_ASK_DELETE = "Auto log ask before deleting"; ConstString KEY_AUTO_LOG_CLEANUP_STRATEGY = "Auto log cleanup strategy"; @@ -689,7 +691,8 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcColorOverride = conf.value(KEY_GROUP_NPC_COLOR_OVERRIDE, false).toBool(); npcHide = conf.value(KEY_GROUP_NPC_HIDE, false).toBool(); npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); - showTokens = conf.value("showTokens", showTokens).toBool(); + showTokens = conf.value(KEY_GROUP_SHOW_TOKENS, true).toBool(); + tokenIconSize = conf.value(KEY_GROUP_TOKEN_ICON_SIZE, 32).toInt(); } void Configuration::MumeClockSettings::read(const QSettings &conf) @@ -856,7 +859,8 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_NPC_COLOR_OVERRIDE, npcColorOverride); conf.setValue(KEY_GROUP_NPC_HIDE, npcHide); conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom); - conf.setValue("showTokens", showTokens); + conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); + conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize); } void Configuration::MumeClockSettings::write(QSettings &conf) const diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index d507893a0..498f13bde 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -291,7 +291,8 @@ class NODISCARD Configuration final bool npcColorOverride = false; bool npcSortBottom = false; bool npcHide = false; - bool showTokens = true; + bool showTokens = true; + int tokenIconSize = 32; private: SUBGROUP(); diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 38700483d..e2322d029 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -752,8 +753,11 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget layout->addWidget(m_table); // Minimize row height - m_table->verticalHeader()->setDefaultSectionSize(32); - m_table->setIconSize(QSize(32, 32)); + const int icon = getConfig().groupManager.tokenIconSize; + const int row = std::max(icon, m_table->fontMetrics().height() + 4); + + m_table->verticalHeader()->setDefaultSectionSize(row); + m_table->setIconSize(QSize(icon, icon)); m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { @@ -858,5 +862,16 @@ void GroupWidget::updateColumnVisibility() const bool hide_tokens = !getConfig().groupManager.showTokens; m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); + + // Apply current icon-size preference every time settings change + { + const int icon = getConfig().groupManager.tokenIconSize; + m_table->setIconSize(QSize(icon, icon)); + + QFontMetrics fm = m_table->fontMetrics(); + int row = std::max(icon, fm.height() + 4); + m_table->verticalHeader()->setDefaultSectionSize(row); + } } + diff --git a/src/preferences/grouppage.cpp b/src/preferences/grouppage.cpp index 5152ac364..fd3389474 100644 --- a/src/preferences/grouppage.cpp +++ b/src/preferences/grouppage.cpp @@ -40,6 +40,15 @@ GroupPage::GroupPage(QWidget *const parent) setConfig().groupManager.showTokens = checked; emit sig_groupSettingsChanged(); }); + ui->tokenSizeComboBox->setCurrentText(QString::number(getConfig().groupManager.tokenIconSize) + " px"); + + connect(ui->tokenSizeComboBox, &QComboBox::currentTextChanged, this, + [this](const QString &txt) { + // strip " px" and convert to int + int value = txt.section(' ', 0, 0).toInt(); + setConfig().groupManager.tokenIconSize = value; + emit sig_groupSettingsChanged(); // live update + }); slot_loadConfig(); } @@ -65,6 +74,7 @@ void GroupPage::slot_loadConfig() ui->npcSortBottomCheckbox->setChecked(settings.npcSortBottom); ui->npcHideCheckbox->setChecked(settings.npcHide); ui->showTokensCheckbox->setChecked(settings.showTokens); + ui->tokenSizeComboBox->setCurrentText(QString::number(settings.tokenIconSize) + " px"); } void GroupPage::slot_chooseColor() diff --git a/src/preferences/grouppage.ui b/src/preferences/grouppage.ui index e2ac696f4..cdbfd3aa4 100644 --- a/src/preferences/grouppage.ui +++ b/src/preferences/grouppage.ui @@ -7,7 +7,7 @@ 0 0 329 - 262 + 359 @@ -86,10 +86,20 @@ Filtering and Order - - + + - Sort NPCs to bottom + Show Character Images + + + true + + + + + + + Hide NPCs @@ -106,10 +116,41 @@ - - + + - Hide NPCs + Sort NPCs to bottom + + + + + + + + 16 px + + + + + 32 px + + + + + 48 px + + + + + 64 px + + + + + + + + Character Image Size From 200fb40fff440aa036e9771e1e826780a5c21d70 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 25 Jun 2025 09:40:06 +1000 Subject: [PATCH 04/43] GroupManager: add per-row icon picker and default-icon reset --- src/configuration/configuration.cpp | 13 ++++++ src/configuration/configuration.h | 1 + src/group/groupwidget.cpp | 71 +++++++++++++++++++++++++++++ src/group/groupwidget.h | 2 + src/group/tokenmanager.cpp | 38 ++++++++++----- src/group/tokenmanager.h | 9 +++- src/preferences/grouppage.ui | 5 ++ 7 files changed, 126 insertions(+), 13 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index ac9060c79..e13975aaf 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -242,6 +242,7 @@ ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom"; ConstString KEY_GROUP_NPC_HIDE = "npc hide"; ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size"; +ConstString KEY_GROUP_TOKEN_OVERRIDES = "token overrides"; ConstString KEY_AUTO_LOG = "Auto log"; ConstString KEY_AUTO_LOG_ASK_DELETE = "Auto log ask before deleting"; ConstString KEY_AUTO_LOG_CLEANUP_STRATEGY = "Auto log cleanup strategy"; @@ -693,6 +694,13 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); showTokens = conf.value(KEY_GROUP_SHOW_TOKENS, true).toBool(); tokenIconSize = conf.value(KEY_GROUP_TOKEN_ICON_SIZE, 32).toInt(); + tokenOverrides.clear(); + QSettings &rw = const_cast(conf); + rw.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); + const QStringList keys = rw.childKeys(); + for (const QString &k : keys) + tokenOverrides.insert(k, rw.value(k).toString()); + rw.endGroup(); } void Configuration::MumeClockSettings::read(const QSettings &conf) @@ -861,6 +869,11 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom); conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize); + conf.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); + conf.remove(""); // wipe old map entries + for (auto it = tokenOverrides.cbegin(); it != tokenOverrides.cend(); ++it) + conf.setValue(it.key(), it.value()); + conf.endGroup(); } void Configuration::MumeClockSettings::write(QSettings &conf) const diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 498f13bde..28829bf14 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -293,6 +293,7 @@ class NODISCARD Configuration final bool npcHide = false; bool showTokens = true; int tokenIconSize = 32; + QMap tokenOverrides; private: SUBGROUP(); diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index e2322d029..2dcf53b10 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -13,6 +13,7 @@ #include "CGroupChar.h" #include "enums.h" #include "mmapper2group.h" +#include "tokenmanager.h" #include #include @@ -30,6 +31,12 @@ #include #include #include +#include +#include +#include +#include + +extern const QString kForceFallback; static constexpr const char *GROUP_MIME_TYPE = "application/vnd.mm_groupchar.row"; @@ -791,6 +798,64 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget } }); + // ── Set-icon action ─────────────────────────────────────────── + m_setIcon = new QAction(QIcon(":/icons/group-set-icon.png"), tr("Set &Icon…"), this); + + connect(m_setIcon, &QAction::triggered, this, [this]() { + + if (!selectedCharacter) // safety + return; + + // 1. Character name (key) + const QString charName = selectedCharacter->getDisplayName().trimmed(); + + // 2. Tokens folder (= /tokens ) + const QString tokensDir = + QDir(getConfig().canvas.resourcesDirectory).filePath("tokens"); + + if (!QDir(tokensDir).exists()) { + QMessageBox::information( + this, + tr("Tokens folder not found"), + tr("No 'tokens' folder was found at:\n%1\n\n" + "Create a folder named 'tokens' inside that directory, " + "put your images there, then restart MMapper.") + .arg(tokensDir)); + return; // abort setting an icon + } + + const QString file = QFileDialog::getOpenFileName( + this, + tr("Choose icon for %1").arg(charName), + tokensDir, + tr("Images (*.png *.jpg *.bmp *.svg)")); + + if (file.isEmpty()) + return; // user cancelled + + // 3. store only the basename (without path / extension) + const QString base = QFileInfo(file).completeBaseName(); + setConfig().groupManager.tokenOverrides[charName] = base; + + // 4. immediately refresh this widget + slot_updateLabels(); + }); + + m_useDefaultIcon = new QAction(QIcon(":/icons/group-clear-icon.png"), + tr("&Use default icon"), this); + + connect(m_useDefaultIcon, &QAction::triggered, this, [this]() { + if (!selectedCharacter) + return; + + const QString charName = selectedCharacter->getDisplayName().trimmed(); + + // store the sentinel so TokenManager shows char-room-sel.png + setConfig().groupManager.tokenOverrides[charName] = kForceFallback; + + slot_updateLabels(); // live refresh + }); + connect(m_table, &QAbstractItemView::clicked, this, [this](const QModelIndex &proxyIndex) { if (!proxyIndex.isValid()) { return; @@ -810,10 +875,16 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_recolor->setText(QString("&Recolor %1").arg(selectedCharacter->getName().toQString())); m_center->setDisabled(!selectedCharacter->isYou() && selectedCharacter->getServerId() == INVALID_SERVER_ROOMID); + m_setIcon->setText(QString("&Set icon for %1…") + .arg(selectedCharacter->getName().toQString())); + m_useDefaultIcon->setText(QString("&Use default icon for %1") + .arg(selectedCharacter->getName().toQString())); QMenu contextMenu(tr("Context menu"), this); contextMenu.addAction(m_center); contextMenu.addAction(m_recolor); + contextMenu.addAction(m_setIcon); + contextMenu.addAction(m_useDefaultIcon); contextMenu.exec(QCursor::pos()); } }); diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index 6ba6f2837..1da89f54a 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -165,6 +165,8 @@ class NODISCARD_QOBJECT GroupWidget final : public QWidget private: QAction *m_center = nullptr; QAction *m_recolor = nullptr; + QAction *m_setIcon = nullptr; + QAction *m_useDefaultIcon = nullptr; SharedGroupChar selectedCharacter; public: diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp index a9e013db3..ff3c3df18 100644 --- a/src/group/tokenmanager.cpp +++ b/src/group/tokenmanager.cpp @@ -8,16 +8,29 @@ #include #include -QString TokenManager::normalizeKey(const QString &rawKey) +const QString kForceFallback(QStringLiteral("__force_fallback__")); + +static QString normalizeKey(QString key) { - QString key = rawKey.trimmed().toLower(); - key.replace(QRegularExpression("[^a-z0-9_]+"), "_"); // Replace spaces and punctuation + static const QRegularExpression nonWordReg( + QStringLiteral("[^a-z0-9_]+")); + + key = key.toLower(); + key.replace(nonWordReg, QStringLiteral("_")); return key; } +QString TokenManager::overrideFor(const QString &displayName) +{ + const auto &over = getConfig().groupManager.tokenOverrides; + auto it = over.constFind(displayName.trimmed()); + return (it != over.constEnd()) ? it.value() : QString(); +} + TokenManager::TokenManager() { scanDirectories(); + m_fallbackPixmap.load(":/pixmaps/char-room-sel.png"); } void TokenManager::scanDirectories() @@ -48,7 +61,7 @@ void TokenManager::scanDirectories() if (formats.contains(suffix.toUtf8())) { - QString key = info.baseName(); + QString key = normalizeKey(info.baseName()); if (!m_availableFiles.contains(key)) { m_availableFiles.insert(key, path); @@ -61,10 +74,17 @@ void TokenManager::scanDirectories() QPixmap TokenManager::getToken(const QString &key) { - //qDebug() << "TokenManager: Received raw key:" << key; + if (key == kForceFallback) { + return m_fallbackPixmap; + } + + QString lookup = key; + const QString ov = overrideFor(key); + if (!ov.isEmpty()) + lookup = ov; // use the user-chosen icon basename + + QString resolvedKey = normalizeKey(lookup); - // Normalize the key - QString resolvedKey = normalizeKey(key); //qDebug() << "TokenManager: Normalized key:" << resolvedKey; if (resolvedKey.isEmpty()) { @@ -74,7 +94,6 @@ QPixmap TokenManager::getToken(const QString &key) //qDebug() << "TokenManager: Requested key:" << resolvedKey; - // ✅ Step 1: Check path cache first if (m_tokenPathCache.contains(resolvedKey)) { QString path = m_tokenPathCache[resolvedKey]; QPixmap cached; @@ -88,10 +107,8 @@ QPixmap TokenManager::getToken(const QString &key) return pix; } qWarning() << "TokenManager: Cached path was invalid:" << path; - // Fall through to re-resolve path } - // Case-insensitive match against available keys QString matchedKey; for (const QString &k : m_availableFiles.keys()) { if (k.compare(resolvedKey, Qt::CaseInsensitive) == 0) { @@ -110,7 +127,6 @@ QPixmap TokenManager::getToken(const QString &key) const QString &path = m_availableFiles.value(matchedKey); qDebug() << "TokenManager: Found path for key:" << matchedKey << "->" << path; - // ✅ Step 2: Cache resolved path m_tokenPathCache[resolvedKey] = path; QPixmap cached; diff --git a/src/group/tokenmanager.h b/src/group/tokenmanager.h index b73db60d9..04b0c8031 100644 --- a/src/group/tokenmanager.h +++ b/src/group/tokenmanager.h @@ -13,6 +13,7 @@ class TokenManager TokenManager(); QPixmap getToken(const QString &key); + static QString overrideFor(const QString &displayName); const QMap &availableFiles() const; private: @@ -20,7 +21,11 @@ class TokenManager QMap m_availableFiles; QFileSystemWatcher m_watcher; - static QString normalizeKey(const QString &rawKey); -mutable QMap m_tokenPathCache; + mutable QMap m_tokenPathCache; + QPixmap m_fallbackPixmap; }; + +// Global sentinel that forces the built-in placeholder +extern const QString kForceFallback; + #endif diff --git a/src/preferences/grouppage.ui b/src/preferences/grouppage.ui index cdbfd3aa4..b4e779371 100644 --- a/src/preferences/grouppage.ui +++ b/src/preferences/grouppage.ui @@ -10,6 +10,11 @@ 359 + + + .AppleSystemUIFont + + Form From adc22eed1be80c04e29218d7263a20c27c90fe2e Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 10 Jul 2025 11:10:03 +1000 Subject: [PATCH 05/43] feat(display): character & NPC map tokens * Uploads token PNGs once and registers them in the live OpenGL renderer * Player token drawn immediately; mounts/NPCs hidden in player???s room * Prevents duplicate overlays and black fallback --- src/configuration/configuration.cpp | 9 +- src/configuration/configuration.h | 1 + src/display/Characters.cpp | 141 ++++++++++++++++++++++------ src/display/Characters.h | 6 +- src/group/groupwidget.cpp | 5 +- src/group/groupwidget.h | 2 +- src/group/tokenmanager.cpp | 100 +++++++++++++++++--- src/group/tokenmanager.h | 32 ++++++- src/preferences/grouppage.cpp | 8 ++ src/preferences/grouppage.ui | 134 +++++++++++++++----------- 10 files changed, 328 insertions(+), 110 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index e13975aaf..65921dbfc 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -241,6 +241,7 @@ ConstString KEY_GROUP_NPC_COLOR_OVERRIDE = "npc color override"; ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom"; ConstString KEY_GROUP_NPC_HIDE = "npc hide"; ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; +ConstString KEY_GROUP_SHOW_MAP_TOKENS = "show map tokens"; ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size"; ConstString KEY_GROUP_TOKEN_OVERRIDES = "token overrides"; ConstString KEY_AUTO_LOG = "Auto log"; @@ -693,7 +694,9 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcHide = conf.value(KEY_GROUP_NPC_HIDE, false).toBool(); npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); showTokens = conf.value(KEY_GROUP_SHOW_TOKENS, true).toBool(); + showMapTokens = conf.value(KEY_GROUP_SHOW_MAP_TOKENS, true).toBool(); tokenIconSize = conf.value(KEY_GROUP_TOKEN_ICON_SIZE, 32).toInt(); + tokenOverrides.clear(); QSettings &rw = const_cast(conf); rw.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); @@ -867,10 +870,12 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_NPC_COLOR_OVERRIDE, npcColorOverride); conf.setValue(KEY_GROUP_NPC_HIDE, npcHide); conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom); - conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); + conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); + conf.setValue(KEY_GROUP_SHOW_MAP_TOKENS, showMapTokens); conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize); + conf.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); - conf.remove(""); // wipe old map entries + conf.remove(""); // wipe old map entries for (auto it = tokenOverrides.cbegin(); it != tokenOverrides.cend(); ++it) conf.setValue(it.key(), it.value()); conf.endGroup(); diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 28829bf14..342266677 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -292,6 +292,7 @@ class NODISCARD Configuration final bool npcSortBottom = false; bool npcHide = false; bool showTokens = true; + bool showMapTokens = true; int tokenIconSize = 32; QMap tokenOverrides; diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index cf0ea2fd0..ad9ca26bb 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -6,6 +6,7 @@ #include "../configuration/configuration.h" #include "../group/CGroupChar.h" #include "../group/mmapper2group.h" +#include "../group/tokenmanager.h" #include "../map/room.h" #include "../map/roomid.h" #include "../mapdata/mapdata.h" @@ -48,7 +49,7 @@ bool CharacterBatch::isVisible(const Coordinate &c, float margin) const return m_mapScreen.isRoomVisible(c, margin); } -void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool fill) +void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool fill,const QString &dispName) { const Configuration::CanvasSettings &settings = getConfig().canvas; @@ -99,8 +100,7 @@ void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool } const bool beacon = visible && !differentLayer && wantBeacons; - gl.drawBox(c, fill, beacon, isFar); -} + gl.drawBox(c, fill, beacon, isFar, dispName);} void CharacterBatch::drawPreSpammedPath(const Coordinate &c1, const std::vector &path, @@ -214,8 +214,10 @@ void CharacterBatch::CharFakeGL::drawQuadCommon(const glm::vec2 &in_a, void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, bool fill, bool beacon, - const bool isFar) + const bool isFar, + const QString &dispName) { + const bool dontFillRotatedQuads = true; const bool shrinkRotatedQuads = false; // REVISIT: make this a user option? @@ -274,6 +276,34 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, addTransformed(c); addTransformed(d); + // ── NEW: also queue a token quad (drawn under coloured overlay) ── + if (!dispName.isEmpty() // only if we have a key + && getConfig().groupManager.showMapTokens) + { + const Color tokenColor{1.f, 1.f, 1.f, 1.f}; // opaque white + const auto &mtx = m_stack.top().modelView; + + auto pushVert = [this, &tokenColor, &mtx](const glm::vec2 &roomPos, + const glm::vec2 &uv) { + const auto tmp = mtx * glm::vec4(roomPos, 0.f, 1.f); + m_charTokenQuads.emplace_back(tokenColor, uv, glm::vec3{tmp / tmp.w}); + }; + + /* four corners, matching the room square */ + pushVert(a, {0.f, 0.f}); // lower-left + pushVert(b, {1.f, 0.f}); // lower-right + pushVert(c, {1.f, 1.f}); // upper-right + pushVert(d, {0.f, 1.f}); // upper-left + + QString key = TokenManager::overrideFor(dispName); + if (key.isEmpty()) + key = canonicalTokenKey(dispName); + else + key = canonicalTokenKey(key); + + m_charTokenKeys.emplace_back(key); + } + if (beacon) { drawQuadCommon(a, b, c, d, QuadOptsEnum::BEACON); } @@ -312,6 +342,38 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva gl.renderColoredQuads(m_charBeaconQuads, blended_noDepth.withCulling(CullingEnum::FRONT)); } + // ── draw map-tokens underneath the coloured overlay ── + for (size_t q = 0; q < m_charTokenKeys.size(); ++q) { + const size_t base = q * 4; + if (base + 3 >= m_charTokenQuads.size()) + break; + + const QString &key = m_charTokenKeys[q]; + MMTextureId id = tokenManager().textureIdFor(key); + + if (id == INVALID_MM_TEXTURE_ID) { + QPixmap px = tokenManager().getToken(key); + id = tokenManager().uploadNow(key, px); + } + + if (id == INVALID_MM_TEXTURE_ID) + continue; + + SharedMMTexture tex = tokenManager().textureById(id); + if (tex) + gl.setTextureLookup(id, tex); + + if (id == INVALID_MM_TEXTURE_ID) + continue; + + gl.renderColoredTexturedQuads( + {m_charTokenQuads[base + 0], + m_charTokenQuads[base + 1], + m_charTokenQuads[base + 2], + m_charTokenQuads[base + 3]}, + blended_noDepth.withTexture0(id)); + } + if (!m_charRoomQuads.empty()) { gl.renderColoredTexturedQuads(m_charRoomQuads, blended_noDepth.withTexture0(textures.char_room_sel->getId())); @@ -335,6 +397,9 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva gl.renderFont3d(textures.char_arrows, m_screenSpaceArrows); m_screenSpaceArrows.clear(); } + + m_charTokenQuads.clear(); + m_charTokenKeys.clear(); } void CharacterBatch::CharFakeGL::reallyDrawPaths(OpenGL &gl) @@ -383,9 +448,16 @@ void MapCanvas::paintCharacters() } CharacterBatch characterBatch{m_mapScreen, m_currentLayer, getTotalScaleFactor()}; + const CGroupChar *playerChar = nullptr; + for (const auto &pCharacter : m_groupManager.selectAll()) { + if (pCharacter->isYou()) { // ← ‘isYou()’ marks the local player + playerChar = pCharacter.get(); + break; + } + } // IIFE to abuse return to avoid duplicate else branches - [this, &characterBatch]() { + [this, &characterBatch, playerChar]() { if (const std::optional opt_pos = m_data.getCurrentRoomId()) { const auto &id = opt_pos.value(); if (const auto room = m_data.findRoomHandle(id)) { @@ -397,7 +469,11 @@ void MapCanvas::paintCharacters() // paint char current position const Color color{getConfig().groupManager.color}; - characterBatch.drawCharacter(pos, color); + characterBatch.drawCharacter(pos, color, + /*fill =*/ true, + /*name =*/ playerChar + ? playerChar->getDisplayName() + : QString()); // paint prespam const auto prespam = m_data.getPath(id, m_prespammedPath.getQueue()); @@ -417,40 +493,45 @@ void MapCanvas::paintCharacters() void MapCanvas::drawGroupCharacters(CharacterBatch &batch) { - if (m_data.isEmpty()) { + if (m_data.isEmpty()) return; + + const Map &map = m_data.getCurrentMap(); + + /* Find the player's room once */ + const CGroupChar *playerChar = nullptr; + RoomId playerRoomId = INVALID_ROOMID; + for (const auto &p : m_groupManager.selectAll()) { + if (p->isYou()) { + playerChar = p.get(); + if (const auto r = map.findRoomHandle(p->getServerId())) + playerRoomId = r.getId(); + break; + } } RoomIdSet drawnRoomIds; - const Map &map = m_data.getCurrentMap(); + for (const auto &pCharacter : m_groupManager.selectAll()) { - // Omit player so that they know group members are below them - if (pCharacter->isYou()) - continue; - const CGroupChar &character = deref(pCharacter); + const CGroupChar &character = *pCharacter; + if (character.isYou()) + continue; + const auto r = map.findRoomHandle(character.getServerId()); + if (!r) continue; // skip Unknown rooms - const auto &r = [&character, &map]() -> RoomHandle { - const ServerRoomId srvId = character.getServerId(); - if (srvId != INVALID_SERVER_ROOMID) { - if (const auto &room = map.findRoomHandle(srvId)) { - return room; - } - } - return RoomHandle{}; - }(); + const RoomId id = r.getId(); + const Coordinate & pos = r.getPosition(); + const Color col = Color{character.getColor()}; + const bool fill = !drawnRoomIds.contains(id); - // Do not draw the character if they're in an "Unknown" room - if (!r) { - continue; - } + const bool showToken = (id != playerRoomId); - const RoomId id = r.getId(); - const auto &pos = r.getPosition(); - const auto color = Color{character.getColor()}; - const bool fill = !drawnRoomIds.contains(id); + const QString tokenKey = showToken ? character.getDisplayName() + : QString(); // empty → no token - batch.drawCharacter(pos, color, fill); + batch.drawCharacter(pos, col, fill, tokenKey); drawnRoomIds.insert(id); } } + diff --git a/src/display/Characters.h b/src/display/Characters.h index da59bb5ab..d0cda2a25 100644 --- a/src/display/Characters.h +++ b/src/display/Characters.h @@ -102,6 +102,8 @@ class NODISCARD CharacterBatch final MatrixStack m_stack; std::vector m_charTris; std::vector m_charBeaconQuads; + std::vector m_charTokenQuads; + std::vector m_charTokenKeys; std::vector m_charLines; std::vector m_pathPoints; std::vector m_pathLineVerts; @@ -149,7 +151,7 @@ class NODISCARD CharacterBatch final m = glm::translate(m, v); } void drawArrow(bool fill, bool beacon); - void drawBox(const Coordinate &coord, bool fill, bool beacon, bool isFar); + void drawBox(const Coordinate &coord, bool fill, bool beacon, bool isFar,const QString &dispName); void addScreenSpaceArrow(const glm::vec3 &pos, float degrees, const Color &color, bool fill); // with blending, without depth; always size 4 @@ -215,7 +217,7 @@ class NODISCARD CharacterBatch final NODISCARD bool isVisible(const Coordinate &c, float margin) const; public: - void drawCharacter(const Coordinate &coordinate, const Color &color, bool fill = true); + void drawCharacter(const Coordinate &coordinate, const Color &color, bool fill = true,const QString &dispName = QString()); void drawPreSpammedPath(const Coordinate &coordinate, const std::vector &path, diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 2dcf53b10..5b4e7f7d6 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -38,6 +38,9 @@ extern const QString kForceFallback; +static_assert(GROUP_COLUMN_COUNT == static_cast(ColumnTypeEnum::ROOM_NAME) + 1, + "# of columns"); + static constexpr const char *GROUP_MIME_TYPE = "application/vnd.mm_groupchar.row"; namespace { // anonymous @@ -732,7 +735,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget } else { m_model.setCharacters({}); } - m_model.setTokenManager(&tokenManager); + m_model.setTokenManager(&tokenManager()); auto *layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignTop); diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index 1da89f54a..0071f3850 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -158,7 +158,7 @@ class NODISCARD_QOBJECT GroupWidget final : public QWidget MapData *m_map = nullptr; GroupProxyModel *m_proxyModel = nullptr; GroupModel m_model; - TokenManager tokenManager; + // TokenManager tokenManager; void updateColumnVisibility(); diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp index ff3c3df18..77a0b055a 100644 --- a/src/group/tokenmanager.cpp +++ b/src/group/tokenmanager.cpp @@ -7,6 +7,13 @@ #include #include #include +#include +#include +#include + +#include "../display/Textures.h" +#include "../opengl/OpenGL.h" +#include "../opengl/OpenGLTypes.h" const QString kForceFallback(QStringLiteral("__force_fallback__")); @@ -27,10 +34,28 @@ QString TokenManager::overrideFor(const QString &displayName) return (it != over.constEnd()) ? it.value() : QString(); } +static SharedMMTexture makeTextureFromPixmap(const QPixmap &px) +{ + using QT = QOpenGLTexture; + + auto mmtex = MMTexture::alloc( + QT::Target2D, + [&px](QT &tex) { tex.setData(px.toImage().mirrored()); }, + /*forbidUpdates = */ true); + + auto *tex = mmtex->get(); + tex->setWrapMode(QT::ClampToEdge); + tex->setMinMagFilters(QT::Linear, QT::Linear); + + const MMTextureId internalId = allocateTextureId(); + mmtex->setId(internalId); + + return mmtex; +} + TokenManager::TokenManager() { scanDirectories(); - m_fallbackPixmap.load(":/pixmaps/char-room-sel.png"); } void TokenManager::scanDirectories() @@ -65,7 +90,6 @@ void TokenManager::scanDirectories() if (!m_availableFiles.contains(key)) { m_availableFiles.insert(key, path); - qDebug() << "TokenManager: Found token image" << key << "at" << path; m_watcher.addPath(path); } } @@ -74,6 +98,9 @@ void TokenManager::scanDirectories() QPixmap TokenManager::getToken(const QString &key) { + if (m_fallbackPixmap.isNull()) + m_fallbackPixmap.load(":/pixmaps/char-room-sel.png"); + if (key == kForceFallback) { return m_fallbackPixmap; } @@ -85,20 +112,15 @@ QPixmap TokenManager::getToken(const QString &key) QString resolvedKey = normalizeKey(lookup); - //qDebug() << "TokenManager: Normalized key:" << resolvedKey; - if (resolvedKey.isEmpty()) { qWarning() << "TokenManager: Received empty key — defaulting to 'blank_character'"; resolvedKey = "blank_character"; } - //qDebug() << "TokenManager: Requested key:" << resolvedKey; - if (m_tokenPathCache.contains(resolvedKey)) { QString path = m_tokenPathCache[resolvedKey]; QPixmap cached; if (QPixmapCache::find(path, &cached)) { - qDebug() << "TokenManager: Using cached pixmap (via cache) for" << path; return cached; } QPixmap pix; @@ -117,28 +139,23 @@ QPixmap TokenManager::getToken(const QString &key) } } - qDebug() << "TokenManager: Available token keys:"; for (const auto &availableKey : m_availableFiles.keys()) { - qDebug() << " - " << availableKey; } if (!matchedKey.isEmpty()) { const QString &path = m_availableFiles.value(matchedKey); - qDebug() << "TokenManager: Found path for key:" << matchedKey << "->" << path; m_tokenPathCache[resolvedKey] = path; QPixmap cached; if (QPixmapCache::find(path, &cached)) { - qDebug() << "TokenManager: Using cached pixmap for" << path; return cached; } QPixmap pix; if (pix.load(path)) { - qDebug() << "TokenManager: Loaded and caching image for" << matchedKey; QPixmapCache::insert(path, pix); return pix; } @@ -155,14 +172,12 @@ QPixmap TokenManager::getToken(const QString &key) // Fallback: user-defined blank_character.png in tokens folder QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tokens/blank_character.png"; if (QFile::exists(userFallback)) { - qDebug() << "TokenManager: Using fallback image from modding folder:" << userFallback; m_tokenPathCache[resolvedKey] = userFallback; // ✅ Cache fallback return QPixmap(userFallback); } // Final fallback: built-in resource image QString finalFallback = ":/pixmaps/char-room-sel.png"; - qDebug() << "TokenManager: Using final fallback image"; m_tokenPathCache[resolvedKey] = finalFallback; // ✅ Cache fallback return QPixmap(finalFallback); } @@ -171,3 +186,60 @@ const QMap &TokenManager::availableFiles() const { return m_availableFiles; } + +TokenManager &tokenManager() +{ + static TokenManager instance; // created on first call (post-QGuiApp) + return instance; +} + +MMTextureId TokenManager::textureIdFor(const QString &key) +{ + if (m_textureCache.contains(key)) + return m_textureCache.value(key); + + /* NOT current – do NOT try to upload, just remember we need to. */ + if (!m_pendingUploads.contains(key)) + m_pendingUploads.append(key); + return INVALID_MM_TEXTURE_ID; +} + +QString canonicalTokenKey(const QString &name) +{ + return normalizeKey(name); // reuse the existing static helper +} + +MMTextureId TokenManager::uploadNow(const QString &key, + const QPixmap &px) +{ + SharedMMTexture tex = makeTextureFromPixmap(px); + MMTextureId id = tex->getId(); + + if (id == INVALID_MM_TEXTURE_ID) + return id; + + m_ownedTextures.push_back(std::move(tex)); + m_textureCache.insert(key, id); + return id; +} + +// keep tex alive + cache the id +void TokenManager::rememberUpload(const QString &key, + MMTextureId id, + SharedMMTexture tex) +{ + if (id == INVALID_MM_TEXTURE_ID) + return; + + m_ownedTextures.push_back(std::move(tex)); + m_textureCache.insert(key, id); +} + +// retrieve pointer later +SharedMMTexture TokenManager::textureById(MMTextureId id) const +{ + for (const auto &ptr : m_ownedTextures) + if (ptr->getId() == id) + return ptr; + return {}; +} diff --git a/src/group/tokenmanager.h b/src/group/tokenmanager.h index 04b0c8031..39a8fca6d 100644 --- a/src/group/tokenmanager.h +++ b/src/group/tokenmanager.h @@ -3,9 +3,17 @@ #include #include -#include #include #include +#include +#include +#include + +#include "../opengl/OpenGLTypes.h" // MMTextureId forward-declared here + +class MMTexture; // forward +using SharedMMTexture = std::shared_ptr; // … +QString canonicalTokenKey(const QString &name); class TokenManager { @@ -16,16 +24,30 @@ class TokenManager static QString overrideFor(const QString &displayName); const QMap &availableFiles() const; + MMTextureId textureIdFor(const QString &key); // ← per-token cache + MMTextureId uploadNow(const QString &key, const QPixmap &px); + QList m_pendingUploads; + + void rememberUpload(const QString &key, + MMTextureId id, + SharedMMTexture tex); + + SharedMMTexture textureById(MMTextureId id) const; + private: void scanDirectories(); - QMap m_availableFiles; - QFileSystemWatcher m_watcher; + QMap m_availableFiles; + QFileSystemWatcher m_watcher; mutable QMap m_tokenPathCache; - QPixmap m_fallbackPixmap; + QPixmap m_fallbackPixmap; + + QHash m_textureCache; // key → GL id + QVector m_ownedTextures; // keep textures alive }; -// Global sentinel that forces the built-in placeholder +// sentinel extern const QString kForceFallback; +TokenManager &tokenManager(); #endif diff --git a/src/preferences/grouppage.cpp b/src/preferences/grouppage.cpp index fd3389474..33ff7b166 100644 --- a/src/preferences/grouppage.cpp +++ b/src/preferences/grouppage.cpp @@ -36,10 +36,17 @@ GroupPage::GroupPage(QWidget *const parent) setConfig().groupManager.npcHide = checked; emit sig_groupSettingsChanged(); }); + ui->showTokensCheckbox->setChecked(getConfig().groupManager.showTokens); connect(ui->showTokensCheckbox, &QCheckBox::stateChanged, this, [this](int checked) { setConfig().groupManager.showTokens = checked; emit sig_groupSettingsChanged(); }); + ui->showMapTokensCheckbox->setChecked(getConfig().groupManager.showMapTokens); + connect(ui->showMapTokensCheckbox, &QCheckBox::stateChanged, this, + [this](int checked) { + setConfig().groupManager.showMapTokens = checked; + emit sig_groupSettingsChanged(); // refresh map instantly + }); ui->tokenSizeComboBox->setCurrentText(QString::number(getConfig().groupManager.tokenIconSize) + " px"); connect(ui->tokenSizeComboBox, &QComboBox::currentTextChanged, this, @@ -74,6 +81,7 @@ void GroupPage::slot_loadConfig() ui->npcSortBottomCheckbox->setChecked(settings.npcSortBottom); ui->npcHideCheckbox->setChecked(settings.npcHide); ui->showTokensCheckbox->setChecked(settings.showTokens); + ui->showMapTokensCheckbox->setChecked(settings.showMapTokens); ui->tokenSizeComboBox->setCurrentText(QString::number(settings.tokenIconSize) + " px"); } diff --git a/src/preferences/grouppage.ui b/src/preferences/grouppage.ui index b4e779371..93d4635b4 100644 --- a/src/preferences/grouppage.ui +++ b/src/preferences/grouppage.ui @@ -38,6 +38,30 @@ + + + + Your color: + + + yourColorPushButton + + + + + + + + + + + + + + Select + + + @@ -51,34 +75,75 @@ - - + + - + Select - - + + - Your color: + Character Image Size: - - yourColorPushButton + + + + + + + 16 px + + + + + 32 px + + + + + 48 px + + + + + 64 px + + + + + + + + + + + true - - + + - Select + Show Character Images - - + + - Select + + + + true + + + + + + + Show Images on Map @@ -91,16 +156,6 @@ Filtering and Order - - - - Show Character Images - - - true - - - @@ -128,37 +183,6 @@ - - - - - 16 px - - - - - 32 px - - - - - 48 px - - - - - 64 px - - - - - - - - Character Image Size - - - From 84ac8f81d74d8b01aba1f893391bb6579576729f Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Sun, 15 Jun 2025 07:55:48 -0500 Subject: [PATCH 06/43] WIP: Preserve widget edits before switching branch --- src/group/groupwidget.cpp | 4 ++++ src/group/groupwidget.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 5b4e7f7d6..9cd0b3e7b 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -948,4 +948,8 @@ void GroupWidget::updateColumnVisibility() } } +void GroupWidget::slot_updateLabels() +{ + m_model.resetModel(); // This re-fetches characters and refreshes the table +} diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index 0071f3850..2201f0606 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -189,4 +189,5 @@ private slots: void slot_onCharacterRemoved(GroupId characterId); void slot_onCharacterUpdated(SharedGroupChar character); void slot_onGroupReset(const GroupVector &newCharacterList); + void slot_updateLabels(); }; From b16c197a62203e65d2b3bae19d726ce3abb78f40 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 09:29:57 +1000 Subject: [PATCH 07/43] groupwidget: refresh token column after preference change --- src/group/groupwidget.cpp | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 9cd0b3e7b..81347cb24 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -768,7 +768,6 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->verticalHeader()->setDefaultSectionSize(row); m_table->setIconSize(QSize(icon, icon)); - m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { // Center map on the clicked character @@ -948,6 +947,33 @@ void GroupWidget::updateColumnVisibility() } } +void GroupWidget::slot_onCharacterAdded(SharedGroupChar character) +{ + assert(character); + m_model.insertCharacter(character); + updateColumnVisibility(); +} + +void GroupWidget::slot_onCharacterRemoved(const GroupId characterId) +{ + assert(characterId != INVALID_GROUPID); + m_model.removeCharacterById(characterId); + updateColumnVisibility(); +} + +void GroupWidget::slot_onCharacterUpdated(SharedGroupChar character) +{ + assert(character); + m_model.updateCharacter(character); + updateColumnVisibility(); +} + +void GroupWidget::slot_onGroupReset(const GroupVector &newCharacterList) +{ + m_model.setCharacters(newCharacterList); + updateColumnVisibility(); +} + void GroupWidget::slot_updateLabels() { m_model.resetModel(); // This re-fetches characters and refreshes the table From 538170b4c06e3ec9c1847805ee254806605b1606 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 10:19:06 +1000 Subject: [PATCH 08/43] Update map immediately after token change --- src/group/groupwidget.cpp | 2 ++ src/group/groupwidget.h | 1 + src/mainwindow/mainwindow.cpp | 3 +++ 3 files changed, 6 insertions(+) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 81347cb24..ef0815263 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -841,6 +841,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget // 4. immediately refresh this widget slot_updateLabels(); + emit sig_characterUpdated(selectedCharacter); }); m_useDefaultIcon = new QAction(QIcon(":/icons/group-clear-icon.png"), @@ -856,6 +857,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget setConfig().groupManager.tokenOverrides[charName] = kForceFallback; slot_updateLabels(); // live refresh + emit sig_characterUpdated(selectedCharacter); }); connect(m_table, &QAbstractItemView::clicked, this, [this](const QModelIndex &proxyIndex) { diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index 2201f0606..fcaca0cf5 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -179,6 +179,7 @@ class NODISCARD_QOBJECT GroupWidget final : public QWidget signals: void sig_kickCharacter(const QString &); void sig_center(glm::vec2); + void sig_characterUpdated(SharedGroupChar character); public slots: void slot_mapUnloaded() { m_model.setMapLoaded(false); } diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 939d3660f..c9256f4fa 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -185,6 +185,9 @@ MainWindow::MainWindow() addDockWidget(Qt::TopDockWidgetArea, m_dockDialogGroup); m_dockDialogGroup->setWidget(m_groupWidget); connect(m_groupWidget, &GroupWidget::sig_center, m_mapWindow, &MapWindow::slot_centerOnWorldPos); + auto *canvas = getCanvas(); + connect(m_groupWidget, &GroupWidget::sig_characterUpdated, + canvas, [canvas](SharedGroupChar) { canvas->slot_requestUpdate(); }); // View -> Side Panels -> Room Panel (Mobs) m_roomManager = new RoomManager(this); From 29130fbdd8f6f880498773d38f87aa45d72a4d56 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 10:21:32 +1000 Subject: [PATCH 09/43] grouppage: tidy header before PR --- src/preferences/grouppage.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/preferences/grouppage.h b/src/preferences/grouppage.h index 866ff13be..3dbc4a13b 100644 --- a/src/preferences/grouppage.h +++ b/src/preferences/grouppage.h @@ -29,6 +29,7 @@ class NODISCARD_QOBJECT GroupPage final : public QWidget signals: void sig_groupSettingsChanged(); + void sig_showTokensChanged(bool); public slots: void slot_loadConfig(); From 0faf3c4b3fe33d5530cfd6777e8ad1e1c9ceb60c Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 11:21:11 +1000 Subject: [PATCH 10/43] style: whitespace and clang-format clean-up --- src/group/groupwidget.cpp | 65 ++++++++++++++++------------------- src/group/tokenmanager.cpp | 62 ++++++++++++++------------------- src/group/tokenmanager.h | 33 +++++++++--------- src/preferences/grouppage.cpp | 27 +++++++-------- 4 files changed, 84 insertions(+), 103 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index ef0815263..bd94df9aa 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -15,15 +15,19 @@ #include "mmapper2group.h" #include "tokenmanager.h" +#include #include #include -#include #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -31,15 +35,10 @@ #include #include #include -#include -#include -#include -#include extern const QString kForceFallback; -static_assert(GROUP_COLUMN_COUNT == static_cast(ColumnTypeEnum::ROOM_NAME) + 1, - "# of columns"); +static_assert(GROUP_COLUMN_COUNT == static_cast(ColumnTypeEnum::ROOM_NAME) + 1, "# of columns"); static constexpr const char *GROUP_MIME_TYPE = "application/vnd.mm_groupchar.row"; @@ -480,7 +479,7 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, switch (role) { case Qt::DecorationRole: if (column == ColumnTypeEnum::CHARACTER_TOKEN && m_tokenManager) { - QString key = character.getDisplayName(); // Use display name + QString key = character.getDisplayName(); // Use display name qDebug() << "GroupModel: Requesting token for display name:" << key; return QIcon(m_tokenManager->getToken(key)); } @@ -764,7 +763,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget // Minimize row height const int icon = getConfig().groupManager.tokenIconSize; - const int row = std::max(icon, m_table->fontMetrics().height() + 4); + const int row = std::max(icon, m_table->fontMetrics().height() + 4); m_table->verticalHeader()->setDefaultSectionSize(row); m_table->setIconSize(QSize(icon, icon)); @@ -804,36 +803,32 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_setIcon = new QAction(QIcon(":/icons/group-set-icon.png"), tr("Set &Icon…"), this); connect(m_setIcon, &QAction::triggered, this, [this]() { - - if (!selectedCharacter) // safety + if (!selectedCharacter) // safety return; // 1. Character name (key) const QString charName = selectedCharacter->getDisplayName().trimmed(); // 2. Tokens folder (= /tokens ) - const QString tokensDir = - QDir(getConfig().canvas.resourcesDirectory).filePath("tokens"); + const QString tokensDir = QDir(getConfig().canvas.resourcesDirectory).filePath("tokens"); if (!QDir(tokensDir).exists()) { - QMessageBox::information( - this, - tr("Tokens folder not found"), - tr("No 'tokens' folder was found at:\n%1\n\n" - "Create a folder named 'tokens' inside that directory, " - "put your images there, then restart MMapper.") - .arg(tokensDir)); - return; // abort setting an icon + QMessageBox::information(this, + tr("Tokens folder not found"), + tr("No 'tokens' folder was found at:\n%1\n\n" + "Create a folder named 'tokens' inside that directory, " + "put your images there, then restart MMapper.") + .arg(tokensDir)); + return; // abort setting an icon } - const QString file = QFileDialog::getOpenFileName( - this, - tr("Choose icon for %1").arg(charName), - tokensDir, - tr("Images (*.png *.jpg *.bmp *.svg)")); + const QString file = QFileDialog::getOpenFileName(this, + tr("Choose icon for %1").arg(charName), + tokensDir, + tr("Images (*.png *.jpg *.bmp *.svg)")); if (file.isEmpty()) - return; // user cancelled + return; // user cancelled // 3. store only the basename (without path / extension) const QString base = QFileInfo(file).completeBaseName(); @@ -845,7 +840,8 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget }); m_useDefaultIcon = new QAction(QIcon(":/icons/group-clear-icon.png"), - tr("&Use default icon"), this); + tr("&Use default icon"), + this); connect(m_useDefaultIcon, &QAction::triggered, this, [this]() { if (!selectedCharacter) @@ -856,7 +852,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget // store the sentinel so TokenManager shows char-room-sel.png setConfig().groupManager.tokenOverrides[charName] = kForceFallback; - slot_updateLabels(); // live refresh + slot_updateLabels(); // live refresh emit sig_characterUpdated(selectedCharacter); }); @@ -879,10 +875,10 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_recolor->setText(QString("&Recolor %1").arg(selectedCharacter->getName().toQString())); m_center->setDisabled(!selectedCharacter->isYou() && selectedCharacter->getServerId() == INVALID_SERVER_ROOMID); - m_setIcon->setText(QString("&Set icon for %1…") - .arg(selectedCharacter->getName().toQString())); - m_useDefaultIcon->setText(QString("&Use default icon for %1") - .arg(selectedCharacter->getName().toQString())); + m_setIcon->setText( + QString("&Set icon for %1…").arg(selectedCharacter->getName().toQString())); + m_useDefaultIcon->setText( + QString("&Use default icon for %1").arg(selectedCharacter->getName().toQString())); QMenu contextMenu(tr("Context menu"), this); contextMenu.addAction(m_center); @@ -978,6 +974,5 @@ void GroupWidget::slot_onGroupReset(const GroupVector &newCharacterList) void GroupWidget::slot_updateLabels() { - m_model.resetModel(); // This re-fetches characters and refreshes the table + m_model.resetModel(); // This re-fetches characters and refreshes the table } - diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp index 77a0b055a..4ffad0736 100644 --- a/src/group/tokenmanager.cpp +++ b/src/group/tokenmanager.cpp @@ -1,26 +1,25 @@ #include "tokenmanager.h" + #include "../configuration/configuration.h" +#include "../display/Textures.h" +#include "../opengl/OpenGL.h" +#include "../opengl/OpenGLTypes.h" +#include #include #include #include #include -#include -#include +#include #include #include -#include - -#include "../display/Textures.h" -#include "../opengl/OpenGL.h" -#include "../opengl/OpenGLTypes.h" +#include const QString kForceFallback(QStringLiteral("__force_fallback__")); static QString normalizeKey(QString key) { - static const QRegularExpression nonWordReg( - QStringLiteral("[^a-z0-9_]+")); + static const QRegularExpression nonWordReg(QStringLiteral("[^a-z0-9_]+")); key = key.toLower(); key.replace(nonWordReg, QStringLiteral("_")); @@ -78,17 +77,14 @@ void TokenManager::scanDirectories() QSet formats(supportedFormats.begin(), supportedFormats.end()); QDirIterator it(tokensDir, QDir::Files, QDirIterator::Subdirectories); - while (it.hasNext()) - { + while (it.hasNext()) { QString path = it.next(); QFileInfo info(path); QString suffix = info.suffix().toLower(); - if (formats.contains(suffix.toUtf8())) - { + if (formats.contains(suffix.toUtf8())) { QString key = normalizeKey(info.baseName()); - if (!m_availableFiles.contains(key)) - { + if (!m_availableFiles.contains(key)) { m_availableFiles.insert(key, path); m_watcher.addPath(path); } @@ -108,7 +104,7 @@ QPixmap TokenManager::getToken(const QString &key) QString lookup = key; const QString ov = overrideFor(key); if (!ov.isEmpty()) - lookup = ov; // use the user-chosen icon basename + lookup = ov; // use the user-chosen icon basename QString resolvedKey = normalizeKey(lookup); @@ -142,8 +138,7 @@ QPixmap TokenManager::getToken(const QString &key) for (const auto &availableKey : m_availableFiles.keys()) { } - if (!matchedKey.isEmpty()) - { + if (!matchedKey.isEmpty()) { const QString &path = m_availableFiles.value(matchedKey); m_tokenPathCache[resolvedKey] = path; @@ -154,31 +149,27 @@ QPixmap TokenManager::getToken(const QString &key) } QPixmap pix; - if (pix.load(path)) - { + if (pix.load(path)) { QPixmapCache::insert(path, pix); return pix; - } - else - { + } else { qWarning() << "TokenManager: Failed to load image from path:" << path; } - } - else - { + } else { qWarning() << "TokenManager: No match found for key:" << resolvedKey; } // Fallback: user-defined blank_character.png in tokens folder - QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tokens/blank_character.png"; + QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + "/tokens/blank_character.png"; if (QFile::exists(userFallback)) { - m_tokenPathCache[resolvedKey] = userFallback; // ✅ Cache fallback + m_tokenPathCache[resolvedKey] = userFallback; // ✅ Cache fallback return QPixmap(userFallback); } // Final fallback: built-in resource image QString finalFallback = ":/pixmaps/char-room-sel.png"; - m_tokenPathCache[resolvedKey] = finalFallback; // ✅ Cache fallback + m_tokenPathCache[resolvedKey] = finalFallback; // ✅ Cache fallback return QPixmap(finalFallback); } @@ -189,7 +180,7 @@ const QMap &TokenManager::availableFiles() const TokenManager &tokenManager() { - static TokenManager instance; // created on first call (post-QGuiApp) + static TokenManager instance; // created on first call (post-QGuiApp) return instance; } @@ -206,14 +197,13 @@ MMTextureId TokenManager::textureIdFor(const QString &key) QString canonicalTokenKey(const QString &name) { - return normalizeKey(name); // reuse the existing static helper + return normalizeKey(name); // reuse the existing static helper } -MMTextureId TokenManager::uploadNow(const QString &key, - const QPixmap &px) +MMTextureId TokenManager::uploadNow(const QString &key, const QPixmap &px) { SharedMMTexture tex = makeTextureFromPixmap(px); - MMTextureId id = tex->getId(); + MMTextureId id = tex->getId(); if (id == INVALID_MM_TEXTURE_ID) return id; @@ -224,9 +214,7 @@ MMTextureId TokenManager::uploadNow(const QString &key, } // keep tex alive + cache the id -void TokenManager::rememberUpload(const QString &key, - MMTextureId id, - SharedMMTexture tex) +void TokenManager::rememberUpload(const QString &key, MMTextureId id, SharedMMTexture tex) { if (id == INVALID_MM_TEXTURE_ID) return; diff --git a/src/group/tokenmanager.h b/src/group/tokenmanager.h index 39a8fca6d..c8b704020 100644 --- a/src/group/tokenmanager.h +++ b/src/group/tokenmanager.h @@ -1,17 +1,18 @@ #ifndef TOKENMANAGER_H #define TOKENMANAGER_H -#include -#include -#include +#include "../opengl/OpenGLTypes.h" // MMTextureId forward-declared here + +#include + #include #include +#include +#include +#include #include -#include - -#include "../opengl/OpenGLTypes.h" // MMTextureId forward-declared here -class MMTexture; // forward +class MMTexture; // forward using SharedMMTexture = std::shared_ptr; // … QString canonicalTokenKey(const QString &name); @@ -24,26 +25,24 @@ class TokenManager static QString overrideFor(const QString &displayName); const QMap &availableFiles() const; - MMTextureId textureIdFor(const QString &key); // ← per-token cache + MMTextureId textureIdFor(const QString &key); // ← per-token cache MMTextureId uploadNow(const QString &key, const QPixmap &px); - QList m_pendingUploads; + QList m_pendingUploads; - void rememberUpload(const QString &key, - MMTextureId id, - SharedMMTexture tex); + void rememberUpload(const QString &key, MMTextureId id, SharedMMTexture tex); SharedMMTexture textureById(MMTextureId id) const; private: void scanDirectories(); - QMap m_availableFiles; - QFileSystemWatcher m_watcher; + QMap m_availableFiles; + QFileSystemWatcher m_watcher; mutable QMap m_tokenPathCache; - QPixmap m_fallbackPixmap; + QPixmap m_fallbackPixmap; - QHash m_textureCache; // key → GL id - QVector m_ownedTextures; // keep textures alive + QHash m_textureCache; // key → GL id + QVector m_ownedTextures; // keep textures alive }; // sentinel diff --git a/src/preferences/grouppage.cpp b/src/preferences/grouppage.cpp index 33ff7b166..89d6b5aee 100644 --- a/src/preferences/grouppage.cpp +++ b/src/preferences/grouppage.cpp @@ -42,20 +42,19 @@ GroupPage::GroupPage(QWidget *const parent) emit sig_groupSettingsChanged(); }); ui->showMapTokensCheckbox->setChecked(getConfig().groupManager.showMapTokens); - connect(ui->showMapTokensCheckbox, &QCheckBox::stateChanged, this, - [this](int checked) { - setConfig().groupManager.showMapTokens = checked; - emit sig_groupSettingsChanged(); // refresh map instantly - }); - ui->tokenSizeComboBox->setCurrentText(QString::number(getConfig().groupManager.tokenIconSize) + " px"); - - connect(ui->tokenSizeComboBox, &QComboBox::currentTextChanged, this, - [this](const QString &txt) { - // strip " px" and convert to int - int value = txt.section(' ', 0, 0).toInt(); - setConfig().groupManager.tokenIconSize = value; - emit sig_groupSettingsChanged(); // live update - }); + connect(ui->showMapTokensCheckbox, &QCheckBox::stateChanged, this, [this](int checked) { + setConfig().groupManager.showMapTokens = checked; + emit sig_groupSettingsChanged(); // refresh map instantly + }); + ui->tokenSizeComboBox->setCurrentText(QString::number(getConfig().groupManager.tokenIconSize) + + " px"); + + connect(ui->tokenSizeComboBox, &QComboBox::currentTextChanged, this, [this](const QString &txt) { + // strip " px" and convert to int + int value = txt.section(' ', 0, 0).toInt(); + setConfig().groupManager.tokenIconSize = value; + emit sig_groupSettingsChanged(); // live update + }); slot_loadConfig(); } From 39abe5fd6db5b1873087c38fec28e5823b681fdf Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 11:27:24 +1000 Subject: [PATCH 11/43] style: remove two redundant blank lines (CodeFactor) --- src/display/Characters.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index ad9ca26bb..1fb2ffb0b 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -217,7 +217,6 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, const bool isFar, const QString &dispName) { - const bool dontFillRotatedQuads = true; const bool shrinkRotatedQuads = false; // REVISIT: make this a user option? @@ -513,7 +512,6 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) RoomIdSet drawnRoomIds; for (const auto &pCharacter : m_groupManager.selectAll()) { - const CGroupChar &character = *pCharacter; if (character.isYou()) continue; From fb321aded524e317954bcc4fd808dab026c19085 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 12:12:11 +1000 Subject: [PATCH 12/43] refactor: simplify getToken() and extract helper funcs --- src/group/tokenmanager.cpp | 110 ++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp index 4ffad0736..7424d934f 100644 --- a/src/group/tokenmanager.cpp +++ b/src/group/tokenmanager.cpp @@ -15,6 +15,33 @@ #include #include +namespace { + +/// Return a cached pixmap (or load & cache it). Null QPixmap if load fails. +static QPixmap fetchPixmap(const QString &path) +{ + QPixmap px; + if (QPixmapCache::find(path, &px)) + return px; + if (px.load(path)) { + QPixmapCache::insert(path, px); + return px; + } + return {}; // null means load failed +} + +/// Case-insensitive lookup: “mount_pony” matches “Mount_Pony” +static QString matchAvailableKey(const QMap &files, + const QString &resolvedKey) +{ + for (const QString &k : files.keys()) + if (k.compare(resolvedKey, Qt::CaseInsensitive) == 0) + return k; + return {}; +} + +} + const QString kForceFallback(QStringLiteral("__force_fallback__")); static QString normalizeKey(QString key) @@ -94,83 +121,54 @@ void TokenManager::scanDirectories() QPixmap TokenManager::getToken(const QString &key) { + // 0. ensure built-in fallback is ready if (m_fallbackPixmap.isNull()) m_fallbackPixmap.load(":/pixmaps/char-room-sel.png"); - if (key == kForceFallback) { + if (key == kForceFallback) return m_fallbackPixmap; - } - - QString lookup = key; - const QString ov = overrideFor(key); - if (!ov.isEmpty()) - lookup = ov; // use the user-chosen icon basename - - QString resolvedKey = normalizeKey(lookup); + // 1. resolve overrides and normalise key + const QString lookup = overrideFor(key).isEmpty() ? key : overrideFor(key); + QString resolvedKey = normalizeKey(lookup); if (resolvedKey.isEmpty()) { - qWarning() << "TokenManager: Received empty key — defaulting to 'blank_character'"; + qWarning() << "TokenManager: empty key — defaulting to 'blank_character'"; resolvedKey = "blank_character"; } + // 2. fast path: cached path ➜ cached pixmap if (m_tokenPathCache.contains(resolvedKey)) { - QString path = m_tokenPathCache[resolvedKey]; - QPixmap cached; - if (QPixmapCache::find(path, &cached)) { - return cached; - } - QPixmap pix; - if (pix.load(path)) { - QPixmapCache::insert(path, pix); - return pix; - } - qWarning() << "TokenManager: Cached path was invalid:" << path; - } - - QString matchedKey; - for (const QString &k : m_availableFiles.keys()) { - if (k.compare(resolvedKey, Qt::CaseInsensitive) == 0) { - matchedKey = k; - break; - } - } - - for (const auto &availableKey : m_availableFiles.keys()) { + const QString &path = m_tokenPathCache[resolvedKey]; + if (QPixmap px = fetchPixmap(path); !px.isNull()) + return px; + qWarning() << "TokenManager: cached path invalid:" << path; } + // 3. search tokens directory + const QString matchedKey = matchAvailableKey(m_availableFiles, resolvedKey); if (!matchedKey.isEmpty()) { const QString &path = m_availableFiles.value(matchedKey); - m_tokenPathCache[resolvedKey] = path; - - QPixmap cached; - if (QPixmapCache::find(path, &cached)) { - return cached; - } - - QPixmap pix; - if (pix.load(path)) { - QPixmapCache::insert(path, pix); - return pix; - } else { - qWarning() << "TokenManager: Failed to load image from path:" << path; - } + if (QPixmap px = fetchPixmap(path); !px.isNull()) + return px; + qWarning() << "TokenManager: failed to load image:" << path; } else { - qWarning() << "TokenManager: No match found for key:" << resolvedKey; + qWarning() << "TokenManager: no match for key:" << resolvedKey; } - // Fallback: user-defined blank_character.png in tokens folder - QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) - + "/tokens/blank_character.png"; + // 4. user-defined fallback (AppData/tokens/blank_character.png) + const QString userFallback = + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + "/tokens/blank_character.png"; if (QFile::exists(userFallback)) { - m_tokenPathCache[resolvedKey] = userFallback; // ✅ Cache fallback - return QPixmap(userFallback); + m_tokenPathCache[resolvedKey] = userFallback; + return fetchPixmap(userFallback); } - // Final fallback: built-in resource image - QString finalFallback = ":/pixmaps/char-room-sel.png"; - m_tokenPathCache[resolvedKey] = finalFallback; // ✅ Cache fallback - return QPixmap(finalFallback); + // 5. built-in fallback resource + const QString resFallback = ":/pixmaps/char-room-sel.png"; + m_tokenPathCache[resolvedKey] = resFallback; + return m_fallbackPixmap; } const QMap &TokenManager::availableFiles() const From f2385ef5655a2c9fde0683e1690e1b496a255661 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 12:50:03 +1000 Subject: [PATCH 13/43] refactor: simplify dataForCharacter() and remove duplicate ToolTipRole --- src/group/groupwidget.cpp | 266 ++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 139 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index bd94df9aa..858de0730 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -434,109 +434,138 @@ NODISCARD static QStringView getPrettyName(const CharacterAffectEnum affect) #undef X_CASE } -QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, - const ColumnTypeEnum column, - const int role) const -{ - const CGroupChar &character = deref(pCharacter); - - const auto formatStat = - [](int numerator, int denomenator, ColumnTypeEnum statColumn) -> QString { - if (denomenator == 0 - && (statColumn == ColumnTypeEnum::HP_PERCENT - || statColumn == ColumnTypeEnum::MANA_PERCENT - || statColumn == ColumnTypeEnum::MOVES_PERCENT)) { - return QLatin1String(""); - } - // The NPC check for ratio is handled below in the switch statement - if ((numerator == 0 && denomenator == 0) - && (statColumn == ColumnTypeEnum::HP || statColumn == ColumnTypeEnum::MANA - || statColumn == ColumnTypeEnum::MOVES)) { - return QLatin1String(""); +/*───────────────────── complexity helpers ─────────────────────*/ +namespace { + +static QString formatStatHelper(int num, + int den, + ColumnTypeEnum col, + bool isNpc) +{ + if (col == ColumnTypeEnum::HP_PERCENT + || col == ColumnTypeEnum::MANA_PERCENT + || col == ColumnTypeEnum::MOVES_PERCENT) { + if (den == 0) + return {}; + int pct = static_cast(100.0 * double(num) / double(den)); + return QString("%1%").arg(pct); + } + + if (col == ColumnTypeEnum::HP + || col == ColumnTypeEnum::MANA + || col == ColumnTypeEnum::MOVES) { + // hide “0/0” for NPCs -- same behaviour as before + if (isNpc && num == 0 && den == 0) + return {}; + return QString("%1/%2").arg(num).arg(den); + } + return {}; +} + +/// Big switch for DisplayRole, extracted out of dataForCharacter +static QVariant makeDisplayRole(const CGroupChar &ch, ColumnTypeEnum c, + TokenManager *tm) +{ + switch (c) { + case ColumnTypeEnum::CHARACTER_TOKEN: + return tm ? QIcon(tm->getToken(ch.getDisplayName())) : QVariant(); + case ColumnTypeEnum::NAME: + if (ch.getLabel().isEmpty() + || ch.getName().getStdStringViewUtf8() + == ch.getLabel().getStdStringViewUtf8()) { + return ch.getName().toQString(); + } else { + return QString("%1 (%2)") + .arg(ch.getName().toQString(), + ch.getLabel().toQString()); } + case ColumnTypeEnum::HP_PERCENT: + return formatStatHelper(ch.getHits(), ch.getMaxHits(), c, false); + case ColumnTypeEnum::MANA_PERCENT: + return formatStatHelper(ch.getMana(), ch.getMaxMana(), c, false); + case ColumnTypeEnum::MOVES_PERCENT: + return formatStatHelper(ch.getMoves(), ch.getMaxMoves(), c, false); + case ColumnTypeEnum::HP: + return formatStatHelper(ch.getHits(), ch.getMaxHits(), c, + ch.getType() == CharacterTypeEnum::NPC); + case ColumnTypeEnum::MANA: + return formatStatHelper(ch.getMana(), ch.getMaxMana(), c, + ch.getType() == CharacterTypeEnum::NPC); + case ColumnTypeEnum::MOVES: + return formatStatHelper(ch.getMoves(), ch.getMaxMoves(), c, + ch.getType() == CharacterTypeEnum::NPC); + case ColumnTypeEnum::STATE: + return QVariant::fromValue( + GroupStateData(ch.getColor(), + ch.getPosition(), + ch.getAffects())); + case ColumnTypeEnum::ROOM_NAME: + return ch.getRoomName().isEmpty() + ? QStringLiteral("Somewhere") + : ch.getRoomName().toQString(); + default: + return QVariant(); + } +} - switch (statColumn) { - case ColumnTypeEnum::HP_PERCENT: - case ColumnTypeEnum::MANA_PERCENT: - case ColumnTypeEnum::MOVES_PERCENT: { - int percentage = static_cast(100.0 * static_cast(numerator) - / static_cast(denomenator)); - return QString("%1%").arg(percentage); - } - case ColumnTypeEnum::HP: - case ColumnTypeEnum::MANA: - case ColumnTypeEnum::MOVES: - return QString("%1/%2").arg(numerator).arg(denomenator); - default: - case ColumnTypeEnum::NAME: - case ColumnTypeEnum::STATE: - case ColumnTypeEnum::ROOM_NAME: - return QLatin1String(""); - } +/// Big switch for ToolTipRole, extracted out as well +static QVariant makeTooltipRole(const CGroupChar &ch, ColumnTypeEnum c, + bool useStatFmt) +{ + auto statTip = [&](int n, int d) -> QVariant { + return useStatFmt ? formatStatHelper(n, d, c, false) : QVariant(); }; - // Map column to data + switch (c) { + case ColumnTypeEnum::CHARACTER_TOKEN: + return QVariant(); + case ColumnTypeEnum::HP_PERCENT: + return statTip(ch.getHits(), ch.getMaxHits()); + case ColumnTypeEnum::MANA_PERCENT: + return statTip(ch.getMana(), ch.getMaxMana()); + case ColumnTypeEnum::MOVES_PERCENT: + return statTip(ch.getMoves(), ch.getMaxMoves()); + case ColumnTypeEnum::STATE: { + QString pretty = getPrettyName(ch.getPosition()).toString(); + for (const CharacterAffectEnum a : ALL_CHARACTER_AFFECTS) + if (ch.getAffects().contains(a)) + pretty.append(", ").append(getPrettyName(a)); + return pretty; + } + case ColumnTypeEnum::ROOM_NAME: + if (ch.getServerId() != INVALID_SERVER_ROOMID) + return QString::number(ch.getServerId().asUint32()); + return QVariant(); + default: + return QVariant(); + } +} + +} // anonymous namespace +/*──────────────────────────────────────────────────────────────────*/ + +QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, + const ColumnTypeEnum column, + const int role) const +{ + const CGroupChar &character = deref(pCharacter); + switch (role) { + + /* display / icons ---------------------------------------------------- */ case Qt::DecorationRole: - if (column == ColumnTypeEnum::CHARACTER_TOKEN && m_tokenManager) { - QString key = character.getDisplayName(); // Use display name - qDebug() << "GroupModel: Requesting token for display name:" << key; - return QIcon(m_tokenManager->getToken(key)); - } - return QVariant(); case Qt::DisplayRole: - switch (column) { - case ColumnTypeEnum::CHARACTER_TOKEN: - return QVariant(); - case ColumnTypeEnum::NAME: - if (character.getLabel().isEmpty() - || character.getName().getStdStringViewUtf8() - == character.getLabel().getStdStringViewUtf8()) { - return character.getName().toQString(); - } else { - return QString("%1 (%2)").arg(character.getName().toQString(), - character.getLabel().toQString()); - } - case ColumnTypeEnum::HP_PERCENT: - return formatStat(character.getHits(), character.getMaxHits(), column); - case ColumnTypeEnum::MANA_PERCENT: - return formatStat(character.getMana(), character.getMaxMana(), column); - case ColumnTypeEnum::MOVES_PERCENT: - return formatStat(character.getMoves(), character.getMaxMoves(), column); - case ColumnTypeEnum::HP: - if (character.getType() == CharacterTypeEnum::NPC) { - return QLatin1String(""); - } else { - return formatStat(character.getHits(), character.getMaxHits(), column); - } - case ColumnTypeEnum::MANA: - if (character.getType() == CharacterTypeEnum::NPC) { - return QLatin1String(""); - } else { - return formatStat(character.getMana(), character.getMaxMana(), column); - } - case ColumnTypeEnum::MOVES: - if (character.getType() == CharacterTypeEnum::NPC) { - return QLatin1String(""); - } else { - return formatStat(character.getMoves(), character.getMaxMoves(), column); - } - case ColumnTypeEnum::STATE: - return QVariant::fromValue(GroupStateData(character.getColor(), - character.getPosition(), - character.getAffects())); - case ColumnTypeEnum::ROOM_NAME: - if (character.getRoomName().isEmpty()) { - return QStringLiteral("Somewhere"); - } else { - return character.getRoomName().toQString(); - } - default: - qWarning() << "Unsupported column" << static_cast(column); - break; - } - break; + return makeDisplayRole(character, column, m_tokenManager); + + /* tooltips ----------------------------------------------------------- */ + case Qt::ToolTipRole: + return makeTooltipRole(character, + column, + (column == ColumnTypeEnum::HP_PERCENT + || column == ColumnTypeEnum::MANA_PERCENT + || column == ColumnTypeEnum::MOVES_PERCENT)); + /* colours & alignment ------------------------------------------------ */ case Qt::BackgroundRole: return character.getColor(); @@ -544,54 +573,13 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, return mmqt::textColor(character.getColor()); case Qt::TextAlignmentRole: - if (column != ColumnTypeEnum::NAME && column != ColumnTypeEnum::ROOM_NAME) { - // NOTE: There's no QVariant(AlignmentFlag) constructor. + if (column != ColumnTypeEnum::NAME && + column != ColumnTypeEnum::ROOM_NAME) { + // QVariant(AlignmentFlag) ctor doesn’t exist; return static_cast(Qt::AlignCenter); } break; - case Qt::ToolTipRole: { - const auto getRatioTooltip = [&](int numerator, int denomenator) -> QVariant { - if (character.getType() == CharacterTypeEnum::NPC) { - return QVariant(); - } else { - return formatStat(numerator, denomenator, column); - } - }; - - switch (column) { - case ColumnTypeEnum::CHARACTER_TOKEN: - return QVariant(); // or appropriate fallback - case ColumnTypeEnum::HP_PERCENT: - return getRatioTooltip(character.getHits(), character.getMaxHits()); - case ColumnTypeEnum::MANA_PERCENT: - return getRatioTooltip(character.getMana(), character.getMaxMana()); - case ColumnTypeEnum::MOVES_PERCENT: - return getRatioTooltip(character.getMoves(), character.getMaxMoves()); - case ColumnTypeEnum::STATE: { - QString prettyName; - prettyName += getPrettyName(character.getPosition()); - for (const CharacterAffectEnum affect : ALL_CHARACTER_AFFECTS) { - if (character.getAffects().contains(affect)) { - prettyName.append(QStringLiteral(", ")).append(getPrettyName(affect)); - } - } - return prettyName; - } - case ColumnTypeEnum::NAME: - case ColumnTypeEnum::HP: - case ColumnTypeEnum::MANA: - case ColumnTypeEnum::MOVES: - break; - case ColumnTypeEnum::ROOM_NAME: - if (character.getServerId() != INVALID_SERVER_ROOMID) { - return QString("%1").arg(character.getServerId().asUint32()); - } - break; - } - break; - } - default: break; } From dedd0b8565703ee330658f65309545e0243f3d72 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 13:18:44 +1000 Subject: [PATCH 14/43] refactor: shorten setCharacters() and move context-menu to helper slots --- src/group/groupwidget.cpp | 125 +++++++++++++++++++------------------- src/group/groupwidget.h | 2 + 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 858de0730..cff684bbe 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -273,37 +273,29 @@ void GroupModel::setCharacters(const GroupVector &newGameChars) } } - // Insert the newly identified characters into the resulting list based on configuration. - if (getConfig().groupManager.npcSortBottom) { - // Find the insertion point for new players: before the first NPC in the preserved list. - auto itPlayerInsertPos = resultingCharacterList.begin(); - while (itPlayerInsertPos != resultingCharacterList.end()) { - if (*itPlayerInsertPos && (*itPlayerInsertPos)->isNpc()) { - break; - } - ++itPlayerInsertPos; + // Insert the newly identified characters with one small helper + auto insertNewChars = [&](GroupVector &dest, + const GroupVector &newPlayers, + const GroupVector &newNpcs, + const GroupVector &newAll) { + if (getConfig().groupManager.npcSortBottom) { + // players go before first NPC + auto firstNpc = std::find_if(dest.begin(), + dest.end(), + [](const SharedGroupChar &c) { + return c && c->isNpc(); + }); + dest.insert(firstNpc, newPlayers.begin(), newPlayers.end()); + dest.insert(dest.end(), newNpcs.begin(), newNpcs.end()); + } else { + dest.insert(dest.end(), newAll.begin(), newAll.end()); } + }; - // Insert truly new players at the determined position. - if (!trulyNewPlayers.empty()) { - resultingCharacterList.insert(itPlayerInsertPos, - trulyNewPlayers.begin(), - trulyNewPlayers.end()); - } - // Insert truly new NPCs at the end of the list. - if (!trulyNewNpcs.empty()) { - resultingCharacterList.insert(resultingCharacterList.end(), - trulyNewNpcs.begin(), - trulyNewNpcs.end()); - } - } else { - // If no special NPC sorting, just append all truly new characters in their original order. - if (!allTrulyNewCharsInOriginalOrder.empty()) { - resultingCharacterList.insert(resultingCharacterList.end(), - allTrulyNewCharsInOriginalOrder.begin(), - allTrulyNewCharsInOriginalOrder.end()); - } - } + insertNewChars(resultingCharacterList, + trulyNewPlayers, + trulyNewNpcs, + allTrulyNewCharsInOriginalOrder); beginResetModel(); m_characters = std::move(resultingCharacterList); @@ -551,7 +543,6 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, const CGroupChar &character = deref(pCharacter); switch (role) { - /* display / icons ---------------------------------------------------- */ case Qt::DecorationRole: case Qt::DisplayRole: @@ -844,38 +835,10 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget emit sig_characterUpdated(selectedCharacter); }); - connect(m_table, &QAbstractItemView::clicked, this, [this](const QModelIndex &proxyIndex) { - if (!proxyIndex.isValid()) { - return; - } - - QModelIndex sourceIndex = m_proxyModel->mapToSource(proxyIndex); - - if (!sourceIndex.isValid()) { - return; - } - - selectedCharacter = m_model.getCharacter(sourceIndex.row()); - if (selectedCharacter) { - // Build Context menu - m_center->setText( - QString("&Center on %1").arg(selectedCharacter->getName().toQString())); - m_recolor->setText(QString("&Recolor %1").arg(selectedCharacter->getName().toQString())); - m_center->setDisabled(!selectedCharacter->isYou() - && selectedCharacter->getServerId() == INVALID_SERVER_ROOMID); - m_setIcon->setText( - QString("&Set icon for %1…").arg(selectedCharacter->getName().toQString())); - m_useDefaultIcon->setText( - QString("&Use default icon for %1").arg(selectedCharacter->getName().toQString())); - - QMenu contextMenu(tr("Context menu"), this); - contextMenu.addAction(m_center); - contextMenu.addAction(m_recolor); - contextMenu.addAction(m_setIcon); - contextMenu.addAction(m_useDefaultIcon); - contextMenu.exec(QCursor::pos()); - } - }); + connect(m_table, + &QAbstractItemView::clicked, + this, + &GroupWidget::showContextMenu); connect(m_group, &Mmapper2Group::sig_characterAdded, this, &GroupWidget::slot_onCharacterAdded); connect(m_group, @@ -964,3 +927,41 @@ void GroupWidget::slot_updateLabels() { m_model.resetModel(); // This re-fetches characters and refreshes the table } + +// ───────────────────────── context-menu helpers ───────────────────────── +void GroupWidget::showContextMenu(const QModelIndex &proxyIndex) +{ + if (!proxyIndex.isValid()) + return; + + QModelIndex src = m_proxyModel->mapToSource(proxyIndex); + if (!src.isValid()) + return; + + selectedCharacter = m_model.getCharacter(src.row()); + if (!selectedCharacter) + return; + + buildAndExecMenu(); +} + +void GroupWidget::buildAndExecMenu() +{ + const CGroupChar &c = deref(selectedCharacter); + + m_center->setText(QString("&Center on %1").arg(c.getName().toQString())); + m_center->setDisabled(!c.isYou() && c.getServerId() == INVALID_SERVER_ROOMID); + + m_recolor->setText(QString("&Recolor %1").arg(c.getName().toQString())); + m_setIcon->setText(QString("&Set icon for %1…").arg(c.getName().toQString())); + m_useDefaultIcon->setText(QString("&Use default icon for %1") + .arg(c.getName().toQString())); + + QMenu menu(tr("Context menu"), this); + menu.addAction(m_center); + menu.addAction(m_recolor); + menu.addAction(m_setIcon); + menu.addAction(m_useDefaultIcon); + menu.exec(QCursor::pos()); +} +// ───────────────────────────────────────────────────────────────────────── diff --git a/src/group/groupwidget.h b/src/group/groupwidget.h index fcaca0cf5..ea6319df2 100644 --- a/src/group/groupwidget.h +++ b/src/group/groupwidget.h @@ -161,6 +161,8 @@ class NODISCARD_QOBJECT GroupWidget final : public QWidget // TokenManager tokenManager; void updateColumnVisibility(); + void showContextMenu(const QModelIndex &proxyIndex); + void buildAndExecMenu(); private: QAction *m_center = nullptr; From 7b5ccce3f9f2900967a9a57f672fa3f613ce0085 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 13:26:14 +1000 Subject: [PATCH 15/43] fix: rename remaining insertNewChars call to insertNewCharactersInto --- src/group/groupwidget.cpp | 47 ++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index cff684bbe..3cbb661b9 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -227,6 +227,25 @@ GroupModel::GroupModel(QObject *const parent) : QAbstractTableModel(parent) {} +namespace { // anonymous – helpers local to this file +static void insertNewCharactersInto(GroupVector &dest, + bool npcSortBottom, + const GroupVector &newPlayers, + const GroupVector &newNpcs, + const GroupVector &newAll) +{ + if (npcSortBottom) { + // players go before first NPC already present + auto firstNpc = std::find_if(dest.begin(), dest.end(), + [](const SharedGroupChar &c) { return c && c->isNpc(); }); + dest.insert(firstNpc, newPlayers.begin(), newPlayers.end()); + dest.insert(dest.end(), newNpcs.begin(), newNpcs.end()); + } else { + dest.insert(dest.end(), newAll.begin(), newAll.end()); + } +} +} // namespace + void GroupModel::setCharacters(const GroupVector &newGameChars) { DECL_TIMER(t, __FUNCTION__); @@ -273,29 +292,11 @@ void GroupModel::setCharacters(const GroupVector &newGameChars) } } - // Insert the newly identified characters with one small helper - auto insertNewChars = [&](GroupVector &dest, - const GroupVector &newPlayers, - const GroupVector &newNpcs, - const GroupVector &newAll) { - if (getConfig().groupManager.npcSortBottom) { - // players go before first NPC - auto firstNpc = std::find_if(dest.begin(), - dest.end(), - [](const SharedGroupChar &c) { - return c && c->isNpc(); - }); - dest.insert(firstNpc, newPlayers.begin(), newPlayers.end()); - dest.insert(dest.end(), newNpcs.begin(), newNpcs.end()); - } else { - dest.insert(dest.end(), newAll.begin(), newAll.end()); - } - }; - - insertNewChars(resultingCharacterList, - trulyNewPlayers, - trulyNewNpcs, - allTrulyNewCharsInOriginalOrder); + insertNewCharactersInto(resultingCharacterList, + getConfig().groupManager.npcSortBottom, + trulyNewPlayers, + trulyNewNpcs, + allTrulyNewCharsInOriginalOrder); beginResetModel(); m_characters = std::move(resultingCharacterList); From f2f790e3255a1c76bcfea3b5955d0057cc29b95c Mon Sep 17 00:00:00 2001 From: Sunnyl75 <132520322+Sunnyl75@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:12:27 +1000 Subject: [PATCH 16/43] Create build-tokens --- .github/workflows/build-tokens | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/build-tokens diff --git a/.github/workflows/build-tokens b/.github/workflows/build-tokens new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.github/workflows/build-tokens @@ -0,0 +1 @@ + From 14de596b27a766b887cf2ecafb6137e25dc634dc Mon Sep 17 00:00:00 2001 From: Sunnyl75 <132520322+Sunnyl75@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:16:58 +1000 Subject: [PATCH 17/43] Create build-map-tokens-clean --- .github/workflows/build-map-tokens-clean | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/build-map-tokens-clean diff --git a/.github/workflows/build-map-tokens-clean b/.github/workflows/build-map-tokens-clean new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.github/workflows/build-map-tokens-clean @@ -0,0 +1 @@ + From 19f20f17756efc5781378680c10172ccc2066e3f Mon Sep 17 00:00:00 2001 From: Sunnyl75 <132520322+Sunnyl75@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:17:19 +1000 Subject: [PATCH 18/43] Update build-map-tokens-clean --- .github/workflows/build-map-tokens-clean | 157 +++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/.github/workflows/build-map-tokens-clean b/.github/workflows/build-map-tokens-clean index 8b1378917..e21aca6c8 100644 --- a/.github/workflows/build-map-tokens-clean +++ b/.github/workflows/build-map-tokens-clean @@ -1 +1,158 @@ +name: Build Map-Tokens-Clean + +# Only trigger on this branch +on: + push: + branches: + - feature/map-tokens-clean + pull_request: + branches: + - feature/map-tokens-clean + workflow_dispatch: + +jobs: + build: + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: false + matrix: + os: [windows-2022, macos-13, macos-14, ubuntu-22.04] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Get Git tag description + id: git_info + run: | + echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV + + - uses: lukka/get-cmake@latest + + # Linux deps + - if: runner.os == 'Linux' + name: Install Packages for Ubuntu + run: | + sudo apt update -qq + sudo apt install -y build-essential git qt5-qmake libqt5opengl5-dev \ + libqt5websockets5-dev zlib1g-dev libssl-dev qt5keychain-dev \ + mold gcc-12 g++-12 + echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV + echo "CC=gcc-12" >> $GITHUB_ENV + echo "CXX=g++-12" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV + + # macOS deps + - if: runner.os == 'macOS' + name: Install Packages for Mac + run: | + brew install qt5 + brew link qt5 --force + echo "$(brew --prefix qt5)/bin" >> $GITHUB_PATH + + # Windows deps + - if: runner.os == 'Windows' + name: Install Qt for Windows (MinGW) + uses: jurplel/install-qt-action@v4 + with: + version: 5.15.2 + dir: 'C:\' + arch: win64_mingw81 + cache: true + tools: 'tools_mingw1310' + - if: runner.os == 'Windows' + name: Install Packages for Windows + uses: crazy-max/ghaction-chocolatey@v3 + with: + args: install openssl --version=1.1.1.2100 + + # Build on Windows + - if: runner.os == 'Windows' + name: Build MMapper for Windows + shell: pwsh + run: | + xcopy "C:\Program Files\OpenSSL" "C:\OpenSSL" /E /I /H /K /Y + mkdir ${{ github.workspace }}/artifact + mkdir build + cd build + cmake --version + echo "C:/Qt/Tools/mingw1310_64/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "C:\Qt\Tools\mingw1310_64\bin;$env:PATH" + $packageDir = ($env:GITHUB_WORKSPACE -replace '\\', '/') + "/artifact" + cmake -G "MinGW Makefiles" \ + -DCPACK_PACKAGE_DIRECTORY="$packageDir" \ + -DCMAKE_PREFIX_PATH="C:\Qt\5.15.2\mingw81_64" \ + -DOPENSSL_ROOT_DIR=C:/OpenSSL/ \ + -DWITH_WEBSOCKET=ON \ + -DWITH_QTKEYCHAIN=ON \ + -S .. || exit -1 + cmake --build . -j $env:NUMBER_OF_PROCESSORS + + # Build on macOS & Linux + - if: runner.os == 'Linux' || runner.os == 'macOS' + name: Build MMapper for Linux and Mac + run: | + mkdir -p build ${{ github.workspace }}/artifact + cd build + cmake --version + cmake -G 'Ninja' \ + -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact \ + $MMAPPER_CMAKE_EXTRA \ + -DWITH_WEBSOCKET=ON \ + -DWITH_QTKEYCHAIN=ON \ + -S .. || exit -1 + cmake --build . + + # Package + - name: Package MMapper + run: cd build && cpack + + # Capture package names + - if: runner.os == 'Linux' + name: Get Linux package file base name + id: get_linux_package_name + run: | + PACKAGE_FILE=$(ls ${{ github.workspace }}/artifact/*.deb) + PACKAGE_BASENAME=$(basename "$PACKAGE_FILE" | sed 's/\./-/g') + echo "PACKAGE_BASENAME=$PACKAGE_BASENAME" >> $GITHUB_OUTPUT + + - if: runner.os == 'macOS' + name: Get Mac package file base name + id: get_mac_package_name + run: | + PACKAGE_FILE=$(ls ${{ github.workspace }}/artifact/*.dmg) + PACKAGE_BASENAME=$(basename "$PACKAGE_FILE" | sed 's/\./-/g') + echo "PACKAGE_BASENAME=$PACKAGE_BASENAME" >> $GITHUB_OUTPUT + + - if: runner.os == 'Windows' + name: Get Windows package file base name + id: get_windows_package_name + shell: pwsh + run: | + $packageFile = Get-ChildItem -Path "${{ github.workspace }}/artifact/*.exe" | Select-Object -First 1 + $packageBasename = $packageFile.BaseName -replace '\.', '-' + "PACKAGE_BASENAME=$packageBasename" >> $env:GITHUB_OUTPUT + + # Upload artifacts + - if: runner.os == 'macOS' + name: Upload Package for Mac + uses: actions/upload-artifact@v4 + with: + name: release-${{ steps.get_mac_package_name.outputs.PACKAGE_BASENAME }} + path: ${{ github.workspace }}/artifact/*.dmg* + - if: runner.os == 'Linux' + name: Upload Package for Ubuntu + uses: actions/upload-artifact@v4 + with: + name: release-${{ steps.get_linux_package_name.outputs.PACKAGE_BASENAME }} + path: ${{ github.workspace }}/artifact/*.deb* + - if: runner.os == 'Windows' + name: Upload Package for Windows + uses: actions/upload-artifact@v4 + with: + name: release-${{ steps.get_windows_package_name.outputs.PACKAGE_BASENAME }} + path: ${{ github.workspace }}/artifact/*.exe* From d5a6489e46dbe0b957cc64f45114e9d8f3e248ef Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 13:58:15 +1000 Subject: [PATCH 19/43] chore: add GhostRegistry skeleton and global instance --- src/display/Characters.cpp | 3 +++ src/display/GhostRegistry.h | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/display/GhostRegistry.h diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 1fb2ffb0b..fea1601fc 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -17,6 +17,7 @@ #include "Textures.h" #include "mapcanvas.h" #include "prespammedpath.h" +#include "GhostRegistry.h" #include #include @@ -27,6 +28,8 @@ #include +std::unordered_map g_ghosts; + static constexpr float CHAR_ARROW_LINE_WIDTH = 2.f; static constexpr float PATH_LINE_WIDTH = 4.f; static constexpr float PATH_POINT_SIZE = 8.f; diff --git a/src/display/GhostRegistry.h b/src/display/GhostRegistry.h new file mode 100644 index 000000000..185cef4aa --- /dev/null +++ b/src/display/GhostRegistry.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + + +#include "../map/roomid.h" +#include "../group/CGroupChar.h" + +struct GhostInfo { + ServerRoomId roomSid; + QString tokenKey; +}; + +extern std::unordered_map g_ghosts; From 5df56050fde624b6fe8328e2fa3637911c3f2dc8 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:09:52 +1000 Subject: [PATCH 20/43] feat: add provisional isMount() helper (currently == isNpc()) --- src/group/CGroupChar.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/group/CGroupChar.h b/src/group/CGroupChar.h index 1fa50f77f..aacf6cb30 100644 --- a/src/group/CGroupChar.h +++ b/src/group/CGroupChar.h @@ -121,6 +121,13 @@ class NODISCARD CGroupChar final : public std::enable_shared_from_this Date: Sat, 19 Jul 2025 14:10:20 +1000 Subject: [PATCH 21/43] feat: add provisional isMount() helper and capture GhostInfo on mount removal --- src/group/groupwidget.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 3cbb661b9..8fb84e84b 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -6,6 +6,7 @@ #include "../configuration/configuration.h" #include "../display/Filenames.h" +#include "display/GhostRegistry.h" #include "../global/Timer.h" #include "../map/roomid.h" #include "../mapdata/mapdata.h" @@ -345,8 +346,14 @@ void GroupModel::insertCharacter(const SharedGroupChar &newCharacter) void GroupModel::removeCharacterById(const GroupId charId) { const int index = findIndexById(charId); - if (index == -1) { + if (index == -1) return; + + SharedGroupChar &c = m_characters[static_cast(index)]; + + /*** NEW: store a ghost entry if this row is a mount ***/ + if (c->isMount()) { + g_ghosts[c->getId()] = { c->getServerId(), c->getDisplayName() }; } beginRemoveRows(QModelIndex(), index, index); From 12b6ab9a717766248f10ae382a2cab4378542627 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:21:27 +1000 Subject: [PATCH 22/43] feat: clear ghost entry when mount is visible again --- src/group/groupwidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 8fb84e84b..95dde565b 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -283,6 +283,9 @@ void GroupModel::setCharacters(const GroupVector &newGameChars) // Identify truly new characters and categorize them as NPC or player for (const auto &pGameChar : newGameChars) { const auto &gameChar = deref(pGameChar); + + g_ghosts.erase(gameChar.getId()); + if (existingIds.find(gameChar.getId()) == existingIds.end()) { allTrulyNewCharsInOriginalOrder.push_back(pGameChar); if (gameChar.isNpc()) { From 65ffab75a8e667b0c4445dab41d2ecdd04bf6d34 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:32:39 +1000 Subject: [PATCH 23/43] feat: add last-seen mount ghost token --- src/display/Characters.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index fea1601fc..7b1d4ec38 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -502,10 +502,12 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) /* Find the player's room once */ const CGroupChar *playerChar = nullptr; + ServerRoomId playerRoomSid = INVALID_SERVER_ROOMID; RoomId playerRoomId = INVALID_ROOMID; for (const auto &p : m_groupManager.selectAll()) { if (p->isYou()) { playerChar = p.get(); + playerRoomSid = p->getServerId(); if (const auto r = map.findRoomHandle(p->getServerId())) playerRoomId = r.getId(); break; @@ -534,5 +536,21 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) batch.drawCharacter(pos, col, fill, tokenKey); drawnRoomIds.insert(id); } + /* ---------- draw persistent ghost tokens ------------------------------ */ + for (const auto &[gid, g] : g_ghosts) { + if (g.roomSid == playerRoomSid) // skip if ghost is in player room + continue; + + if (const auto h = map.findRoomHandle(g.roomSid)) { + const Coordinate &pos = h.getPosition(); + Color col = Color{Qt::white}.withAlpha(0.70f); // 70 % opacity + const bool fill = !drawnRoomIds.contains(h.getId()); + + /* If your drawCharacter() has a scale param, pass 0.9f; otherwise omit it */ + batch.drawCharacter(pos, col, fill, g.tokenKey); + drawnRoomIds.insert(h.getId()); + } + } + } From 8322add063c5ce84a07a5c55886f9996d30bbcd8 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 15:03:17 +1000 Subject: [PATCH 24/43] fix: token quad now uses caller color, enabling 70% ghost opacity --- src/display/Characters.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 7b1d4ec38..8e3b2d559 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -27,6 +27,7 @@ #include #include +#include std::unordered_map g_ghosts; @@ -282,7 +283,7 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, if (!dispName.isEmpty() // only if we have a key && getConfig().groupManager.showMapTokens) { - const Color tokenColor{1.f, 1.f, 1.f, 1.f}; // opaque white + const Color &tokenColor = color; // inherits alpha from caller const auto &mtx = m_stack.top().modelView; auto pushVert = [this, &tokenColor, &mtx](const glm::vec2 &roomPos, @@ -538,19 +539,20 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) } /* ---------- draw persistent ghost tokens ------------------------------ */ for (const auto &[gid, g] : g_ghosts) { - if (g.roomSid == playerRoomSid) // skip if ghost is in player room + if (g.roomSid == playerRoomSid) continue; if (const auto h = map.findRoomHandle(g.roomSid)) { const Coordinate &pos = h.getPosition(); - Color col = Color{Qt::white}.withAlpha(0.70f); // 70 % opacity - const bool fill = !drawnRoomIds.contains(h.getId()); - /* If your drawCharacter() has a scale param, pass 0.9f; otherwise omit it */ - batch.drawCharacter(pos, col, fill, g.tokenKey); + QColor tint(Qt::white); + tint.setAlphaF(0.70f); // 70 % opacity + Color col(tint); + + const bool fill = !drawnRoomIds.contains(h.getId()); + batch.drawCharacter(pos, col, fill, g.tokenKey /*, 0.9f scale if supported */); drawnRoomIds.insert(h.getId()); } } - } From c6bf0fc377a916fad97cd3ddb02e4c7588695f17 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 15:06:25 +1000 Subject: [PATCH 25/43] fix: token quad now uses caller color, enabling 50% ghost opacity --- src/display/Characters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 8e3b2d559..45dd201dc 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -546,7 +546,7 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) const Coordinate &pos = h.getPosition(); QColor tint(Qt::white); - tint.setAlphaF(0.70f); // 70 % opacity + tint.setAlphaF(0.50f); // 50 % opacity Color col(tint); const bool fill = !drawnRoomIds.contains(h.getId()); From 3fc7e9eb2d49648651073e54dc0354b09701befe Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:01:56 +1000 Subject: [PATCH 26/43] fix: purge stale ghost using roomSid while keeping string-keyed registry --- src/display/Characters.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 45dd201dc..df6301927 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -29,7 +29,7 @@ #include #include -std::unordered_map g_ghosts; +std::unordered_map g_ghosts; static constexpr float CHAR_ARROW_LINE_WIDTH = 2.f; static constexpr float PATH_LINE_WIDTH = 4.f; @@ -537,22 +537,29 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) batch.drawCharacter(pos, col, fill, tokenKey); drawnRoomIds.insert(id); } - /* ---------- draw persistent ghost tokens ------------------------------ */ - for (const auto &[gid, g] : g_ghosts) { - if (g.roomSid == playerRoomSid) - continue; + /* ---------- draw (and purge) ghost tokens ------------------------------ */ + for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) { + const auto &ghostInfo = it->second; // key is QString, ignore it here + + /* If the player is now in that same room, drop the stale ghost entry */ + if (ghostInfo.roomSid == playerRoomSid) { // compare room ids + it = g_ghosts.erase(it); // erase returns next iterator + continue; // nothing to draw + } - if (const auto h = map.findRoomHandle(g.roomSid)) { + /* Otherwise draw the ghost icon */ + if (const auto h = map.findRoomHandle(ghostInfo.roomSid)) { // use room id const Coordinate &pos = h.getPosition(); QColor tint(Qt::white); - tint.setAlphaF(0.50f); // 50 % opacity + tint.setAlphaF(0.50f); // 50 % opacity Color col(tint); const bool fill = !drawnRoomIds.contains(h.getId()); - batch.drawCharacter(pos, col, fill, g.tokenKey /*, 0.9f scale if supported */); + batch.drawCharacter(pos, col, fill, ghostInfo.tokenKey /*, 0.9f */); drawnRoomIds.insert(h.getId()); } + ++it; // manual increment } } From 29e4ee5a741fde3faa55cf7099818d684b481966 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:18:00 +1000 Subject: [PATCH 27/43] fix: use room-id map key in ghost draw loop (struct no longer stores roomSid) --- src/display/Characters.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index df6301927..a49af1cc9 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -29,7 +29,7 @@ #include #include -std::unordered_map g_ghosts; +std::unordered_map g_ghosts; static constexpr float CHAR_ARROW_LINE_WIDTH = 2.f; static constexpr float PATH_LINE_WIDTH = 4.f; @@ -539,27 +539,26 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) } /* ---------- draw (and purge) ghost tokens ------------------------------ */ for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) { - const auto &ghostInfo = it->second; // key is QString, ignore it here + ServerRoomId ghostSid = it->first; // map key is the room id + const QString tokenKey = it->second.tokenKey; - /* If the player is now in that same room, drop the stale ghost entry */ - if (ghostInfo.roomSid == playerRoomSid) { // compare room ids - it = g_ghosts.erase(it); // erase returns next iterator - continue; // nothing to draw + if (ghostSid == playerRoomSid) { // player in same room → purge + it = g_ghosts.erase(it); + continue; } - /* Otherwise draw the ghost icon */ - if (const auto h = map.findRoomHandle(ghostInfo.roomSid)) { // use room id + /* use ghostSid here ▾ instead of ghostInfo.roomSid */ + if (const auto h = map.findRoomHandle(ghostSid)) { const Coordinate &pos = h.getPosition(); - QColor tint(Qt::white); - tint.setAlphaF(0.50f); // 50 % opacity + QColor tint(Qt::white); tint.setAlphaF(0.50f); Color col(tint); const bool fill = !drawnRoomIds.contains(h.getId()); - batch.drawCharacter(pos, col, fill, ghostInfo.tokenKey /*, 0.9f */); + batch.drawCharacter(pos, col, fill, tokenKey /*, 0.9f */); drawnRoomIds.insert(h.getId()); } - ++it; // manual increment + ++it; } } From c846571b45b1d250248ccab449b3e52be0b77f7f Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:51:29 +1000 Subject: [PATCH 28/43] config: define KEY_GROUP_SHOW_NPC_GHOSTS constant --- src/configuration/configuration.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 65921dbfc..5a5fbc31c 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -240,8 +240,9 @@ ConstString KEY_GROUP_NPC_COLOR = "npc color"; ConstString KEY_GROUP_NPC_COLOR_OVERRIDE = "npc color override"; ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom"; ConstString KEY_GROUP_NPC_HIDE = "npc hide"; -ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; ConstString KEY_GROUP_SHOW_MAP_TOKENS = "show map tokens"; +ConstString KEY_GROUP_SHOW_NPC_GHOSTS = "show npc ghosts"; +ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size"; ConstString KEY_GROUP_TOKEN_OVERRIDES = "token overrides"; ConstString KEY_AUTO_LOG = "Auto log"; @@ -696,6 +697,7 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) showTokens = conf.value(KEY_GROUP_SHOW_TOKENS, true).toBool(); showMapTokens = conf.value(KEY_GROUP_SHOW_MAP_TOKENS, true).toBool(); tokenIconSize = conf.value(KEY_GROUP_TOKEN_ICON_SIZE, 32).toInt(); + showNpcGhosts = conf.value(KEY_GROUP_SHOW_NPC_GHOSTS, true).toBool(); tokenOverrides.clear(); QSettings &rw = const_cast(conf); @@ -873,6 +875,7 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); conf.setValue(KEY_GROUP_SHOW_MAP_TOKENS, showMapTokens); conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize); + conf.setValue(KEY_GROUP_SHOW_NPC_GHOSTS, showNpcGhosts); conf.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); conf.remove(""); // wipe old map entries From 9f115ed969f5ad931487c9eab2c13f0a689f1567 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:51:50 +1000 Subject: [PATCH 29/43] config: add showNpcGhosts flag (default true) --- src/configuration/configuration.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 342266677..84ce18ed1 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -294,6 +294,7 @@ class NODISCARD Configuration final bool showTokens = true; bool showMapTokens = true; int tokenIconSize = 32; + bool showNpcGhosts = true; QMap tokenOverrides; private: From a345c594d12003e1654884f7f8ba3b6dd95b4a28 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sun, 20 Jul 2025 17:57:21 +1000 Subject: [PATCH 30/43] feat: honour 'Show last-seen NPC icon' preference in ghost logic --- src/display/Characters.cpp | 3 +++ src/group/groupwidget.cpp | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index a49af1cc9..2f93e7baa 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -538,6 +538,9 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) drawnRoomIds.insert(id); } /* ---------- draw (and purge) ghost tokens ------------------------------ */ + if (!getConfig().groupManager.showNpcGhosts) { + g_ghosts.clear(); // purge any stale registry entries + } else for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) { ServerRoomId ghostSid = it->first; // map key is the room id const QString tokenKey = it->second.tokenKey; diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 95dde565b..a700b2513 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -235,14 +235,23 @@ static void insertNewCharactersInto(GroupVector &dest, const GroupVector &newNpcs, const GroupVector &newAll) { + auto eraseGhosts = [](const GroupVector &vec) { + if (!getConfig().groupManager.showNpcGhosts) + return; + for (const auto &p : vec) + g_ghosts.erase(deref(p).getServerId()); + }; if (npcSortBottom) { // players go before first NPC already present auto firstNpc = std::find_if(dest.begin(), dest.end(), [](const SharedGroupChar &c) { return c && c->isNpc(); }); dest.insert(firstNpc, newPlayers.begin(), newPlayers.end()); + eraseGhosts(newPlayers); dest.insert(dest.end(), newNpcs.begin(), newNpcs.end()); + eraseGhosts(newNpcs); } else { dest.insert(dest.end(), newAll.begin(), newAll.end()); + eraseGhosts(newAll); } } } // namespace @@ -284,7 +293,8 @@ void GroupModel::setCharacters(const GroupVector &newGameChars) for (const auto &pGameChar : newGameChars) { const auto &gameChar = deref(pGameChar); - g_ghosts.erase(gameChar.getId()); + if (getConfig().groupManager.showNpcGhosts) + g_ghosts.erase(gameChar.getServerId()); if (existingIds.find(gameChar.getId()) == existingIds.end()) { allTrulyNewCharsInOriginalOrder.push_back(pGameChar); @@ -355,8 +365,8 @@ void GroupModel::removeCharacterById(const GroupId charId) SharedGroupChar &c = m_characters[static_cast(index)]; /*** NEW: store a ghost entry if this row is a mount ***/ - if (c->isMount()) { - g_ghosts[c->getId()] = { c->getServerId(), c->getDisplayName() }; + if (getConfig().groupManager.showNpcGhosts && c->isNpc()) { + g_ghosts[c->getServerId()] = { c->getDisplayName() }; } beginRemoveRows(QModelIndex(), index, index); From 1bbcb9e5bf62c16dd86faafc1fb68d14cc074a08 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 21 Jul 2025 12:36:08 +1000 Subject: [PATCH 31/43] feat: preferences checkbox & build tweaks for NPC ghost feature --- src/CMakeLists.txt | 1 + src/display/GhostRegistry.h | 10 +-- src/preferences/grouppage.cpp | 7 +- src/preferences/grouppage.ui | 124 +++++++++++++++++++--------------- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1bcbe2295..f9d4ed37b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -650,6 +650,7 @@ add_executable(mmapper WIN32 MACOSX_BUNDLE ${mmapper_RCS} ${mmapper_QRC} ${mmapper_DATA} + display/GhostRegistry.h ) set(mm_libs mm_map mm_global) diff --git a/src/display/GhostRegistry.h b/src/display/GhostRegistry.h index 185cef4aa..6d63625b4 100644 --- a/src/display/GhostRegistry.h +++ b/src/display/GhostRegistry.h @@ -1,14 +1,10 @@ #pragma once #include #include - - -#include "../map/roomid.h" -#include "../group/CGroupChar.h" +#include "../map/roomid.h" // ServerRoomId struct GhostInfo { - ServerRoomId roomSid; - QString tokenKey; + QString tokenKey; // icon to draw (display name) }; -extern std::unordered_map g_ghosts; +extern std::unordered_map g_ghosts; diff --git a/src/preferences/grouppage.cpp b/src/preferences/grouppage.cpp index 89d6b5aee..042bab4a9 100644 --- a/src/preferences/grouppage.cpp +++ b/src/preferences/grouppage.cpp @@ -55,7 +55,11 @@ GroupPage::GroupPage(QWidget *const parent) setConfig().groupManager.tokenIconSize = value; emit sig_groupSettingsChanged(); // live update }); - + ui->chkShowNpcGhosts->setChecked(getConfig().groupManager.showNpcGhosts); + connect(ui->chkShowNpcGhosts, &QCheckBox::stateChanged, this, [this](int checked) { + setConfig().groupManager.showNpcGhosts = checked; + emit sig_groupSettingsChanged(); + }); slot_loadConfig(); } @@ -81,6 +85,7 @@ void GroupPage::slot_loadConfig() ui->npcHideCheckbox->setChecked(settings.npcHide); ui->showTokensCheckbox->setChecked(settings.showTokens); ui->showMapTokensCheckbox->setChecked(settings.showMapTokens); + ui->chkShowNpcGhosts->setChecked(settings.showNpcGhosts); ui->tokenSizeComboBox->setCurrentText(QString::number(settings.tokenIconSize) + " px"); } diff --git a/src/preferences/grouppage.ui b/src/preferences/grouppage.ui index 93d4635b4..9e0464011 100644 --- a/src/preferences/grouppage.ui +++ b/src/preferences/grouppage.ui @@ -25,16 +25,33 @@ Appearance - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - Override NPC color: + - + true - - npcOverrideColorPushButton + + + + + + @@ -48,11 +65,27 @@ - - + + + + Override NPC color: + + + true + + + npcOverrideColorPushButton + + + + + + + true + @@ -62,34 +95,42 @@ - - - - Qt::Horizontal - - - - 40 - 20 - + + + + - + - - + + - Select + Dude, Where's my Horse? - + - Character Image Size: + Show Images on Map - + + + + Show GM Character Images + + + + + + + Select + + + + @@ -113,37 +154,10 @@ - - - - - - - true - - - - - - - Show Character Images - - - - - - - - - - true - - - - - + + - Show Images on Map + GM Character Image Size: From 6d749666e34f7a07b91bbfae644b7cc199aed7a5 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 21 Jul 2025 14:34:02 +1000 Subject: [PATCH 32/43] ci: trigger map-tokens-clean build --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d238e117..546c07ee0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ MMapper [![Code Coverage](https://codecov.io/gh/MUME/MMapper/branch/master/graph/badge.svg)](https://codecov.io/gh/MUME/MMapper) [![GitHub](https://img.shields.io/github/license/MUME/MMapper.svg)](https://github.com/MUME/MMapper/blob/master/COPYING.txt) [![snapcraft](https://snapcraft.io/mmapper/badge.svg)](https://snapcraft.io/mmapper) -[![Flathub Downloads](https://img.shields.io/flathub/downloads/org.mume.MMapper)](https://flathub.org/apps/org.mume.MMapper) [![MMapper Screenshot](/../master/appdata/screenshot1.png?raw=true "MMapper")
Download the latest version of MMapper](https://github.com/MUME/MMapper/releases) @@ -28,4 +27,5 @@ MMapper is a graphical mapping tool for the MUD (Multi-User Dungeon) game MUME ( To get started with MMapper, follow the [setup instructions](https://github.com/MUME/MMapper/wiki) in our wiki. It includes detailed steps for configuring MMapper with your MUME client and installing it on Linux, Windows, and macOS. Once installed, you'll be ready to start your mapping with ease. ## Contributing -We welcome contributions to MMapper! If you're interested in improving the tool, simply submit a pull request on GitHub. Your contributions will help improve the experience for all MUME players. You can also read the [BUILD.md](BUILD.md) to get started with building MMapper from source. +We welcome contributions to MMapper! If you're interested in improving the tool, simply submit a pull request on GitHub. Your contributions will help improve the experience for all MUME players. You can also check out the [build instructions](https://github.com/MUME/MMapper/wiki/Build) on the wiki to get started with building MMapper from source. +# trigger CI From ae14737f75c86348e5c0622fb8be7bd06c52208d Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 5 Aug 2025 12:40:27 +1000 Subject: [PATCH 33/43] tokens: draw map tokens at 85% of room size, centered (no asset changes) --- src/display/Characters.cpp | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 2f93e7baa..6d260ebff 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -280,10 +280,9 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, addTransformed(d); // ── NEW: also queue a token quad (drawn under coloured overlay) ── - if (!dispName.isEmpty() // only if we have a key - && getConfig().groupManager.showMapTokens) + if (!dispName.isEmpty() && getConfig().groupManager.showMapTokens) { - const Color &tokenColor = color; // inherits alpha from caller + const Color &tokenColor = color; // inherits alpha from caller const auto &mtx = m_stack.top().modelView; auto pushVert = [this, &tokenColor, &mtx](const glm::vec2 &roomPos, @@ -292,18 +291,26 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, m_charTokenQuads.emplace_back(tokenColor, uv, glm::vec3{tmp / tmp.w}); }; - /* four corners, matching the room square */ - pushVert(a, {0.f, 0.f}); // lower-left - pushVert(b, {1.f, 0.f}); // lower-right - pushVert(c, {1.f, 1.f}); // upper-right - pushVert(d, {0.f, 1.f}); // upper-left + // Scale the room quad around its center to 85% + static constexpr float kTokenScale = 0.85f; + const glm::vec2 center = 0.5f * (a + c); // midpoint of opposite corners + const auto scaleAround = [&](const glm::vec2 &p) { + return center + (p - center) * kTokenScale; + }; - QString key = TokenManager::overrideFor(dispName); - if (key.isEmpty()) - key = canonicalTokenKey(dispName); - else - key = canonicalTokenKey(key); + const glm::vec2 sa = scaleAround(a); + const glm::vec2 sb = scaleAround(b); + const glm::vec2 sc = scaleAround(c); + const glm::vec2 sd = scaleAround(d); + // Keep full UVs so the whole texture shows on the smaller quad + pushVert(sa, {0.f, 0.f}); // lower-left + pushVert(sb, {1.f, 0.f}); // lower-right + pushVert(sc, {1.f, 1.f}); // upper-right + pushVert(sd, {0.f, 1.f}); // upper-left + + QString key = TokenManager::overrideFor(dispName); + key = key.isEmpty() ? canonicalTokenKey(dispName) : canonicalTokenKey(key); m_charTokenKeys.emplace_back(key); } From b19d1ef68941cb2f01c67257859dc383faa583ec Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 5 Aug 2025 13:59:27 +1000 Subject: [PATCH 34/43] group: keep row height; render token icons at ~85% of row (no centering) --- src/group/groupwidget.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index a700b2513..72cd3477c 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -761,12 +761,14 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setItemDelegate(new GroupDelegate(this)); layout->addWidget(m_table); - // Minimize row height + // Row height follows configured size; draw token at 85% of that const int icon = getConfig().groupManager.tokenIconSize; - const int row = std::max(icon, m_table->fontMetrics().height() + 4); - + const int row = std::max(icon, m_table->fontMetrics().height() + 4); m_table->verticalHeader()->setDefaultSectionSize(row); - m_table->setIconSize(QSize(icon, icon)); + + const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); + m_table->setIconSize(QSize(shown, shown)); + m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { // Center map on the clicked character @@ -909,11 +911,12 @@ void GroupWidget::updateColumnVisibility() // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; - m_table->setIconSize(QSize(icon, icon)); - QFontMetrics fm = m_table->fontMetrics(); - int row = std::max(icon, fm.height() + 4); + const int row = std::max(icon, fm.height() + 4); m_table->verticalHeader()->setDefaultSectionSize(row); + + const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); + m_table->setIconSize(QSize(shown, shown)); } } From 1e652562afb8f1d6487f8a3d17ac72a68f971f1f Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 10:33:59 +1000 Subject: [PATCH 35/43] Group Manager: center character token icons and size column to scaled icon; paint background first; move header resize after model set; recalc token column/rows on icon-size change --- src/group/groupwidget.cpp | 75 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 72cd3477c..71e445393 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -101,6 +101,50 @@ NODISCARD static const char *getColumnFriendlyName(const ColumnTypeEnum column) } } // namespace +namespace { +class TokenCenteringDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + if (index.column() == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { + // Draw the default cell background (selection, grid, etc.) + QStyledItemDelegate::paint(painter, option, index); + + // Draw the icon centered over the background + QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); + if (!icon.isNull()) { + QSize iconSize = option.decorationSize; + QRect iconRect = QStyle::alignedRect( + option.direction, + Qt::AlignCenter, + iconSize, + option.rect + ); + icon.paint(painter, iconRect, Qt::AlignCenter); + } + return; + } + + QStyledItemDelegate::paint(painter, option, index); + } + + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + if (index.column() == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { + // Match icon size with padding to avoid clipping + return option.decorationSize + QSize(4, 4); + } + return QStyledItemDelegate::sizeHint(option, index); + } +}; +} // namespace + GroupProxyModel::GroupProxyModel(QObject *const parent) : QSortFilterProxyModel(parent) {} @@ -745,13 +789,25 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); - m_table->horizontalHeader()->setStretchLastSection(true); - m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_proxyModel = new GroupProxyModel(this); m_proxyModel->setSourceModel(&m_model); m_table->setModel(m_proxyModel); + // Token column: size to contents so it tracks icon size. + // Name & Room: stretch to fill; others: contents is fine. + auto *hh = m_table->horizontalHeader(); + hh->setStretchLastSection(false); + for (int c = 0; c < m_table->model()->columnCount(); ++c) { + if (c == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { + hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); + } else if (c == static_cast(ColumnTypeEnum::NAME) + || c == static_cast(ColumnTypeEnum::ROOM_NAME)) { + hh->setSectionResizeMode(c, QHeaderView::Stretch); + } else { + hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); + } + } + m_table->setDragEnabled(true); m_table->setAcceptDrops(true); m_table->setDragDropMode(QAbstractItemView::InternalMove); @@ -759,6 +815,9 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setDropIndicatorShown(true); m_table->setItemDelegate(new GroupDelegate(this)); + m_table->setItemDelegateForColumn(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), + new TokenCenteringDelegate(m_table)); + layout->addWidget(m_table); // Row height follows configured size; draw token at 85% of that @@ -768,6 +827,9 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); m_table->setIconSize(QSize(shown, shown)); + m_table->resizeColumnToContents(static_cast(ColumnTypeEnum::CHARACTER_TOKEN)); + m_table->resizeRowsToContents(); + m_table->viewport()->update(); m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { @@ -908,6 +970,8 @@ void GroupWidget::updateColumnVisibility() const bool hide_tokens = !getConfig().groupManager.showTokens; m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); + // m_table->resizeColumnsToContents(); + // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; @@ -917,6 +981,11 @@ void GroupWidget::updateColumnVisibility() const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); m_table->setIconSize(QSize(shown, shown)); + + // Recalculate column/row sizes for token column + m_table->resizeColumnToContents(static_cast(ColumnTypeEnum::CHARACTER_TOKEN)); + m_table->resizeRowsToContents(); + m_table->viewport()->update(); } } From 0208c4e99811b2150b3be666606e369a66b506bc Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 10:42:33 +1000 Subject: [PATCH 36/43] =?UTF-8?q?Group=20Manager:=20token=20column=20?= =?UTF-8?q?=E2=80=94=20paint=20background=20then=20centered=20icon;=20size?= =?UTF-8?q?Hint=20matches=20scaled=20icon;=20revert=20all-columns=20auto-r?= =?UTF-8?q?esize;=20keep=20per-column=20header=20setup=20after=20model=20s?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/group/groupwidget.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 71e445393..aa32598e7 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -970,8 +970,6 @@ void GroupWidget::updateColumnVisibility() const bool hide_tokens = !getConfig().groupManager.showTokens; m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); - // m_table->resizeColumnsToContents(); - // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; From 2c12e09e0a50676490e7f5ac939f7aa2aa1064be Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 11:08:18 +1000 Subject: [PATCH 37/43] CI: add Qt6 tripack workflow (Linux/macOS/Windows) and package as Mmapper-Token-Beta --- .github/workflows/qt6-tripack.yml | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/qt6-tripack.yml diff --git a/.github/workflows/qt6-tripack.yml b/.github/workflows/qt6-tripack.yml new file mode 100644 index 000000000..2391f83cb --- /dev/null +++ b/.github/workflows/qt6-tripack.yml @@ -0,0 +1,116 @@ +name: Qt6 Tripack (Linux/macOS/Windows) + +on: + push: + branches: [ feature/map-tokens-clean, master ] + pull_request: + branches: [ feature/map-tokens-clean, master ] + workflow_dispatch: + +jobs: + linux: + name: Linux (Ubuntu) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: jurplel/install-qt-action@v4 + with: + version: '6.8.*' + host: 'linux' + target: 'desktop' + arch: 'linux_gcc_64' + - name: Install deps + run: | + sudo apt-get update -qq + sudo apt-get install -y ninja-build libgl1-mesa-dev libglu1-mesa-dev zlib1g-dev libssl-dev qtkeychain-qt6-dev + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build --config Release --parallel + - name: Package artifact (rename binary) + run: | + mkdir -p out/Linux + if [ -f build/src/mmapper ]; then + cp build/src/mmapper out/Linux/Mmapper-Token-Beta + else + cp build/mmapper out/Linux/Mmapper-Token-Beta || true + fi + tar -C out -czf Mmapper-Token-Beta-Linux.tar.gz Linux + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Mmapper-Token-Beta-Linux + path: Mmapper-Token-Beta-Linux.tar.gz + + macos: + name: macOS (Intel) + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - uses: jurplel/install-qt-action@v4 + with: + version: '6.8.*' + host: 'mac' + target: 'desktop' + arch: 'clang_64' + - name: Ensure Ninja + run: brew install ninja + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build --config Release --parallel + - name: Deploy .app with Qt frameworks + run: | + APP_PATH="build/src/mmapper.app" + if [ ! -d "$APP_PATH" ]; then + APP_PATH="$(/usr/bin/find build -name 'mmapper.app' -maxdepth 3 | head -n1)" + fi + echo "Using app: $APP_PATH" + macdeployqt "$APP_PATH" -always-overwrite + DEST="Mmapper-Token-Beta.app" + rm -rf "$DEST" + cp -R "$APP_PATH" "$DEST" + /usr/bin/zip -r Mmapper-Token-Beta-macOS.zip "$DEST" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Mmapper-Token-Beta-macOS + path: Mmapper-Token-Beta-macOS.zip + + windows: + name: Windows (MSVC) + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: jurplel/install-qt-action@v4 + with: + version: '6.8.*' + host: 'windows' + target: 'desktop' + arch: 'win64_msvc2019_64' + - name: Install Ninja + run: choco install ninja --no-progress + - name: Configure + shell: bash + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + - name: Build + shell: bash + run: cmake --build build --config Release --parallel + - name: Deploy with windeployqt and package + shell: bash + run: | + set -e + EXE="$(/usr/bin/find build -name 'mmapper.exe' -maxdepth 4 | head -n1)" + echo "Using exe: $EXE" + OUT="Mmapper-Token-Beta" + rm -rf "$OUT" + mkdir -p "$OUT" + cp "$EXE" "$OUT/Mmapper-Token-Beta.exe" + windeployqt --release "$OUT/Mmapper-Token-Beta.exe" + powershell -Command "Compress-Archive -Path '$OUT/*' -DestinationPath 'Mmapper-Token-Beta-Windows.zip' -Force" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Mmapper-Token-Beta-Windows + path: Mmapper-Token-Beta-Windows.zip From 4cffdf9d43a5de5392aaa17b79c2b031aae318d7 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 14 Aug 2025 11:12:21 +1000 Subject: [PATCH 38/43] CI: align workflows with upstream; remove custom workflows --- .github/workflows/build-map-tokens-clean | 158 ----------------------- .github/workflows/build-tokens | 1 - .github/workflows/qt6-tripack.yml | 116 ----------------- 3 files changed, 275 deletions(-) delete mode 100644 .github/workflows/build-map-tokens-clean delete mode 100644 .github/workflows/build-tokens delete mode 100644 .github/workflows/qt6-tripack.yml diff --git a/.github/workflows/build-map-tokens-clean b/.github/workflows/build-map-tokens-clean deleted file mode 100644 index e21aca6c8..000000000 --- a/.github/workflows/build-map-tokens-clean +++ /dev/null @@ -1,158 +0,0 @@ -name: Build Map-Tokens-Clean - -# Only trigger on this branch -on: - push: - branches: - - feature/map-tokens-clean - pull_request: - branches: - - feature/map-tokens-clean - workflow_dispatch: - -jobs: - build: - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: [windows-2022, macos-13, macos-14, ubuntu-22.04] - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Get Git tag description - id: git_info - run: | - echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV - - - uses: lukka/get-cmake@latest - - # Linux deps - - if: runner.os == 'Linux' - name: Install Packages for Ubuntu - run: | - sudo apt update -qq - sudo apt install -y build-essential git qt5-qmake libqt5opengl5-dev \ - libqt5websockets5-dev zlib1g-dev libssl-dev qt5keychain-dev \ - mold gcc-12 g++-12 - echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV - echo "CC=gcc-12" >> $GITHUB_ENV - echo "CXX=g++-12" >> $GITHUB_ENV - echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV - - # macOS deps - - if: runner.os == 'macOS' - name: Install Packages for Mac - run: | - brew install qt5 - brew link qt5 --force - echo "$(brew --prefix qt5)/bin" >> $GITHUB_PATH - - # Windows deps - - if: runner.os == 'Windows' - name: Install Qt for Windows (MinGW) - uses: jurplel/install-qt-action@v4 - with: - version: 5.15.2 - dir: 'C:\' - arch: win64_mingw81 - cache: true - tools: 'tools_mingw1310' - - if: runner.os == 'Windows' - name: Install Packages for Windows - uses: crazy-max/ghaction-chocolatey@v3 - with: - args: install openssl --version=1.1.1.2100 - - # Build on Windows - - if: runner.os == 'Windows' - name: Build MMapper for Windows - shell: pwsh - run: | - xcopy "C:\Program Files\OpenSSL" "C:\OpenSSL" /E /I /H /K /Y - mkdir ${{ github.workspace }}/artifact - mkdir build - cd build - cmake --version - echo "C:/Qt/Tools/mingw1310_64/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $env:PATH = "C:\Qt\Tools\mingw1310_64\bin;$env:PATH" - $packageDir = ($env:GITHUB_WORKSPACE -replace '\\', '/') + "/artifact" - cmake -G "MinGW Makefiles" \ - -DCPACK_PACKAGE_DIRECTORY="$packageDir" \ - -DCMAKE_PREFIX_PATH="C:\Qt\5.15.2\mingw81_64" \ - -DOPENSSL_ROOT_DIR=C:/OpenSSL/ \ - -DWITH_WEBSOCKET=ON \ - -DWITH_QTKEYCHAIN=ON \ - -S .. || exit -1 - cmake --build . -j $env:NUMBER_OF_PROCESSORS - - # Build on macOS & Linux - - if: runner.os == 'Linux' || runner.os == 'macOS' - name: Build MMapper for Linux and Mac - run: | - mkdir -p build ${{ github.workspace }}/artifact - cd build - cmake --version - cmake -G 'Ninja' \ - -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact \ - $MMAPPER_CMAKE_EXTRA \ - -DWITH_WEBSOCKET=ON \ - -DWITH_QTKEYCHAIN=ON \ - -S .. || exit -1 - cmake --build . - - # Package - - name: Package MMapper - run: cd build && cpack - - # Capture package names - - if: runner.os == 'Linux' - name: Get Linux package file base name - id: get_linux_package_name - run: | - PACKAGE_FILE=$(ls ${{ github.workspace }}/artifact/*.deb) - PACKAGE_BASENAME=$(basename "$PACKAGE_FILE" | sed 's/\./-/g') - echo "PACKAGE_BASENAME=$PACKAGE_BASENAME" >> $GITHUB_OUTPUT - - - if: runner.os == 'macOS' - name: Get Mac package file base name - id: get_mac_package_name - run: | - PACKAGE_FILE=$(ls ${{ github.workspace }}/artifact/*.dmg) - PACKAGE_BASENAME=$(basename "$PACKAGE_FILE" | sed 's/\./-/g') - echo "PACKAGE_BASENAME=$PACKAGE_BASENAME" >> $GITHUB_OUTPUT - - - if: runner.os == 'Windows' - name: Get Windows package file base name - id: get_windows_package_name - shell: pwsh - run: | - $packageFile = Get-ChildItem -Path "${{ github.workspace }}/artifact/*.exe" | Select-Object -First 1 - $packageBasename = $packageFile.BaseName -replace '\.', '-' - "PACKAGE_BASENAME=$packageBasename" >> $env:GITHUB_OUTPUT - - # Upload artifacts - - if: runner.os == 'macOS' - name: Upload Package for Mac - uses: actions/upload-artifact@v4 - with: - name: release-${{ steps.get_mac_package_name.outputs.PACKAGE_BASENAME }} - path: ${{ github.workspace }}/artifact/*.dmg* - - if: runner.os == 'Linux' - name: Upload Package for Ubuntu - uses: actions/upload-artifact@v4 - with: - name: release-${{ steps.get_linux_package_name.outputs.PACKAGE_BASENAME }} - path: ${{ github.workspace }}/artifact/*.deb* - - if: runner.os == 'Windows' - name: Upload Package for Windows - uses: actions/upload-artifact@v4 - with: - name: release-${{ steps.get_windows_package_name.outputs.PACKAGE_BASENAME }} - path: ${{ github.workspace }}/artifact/*.exe* - diff --git a/.github/workflows/build-tokens b/.github/workflows/build-tokens deleted file mode 100644 index 8b1378917..000000000 --- a/.github/workflows/build-tokens +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.github/workflows/qt6-tripack.yml b/.github/workflows/qt6-tripack.yml deleted file mode 100644 index 2391f83cb..000000000 --- a/.github/workflows/qt6-tripack.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Qt6 Tripack (Linux/macOS/Windows) - -on: - push: - branches: [ feature/map-tokens-clean, master ] - pull_request: - branches: [ feature/map-tokens-clean, master ] - workflow_dispatch: - -jobs: - linux: - name: Linux (Ubuntu) - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: jurplel/install-qt-action@v4 - with: - version: '6.8.*' - host: 'linux' - target: 'desktop' - arch: 'linux_gcc_64' - - name: Install deps - run: | - sudo apt-get update -qq - sudo apt-get install -y ninja-build libgl1-mesa-dev libglu1-mesa-dev zlib1g-dev libssl-dev qtkeychain-qt6-dev - - name: Configure - run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - name: Build - run: cmake --build build --config Release --parallel - - name: Package artifact (rename binary) - run: | - mkdir -p out/Linux - if [ -f build/src/mmapper ]; then - cp build/src/mmapper out/Linux/Mmapper-Token-Beta - else - cp build/mmapper out/Linux/Mmapper-Token-Beta || true - fi - tar -C out -czf Mmapper-Token-Beta-Linux.tar.gz Linux - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: Mmapper-Token-Beta-Linux - path: Mmapper-Token-Beta-Linux.tar.gz - - macos: - name: macOS (Intel) - runs-on: macos-13 - steps: - - uses: actions/checkout@v4 - - uses: jurplel/install-qt-action@v4 - with: - version: '6.8.*' - host: 'mac' - target: 'desktop' - arch: 'clang_64' - - name: Ensure Ninja - run: brew install ninja - - name: Configure - run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - name: Build - run: cmake --build build --config Release --parallel - - name: Deploy .app with Qt frameworks - run: | - APP_PATH="build/src/mmapper.app" - if [ ! -d "$APP_PATH" ]; then - APP_PATH="$(/usr/bin/find build -name 'mmapper.app' -maxdepth 3 | head -n1)" - fi - echo "Using app: $APP_PATH" - macdeployqt "$APP_PATH" -always-overwrite - DEST="Mmapper-Token-Beta.app" - rm -rf "$DEST" - cp -R "$APP_PATH" "$DEST" - /usr/bin/zip -r Mmapper-Token-Beta-macOS.zip "$DEST" - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: Mmapper-Token-Beta-macOS - path: Mmapper-Token-Beta-macOS.zip - - windows: - name: Windows (MSVC) - runs-on: windows-2022 - steps: - - uses: actions/checkout@v4 - - uses: ilammy/msvc-dev-cmd@v1 - - uses: jurplel/install-qt-action@v4 - with: - version: '6.8.*' - host: 'windows' - target: 'desktop' - arch: 'win64_msvc2019_64' - - name: Install Ninja - run: choco install ninja --no-progress - - name: Configure - shell: bash - run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - - name: Build - shell: bash - run: cmake --build build --config Release --parallel - - name: Deploy with windeployqt and package - shell: bash - run: | - set -e - EXE="$(/usr/bin/find build -name 'mmapper.exe' -maxdepth 4 | head -n1)" - echo "Using exe: $EXE" - OUT="Mmapper-Token-Beta" - rm -rf "$OUT" - mkdir -p "$OUT" - cp "$EXE" "$OUT/Mmapper-Token-Beta.exe" - windeployqt --release "$OUT/Mmapper-Token-Beta.exe" - powershell -Command "Compress-Archive -Path '$OUT/*' -DestinationPath 'Mmapper-Token-Beta-Windows.zip' -Force" - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: Mmapper-Token-Beta-Windows - path: Mmapper-Token-Beta-Windows.zip From 91ca251c504a8e0796ef1739018f8bf3d7a37b2b Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 18 Sep 2025 08:07:00 +1000 Subject: [PATCH 39/43] =?UTF-8?q?Revert=20"Group=20Manager:=20token=20colu?= =?UTF-8?q?mn=20=E2=80=94=20paint=20background=20then=20centered=20icon;?= =?UTF-8?q?=20sizeHint=20matches=20scaled=20icon;=20revert=20all-columns?= =?UTF-8?q?=20auto-resize;=20keep=20per-column=20header=20setup=20after=20?= =?UTF-8?q?model=20set"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bf166d2c42bd41b42424eec8ef70799e8417518d. --- src/group/groupwidget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index aa32598e7..71e445393 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -970,6 +970,8 @@ void GroupWidget::updateColumnVisibility() const bool hide_tokens = !getConfig().groupManager.showTokens; m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); + // m_table->resizeColumnsToContents(); + // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; From bda71ce125ccc48b63835f835de4277603fc1dc3 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 18 Sep 2025 08:07:00 +1000 Subject: [PATCH 40/43] Revert "Group Manager: center character token icons and size column to scaled icon; paint background first; move header resize after model set; recalc token column/rows on icon-size change" This reverts commit a1bde366db072c3cc0e1ddef09fb7cf36620d840. --- src/group/groupwidget.cpp | 75 ++------------------------------------- 1 file changed, 3 insertions(+), 72 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 71e445393..72cd3477c 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -101,50 +101,6 @@ NODISCARD static const char *getColumnFriendlyName(const ColumnTypeEnum column) } } // namespace -namespace { -class TokenCenteringDelegate : public QStyledItemDelegate -{ -public: - using QStyledItemDelegate::QStyledItemDelegate; - - void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const override - { - if (index.column() == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { - // Draw the default cell background (selection, grid, etc.) - QStyledItemDelegate::paint(painter, option, index); - - // Draw the icon centered over the background - QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); - if (!icon.isNull()) { - QSize iconSize = option.decorationSize; - QRect iconRect = QStyle::alignedRect( - option.direction, - Qt::AlignCenter, - iconSize, - option.rect - ); - icon.paint(painter, iconRect, Qt::AlignCenter); - } - return; - } - - QStyledItemDelegate::paint(painter, option, index); - } - - - QSize sizeHint(const QStyleOptionViewItem &option, - const QModelIndex &index) const override - { - if (index.column() == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { - // Match icon size with padding to avoid clipping - return option.decorationSize + QSize(4, 4); - } - return QStyledItemDelegate::sizeHint(option, index); - } -}; -} // namespace - GroupProxyModel::GroupProxyModel(QObject *const parent) : QSortFilterProxyModel(parent) {} @@ -789,25 +745,13 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->horizontalHeader()->setStretchLastSection(true); + m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_proxyModel = new GroupProxyModel(this); m_proxyModel->setSourceModel(&m_model); m_table->setModel(m_proxyModel); - // Token column: size to contents so it tracks icon size. - // Name & Room: stretch to fill; others: contents is fine. - auto *hh = m_table->horizontalHeader(); - hh->setStretchLastSection(false); - for (int c = 0; c < m_table->model()->columnCount(); ++c) { - if (c == static_cast(ColumnTypeEnum::CHARACTER_TOKEN)) { - hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); - } else if (c == static_cast(ColumnTypeEnum::NAME) - || c == static_cast(ColumnTypeEnum::ROOM_NAME)) { - hh->setSectionResizeMode(c, QHeaderView::Stretch); - } else { - hh->setSectionResizeMode(c, QHeaderView::ResizeToContents); - } - } - m_table->setDragEnabled(true); m_table->setAcceptDrops(true); m_table->setDragDropMode(QAbstractItemView::InternalMove); @@ -815,9 +759,6 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setDropIndicatorShown(true); m_table->setItemDelegate(new GroupDelegate(this)); - m_table->setItemDelegateForColumn(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), - new TokenCenteringDelegate(m_table)); - layout->addWidget(m_table); // Row height follows configured size; draw token at 85% of that @@ -827,9 +768,6 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); m_table->setIconSize(QSize(shown, shown)); - m_table->resizeColumnToContents(static_cast(ColumnTypeEnum::CHARACTER_TOKEN)); - m_table->resizeRowsToContents(); - m_table->viewport()->update(); m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { @@ -970,8 +908,6 @@ void GroupWidget::updateColumnVisibility() const bool hide_tokens = !getConfig().groupManager.showTokens; m_table->setColumnHidden(static_cast(ColumnTypeEnum::CHARACTER_TOKEN), hide_tokens); - // m_table->resizeColumnsToContents(); - // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; @@ -981,11 +917,6 @@ void GroupWidget::updateColumnVisibility() const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); m_table->setIconSize(QSize(shown, shown)); - - // Recalculate column/row sizes for token column - m_table->resizeColumnToContents(static_cast(ColumnTypeEnum::CHARACTER_TOKEN)); - m_table->resizeRowsToContents(); - m_table->viewport()->update(); } } From 96772c6dcaf6048dbec13fa28c1a76d47941583d Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 18 Sep 2025 08:07:00 +1000 Subject: [PATCH 41/43] Revert "group: keep row height; render token icons at ~85% of row (no centering)" This reverts commit 4f3de6d42ae8c592240c9b7e3d4e3028427268c9. --- src/group/groupwidget.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index 72cd3477c..a700b2513 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -761,14 +761,12 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget m_table->setItemDelegate(new GroupDelegate(this)); layout->addWidget(m_table); - // Row height follows configured size; draw token at 85% of that + // Minimize row height const int icon = getConfig().groupManager.tokenIconSize; - const int row = std::max(icon, m_table->fontMetrics().height() + 4); - m_table->verticalHeader()->setDefaultSectionSize(row); - - const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); - m_table->setIconSize(QSize(shown, shown)); + const int row = std::max(icon, m_table->fontMetrics().height() + 4); + m_table->verticalHeader()->setDefaultSectionSize(row); + m_table->setIconSize(QSize(icon, icon)); m_center = new QAction(QIcon(":/icons/roomfind.png"), tr("&Center"), this); connect(m_center, &QAction::triggered, this, [this]() { // Center map on the clicked character @@ -911,12 +909,11 @@ void GroupWidget::updateColumnVisibility() // Apply current icon-size preference every time settings change { const int icon = getConfig().groupManager.tokenIconSize; + m_table->setIconSize(QSize(icon, icon)); + QFontMetrics fm = m_table->fontMetrics(); - const int row = std::max(icon, fm.height() + 4); + int row = std::max(icon, fm.height() + 4); m_table->verticalHeader()->setDefaultSectionSize(row); - - const int shown = std::max(1, static_cast(std::lround(row * 0.85f))); - m_table->setIconSize(QSize(shown, shown)); } } From 1962896cb6d067553285852411b8cba95551f31c Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 22 Oct 2025 13:47:40 +1100 Subject: [PATCH 42/43] UI: default unchecked checkboxes; default token size 32px --- src/configuration/configuration.cpp | 6 +- src/configuration/configuration.h | 4 +- src/display/Characters.cpp | 114 ++++++++++++++-------------- src/display/Characters.h | 8 +- src/display/GhostRegistry.h | 9 ++- src/group/CGroupChar.h | 13 +--- src/group/groupwidget.cpp | 87 +++++++++------------ src/group/tokenmanager.cpp | 16 ++-- src/mainwindow/mainwindow.cpp | 5 +- src/preferences/grouppage.ui | 13 +++- 10 files changed, 135 insertions(+), 140 deletions(-) diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 5a5fbc31c..2cc148e6d 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -242,7 +242,7 @@ ConstString KEY_GROUP_NPC_SORT_BOTTOM = "npc sort bottom"; ConstString KEY_GROUP_NPC_HIDE = "npc hide"; ConstString KEY_GROUP_SHOW_MAP_TOKENS = "show map tokens"; ConstString KEY_GROUP_SHOW_NPC_GHOSTS = "show npc ghosts"; -ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; +ConstString KEY_GROUP_SHOW_TOKENS = "show tokens"; ConstString KEY_GROUP_TOKEN_ICON_SIZE = "token icon size"; ConstString KEY_GROUP_TOKEN_OVERRIDES = "token overrides"; ConstString KEY_AUTO_LOG = "Auto log"; @@ -872,13 +872,13 @@ void Configuration::GroupManagerSettings::write(QSettings &conf) const conf.setValue(KEY_GROUP_NPC_COLOR_OVERRIDE, npcColorOverride); conf.setValue(KEY_GROUP_NPC_HIDE, npcHide); conf.setValue(KEY_GROUP_NPC_SORT_BOTTOM, npcSortBottom); - conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); + conf.setValue(KEY_GROUP_SHOW_TOKENS, showTokens); conf.setValue(KEY_GROUP_SHOW_MAP_TOKENS, showMapTokens); conf.setValue(KEY_GROUP_TOKEN_ICON_SIZE, tokenIconSize); conf.setValue(KEY_GROUP_SHOW_NPC_GHOSTS, showNpcGhosts); conf.beginGroup(KEY_GROUP_TOKEN_OVERRIDES); - conf.remove(""); // wipe old map entries + conf.remove(""); // wipe old map entries for (auto it = tokenOverrides.cbegin(); it != tokenOverrides.cend(); ++it) conf.setValue(it.key(), it.value()); conf.endGroup(); diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 84ce18ed1..f7f6fe0c6 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -291,10 +291,10 @@ class NODISCARD Configuration final bool npcColorOverride = false; bool npcSortBottom = false; bool npcHide = false; - bool showTokens = true; + bool showTokens = true; bool showMapTokens = true; int tokenIconSize = 32; - bool showNpcGhosts = true; + bool showNpcGhosts = true; QMap tokenOverrides; private: diff --git a/src/display/Characters.cpp b/src/display/Characters.cpp index 6d260ebff..2bc268878 100644 --- a/src/display/Characters.cpp +++ b/src/display/Characters.cpp @@ -13,11 +13,11 @@ #include "../mapdata/roomselection.h" #include "../opengl/OpenGL.h" #include "../opengl/OpenGLTypes.h" +#include "GhostRegistry.h" #include "MapCanvasData.h" #include "Textures.h" #include "mapcanvas.h" #include "prespammedpath.h" -#include "GhostRegistry.h" #include #include @@ -26,8 +26,8 @@ #include -#include #include +#include std::unordered_map g_ghosts; @@ -53,7 +53,10 @@ bool CharacterBatch::isVisible(const Coordinate &c, float margin) const return m_mapScreen.isRoomVisible(c, margin); } -void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool fill,const QString &dispName) +void CharacterBatch::drawCharacter(const Coordinate &c, + const Color &color, + bool fill, + const QString &dispName) { const Configuration::CanvasSettings &settings = getConfig().canvas; @@ -104,7 +107,8 @@ void CharacterBatch::drawCharacter(const Coordinate &c, const Color &color, bool } const bool beacon = visible && !differentLayer && wantBeacons; - gl.drawBox(c, fill, beacon, isFar, dispName);} + gl.drawBox(c, fill, beacon, isFar, dispName); +} void CharacterBatch::drawPreSpammedPath(const Coordinate &c1, const std::vector &path, @@ -215,11 +219,8 @@ void CharacterBatch::CharFakeGL::drawQuadCommon(const glm::vec2 &in_a, } } -void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, - bool fill, - bool beacon, - const bool isFar, - const QString &dispName) +void CharacterBatch::CharFakeGL::drawBox( + const Coordinate &coord, bool fill, bool beacon, const bool isFar, const QString &dispName) { const bool dontFillRotatedQuads = true; const bool shrinkRotatedQuads = false; // REVISIT: make this a user option? @@ -280,9 +281,8 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, addTransformed(d); // ── NEW: also queue a token quad (drawn under coloured overlay) ── - if (!dispName.isEmpty() && getConfig().groupManager.showMapTokens) - { - const Color &tokenColor = color; // inherits alpha from caller + if (!dispName.isEmpty() && getConfig().groupManager.showMapTokens) { + const Color &tokenColor = color; // inherits alpha from caller const auto &mtx = m_stack.top().modelView; auto pushVert = [this, &tokenColor, &mtx](const glm::vec2 &roomPos, @@ -293,7 +293,7 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, // Scale the room quad around its center to 85% static constexpr float kTokenScale = 0.85f; - const glm::vec2 center = 0.5f * (a + c); // midpoint of opposite corners + const glm::vec2 center = 0.5f * (a + c); // midpoint of opposite corners const auto scaleAround = [&](const glm::vec2 &p) { return center + (p - center) * kTokenScale; }; @@ -304,10 +304,10 @@ void CharacterBatch::CharFakeGL::drawBox(const Coordinate &coord, const glm::vec2 sd = scaleAround(d); // Keep full UVs so the whole texture shows on the smaller quad - pushVert(sa, {0.f, 0.f}); // lower-left - pushVert(sb, {1.f, 0.f}); // lower-right - pushVert(sc, {1.f, 1.f}); // upper-right - pushVert(sd, {0.f, 1.f}); // upper-left + pushVert(sa, {0.f, 0.f}); // lower-left + pushVert(sb, {1.f, 0.f}); // lower-right + pushVert(sc, {1.f, 1.f}); // upper-right + pushVert(sd, {0.f, 1.f}); // upper-left QString key = TokenManager::overrideFor(dispName); key = key.isEmpty() ? canonicalTokenKey(dispName) : canonicalTokenKey(key); @@ -359,7 +359,7 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva break; const QString &key = m_charTokenKeys[q]; - MMTextureId id = tokenManager().textureIdFor(key); + MMTextureId id = tokenManager().textureIdFor(key); if (id == INVALID_MM_TEXTURE_ID) { QPixmap px = tokenManager().getToken(key); @@ -376,12 +376,11 @@ void CharacterBatch::CharFakeGL::reallyDrawCharacters(OpenGL &gl, const MapCanva if (id == INVALID_MM_TEXTURE_ID) continue; - gl.renderColoredTexturedQuads( - {m_charTokenQuads[base + 0], - m_charTokenQuads[base + 1], - m_charTokenQuads[base + 2], - m_charTokenQuads[base + 3]}, - blended_noDepth.withTexture0(id)); + gl.renderColoredTexturedQuads({m_charTokenQuads[base + 0], + m_charTokenQuads[base + 1], + m_charTokenQuads[base + 2], + m_charTokenQuads[base + 3]}, + blended_noDepth.withTexture0(id)); } if (!m_charRoomQuads.empty()) { @@ -460,7 +459,7 @@ void MapCanvas::paintCharacters() CharacterBatch characterBatch{m_mapScreen, m_currentLayer, getTotalScaleFactor()}; const CGroupChar *playerChar = nullptr; for (const auto &pCharacter : m_groupManager.selectAll()) { - if (pCharacter->isYou()) { // ← ‘isYou()’ marks the local player + if (pCharacter->isYou()) { // ← ‘isYou()’ marks the local player playerChar = pCharacter.get(); break; } @@ -479,11 +478,11 @@ void MapCanvas::paintCharacters() // paint char current position const Color color{getConfig().groupManager.color}; - characterBatch.drawCharacter(pos, color, - /*fill =*/ true, - /*name =*/ playerChar - ? playerChar->getDisplayName() - : QString()); + characterBatch + .drawCharacter(pos, + color, + /*fill =*/true, + /*name =*/playerChar ? playerChar->getDisplayName() : QString()); // paint prespam const auto prespam = m_data.getPath(id, m_prespammedPath.getQueue()); @@ -510,11 +509,11 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) /* Find the player's room once */ const CGroupChar *playerChar = nullptr; - ServerRoomId playerRoomSid = INVALID_SERVER_ROOMID; + ServerRoomId playerRoomSid = INVALID_SERVER_ROOMID; RoomId playerRoomId = INVALID_ROOMID; for (const auto &p : m_groupManager.selectAll()) { if (p->isYou()) { - playerChar = p.get(); + playerChar = p.get(); playerRoomSid = p->getServerId(); if (const auto r = map.findRoomHandle(p->getServerId())) playerRoomId = r.getId(); @@ -529,46 +528,47 @@ void MapCanvas::drawGroupCharacters(CharacterBatch &batch) if (character.isYou()) continue; const auto r = map.findRoomHandle(character.getServerId()); - if (!r) continue; // skip Unknown rooms + if (!r) + continue; // skip Unknown rooms - const RoomId id = r.getId(); - const Coordinate & pos = r.getPosition(); - const Color col = Color{character.getColor()}; - const bool fill = !drawnRoomIds.contains(id); + const RoomId id = r.getId(); + const Coordinate &pos = r.getPosition(); + const Color col = Color{character.getColor()}; + const bool fill = !drawnRoomIds.contains(id); const bool showToken = (id != playerRoomId); const QString tokenKey = showToken ? character.getDisplayName() - : QString(); // empty → no token + : QString(); // empty → no token batch.drawCharacter(pos, col, fill, tokenKey); drawnRoomIds.insert(id); } /* ---------- draw (and purge) ghost tokens ------------------------------ */ if (!getConfig().groupManager.showNpcGhosts) { - g_ghosts.clear(); // purge any stale registry entries + g_ghosts.clear(); // purge any stale registry entries } else - for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) { - ServerRoomId ghostSid = it->first; // map key is the room id - const QString tokenKey = it->second.tokenKey; + for (auto it = g_ghosts.begin(); it != g_ghosts.end(); /* ++ in body */) { + ServerRoomId ghostSid = it->first; // map key is the room id + const QString tokenKey = it->second.tokenKey; - if (ghostSid == playerRoomSid) { // player in same room → purge - it = g_ghosts.erase(it); - continue; - } + if (ghostSid == playerRoomSid) { // player in same room → purge + it = g_ghosts.erase(it); + continue; + } - /* use ghostSid here ▾ instead of ghostInfo.roomSid */ - if (const auto h = map.findRoomHandle(ghostSid)) { - const Coordinate &pos = h.getPosition(); + /* use ghostSid here ▾ instead of ghostInfo.roomSid */ + if (const auto h = map.findRoomHandle(ghostSid)) { + const Coordinate &pos = h.getPosition(); - QColor tint(Qt::white); tint.setAlphaF(0.50f); - Color col(tint); + QColor tint(Qt::white); + tint.setAlphaF(0.50f); + Color col(tint); - const bool fill = !drawnRoomIds.contains(h.getId()); - batch.drawCharacter(pos, col, fill, tokenKey /*, 0.9f */); - drawnRoomIds.insert(h.getId()); + const bool fill = !drawnRoomIds.contains(h.getId()); + batch.drawCharacter(pos, col, fill, tokenKey /*, 0.9f */); + drawnRoomIds.insert(h.getId()); + } + ++it; } - ++it; - } } - diff --git a/src/display/Characters.h b/src/display/Characters.h index d0cda2a25..fde011808 100644 --- a/src/display/Characters.h +++ b/src/display/Characters.h @@ -151,7 +151,8 @@ class NODISCARD CharacterBatch final m = glm::translate(m, v); } void drawArrow(bool fill, bool beacon); - void drawBox(const Coordinate &coord, bool fill, bool beacon, bool isFar,const QString &dispName); + void drawBox( + const Coordinate &coord, bool fill, bool beacon, bool isFar, const QString &dispName); void addScreenSpaceArrow(const glm::vec3 &pos, float degrees, const Color &color, bool fill); // with blending, without depth; always size 4 @@ -217,7 +218,10 @@ class NODISCARD CharacterBatch final NODISCARD bool isVisible(const Coordinate &c, float margin) const; public: - void drawCharacter(const Coordinate &coordinate, const Color &color, bool fill = true,const QString &dispName = QString()); + void drawCharacter(const Coordinate &coordinate, + const Color &color, + bool fill = true, + const QString &dispName = QString()); void drawPreSpammedPath(const Coordinate &coordinate, const std::vector &path, diff --git a/src/display/GhostRegistry.h b/src/display/GhostRegistry.h index 6d63625b4..d90acb685 100644 --- a/src/display/GhostRegistry.h +++ b/src/display/GhostRegistry.h @@ -1,10 +1,13 @@ #pragma once +#include "../map/roomid.h" // ServerRoomId + #include + #include -#include "../map/roomid.h" // ServerRoomId -struct GhostInfo { - QString tokenKey; // icon to draw (display name) +struct GhostInfo +{ + QString tokenKey; // icon to draw (display name) }; extern std::unordered_map g_ghosts; diff --git a/src/group/CGroupChar.h b/src/group/CGroupChar.h index aacf6cb30..59077c11a 100644 --- a/src/group/CGroupChar.h +++ b/src/group/CGroupChar.h @@ -124,10 +124,9 @@ class NODISCARD CGroupChar final : public std::enable_shared_from_thisisNpc(); }); + auto firstNpc = std::find_if(dest.begin(), dest.end(), [](const SharedGroupChar &c) { + return c && c->isNpc(); + }); dest.insert(firstNpc, newPlayers.begin(), newPlayers.end()); eraseGhosts(newPlayers); dest.insert(dest.end(), newNpcs.begin(), newNpcs.end()); @@ -366,7 +367,7 @@ void GroupModel::removeCharacterById(const GroupId charId) /*** NEW: store a ghost entry if this row is a mount ***/ if (getConfig().groupManager.showNpcGhosts && c->isNpc()) { - g_ghosts[c->getServerId()] = { c->getDisplayName() }; + g_ghosts[c->getServerId()] = {c->getDisplayName()}; } beginRemoveRows(QModelIndex(), index, index); @@ -450,13 +451,9 @@ NODISCARD static QStringView getPrettyName(const CharacterAffectEnum affect) /*───────────────────── complexity helpers ─────────────────────*/ namespace { -static QString formatStatHelper(int num, - int den, - ColumnTypeEnum col, - bool isNpc) +static QString formatStatHelper(int num, int den, ColumnTypeEnum col, bool isNpc) { - if (col == ColumnTypeEnum::HP_PERCENT - || col == ColumnTypeEnum::MANA_PERCENT + if (col == ColumnTypeEnum::HP_PERCENT || col == ColumnTypeEnum::MANA_PERCENT || col == ColumnTypeEnum::MOVES_PERCENT) { if (den == 0) return {}; @@ -464,9 +461,7 @@ static QString formatStatHelper(int num, return QString("%1%").arg(pct); } - if (col == ColumnTypeEnum::HP - || col == ColumnTypeEnum::MANA - || col == ColumnTypeEnum::MOVES) { + if (col == ColumnTypeEnum::HP || col == ColumnTypeEnum::MANA || col == ColumnTypeEnum::MOVES) { // hide “0/0” for NPCs -- same behaviour as before if (isNpc && num == 0 && den == 0) return {}; @@ -476,21 +471,17 @@ static QString formatStatHelper(int num, } /// Big switch for DisplayRole, extracted out of dataForCharacter -static QVariant makeDisplayRole(const CGroupChar &ch, ColumnTypeEnum c, - TokenManager *tm) +static QVariant makeDisplayRole(const CGroupChar &ch, ColumnTypeEnum c, TokenManager *tm) { switch (c) { case ColumnTypeEnum::CHARACTER_TOKEN: return tm ? QIcon(tm->getToken(ch.getDisplayName())) : QVariant(); case ColumnTypeEnum::NAME: if (ch.getLabel().isEmpty() - || ch.getName().getStdStringViewUtf8() - == ch.getLabel().getStdStringViewUtf8()) { + || ch.getName().getStdStringViewUtf8() == ch.getLabel().getStdStringViewUtf8()) { return ch.getName().toQString(); } else { - return QString("%1 (%2)") - .arg(ch.getName().toQString(), - ch.getLabel().toQString()); + return QString("%1 (%2)").arg(ch.getName().toQString(), ch.getLabel().toQString()); } case ColumnTypeEnum::HP_PERCENT: return formatStatHelper(ch.getHits(), ch.getMaxHits(), c, false); @@ -499,31 +490,32 @@ static QVariant makeDisplayRole(const CGroupChar &ch, ColumnTypeEnum c, case ColumnTypeEnum::MOVES_PERCENT: return formatStatHelper(ch.getMoves(), ch.getMaxMoves(), c, false); case ColumnTypeEnum::HP: - return formatStatHelper(ch.getHits(), ch.getMaxHits(), c, + return formatStatHelper(ch.getHits(), + ch.getMaxHits(), + c, ch.getType() == CharacterTypeEnum::NPC); case ColumnTypeEnum::MANA: - return formatStatHelper(ch.getMana(), ch.getMaxMana(), c, + return formatStatHelper(ch.getMana(), + ch.getMaxMana(), + c, ch.getType() == CharacterTypeEnum::NPC); case ColumnTypeEnum::MOVES: - return formatStatHelper(ch.getMoves(), ch.getMaxMoves(), c, + return formatStatHelper(ch.getMoves(), + ch.getMaxMoves(), + c, ch.getType() == CharacterTypeEnum::NPC); case ColumnTypeEnum::STATE: - return QVariant::fromValue( - GroupStateData(ch.getColor(), - ch.getPosition(), - ch.getAffects())); + return QVariant::fromValue(GroupStateData(ch.getColor(), ch.getPosition(), ch.getAffects())); case ColumnTypeEnum::ROOM_NAME: - return ch.getRoomName().isEmpty() - ? QStringLiteral("Somewhere") - : ch.getRoomName().toQString(); + return ch.getRoomName().isEmpty() ? QStringLiteral("Somewhere") + : ch.getRoomName().toQString(); default: return QVariant(); } } /// Big switch for ToolTipRole, extracted out as well -static QVariant makeTooltipRole(const CGroupChar &ch, ColumnTypeEnum c, - bool useStatFmt) +static QVariant makeTooltipRole(const CGroupChar &ch, ColumnTypeEnum c, bool useStatFmt) { auto statTip = [&](int n, int d) -> QVariant { return useStatFmt ? formatStatHelper(n, d, c, false) : QVariant(); @@ -533,9 +525,9 @@ static QVariant makeTooltipRole(const CGroupChar &ch, ColumnTypeEnum c, case ColumnTypeEnum::CHARACTER_TOKEN: return QVariant(); case ColumnTypeEnum::HP_PERCENT: - return statTip(ch.getHits(), ch.getMaxHits()); + return statTip(ch.getHits(), ch.getMaxHits()); case ColumnTypeEnum::MANA_PERCENT: - return statTip(ch.getMana(), ch.getMaxMana()); + return statTip(ch.getMana(), ch.getMaxMana()); case ColumnTypeEnum::MOVES_PERCENT: return statTip(ch.getMoves(), ch.getMaxMoves()); case ColumnTypeEnum::STATE: { @@ -558,8 +550,8 @@ static QVariant makeTooltipRole(const CGroupChar &ch, ColumnTypeEnum c, /*──────────────────────────────────────────────────────────────────*/ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, - const ColumnTypeEnum column, - const int role) const + const ColumnTypeEnum column, + const int role) const { const CGroupChar &character = deref(pCharacter); @@ -585,8 +577,7 @@ QVariant GroupModel::dataForCharacter(const SharedGroupChar &pCharacter, return mmqt::textColor(character.getColor()); case Qt::TextAlignmentRole: - if (column != ColumnTypeEnum::NAME && - column != ColumnTypeEnum::ROOM_NAME) { + if (column != ColumnTypeEnum::NAME && column != ColumnTypeEnum::ROOM_NAME) { // QVariant(AlignmentFlag) ctor doesn’t exist; return static_cast(Qt::AlignCenter); } @@ -856,10 +847,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget emit sig_characterUpdated(selectedCharacter); }); - connect(m_table, - &QAbstractItemView::clicked, - this, - &GroupWidget::showContextMenu); + connect(m_table, &QAbstractItemView::clicked, this, &GroupWidget::showContextMenu); connect(m_group, &Mmapper2Group::sig_characterAdded, this, &GroupWidget::slot_onCharacterAdded); connect(m_group, @@ -975,8 +963,7 @@ void GroupWidget::buildAndExecMenu() m_recolor->setText(QString("&Recolor %1").arg(c.getName().toQString())); m_setIcon->setText(QString("&Set icon for %1…").arg(c.getName().toQString())); - m_useDefaultIcon->setText(QString("&Use default icon for %1") - .arg(c.getName().toQString())); + m_useDefaultIcon->setText(QString("&Use default icon for %1").arg(c.getName().toQString())); QMenu menu(tr("Context menu"), this); menu.addAction(m_center); diff --git a/src/group/tokenmanager.cpp b/src/group/tokenmanager.cpp index 7424d934f..31dbb905e 100644 --- a/src/group/tokenmanager.cpp +++ b/src/group/tokenmanager.cpp @@ -27,12 +27,11 @@ static QPixmap fetchPixmap(const QString &path) QPixmapCache::insert(path, px); return px; } - return {}; // null means load failed + return {}; // null means load failed } /// Case-insensitive lookup: “mount_pony” matches “Mount_Pony” -static QString matchAvailableKey(const QMap &files, - const QString &resolvedKey) +static QString matchAvailableKey(const QMap &files, const QString &resolvedKey) { for (const QString &k : files.keys()) if (k.compare(resolvedKey, Qt::CaseInsensitive) == 0) @@ -40,7 +39,7 @@ static QString matchAvailableKey(const QMap &files, return {}; } -} +} // namespace const QString kForceFallback(QStringLiteral("__force_fallback__")); @@ -129,8 +128,8 @@ QPixmap TokenManager::getToken(const QString &key) return m_fallbackPixmap; // 1. resolve overrides and normalise key - const QString lookup = overrideFor(key).isEmpty() ? key : overrideFor(key); - QString resolvedKey = normalizeKey(lookup); + const QString lookup = overrideFor(key).isEmpty() ? key : overrideFor(key); + QString resolvedKey = normalizeKey(lookup); if (resolvedKey.isEmpty()) { qWarning() << "TokenManager: empty key — defaulting to 'blank_character'"; resolvedKey = "blank_character"; @@ -157,9 +156,8 @@ QPixmap TokenManager::getToken(const QString &key) } // 4. user-defined fallback (AppData/tokens/blank_character.png) - const QString userFallback = - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + - "/tokens/blank_character.png"; + const QString userFallback = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + "/tokens/blank_character.png"; if (QFile::exists(userFallback)) { m_tokenPathCache[resolvedKey] = userFallback; return fetchPixmap(userFallback); diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index c9256f4fa..a467f879d 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -186,8 +186,9 @@ MainWindow::MainWindow() m_dockDialogGroup->setWidget(m_groupWidget); connect(m_groupWidget, &GroupWidget::sig_center, m_mapWindow, &MapWindow::slot_centerOnWorldPos); auto *canvas = getCanvas(); - connect(m_groupWidget, &GroupWidget::sig_characterUpdated, - canvas, [canvas](SharedGroupChar) { canvas->slot_requestUpdate(); }); + connect(m_groupWidget, &GroupWidget::sig_characterUpdated, canvas, [canvas](SharedGroupChar) { + canvas->slot_requestUpdate(); + }); // View -> Side Panels -> Room Panel (Mobs) m_roomManager = new RoomManager(this); diff --git a/src/preferences/grouppage.ui b/src/preferences/grouppage.ui index 9e0464011..7f5b57004 100644 --- a/src/preferences/grouppage.ui +++ b/src/preferences/grouppage.ui @@ -40,11 +40,14 @@
+ + true + - true + false @@ -80,11 +83,14 @@
+ + true + - true + false @@ -132,6 +138,9 @@
+ + 1 + 16 px From 1baa2f076baa97eb56105ad5e57d117c6df283a3 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 22 Oct 2025 13:51:08 +1100 Subject: [PATCH 43/43] chore: trigger CI