mudlet/test/functional_tests/ProfileSwitchShortcutTest.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

410 lines
17 KiB
C++
Raw Permalink Normal View History

Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
/***************************************************************************
* Copyright (C) 2026 by Vadim Peretokin - vadim.peretokin@mudlet.org *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
* The command line must claim the ShortcutOverride exactly when a live user key
* binding matches a press that would otherwise activate a profile switching
* shortcut (Ctrl+1 to Ctrl+9, Ctrl+Tab) - including presses QShortcutMap
* matches to a differently spelt shortcut - and never otherwise. Claiming it is
* the only way the binding survives, since QShortcutMap otherwise runs the
* shortcut and never delivers the KeyPress.
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
*
* Run with: ctest -R ProfileSwitchShortcutTest -V
*/
#include <QtTest/QtTest>
#include <chrono>
#include "Host.h"
#include "KeyUnit.h"
#include "MudletInstanceCoordinator.h"
#include "TCommandLine.h"
#include "TKey.h"
#include "TLuaInterpreter.h"
#include "ShortcutsManager.h"
#include "TMainConsole.h"
#include "TelnetServerStub.h"
#include "ctelnet.h"
#include "dlgConnectionProfiles.h"
#include "mudlet.h"
#include <QShortcut>
extern "C" {
#if defined(INCLUDE_VERSIONED_LUA_HEADERS)
#include <lua5.1/lauxlib.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#else
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#endif
}
using namespace std::chrono_literals;
extern void qInitResources_mudlet();
extern void qInitResources_qm();
extern void qInitResources_additional_splash_screens();
extern void qInitResources_mudlet_fonts_common();
extern void qInitResources_mudlet_fonts_posix();
void initializeQRCResourcesForProfileSwitchShortcutTest();
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Qt::CTRL is Cmd on macOS, where "next profile" uses Qt::META - see mudlet::mudlet()
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
#if defined(Q_OS_MACOS)
static constexpr Qt::KeyboardModifier nextProfileModifier = Qt::MetaModifier;
#else
static constexpr Qt::KeyboardModifier nextProfileModifier = Qt::ControlModifier;
#endif
class ProfileSwitchShortcutTest : public QObject
{
Q_OBJECT
private:
TelnetServerStub* mpServer = nullptr;
Host* mpHost = nullptr;
const QString mHostname = "ProfileSwitchShortcut-Test";
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
const QString mLocalhost = "localhost";
TCommandLine* commandLine() const
{
if (!mpHost || !mpHost->mpConsole) {
return nullptr;
}
return mpHost->mpConsole->mpCommandLine;
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// QShortcutMap offers the key as an ignored ShortcutOverride and only runs
// the shortcut if nobody accepted it
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
bool overrideClaimed(int key, Qt::KeyboardModifiers modifiers) const
{
QKeyEvent event(QEvent::ShortcutOverride, key, modifiers);
event.ignore();
QApplication::sendEvent(commandLine(), &event);
return event.isAccepted();
}
void sendKeyPress(int key, Qt::KeyboardModifiers modifiers) const
{
QKeyEvent event(QEvent::KeyPress, key, modifiers);
QApplication::sendEvent(commandLine(), &event);
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Asserted where a claim is expected, so a mis-mapped sequence cannot
// masquerade as a code failure
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
bool shortcutInstalledFor(const QKeySequence& sequence) const
{
const auto shortcuts = mudlet::self()->findChildren<QShortcut*>();
for (auto* shortcut : shortcuts) {
if (shortcut->key() == sequence && shortcut->isEnabled()) {
return true;
}
}
return false;
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Alt+E on Linux and Windows, Ctrl+E on macOS
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
std::pair<int, Qt::KeyboardModifiers> scriptEditorShortcut() const
{
auto* sequence = mudlet::self()->shortcutsManager()->getSequence(qsl("Script editor"));
if (!sequence || sequence->isEmpty()) {
return {Qt::Key_unknown, Qt::NoModifier};
}
const QKeyCombination combination = (*sequence)[0];
return {combination.key(), combination.keyboardModifiers()};
}
int luaCounter(const QString& globalName) const
{
lua_State* L = mpHost->mLuaInterpreter.getLuaGlobalState();
lua_getglobal(L, globalName.toUtf8().constData());
const int value = static_cast<int>(lua_tointeger(L, -1));
lua_pop(L, 1);
return value;
}
int createCountingKey(const QString& name, int keycode, int modifier, const QString& counterName, const QString& parent = QString())
{
QString keyName = name;
QString parentName = parent;
QString script = qsl("%1 = (%1 or 0) + 1").arg(counterName);
auto [id, message] = mpHost->mLuaInterpreter.startPermKey(keyName, parentName, keycode, modifier, script);
if (id <= 0) {
qWarning() << "createCountingKey failed:" << message;
}
return id;
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Safe only while every key here is permanent and none is killed or
// uninstalled, leaving KeyUnit's deferred-delete set empty; otherwise this
// has to go through markCleanup()/doCleanup()
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
void removeAllKeys()
{
auto* keyUnit = mpHost->getKeyUnit();
const auto rootKeys = keyUnit->getKeyRootNodeList(); // by value: ~TKey mutates the real list
for (auto* key : rootKeys) {
delete key;
}
}
private slots:
void initTestCase()
{
initializeQRCResourcesForProfileSwitchShortcutTest();
mpServer = new TelnetServerStub(qApp);
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
mPort = QString::number(mpServer->serverPort());
mudlet::start();
mudlet::self()->setupConfig();
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();
mudlet::self()->setStorePasswordsSecurely(false);
deleteProfileDirectory(mHostname);
startProfile(mHostname, mLocalhost, mPort);
mpHost = mudlet::self()->getActiveHost();
QVERIFY2(mpHost, "No active host after profile creation");
QVERIFY2(commandLine(), "No command line available for the test");
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Checked ahead of the claim in TCommandLine::event(), so CtrlTab here
// would take Ctrl+Tab out of these tests' hands
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
mpHost->mCaretShortcut = Host::CaretShortcut::None;
}
void cleanupTestCase()
{
mpHost = nullptr;
delete mpServer;
mpServer = nullptr;
deleteProfileDirectory(mHostname);
delete mudlet::self();
}
void cleanup() { removeAllKeys(); }
void test_userBindingOnCtrlNumberClaimsTheShortcut()
{
QVERIFY2(shortcutInstalledFor(QKeySequence(Qt::CTRL | Qt::Key_1)), "No profile switching shortcut is installed for Ctrl+1, so this test proves nothing");
QVERIFY(createCountingKey(qsl("Ctrl+1 binding"), Qt::Key_1, Qt::ControlModifier, qsl("_testCtrl1")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_1, Qt::ControlModifier), "A user key binding on Ctrl+1 did not claim the key, so the profile switch shortcut swallows it");
}
void test_withoutUserBindingTheShortcutKeepsTheKey()
{
QVERIFY2(!overrideClaimed(Qt::Key_1, Qt::ControlModifier), "Ctrl+1 was claimed even though no user key binding matches it - profile switching would stop working");
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Only nine shortcuts are installed, so Ctrl+0 has nothing to beat
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
void test_bindingOnAnUnshadowedKeyIsNotClaimed()
{
QVERIFY(createCountingKey(qsl("Ctrl+0 binding"), Qt::Key_0, Qt::ControlModifier, qsl("_testCtrl0")) > 0);
QVERIFY2(!overrideClaimed(Qt::Key_0, Qt::ControlModifier), "Ctrl+0 is not a profile switching shortcut, so the command line must not claim it");
}
void test_disabledUserBindingDoesNotClaimTheShortcut()
{
const QString name = qsl("Disabled Ctrl+2 binding");
QVERIFY(createCountingKey(name, Qt::Key_2, Qt::ControlModifier, qsl("_testCtrl2")) > 0);
QVERIFY(overrideClaimed(Qt::Key_2, Qt::ControlModifier));
QVERIFY(mpHost->getKeyUnit()->disableKey(name));
QVERIFY2(!overrideClaimed(Qt::Key_2, Qt::ControlModifier), "A disabled key binding still claimed Ctrl+2");
}
void test_userBindingOnCtrlNineClaimsTheShortcut()
{
QVERIFY2(shortcutInstalledFor(QKeySequence(Qt::CTRL | Qt::Key_9)), "No profile switching shortcut is installed for Ctrl+9, so this test proves nothing");
QVERIFY(createCountingKey(qsl("Ctrl+9 binding"), Qt::Key_9, Qt::ControlModifier, qsl("_testCtrl9")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_9, Qt::ControlModifier), "A user key binding on Ctrl+9 did not claim the key");
}
void test_userBindingInDisabledGroupDoesNotClaimTheShortcut()
{
const int groupId = createCountingKey(qsl("Key Group"), -1, 0, qsl("_testGroup"));
QVERIFY(groupId > 0);
auto* group = mpHost->getKeyUnit()->getKey(groupId);
QVERIFY(group);
group->setIsActive(true);
QVERIFY(createCountingKey(qsl("Grouped Ctrl+3 binding"), Qt::Key_3, Qt::ControlModifier, qsl("_testCtrl3"), qsl("Key Group")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_3, Qt::ControlModifier), "A binding in an enabled group should claim Ctrl+3");
group->setIsActive(false);
QVERIFY2(!overrideClaimed(Qt::Key_3, Qt::ControlModifier), "A binding inside a disabled group still claimed Ctrl+3");
}
void test_userBindingOnCtrlTabClaimsTheShortcut()
{
QVERIFY2(shortcutInstalledFor(QKeySequence(nextProfileModifier | Qt::Key_Tab)), "No 'Next profile' shortcut is installed for Ctrl+Tab, so this test proves nothing");
QVERIFY2(!overrideClaimed(Qt::Key_Tab, nextProfileModifier), "Ctrl+Tab was claimed with no user key binding present");
QVERIFY(createCountingKey(qsl("Ctrl+Tab binding"), Qt::Key_Tab, nextProfileModifier, qsl("_testCtrlTab")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_Tab, nextProfileModifier), "A user key binding on Ctrl+Tab did not claim the key");
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Shift+Tab reaches the widget as Key_Backtab while the sequence is spelt
// with Key_Tab, so the match has to bridge the two spellings
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
void test_userBindingOnCtrlShiftTabClaimsTheShortcut()
{
const auto modifiers = nextProfileModifier | Qt::ShiftModifier;
QVERIFY2(!overrideClaimed(Qt::Key_Backtab, modifiers), "Ctrl+Shift+Tab was claimed with no user key binding present");
QVERIFY(createCountingKey(qsl("Ctrl+Shift+Tab binding"), Qt::Key_Backtab, modifiers, qsl("_testCtrlShiftTab")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_Backtab, modifiers), "A user key binding on Ctrl+Shift+Tab did not claim the key");
}
void test_bindingOnAnotherApplicationShortcutIsNotClaimed()
{
auto [key, modifiers] = scriptEditorShortcut();
QVERIFY2(key != Qt::Key_unknown, "Could not read the script editor shortcut");
QVERIFY(createCountingKey(qsl("Script editor shortcut binding"), key, modifiers, qsl("_testEditor")) > 0);
QVERIFY2(!overrideClaimed(key, modifiers), "A key binding claimed the script editor shortcut, which is outside the profile switching set");
}
void test_aClearedProfileShortcutDoesNotClaimEveryKey()
{
auto* sequence = mudlet::self()->shortcutsManager()->getSequence(qsl("Switch to profile 1"));
QVERIFY2(sequence, "'Switch to profile 1' is not registered with the shortcuts manager");
const QKeySequence saved = *sequence;
*sequence = QKeySequence();
auto [key, modifiers] = scriptEditorShortcut();
QVERIFY(createCountingKey(qsl("Script editor shortcut binding"), key, modifiers, qsl("_testEditorCleared")) > 0);
QVERIFY(createCountingKey(qsl("F5 binding"), Qt::Key_F5, Qt::NoModifier, qsl("_testF5")) > 0);
const bool editorClaimed = overrideClaimed(key, modifiers);
const bool f5Claimed = overrideClaimed(Qt::Key_F5, Qt::NoModifier);
*sequence = saved;
QVERIFY2(!editorClaimed, "A cleared profile switching shortcut made an unrelated bound key claim the override");
QVERIFY2(!f5Claimed, "A cleared profile switching shortcut made an unrelated bound key claim the override");
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// QShortcutMap retries with the keypad modifier stripped, so Ctrl and a
// numpad digit activates the plain Ctrl+1 shortcut
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
void test_userBindingOnAKeypadDigitClaimsTheShortcut()
{
const auto modifiers = Qt::ControlModifier | Qt::KeypadModifier;
QVERIFY(createCountingKey(qsl("Ctrl+keypad 1 binding"), Qt::Key_1, modifiers, qsl("_testKeypad1")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_1, modifiers), "A user key binding on Ctrl and a keypad digit did not claim the key");
}
infrastructure: trim the comments left behind by two merged QA fixes (#9708) #### Brief overview of PR changes/additions - Comment-only. `git diff origin/development...HEAD` changes no statement, expression or declaration - every added and removed line is a comment. 238 comment lines become 98. - Applies the house standard to the comments added by "fix: a trigger that re-creates itself freezes Mudlet" (#9697) and "Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab" (#9703): no historical passages, and the rest cut to what a reader cannot derive from the code. - Corrects four claims that were wrong, two of them inherited from those PRs: a fires-per-line measurement taken with a smaller budget than the one that shipped, an over-general note on `shortcutInstalledFor()`, a `KeyUnit::disableKey()` note that had the mechanism backwards, and a test comment crediting the `isEmpty()` guard for a result it does not produce. #### Motivation for adding to Mudlet Both PRs merged while their comment-reduction pass was still in flight, so the trim never landed with them. #### Other info (issues closed, discussion etc) The gotchas worth keeping survive in shorter form: why the same-line creation budget is counted per pass rather than sharing the `feedTriggers()` depth counter, why permanent triggers get `deactivate()` and not `setIsActive(false)`, why `mCleanupSet` rather than the deactivation is what stops `enableTrigger()` resurrecting a spent trigger, that `QShortcutMap` retries with consumed modifiers stripped, and the `Key_Backtab` versus `Shift+Tab` spelling. The matching trim for "fix: stop treating long-time Mudlet users as brand new players" (#9695) already landed separately as #9707, so it is not repeated here. No demo video: a comment-only change is not observable on screen. **Test case:** `ctest` in the build directory - 79/80, with `TelnetBenchmark` timing out only under parallel load (31s standalone against a 60s limit) on a path this PR does not touch. `TriggerSameLineMatchTest`, `UnitDeferredDeleteTest`, `ProfileSwitchShortcutTest` and `ExperiencedPlayerGateTest` all pass. Assisted-by: Claude:claude-opus-5
2026-08-06 17:43:35 +02:00
// Layouts needing Shift for a top-row digit (French AZERTY) record the
// binding with Shift, and QShortcutMap drops the Shift it consumed
Fix user key bindings on Ctrl+1 to Ctrl+9 and Ctrl+Tab (#9703) #### Brief overview of PR changes/additions - The profile tab switching shortcuts added in #9460 "add: keyboard shortcuts to switch between game tabs" (`b649b60f0`) are `QShortcut`s on the main window, and Qt's `QShortcutMap` consumes a matching key inside `QApplication::notify` before the `KeyPress` ever reaches the command line. `TCommandLine::handleCtrlTabChange()`'s "let user-defined Ctrl+# keys match first" branch became unreachable, so user key bindings on Ctrl+1..Ctrl+9, Ctrl+Tab and Ctrl+Shift+Tab silently stopped working. - `TCommandLine::event()` now claims `QEvent::ShortcutOverride` for exactly those key sequences when a user binding matches, which is the same escape hatch the accessibility caret shortcut already uses. Asking needs a non-executing query, hence `TKey`/`KeyUnit::wouldMatch()` - `keybindingMatched()` runs the binding and would fire it on every override probe. The match reproduces `QShortcutMap`'s own retries, so Ctrl and a numpad digit, and Ctrl+Shift and a digit on layouts that need Shift for the top row (French AZERTY), are covered too. - Precedence, stated explicitly: a user binding wins over the built-in tab switch, which is what that comment always intended. A binding that is disabled, or sits in a disabled group, does not claim the key, and every other application shortcut is unaffected. #### Motivation for adding to Mudlet Ctrl+1 to Ctrl+9 is a common combat/target hotkey range and the one Mudlet's own key editor offers. Upgrading silently broke those bindings with no error and no warning, and the escape hatch (clearing the shortcut in Preferences) is undiscoverable. #### Other info (issues closed, discussion etc) Test case: `ctest -R ProfileSwitchShortcutTest` - 15 cases covering the claim, the no-claim controls, disabled bindings and groups, the keypad and shifted-digit spellings, Ctrl+Shift+Tab's `Key_Backtab` spelling, a cleared shortcut not claiming every key, and that a claimed binding runs exactly once. Verified to fail without the fix. Not fixed here, reported instead: the caret-mode Ctrl+Tab toggle lives on `Host::mCaretShortcut` rather than `ShortcutsManager`, so #9449's shortcut clash warning still cannot see its collision with the "Next profile" default. Assisted-by: Claude:claude-opus-5
2026-08-06 13:33:06 +02:00
void test_userBindingOnAShiftedDigitClaimsTheShortcut()
{
const auto modifiers = Qt::ControlModifier | Qt::ShiftModifier;
QVERIFY(createCountingKey(qsl("Ctrl+Shift+1 binding"), Qt::Key_1, modifiers, qsl("_testShift1")) > 0);
QVERIFY2(overrideClaimed(Qt::Key_1, modifiers), "A user key binding on Ctrl+Shift and a digit did not claim the key");
}
void test_claimedBindingRunsExactlyOnce()
{
QVERIFY(createCountingKey(qsl("Ctrl+4 binding"), Qt::Key_4, Qt::ControlModifier, qsl("_testCtrl4")) > 0);
QCOMPARE(luaCounter(qsl("_testCtrl4")), 0);
QVERIFY(overrideClaimed(Qt::Key_4, Qt::ControlModifier));
QCOMPARE(luaCounter(qsl("_testCtrl4")), 0); // the probe must not execute anything
sendKeyPress(Qt::Key_4, Qt::ControlModifier);
QCOMPARE(luaCounter(qsl("_testCtrl4")), 1);
}
private:
void startProfile(const QString& hostname, const QString& address, const QString& port)
{
QTimer::singleShot(0ms, qApp, [hostname, address, port]() {
mudlet::self()->startAutoLogin({});
QTest::qWait(100ms);
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button, Qt::LeftButton);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), hostname);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), address);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), port);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
});
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
if (!spy.wait(5000)) {
QFAIL("Profile took too long to load.");
}
auto host = mudlet::self()->getActiveHost();
if (!host) {
QFAIL("No active host available for the test.");
}
QSignalSpy connectionSpy(&(host->mTelnet), &cTelnet::signal_connected);
if (!connectionSpy.wait(2000)) {
QFAIL("Could not connect with the host.");
}
}
void deleteProfileDirectory(const QString& profileName)
{
const QString path = mudlet::getMudletPath(enums::profileHomePath, profileName);
QDir dir(path);
if (!dir.exists()) {
return;
}
dir.removeRecursively();
}
};
void initializeQRCResourcesForProfileSwitchShortcutTest()
{
#ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens();
#endif
#ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common();
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix();
#endif
#endif
qInitResources_mudlet();
qInitResources_qm();
}
#include "ProfileSwitchShortcutTest.moc"
QTEST_MAIN(ProfileSwitchShortcutTest)