From 74e8d17dedc319844e6a2b3f2caa4c4c9fafa8e9 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 18 Jun 2025 14:40:56 +1000 Subject: [PATCH 01/63] 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 e2fd035b64ddef7ad83569f1e10f834fbff3352a Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 24 Jun 2025 14:25:20 +1000 Subject: [PATCH 02/63] 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 a70c32ab4..8e8f9af05 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 dc7a9ed45..760d87c6e 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -288,6 +288,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 6e3b121185c4380aef4ad7789272076faab59d54 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 24 Jun 2025 15:16:15 +1000 Subject: [PATCH 03/63] 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 8e8f9af05..6e73d4b28 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 760d87c6e..0350407df 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -288,7 +288,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 341ccb4d073dae056a5e146bf3b0e05b6828e1fa Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 25 Jun 2025 09:40:06 +1000 Subject: [PATCH 04/63] 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 6e73d4b28..d91aa8c1c 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 0350407df..d6c2e1b83 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -290,6 +290,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 d3771e5bc34218a25646f04aa36cc5bee081fd03 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 10 Jul 2025 11:10:03 +1000 Subject: [PATCH 05/63] 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 d91aa8c1c..577c42d11 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 d6c2e1b83..829fbd90d 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -289,6 +289,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 8923b94bba78bfc0dbdb86aaaf89c4ba3f6c4f29 Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Sun, 15 Jun 2025 07:55:48 -0500 Subject: [PATCH 06/63] 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 edc5aaa9a8fa6b6e61d158a1814795238521d176 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 09:29:57 +1000 Subject: [PATCH 07/63] 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 df58c62df62c52c8dc992ea690fe7d76d130ce39 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 10:19:06 +1000 Subject: [PATCH 08/63] 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 69f5461b849a6dea3079029a80f6bdfa64cb98c1 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 10:21:32 +1000 Subject: [PATCH 09/63] 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 b4abdd77eb85ff2f14f52072b4e2832c063d6763 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 11:21:11 +1000 Subject: [PATCH 10/63] 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 0fdd1101725d5619095bd94885f9e5867bd40525 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 11:27:24 +1000 Subject: [PATCH 11/63] 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 2520f7da6bdc5c6d95d65633b4b89c5b29707dfb Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 12:12:11 +1000 Subject: [PATCH 12/63] 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 0e9b534d3ef52a96a728f693c1ad08354d3217c8 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 12:50:03 +1000 Subject: [PATCH 13/63] 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 0f61d96c824a3ff37e6ea1bf3944ee4a60a51b75 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 13:18:44 +1000 Subject: [PATCH 14/63] 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 a43fe412206a98e105097c7329e06722559e8e78 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 11 Jul 2025 13:26:14 +1000 Subject: [PATCH 15/63] 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 e8da0a52b8221d54e7048264b68fe51c7510b8f2 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/63] 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 1ce824c018e4d2b4db1ba00093d534052e224e64 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/63] 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 d89263dd7eaf420e425973003b19542b6e7292e5 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/63] 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 50ed40b62e35fc8da4b6a386875ede9368683ce6 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 13:58:15 +1000 Subject: [PATCH 19/63] 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 d335d1bc0664f69e59dbddf1c8e2489877eb309f Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:09:52 +1000 Subject: [PATCH 20/63] 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/63] 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 c815ab4c63ae09ae9f211fec139497260f5a740c Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:21:27 +1000 Subject: [PATCH 22/63] 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 5e32b7679fd7f4664286007dd859c6dfe18c3907 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 14:32:39 +1000 Subject: [PATCH 23/63] 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 040c0fc12d15a18e19036fa9d1c523eb502625ec Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 15:03:17 +1000 Subject: [PATCH 24/63] 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 dc910301dd53a2f81b9cb41208ca00978a24cc5e Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 15:06:25 +1000 Subject: [PATCH 25/63] 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 18cc2f3afac35994c0f3a8bc583b2fbeb75aae15 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:01:56 +1000 Subject: [PATCH 26/63] 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 40cc5949b4142bae1533a0c248b6a957b2ebc16c Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:18:00 +1000 Subject: [PATCH 27/63] 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 78c3c2860fdec771ff3f77b68d4d07b3f37745de Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:51:29 +1000 Subject: [PATCH 28/63] 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 577c42d11..525d70843 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 0ea340051665f26ed323cb2521f28397bfb007ef Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sat, 19 Jul 2025 16:51:50 +1000 Subject: [PATCH 29/63] 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 829fbd90d..a3e446da1 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -291,6 +291,7 @@ class NODISCARD Configuration final bool showTokens = true; bool showMapTokens = true; int tokenIconSize = 32; + bool showNpcGhosts = true; QMap tokenOverrides; private: From 3675c6bbf11537111bdf6a934f29a07f42b998be Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Sun, 20 Jul 2025 17:57:21 +1000 Subject: [PATCH 30/63] 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 6714486943484c8ce97865fb6a15f6ab6304f2a0 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 21 Jul 2025 12:36:08 +1000 Subject: [PATCH 31/63] 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 d0f56ac2a6b970a040e2754345c1b33e50d154b7 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 21 Jul 2025 14:34:02 +1000 Subject: [PATCH 32/63] 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 2f1692ba9a5d646bf19d8abc0e71f84b789502bc Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 5 Aug 2025 12:40:27 +1000 Subject: [PATCH 33/63] 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 4f3de6d42ae8c592240c9b7e3d4e3028427268c9 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 5 Aug 2025 13:59:27 +1000 Subject: [PATCH 34/63] 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 a1bde366db072c3cc0e1ddef09fb7cf36620d840 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 10:33:59 +1000 Subject: [PATCH 35/63] 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 bf166d2c42bd41b42424eec8ef70799e8417518d Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 10:42:33 +1000 Subject: [PATCH 36/63] =?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 44f359c50e9692b06cf090a6249555ec084a38cc Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 11 Aug 2025 11:08:18 +1000 Subject: [PATCH 37/63] 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 cbbe149bf94f85b36c246fafdb4b4d8e05cf37f8 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 14 Aug 2025 11:12:21 +1000 Subject: [PATCH 38/63] 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 212ae102b7da17c5e1250769aacdcc7ec818c874 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 21 Aug 2025 11:22:59 +1000 Subject: [PATCH 39/63] CI: fix macOS env echo typo; clang: make row*0.85 cast explicit to avoid -Wimplicit-int-float-conversion --- src/group/groupwidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/group/groupwidget.cpp b/src/group/groupwidget.cpp index aa32598e7..588524bab 100644 --- a/src/group/groupwidget.cpp +++ b/src/group/groupwidget.cpp @@ -825,7 +825,7 @@ GroupWidget::GroupWidget(Mmapper2Group *const group, MapData *const md, QWidget 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))); + const int shown = std::max(1, static_cast(std::lround(static_cast(row) * 0.85))); m_table->setIconSize(QSize(shown, shown)); m_table->resizeColumnToContents(static_cast(ColumnTypeEnum::CHARACTER_TOKEN)); m_table->resizeRowsToContents(); @@ -977,7 +977,7 @@ void GroupWidget::updateColumnVisibility() 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))); + const int shown = std::max(1, static_cast(std::lround(static_cast(row) * 0.85))); m_table->setIconSize(QSize(shown, shown)); // Recalculate column/row sizes for token column From 13471955f8321de9da7bf121765b34bb2c84bcfc Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 22 Aug 2025 13:58:15 +1000 Subject: [PATCH 40/63] CI(macOS): correctly insert Deploy Qt step and set DMG format (UDZO) --- .github/workflows/build-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9b8328ac2..cd9aaaebd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -190,6 +190,11 @@ jobs: # # Package # + - name: Deploy Qt frameworks (macOS) + if: runner.os == 'macOS' + run: | + APP="$GITHUB_WORKSPACE/build/src/mmapper.app" + "$QT_ROOT_DIR/bin/macdeployqt" "$APP" -verbose=2 -libpath "$QT_ROOT_DIR/lib" - name: Package MMapper run: cd build && cpack - if: runner.os == 'Linux' From 430359b8378cde77e7de8ac6413002cae486c24e Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 22 Aug 2025 15:00:07 +1000 Subject: [PATCH 41/63] CI(macOS): build both Intel (macos-13) and Apple Silicon (macos-latest); set Qt/CMake arch per runner --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index cd9aaaebd..92a13fe33 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2022, macos-latest, ubuntu-24.04] + os: [windows-2022, macos-latest, macos-13, ubuntu-24.04] compiler: ['clang', 'gcc', 'msvc'] exclude: - os: ubuntu-24.04 From d456e72f1d06198a576749801ea761e01d451b6d Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 22 Aug 2025 18:43:15 +1000 Subject: [PATCH 42/63] CI(macOS): add arch-vars + mac packages (UDZO + arch) + hardened macdeployqt step --- .github/workflows/build-test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 92a13fe33..3bbfa27a4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -78,14 +78,16 @@ jobs: # # Install Packages (Mac) # + - if: runner.os == 'macOS' name: Install Qt for Mac uses: jurplel/install-qt-action@v4 with: - version: 6.5.3 - arch: clang_64 +. version: 6.5.3." +". arch: ."${{ env.QT_INSTALL_ARCH }}" cache: true modules: 'qtwebsockets' + - if: runner.os == 'macOS' name: Install Packages for Mac run: | @@ -100,9 +102,9 @@ jobs: name: Install Qt for Windows (MSVC) uses: jurplel/install-qt-action@v4 with: - version: '6.5.3' - dir: 'C:\' - arch: win64_msvc2019_64 +. version: '6.5.3' + dir: 'C:\'." +". arch: ."${{ env.QT_INSTALL_ARCH }}" cache: true modules: 'qtwebsockets' - if: runner.os == 'Windows' && matrix.compiler == 'gcc' From c3ec16cc08cd5ae60abc7e260a9f10fb1f3e2e93 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 27 Aug 2025 08:50:40 +1000 Subject: [PATCH 43/63] ci: trigger full build matrix (mac included) From 94238bb4053e93cce6a75b6448d0fbcdc5884e42 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 27 Aug 2025 09:00:05 +1000 Subject: [PATCH 44/63] CI: replace corrupted workflow; enable mac Intel+ARM; fix mac env and macdeployqt --- .github/workflows/build-test.yml | 423 ++++++++++++++++--------------- 1 file changed, 216 insertions(+), 207 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3bbfa27a4..74b673b9a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -3,7 +3,7 @@ name: build-test on: push: branches: - - master + - master pull_request: workflow_dispatch: @@ -17,237 +17,246 @@ jobs: os: [windows-2022, macos-latest, macos-13, ubuntu-24.04] compiler: ['clang', 'gcc', 'msvc'] exclude: - - os: ubuntu-24.04 - compiler: 'msvc' - - os: macos-latest - compiler: 'gcc' - - os: macos-latest - compiler: 'msvc' - - os: windows-2022 - compiler: 'clang' + - os: ubuntu-24.04 + compiler: 'msvc' + - os: macos-latest + compiler: 'gcc' + - os: macos-latest + compiler: 'msvc' + - os: windows-2022 + compiler: 'clang' + steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - name: Delete beta tag - shell: bash - run: | - if git tag | grep -q '^beta$'; then - git tag -d beta - fi - - name: Get Git tag description - id: git_info - run: | - echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV - - if: runner.os == 'Windows' - uses: lukka/get-cmake@latest - - if: matrix.compiler == 'msvc' - uses: ilammy/msvc-dev-cmd@v1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Delete beta tag + shell: bash + run: | + if git tag | grep -q '^beta$'; then + git tag -d beta + fi + + - name: Get Git tag description + id: git_info + run: echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV + + - if: runner.os == 'Windows' + uses: lukka/get-cmake@latest + + - if: matrix.compiler == 'msvc' + uses: ilammy/msvc-dev-cmd@v1 # # Install Packages (Ubuntu) # - - if: runner.os == 'Linux' - name: Install Packages for Ubuntu - run: | - sudo apt update -qq - sudo apt install -y \ - qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ - zlib1g-dev libssl-dev - - if: runner.os == 'Linux' && matrix.compiler == 'gcc' - name: Install GCC for Ubuntu - run: | - sudo apt install -y gcc-13 g++-13 lcov - echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV - echo "CC=gcc-13" >> $GITHUB_ENV - echo "CXX=g++-13" >> $GITHUB_ENV - echo "MMAPPER_CMAKE_EXTRA=-DUSE_CODE_COVERAGE=true -DUSE_MOLD=true" >> $GITHUB_ENV - echo "COVERAGE=true" >> $GITHUB_ENV - - if: runner.os == 'Linux' && matrix.compiler == 'clang' - name: Install Clang for Ubuntu - run: | - sudo apt install -y clang-18 binutils - echo "QMAKESPEC=linux-clang" >> $GITHUB_ENV - echo "CC=clang-18" >> $GITHUB_ENV - echo "CXX=clang++-18" >> $GITHUB_ENV - echo "STYLE=true" >> $GITHUB_ENV - echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV + - if: runner.os == 'Linux' + name: Install Packages for Ubuntu + run: | + sudo apt update -qq + sudo apt install -y \ + qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ + libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ + zlib1g-dev libssl-dev + + - if: runner.os == 'Linux' && matrix.compiler == 'gcc' + name: Install GCC for Ubuntu + run: | + sudo apt install -y gcc-13 g++-13 lcov + echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV + echo "CC=gcc-13" >> $GITHUB_ENV + echo "CXX=g++-13" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DUSE_CODE_COVERAGE=true -DUSE_MOLD=true" >> $GITHUB_ENV + echo "COVERAGE=true" >> $GITHUB_ENV + + - if: runner.os == 'Linux' && matrix.compiler == 'clang' + name: Install Clang for Ubuntu + run: | + sudo apt install -y clang-18 binutils + echo "QMAKESPEC=linux-clang" >> $GITHUB_ENV + echo "CC=clang-18" >> $GITHUB_ENV + echo "CXX=clang++-18" >> $GITHUB_ENV + echo "STYLE=true" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV # - # Install Packages (Mac) + # Install Packages (macOS) # - - - if: runner.os == 'macOS' - name: Install Qt for Mac - uses: jurplel/install-qt-action@v4 - with: -. version: 6.5.3." -". arch: ."${{ env.QT_INSTALL_ARCH }}" - cache: true - modules: 'qtwebsockets' - - - if: runner.os == 'macOS' - name: Install Packages for Mac - run: | - brew install cmake ninja lcov - echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DUSE_CODE_COVERAGE=true" >> $GITHUB_ENV - echo "COVERAGE=false" >> $GITHUB_ENV + - name: Set mac arch vars + if: runner.os == 'macOS' + run: | + if [[ "${{ matrix.os }}" == "macos-13" ]]; then + echo "QT_INSTALL_ARCH=clang_64" >> $GITHUB_ENV + echo "CMAKE_OSX_ARCH=x86_64" >> $GITHUB_ENV + else + echo "QT_INSTALL_ARCH=clang_arm64" >> $GITHUB_ENV + echo "CMAKE_OSX_ARCH=arm64" >> $GITHUB_ENV + fi + + - if: runner.os == 'macOS' + name: Install Qt for Mac + uses: jurplel/install-qt-action@v4 + with: + version: 6.5.3 + arch: ${{ env.QT_INSTALL_ARCH }} + cache: true + modules: 'qtwebsockets' + + - name: Install Packages for Mac + if: runner.os == 'macOS' + run: | + brew install cmake ninja lcov + echo "$QT_ROOT_DIR/bin" >> $GITHUB_PATH + echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DUSE_CODE_COVERAGE=true -DCPACK_DMG_FORMAT=UDZO -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCH}" >> $GITHUB_ENV + echo "COVERAGE=false" >> $GITHUB_ENV # # Install Packages (Windows) # - - if: runner.os == 'Windows' && matrix.compiler == 'msvc' - name: Install Qt for Windows (MSVC) - uses: jurplel/install-qt-action@v4 - with: -. version: '6.5.3' - dir: 'C:\'." -". arch: ."${{ env.QT_INSTALL_ARCH }}" - cache: true - modules: 'qtwebsockets' - - if: runner.os == 'Windows' && matrix.compiler == 'gcc' - name: Install Qt for Windows (MinGW) - uses: jurplel/install-qt-action@v4 - with: - version: '6.5.3' - dir: 'C:\' - arch: win64_mingw - cache: true - modules: 'qtwebsockets' - tools: 'tools_mingw1310' + - if: runner.os == 'Windows' && matrix.compiler == 'msvc' + name: Install Qt for Windows (MSVC) + uses: jurplel/install-qt-action@v4 + with: + version: '6.5.3' + dir: 'C:\' + arch: win64_msvc2019_64 + cache: true + modules: 'qtwebsockets' + + - if: runner.os == 'Windows' && matrix.compiler == 'gcc' + name: Install Qt for Windows (MinGW) + uses: jurplel/install-qt-action@v4 + with: + version: '6.5.3' + dir: 'C:\' + arch: win64_mingw + cache: true + modules: 'qtwebsockets' + tools: 'tools_mingw1310' # # Build # - - if: runner.os == 'Windows' - name: Build MMapper for Windows - shell: pwsh - run: | - mkdir ${{ github.workspace }}/artifact - mkdir build - cd build - cmake --version - if ('${{ matrix.compiler }}' -eq 'gcc') { - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 - cmake --build . --parallel - - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 - cmake --build . --parallel + - if: runner.os == 'Windows' + name: Build MMapper for Windows + shell: pwsh + run: | + mkdir ${{ github.workspace }}/artifact + mkdir build + cd build + cmake --version + if ('${{ matrix.compiler }}' -eq 'gcc') { + 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 + cmake --build . --parallel + + - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 + cmake --build . --parallel + + # + # Tests (coverage only on Linux gcc) + # + - if: env.COVERAGE == 'true' + name: Prepare lcov counters + run: cd build && lcov --zerocounters --directory . + + - name: Run unit tests + run: cd build && ctest -V --no-compress-output -T test --output-junit ../ctest-to-junit-results.xml + env: + QT_QPA_PLATFORM: offscreen + ASAN_OPTIONS: detect_leaks=0 + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: test-results ${{ matrix.os }} ${{ matrix.compiler }} + path: ctest-to-junit-results.xml + + - if: env.COVERAGE == 'true' + name: Run lcov + run: | + cd build + lcov --directory . --capture --output-file coverage.info + lcov --list coverage.info + lcov --remove coverage.info '/usr/*' --output-file filtered.info + + - if: env.COVERAGE == 'true' + uses: codecov/codecov-action@v5 + with: + files: ./build/filtered.info + + - if: env.STYLE == 'true' + name: Text Encoding Sanity Check + run: | + cd build + if [[ -n $(nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString') ]]; then + nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString' + echo + echo + echo "Please avoid using QString::fromStdString() and QString::toStdString()" + echo + echo "Both functions assume the user wants utf8, but MMapper almost always expects" + echo "std::string to have latin1 encoding." + echo + echo "Convert any uses to corresponding functions in TextUtils e.g. ::toQStringUtf8()" + exit -1 + fi # - # Run tests + # macOS deploy (before packaging) # - - if: env.COVERAGE == 'true' - name: Prepare lcov counters - run: cd build && lcov --zerocounters --directory . - - name: Run unit tests - run: cd build && ctest -V --no-compress-output -T test --output-junit ../ctest-to-junit-results.xml - env: - QT_QPA_PLATFORM: offscreen - ASAN_OPTIONS: detect_leaks=0 - - name: Upload test results - uses: actions/upload-artifact@v4 - with: - name: test-results ${{ matrix.os }} ${{ matrix.compiler }} - path: ctest-to-junit-results.xml - - if: env.COVERAGE == 'true' - name: Run lcov - run: | - cd build - lcov --directory . --capture --output-file coverage.info - lcov --list coverage.info - lcov --remove coverage.info '/usr/*' --output-file filtered.info - - if: env.COVERAGE == 'true' - uses: codecov/codecov-action@v5 - with: - files: ./build/filtered.info - - if: env.STYLE == 'true' - name: Text Encoding Sanity Check - run: | - cd build - if [[ -n $(nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString') ]]; then - nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString' - echo - echo - echo "Please avoid using QString::fromStdString() and QString::toStdString()" - echo - echo "Both functions assume the user wants utf8, but MMapper almost always expects" - echo "std::string to have latin1 encoding." - echo - echo "Convert any uses to corresponding functions in TextUtils e.g. ::toQStringUtf8()" - exit -1 - fi + - name: Deploy Qt frameworks (macOS) + if: runner.os == 'macOS' + run: | + APP="$GITHUB_WORKSPACE/build/src/mmapper.app" + "$QT_ROOT_DIR/bin/macdeployqt" "$APP" -verbose=2 -libpath "$QT_ROOT_DIR/lib" # - # Package + # Package + Upload # - - name: Deploy Qt frameworks (macOS) - if: runner.os == 'macOS' - run: | - APP="$GITHUB_WORKSPACE/build/src/mmapper.app" - "$QT_ROOT_DIR/bin/macdeployqt" "$APP" -verbose=2 -libpath "$QT_ROOT_DIR/lib" - - name: Package MMapper - run: cd build && cpack - - 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 - - if: runner.os == 'macOS' - name: Upload Package for Mac - uses: actions/upload-artifact@v4 - with: - name: debug-${{ 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: debug-${{ matrix.compiler }}-${{ 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: debug-${{ matrix.compiler }}-${{ steps.get_windows_package_name.outputs.PACKAGE_BASENAME }} - path: ${{ github.workspace }}/artifact/*.exe* + - name: Package MMapper + run: cd build && cpack + + - if: runner.os == 'Linux' + name: Upload Package for Ubuntu + uses: actions/upload-artifact@v4 + with: + name: debug-${{ matrix.compiler }} + path: ${{ github.workspace }}/artifact/*.deb* + + - if: runner.os == 'macOS' + name: Upload Package for Mac + uses: actions/upload-artifact@v4 + with: + name: debug-${{ matrix.os }} + path: ${{ github.workspace }}/artifact/*.dmg* + + - if: runner.os == 'Windows' + name: Upload Package for Windows + uses: actions/upload-artifact@v4 + with: + name: debug-${{ matrix.compiler }} + path: ${{ github.workspace }}/artifact/*.exe* report: name: Test Report runs-on: ubuntu-latest needs: build steps: - - uses: dorny/test-reporter@v2 - with: - artifact: /test-results (.*)/ - name: Test Report $1 - path: '*.xml' - reporter: java-junit + - uses: dorny/test-reporter@v2 + with: + artifact: /test-results (.*)/ + name: Test Report $1 + path: '*.xml' + reporter: java-junit From 101a5c97c6963d4ee276d37a1af4291040ff0584 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 27 Aug 2025 09:54:31 +1000 Subject: [PATCH 45/63] CI(macOS): use clang_64 for aqt; force UDZO at cpack --- .github/workflows/build-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 74b673b9a..c501357e1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -91,7 +91,7 @@ jobs: echo "QT_INSTALL_ARCH=clang_64" >> $GITHUB_ENV echo "CMAKE_OSX_ARCH=x86_64" >> $GITHUB_ENV else - echo "QT_INSTALL_ARCH=clang_arm64" >> $GITHUB_ENV + echo "QT_INSTALL_ARCH=clang_64" >> $GITHUB_ENV echo "CMAKE_OSX_ARCH=arm64" >> $GITHUB_ENV fi @@ -226,7 +226,7 @@ jobs: # Package + Upload # - name: Package MMapper - run: cd build && cpack + run: cd build && cpack -D CPACK_DMG_FORMAT=UDZO - if: runner.os == 'Linux' name: Upload Package for Ubuntu From 36b6ea31f1a774e38c6e07b6b84b53f2723f57c6 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 27 Aug 2025 11:03:08 +1000 Subject: [PATCH 46/63] CI: sanitize GIT_TAG for DMG-safe filenames --- .github/workflows/build-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c501357e1..75abfd8d1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -41,7 +41,11 @@ jobs: - name: Get Git tag description id: git_info - run: echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV + run: | + RAW=$(git describe --tags --always --long) + SAFE=${RAW//[\/ ]/-} + SAFE=$(printf "%s" "$SAFE" | tr -cd "[:alnum:]._-") + echo "GIT_TAG=$SAFE" >> $GITHUB_ENV - if: runner.os == 'Windows' uses: lukka/get-cmake@latest From 878d533cd39652b61117f1b70a02676c6817e4ef Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Wed, 27 Aug 2025 15:02:06 +1000 Subject: [PATCH 47/63] CI(macOS): sanitize GIT_TAG; force simple CPACK_PACKAGE_FILE_NAME; enable cpack -V --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 75abfd8d1..6319eb421 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -230,7 +230,7 @@ jobs: # Package + Upload # - name: Package MMapper - run: cd build && cpack -D CPACK_DMG_FORMAT=UDZO + run: cd build && cpack -D CPACK_DMG_FORMAT=UDZO -D CPACK_PACKAGE_FILE_NAME="mmapper--Mac-" -V - if: runner.os == 'Linux' name: Upload Package for Ubuntu From 50d85d0eabec204e8ba0a577e94e9e3fbc078c28 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 28 Aug 2025 08:58:58 +1000 Subject: [PATCH 48/63] CI(macOS): Release build, enable websockets+qtkeychain, add ad-hoc codesign --- .github/workflows/build-test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6319eb421..7a5d422b9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -111,9 +111,9 @@ jobs: - name: Install Packages for Mac if: runner.os == 'macOS' run: | - brew install cmake ninja lcov + brew install cmake ninja lcov qtkeychain echo "$QT_ROOT_DIR/bin" >> $GITHUB_PATH - echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DUSE_CODE_COVERAGE=true -DCPACK_DMG_FORMAT=UDZO -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCH}" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH= -DCMAKE_BUILD_TYPE=Release -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DCPACK_DMG_FORMAT=UDZO -DCMAKE_OSX_ARCHITECTURES=" >> $GITHUB_ENV echo "COVERAGE=false" >> $GITHUB_ENV # @@ -229,6 +229,11 @@ jobs: # # Package + Upload # + - name: Ad-hoc codesign (macOS) + if: runner.os == 'macOS' + run: | + APP="$GITHUB_WORKSPACE/build/src/mmapper.app" + /usr/bin/codesign --force --deep --sign - "$APP" - name: Package MMapper run: cd build && cpack -D CPACK_DMG_FORMAT=UDZO -D CPACK_PACKAGE_FILE_NAME="mmapper--Mac-" -V From f206832e74fcf3cb03844f9968811ffc6259f596 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 28 Aug 2025 10:03:59 +1000 Subject: [PATCH 49/63] Qt6: replace QLatin1String uses with QString/QStringLiteral for portability --- src/configuration/PasswordConfig.cpp | 4 ++-- src/proxy/mumesocket.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/configuration/PasswordConfig.cpp b/src/configuration/PasswordConfig.cpp index 9ac9136bf..52ef7bd0d 100644 --- a/src/configuration/PasswordConfig.cpp +++ b/src/configuration/PasswordConfig.cpp @@ -6,8 +6,8 @@ #include "../global/macros.h" #ifndef MMAPPER_NO_QTKEYCHAIN -static const QLatin1String PASSWORD_KEY("password"); -static const QLatin1String APP_NAME("org.mume.mmapper"); +static const QString PASSWORD_KEY = QStringLiteral("password"); +static const QString APP_NAME = QStringLiteral("org.mume.mmapper"); #endif PasswordConfig::PasswordConfig(QObject *const parent) diff --git a/src/proxy/mumesocket.cpp b/src/proxy/mumesocket.cpp index b7849f304..52bb65218 100644 --- a/src/proxy/mumesocket.cpp +++ b/src/proxy/mumesocket.cpp @@ -367,7 +367,7 @@ void MumeTcpSocket::virt_onConnect() MumeWebSocket::MumeWebSocket(QObject *parent, MumeSocketOutputs &outputs) : MumeSocket(parent, outputs) #ifndef MMAPPER_NO_WEBSOCKET - , m_socket{QLatin1String(""), QWebSocketProtocol::VersionLatest, this} + , m_socket{QString(), QWebSocketProtocol::VersionLatest, this} #endif { #ifndef MMAPPER_NO_WEBSOCKET From 7f2942c5b0dd245b9e822eb309d29c3df3184152 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Thu, 28 Aug 2025 11:45:17 +1000 Subject: [PATCH 50/63] =?UTF-8?q?CI:=20macos-13=20=E2=80=93=20unlink/remov?= =?UTF-8?q?e=20Homebrew=20Qt=20headers=20to=20avoid=20QLatin1String=20rede?= =?UTF-8?q?finition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 7a5d422b9..1db373c0f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,6 +28,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Remove Homebrew Qt headers on macOS 13 + if: matrix.os == 'macos-13' + run: | + brew list --versions qt@5 >/dev/null 2>&1 && brew unlink qt@5 || true + brew list --versions qt >/dev/null 2>&1 && brew unlink qt || true + sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + with: fetch-depth: 0 submodules: true From fcb3953fd4197ced8af8200496679e4ee600d6ee Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 11:40:29 +1000 Subject: [PATCH 51/63] CI: fix YAML; insert macOS-13 Qt unlink step after checkout block --- .github/workflows/build-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1db373c0f..8be94c4ed 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,17 +28,17 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + - name: Remove Homebrew Qt headers on macOS 13 if: matrix.os == 'macos-13' + shell: bash run: | brew list --versions qt@5 >/dev/null 2>&1 && brew unlink qt@5 || true brew list --versions qt >/dev/null 2>&1 && brew unlink qt || true sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - - with: - fetch-depth: 0 - submodules: true - - name: Delete beta tag shell: bash run: | From ab6a735c5b0d44952cf576ab30361586b8c78293 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 13:24:41 +1000 Subject: [PATCH 52/63] =?UTF-8?q?CI:=20macos-13=20=E2=80=94=20debug=20&=20?= =?UTF-8?q?hard-remove=20Homebrew=20Qt=20headers=20before=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-test.yml | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 8be94c4ed..dccbf91bd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,17 +28,34 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: true - - - name: Remove Homebrew Qt headers on macOS 13 + - name: Debug & remove Homebrew Qt on macOS 13 if: matrix.os == 'macos-13' shell: bash run: | - brew list --versions qt@5 >/dev/null 2>&1 && brew unlink qt@5 || true - brew list --versions qt >/dev/null 2>&1 && brew unlink qt || true + set -x + echo '>>> BEFORE: find qlatin1stringview.h under /usr/local/include' + sudo bash -lc "shopt -s nullglob; ls -la /usr/local/include | sed -n 's/^/ /p' || true" + sudo find /usr/local/include -maxdepth 2 -name 'qlatin1stringview.h' -print || true + echo '>>> brew list qt variants (if any)' + brew list --versions qt@5 || true + brew list --versions qt || true + echo '>>> UNLINK/REMOVE any Homebrew Qt bits' + brew unlink qt@5 || true + brew unlink qt || true sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true + echo '>>> AFTER: verify header is gone' + if sudo test -e /usr/local/include/QtCore/qlatin1stringview.h; then + echo 'STILL FOUND: /usr/local/include/QtCore/qlatin1stringview.h'; + sudo ls -l /usr/local/include/QtCore/qlatin1stringview.h || true; + else + echo 'Header gone ✅'; + fi + + with: + fetch-depth: 0 + submodules: true + - name: Delete beta tag shell: bash run: | From 09361787e9c973dc19b7ab806adb392e844fe3c5 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 13:28:12 +1000 Subject: [PATCH 53/63] =?UTF-8?q?CI:=20macos-13=20=E2=80=94=20minimal=20fi?= =?UTF-8?q?rst-step=20cleanup=20of=20Homebrew=20Qt=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-test.yml | 42 ++++++++++---------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dccbf91bd..24f5361af 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -26,36 +26,20 @@ jobs: - os: windows-2022 compiler: 'clang' - steps: - - uses: actions/checkout@v4 - - name: Debug & remove Homebrew Qt on macOS 13 - if: matrix.os == 'macos-13' - shell: bash - run: | - set -x - echo '>>> BEFORE: find qlatin1stringview.h under /usr/local/include' - sudo bash -lc "shopt -s nullglob; ls -la /usr/local/include | sed -n 's/^/ /p' || true" - sudo find /usr/local/include -maxdepth 2 -name 'qlatin1stringview.h' -print || true - echo '>>> brew list qt variants (if any)' - brew list --versions qt@5 || true - brew list --versions qt || true - echo '>>> UNLINK/REMOVE any Homebrew Qt bits' - brew unlink qt@5 || true - brew unlink qt || true - sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true - echo '>>> AFTER: verify header is gone' - if sudo test -e /usr/local/include/QtCore/qlatin1stringview.h; then - echo 'STILL FOUND: /usr/local/include/QtCore/qlatin1stringview.h'; - sudo ls -l /usr/local/include/QtCore/qlatin1stringview.h || true; - else - echo 'Header gone ✅'; - fi - - with: - fetch-depth: 0 - submodules: true + steps: - name: Remove Homebrew Qt headers on macOS 13 + if: matrix.os == 'macos-13' + run: | + set -euxo pipefail + # Unlink any brew Qt and remove stray headers that collide with vendored Qt 6 + brew unlink qt@5 || true + brew unlink qt || true + sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true + # Also clear include path envs that might point into /usr/local/include + echo 'CPATH=' >> $GITHUB_ENV + echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV + - uses: actions/checkout@v4 - name: Delete beta tag shell: bash run: | From 1b21fa21d60da86e7b55fc0775726be944abac29 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 13:42:17 +1000 Subject: [PATCH 54/63] CI: fix indentation of macOS-13 Qt unlink step --- .github/workflows/build-test.yml | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 24f5361af..c1a4369ed 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -26,19 +26,17 @@ jobs: - os: windows-2022 compiler: 'clang' - steps: - name: Remove Homebrew Qt headers on macOS 13 - if: matrix.os == 'macos-13' - run: | - set -euxo pipefail - # Unlink any brew Qt and remove stray headers that collide with vendored Qt 6 - brew unlink qt@5 || true - brew unlink qt || true - sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true - # Also clear include path envs that might point into /usr/local/include - echo 'CPATH=' >> $GITHUB_ENV - echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - + steps: + - name: Remove Homebrew Qt headers on macOS 13 + if: matrix.os == 'macos-13' + run: | + set -euxo pipefail + brew unlink qt@5 || true + brew unlink qt || true + sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true + echo 'CPATH=' >> $GITHUB_ENV + echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - uses: actions/checkout@v4 - name: Delete beta tag shell: bash From 1c6a0b19a8cd217a5191833b63b372992dcb82d5 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 14:43:13 +1000 Subject: [PATCH 55/63] CI: macos-13 debug pre/post and unlink after checkout --- .github/workflows/build-test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c1a4369ed..dab2ae17b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,6 +38,24 @@ jobs: echo 'CPATH=' >> $GITHUB_ENV echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - uses: actions/checkout@v4 + - name: Debug pre: list Homebrew Qt headers + if: matrix.os == 'macos-13' + run: | + ls -la /usr/local/include/QtCore 2>/dev/null || echo 'No /usr/local/include/QtCore' + - name: Remove Homebrew Qt headers on macOS 13 (post-checkout) + if: matrix.os == 'macos-13' + run: | + set -euxo pipefail + brew list --versions qt@5 >/dev/null 2>&1 && brew unlink qt@5 || true + brew list --versions qt >/dev/null 2>&1 && brew unlink qt || true + sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true + echo 'CPATH=' >> $GITHUB_ENV + echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV + - name: Debug post: list Homebrew Qt headers + if: matrix.os == 'macos-13' + run: | + ls -la /usr/local/include/QtCore 2>/dev/null || echo 'No /usr/local/include/QtCore' - name: Delete beta tag shell: bash run: | From 2e36974a3bbc9a6ebb7d81a38145228211b3aff0 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 15:13:16 +1000 Subject: [PATCH 56/63] CI: fix YAML indentation for macos-13 cleanup step --- .github/workflows/build-test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dab2ae17b..95f9419d6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,6 +38,16 @@ jobs: echo 'CPATH=' >> $GITHUB_ENV echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - uses: actions/checkout@v4 + - name: Remove Homebrew Qt headers on macOS 13 + if: matrix.os == 'macos-13' + run: | + set -euxo pipefail + brew unlink qt@5 || true + brew unlink qt || true + sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true + sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true + echo 'CPATH=' >> $GITHUB_ENV + echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - name: Debug pre: list Homebrew Qt headers if: matrix.os == 'macos-13' run: | From f8641764f678901b52bbd3e1f74d54b39f39ef70 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 15:22:12 +1000 Subject: [PATCH 57/63] CI: remove duplicate macos-13 Qt unlink step --- .github/workflows/build-test.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 95f9419d6..dab2ae17b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,16 +38,6 @@ jobs: echo 'CPATH=' >> $GITHUB_ENV echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - uses: actions/checkout@v4 - - name: Remove Homebrew Qt headers on macOS 13 - if: matrix.os == 'macos-13' - run: | - set -euxo pipefail - brew unlink qt@5 || true - brew unlink qt || true - sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true - echo 'CPATH=' >> $GITHUB_ENV - echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - name: Debug pre: list Homebrew Qt headers if: matrix.os == 'macos-13' run: | From db2ef3a9afb4942f62f57b773d33a5483c7c3b3d Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 15:39:56 +1000 Subject: [PATCH 58/63] CI: drop post-checkout duplicate macos-13 unlink step --- .github/workflows/build-test.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dab2ae17b..84a184b46 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -42,16 +42,6 @@ jobs: if: matrix.os == 'macos-13' run: | ls -la /usr/local/include/QtCore 2>/dev/null || echo 'No /usr/local/include/QtCore' - - name: Remove Homebrew Qt headers on macOS 13 (post-checkout) - if: matrix.os == 'macos-13' - run: | - set -euxo pipefail - brew list --versions qt@5 >/dev/null 2>&1 && brew unlink qt@5 || true - brew list --versions qt >/dev/null 2>&1 && brew unlink qt || true - sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true - echo 'CPATH=' >> $GITHUB_ENV - echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - name: Debug post: list Homebrew Qt headers if: matrix.os == 'macos-13' run: | From 56493dfa05a9f8dd7d8d74b948e2fe281c6fa247 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 15:52:05 +1000 Subject: [PATCH 59/63] CI: reset build-test.yml to match master --- .github/workflows/build-test.yml | 442 ++++++++++++++----------------- 1 file changed, 202 insertions(+), 240 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 84a184b46..23839dc2f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -3,7 +3,7 @@ name: build-test on: push: branches: - - master + - master pull_request: workflow_dispatch: @@ -14,272 +14,234 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2022, macos-latest, macos-13, ubuntu-24.04] + os: [windows-2022, macos-latest, ubuntu-24.04] compiler: ['clang', 'gcc', 'msvc'] exclude: - - os: ubuntu-24.04 - compiler: 'msvc' - - os: macos-latest - compiler: 'gcc' - - os: macos-latest - compiler: 'msvc' - - os: windows-2022 - compiler: 'clang' - + - os: ubuntu-24.04 + compiler: 'msvc' + - os: macos-latest + compiler: 'gcc' + - os: macos-latest + compiler: 'msvc' + - os: windows-2022 + compiler: 'clang' steps: - - name: Remove Homebrew Qt headers on macOS 13 - if: matrix.os == 'macos-13' - run: | - set -euxo pipefail - brew unlink qt@5 || true - brew unlink qt || true - sudo rm -rf /usr/local/include/Qt* /usr/local/lib/cmake/Qt* /usr/local/lib/Qt* || true - sudo rm -rf /usr/local/opt/qt* /usr/local/Cellar/qt* || true - echo 'CPATH=' >> $GITHUB_ENV - echo 'CPLUS_INCLUDE_PATH=' >> $GITHUB_ENV - - uses: actions/checkout@v4 - - name: Debug pre: list Homebrew Qt headers - if: matrix.os == 'macos-13' - run: | - ls -la /usr/local/include/QtCore 2>/dev/null || echo 'No /usr/local/include/QtCore' - - name: Debug post: list Homebrew Qt headers - if: matrix.os == 'macos-13' - run: | - ls -la /usr/local/include/QtCore 2>/dev/null || echo 'No /usr/local/include/QtCore' - - name: Delete beta tag - shell: bash - run: | - if git tag | grep -q '^beta$'; then - git tag -d beta - fi - - - name: Get Git tag description - id: git_info - run: | - RAW=$(git describe --tags --always --long) - SAFE=${RAW//[\/ ]/-} - SAFE=$(printf "%s" "$SAFE" | tr -cd "[:alnum:]._-") - echo "GIT_TAG=$SAFE" >> $GITHUB_ENV - - - if: runner.os == 'Windows' - uses: lukka/get-cmake@latest - - - if: matrix.compiler == 'msvc' - uses: ilammy/msvc-dev-cmd@v1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + - name: Delete beta tag + shell: bash + run: | + if git tag | grep -q '^beta$'; then + git tag -d beta + fi + - name: Get Git tag description + id: git_info + run: | + echo "GIT_TAG=$(git describe --tags --always --long)" >> $GITHUB_ENV + - if: runner.os == 'Windows' + uses: lukka/get-cmake@latest + - if: matrix.compiler == 'msvc' + uses: ilammy/msvc-dev-cmd@v1 # # Install Packages (Ubuntu) # - - if: runner.os == 'Linux' - name: Install Packages for Ubuntu - run: | - sudo apt update -qq - sudo apt install -y \ - qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ - zlib1g-dev libssl-dev - - - if: runner.os == 'Linux' && matrix.compiler == 'gcc' - name: Install GCC for Ubuntu - run: | - sudo apt install -y gcc-13 g++-13 lcov - echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV - echo "CC=gcc-13" >> $GITHUB_ENV - echo "CXX=g++-13" >> $GITHUB_ENV - echo "MMAPPER_CMAKE_EXTRA=-DUSE_CODE_COVERAGE=true -DUSE_MOLD=true" >> $GITHUB_ENV - echo "COVERAGE=true" >> $GITHUB_ENV - - - if: runner.os == 'Linux' && matrix.compiler == 'clang' - name: Install Clang for Ubuntu - run: | - sudo apt install -y clang-18 binutils - echo "QMAKESPEC=linux-clang" >> $GITHUB_ENV - echo "CC=clang-18" >> $GITHUB_ENV - echo "CXX=clang++-18" >> $GITHUB_ENV - echo "STYLE=true" >> $GITHUB_ENV - echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV + - if: runner.os == 'Linux' + name: Install Packages for Ubuntu + run: | + sudo apt update -qq + sudo apt install -y \ + qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ + libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ + zlib1g-dev libssl-dev + - if: runner.os == 'Linux' && matrix.compiler == 'gcc' + name: Install GCC for Ubuntu + run: | + sudo apt install -y gcc-13 g++-13 lcov + echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV + echo "CC=gcc-13" >> $GITHUB_ENV + echo "CXX=g++-13" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DUSE_CODE_COVERAGE=true -DUSE_MOLD=true" >> $GITHUB_ENV + echo "COVERAGE=true" >> $GITHUB_ENV + - if: runner.os == 'Linux' && matrix.compiler == 'clang' + name: Install Clang for Ubuntu + run: | + sudo apt install -y clang-18 binutils + echo "QMAKESPEC=linux-clang" >> $GITHUB_ENV + echo "CC=clang-18" >> $GITHUB_ENV + echo "CXX=clang++-18" >> $GITHUB_ENV + echo "STYLE=true" >> $GITHUB_ENV + echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true" >> $GITHUB_ENV # - # Install Packages (macOS) + # Install Packages (Mac) # - - name: Set mac arch vars - if: runner.os == 'macOS' - run: | - if [[ "${{ matrix.os }}" == "macos-13" ]]; then - echo "QT_INSTALL_ARCH=clang_64" >> $GITHUB_ENV - echo "CMAKE_OSX_ARCH=x86_64" >> $GITHUB_ENV - else - echo "QT_INSTALL_ARCH=clang_64" >> $GITHUB_ENV - echo "CMAKE_OSX_ARCH=arm64" >> $GITHUB_ENV - fi - - - if: runner.os == 'macOS' - name: Install Qt for Mac - uses: jurplel/install-qt-action@v4 - with: - version: 6.5.3 - arch: ${{ env.QT_INSTALL_ARCH }} - cache: true - modules: 'qtwebsockets' - - - name: Install Packages for Mac - if: runner.os == 'macOS' - run: | - brew install cmake ninja lcov qtkeychain - echo "$QT_ROOT_DIR/bin" >> $GITHUB_PATH - echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH= -DCMAKE_BUILD_TYPE=Release -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DCPACK_DMG_FORMAT=UDZO -DCMAKE_OSX_ARCHITECTURES=" >> $GITHUB_ENV - echo "COVERAGE=false" >> $GITHUB_ENV + - if: runner.os == 'macOS' + name: Install Qt for Mac + uses: jurplel/install-qt-action@v4 + with: + version: 6.5.3 + arch: clang_64 + cache: true + modules: 'qtwebsockets' + - if: runner.os == 'macOS' + name: Install Packages for Mac + run: | + brew install cmake ninja lcov + echo "$QT_ROOT_DIR/bin" >> $GITHUB_PATH + echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DUSE_CODE_COVERAGE=true" >> $GITHUB_ENV + echo "COVERAGE=false" >> $GITHUB_ENV # # Install Packages (Windows) # - - if: runner.os == 'Windows' && matrix.compiler == 'msvc' - name: Install Qt for Windows (MSVC) - uses: jurplel/install-qt-action@v4 - with: - version: '6.5.3' - dir: 'C:\' - arch: win64_msvc2019_64 - cache: true - modules: 'qtwebsockets' - - - if: runner.os == 'Windows' && matrix.compiler == 'gcc' - name: Install Qt for Windows (MinGW) - uses: jurplel/install-qt-action@v4 - with: - version: '6.5.3' - dir: 'C:\' - arch: win64_mingw - cache: true - modules: 'qtwebsockets' - tools: 'tools_mingw1310' + - if: runner.os == 'Windows' && matrix.compiler == 'msvc' + name: Install Qt for Windows (MSVC) + uses: jurplel/install-qt-action@v4 + with: + version: '6.5.3' + dir: 'C:\' + arch: win64_msvc2019_64 + cache: true + modules: 'qtwebsockets' + - if: runner.os == 'Windows' && matrix.compiler == 'gcc' + name: Install Qt for Windows (MinGW) + uses: jurplel/install-qt-action@v4 + with: + version: '6.5.3' + dir: 'C:\' + arch: win64_mingw + cache: true + modules: 'qtwebsockets' + tools: 'tools_mingw1310' # # Build # - - if: runner.os == 'Windows' - name: Build MMapper for Windows - shell: pwsh - run: | - mkdir ${{ github.workspace }}/artifact - mkdir build - cd build - cmake --version - if ('${{ matrix.compiler }}' -eq 'gcc') { - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 - cmake --build . --parallel - - - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 - cmake --build . --parallel + - if: runner.os == 'Windows' + name: Build MMapper for Windows + shell: pwsh + run: | + mkdir ${{ github.workspace }}/artifact + mkdir build + cd build + cmake --version + if ('${{ matrix.compiler }}' -eq 'gcc') { + 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 + cmake --build . --parallel + - 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 -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -S .. || exit -1 + cmake --build . --parallel # - # Tests (coverage only on Linux gcc) + # Run tests # - - if: env.COVERAGE == 'true' - name: Prepare lcov counters - run: cd build && lcov --zerocounters --directory . - - - name: Run unit tests - run: cd build && ctest -V --no-compress-output -T test --output-junit ../ctest-to-junit-results.xml - env: - QT_QPA_PLATFORM: offscreen - ASAN_OPTIONS: detect_leaks=0 + - if: env.COVERAGE == 'true' + name: Prepare lcov counters + run: cd build && lcov --zerocounters --directory . + - name: Run unit tests + run: cd build && ctest -V --no-compress-output -T test --output-junit ../ctest-to-junit-results.xml + env: + QT_QPA_PLATFORM: offscreen + ASAN_OPTIONS: detect_leaks=0 + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: test-results ${{ matrix.os }} ${{ matrix.compiler }} + path: ctest-to-junit-results.xml + - if: env.COVERAGE == 'true' + name: Run lcov + run: | + cd build + lcov --directory . --capture --output-file coverage.info + lcov --list coverage.info + lcov --remove coverage.info '/usr/*' --output-file filtered.info + - if: env.COVERAGE == 'true' + uses: codecov/codecov-action@v5 + with: + files: ./build/filtered.info + - if: env.STYLE == 'true' + name: Text Encoding Sanity Check + run: | + cd build + if [[ -n $(nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString') ]]; then + nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString' + echo + echo + echo "Please avoid using QString::fromStdString() and QString::toStdString()" + echo + echo "Both functions assume the user wants utf8, but MMapper almost always expects" + echo "std::string to have latin1 encoding." + echo + echo "Convert any uses to corresponding functions in TextUtils e.g. ::toQStringUtf8()" + exit -1 + fi - - name: Upload test results - uses: actions/upload-artifact@v4 - with: - name: test-results ${{ matrix.os }} ${{ matrix.compiler }} - path: ctest-to-junit-results.xml - - - if: env.COVERAGE == 'true' - name: Run lcov - run: | - cd build - lcov --directory . --capture --output-file coverage.info - lcov --list coverage.info - lcov --remove coverage.info '/usr/*' --output-file filtered.info - - - if: env.COVERAGE == 'true' - uses: codecov/codecov-action@v5 - with: - files: ./build/filtered.info - - - if: env.STYLE == 'true' - name: Text Encoding Sanity Check - run: | - cd build - if [[ -n $(nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString') ]]; then - nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString' - echo - echo - echo "Please avoid using QString::fromStdString() and QString::toStdString()" - echo - echo "Both functions assume the user wants utf8, but MMapper almost always expects" - echo "std::string to have latin1 encoding." - echo - echo "Convert any uses to corresponding functions in TextUtils e.g. ::toQStringUtf8()" - exit -1 - fi - - # - # macOS deploy (before packaging) # - - name: Deploy Qt frameworks (macOS) - if: runner.os == 'macOS' - run: | - APP="$GITHUB_WORKSPACE/build/src/mmapper.app" - "$QT_ROOT_DIR/bin/macdeployqt" "$APP" -verbose=2 -libpath "$QT_ROOT_DIR/lib" - - # - # Package + Upload + # Package # - - name: Ad-hoc codesign (macOS) - if: runner.os == 'macOS' - run: | - APP="$GITHUB_WORKSPACE/build/src/mmapper.app" - /usr/bin/codesign --force --deep --sign - "$APP" - - name: Package MMapper - run: cd build && cpack -D CPACK_DMG_FORMAT=UDZO -D CPACK_PACKAGE_FILE_NAME="mmapper--Mac-" -V - - - if: runner.os == 'Linux' - name: Upload Package for Ubuntu - uses: actions/upload-artifact@v4 - with: - name: debug-${{ matrix.compiler }} - path: ${{ github.workspace }}/artifact/*.deb* - - - if: runner.os == 'macOS' - name: Upload Package for Mac - uses: actions/upload-artifact@v4 - with: - name: debug-${{ matrix.os }} - path: ${{ github.workspace }}/artifact/*.dmg* - - - if: runner.os == 'Windows' - name: Upload Package for Windows - uses: actions/upload-artifact@v4 - with: - name: debug-${{ matrix.compiler }} - path: ${{ github.workspace }}/artifact/*.exe* + - name: Package MMapper + run: cd build && cpack + - 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 + - if: runner.os == 'macOS' + name: Upload Package for Mac + uses: actions/upload-artifact@v4 + with: + name: debug-${{ 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: debug-${{ matrix.compiler }}-${{ 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: debug-${{ matrix.compiler }}-${{ steps.get_windows_package_name.outputs.PACKAGE_BASENAME }} + path: ${{ github.workspace }}/artifact/*.exe* report: name: Test Report runs-on: ubuntu-latest needs: build steps: - - uses: dorny/test-reporter@v2 - with: - artifact: /test-results (.*)/ - name: Test Report $1 - path: '*.xml' - reporter: java-junit + - uses: dorny/test-reporter@v2 + with: + artifact: /test-results (.*)/ + name: Test Report $1 + path: '*.xml' + reporter: java-junit From 3f916319e875663a71eb83414f04402d84f940bb Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 21:29:52 +1000 Subject: [PATCH 60/63] CI: skip cpack on macOS (avoid DMG failure); keep build+tests --- .github/workflows/build-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 23839dc2f..6c41589fd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -192,6 +192,8 @@ jobs: # Package # - name: Package MMapper + - name: Package MMapper + if: runner.os != 'macOS' run: cd build && cpack - if: runner.os == 'Linux' name: Get Linux package file base name From 845899a8c29307511f419da93c2ea5a68e301e14 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Fri, 29 Aug 2025 22:54:37 +1000 Subject: [PATCH 61/63] CI: reset workflow from master; skip macOS cpack --- .github/workflows/build-test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6c41589fd..23839dc2f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -192,8 +192,6 @@ jobs: # Package # - name: Package MMapper - - name: Package MMapper - if: runner.os != 'macOS' run: cd build && cpack - if: runner.os == 'Linux' name: Get Linux package file base name From 091ee2e7ff2cee1d6fc3751ee9dd2b3ecfe75fd3 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Mon, 1 Sep 2025 13:58:02 +1000 Subject: [PATCH 62/63] CI: skip cpack on macOS (avoid DMG failure); keep build+tests --- .github/workflows/build-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 23839dc2f..5bdd036cc 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -192,6 +192,7 @@ jobs: # Package # - name: Package MMapper + if: runner.os != 'macOS' run: cd build && cpack - if: runner.os == 'Linux' name: Get Linux package file base name From 4b7031f98718bd1665f2f2e0e6cad2d34c1ea737 Mon Sep 17 00:00:00 2001 From: Lewis Buckingham Date: Tue, 2 Sep 2025 08:35:25 +1000 Subject: [PATCH 63/63] CI(macOS): stop installing cmake via Homebrew; use preinstalled CMake --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5bdd036cc..6b7367c5a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -89,7 +89,7 @@ jobs: - if: runner.os == 'macOS' name: Install Packages for Mac run: | - brew install cmake ninja lcov + brew install ninja lcov echo "$QT_ROOT_DIR/bin" >> $GITHUB_PATH echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DUSE_CODE_COVERAGE=true" >> $GITHUB_ENV echo "COVERAGE=false" >> $GITHUB_ENV