Skip to content

fix(cdm): mirror presses from click-routed keybinds - #1231

Open
dfrisone wants to merge 2 commits into
EllesmereGaming:mainfrom
dfrisone:cdm-press-mirror-click-routed
Open

fix(cdm): mirror presses from click-routed keybinds#1231
dfrisone wants to merge 2 commits into
EllesmereGaming:mainfrom
dfrisone:cdm-press-mirror-click-routed

Conversation

@dfrisone

@dfrisone dfrisone commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The CDM press mirror hangs off ActionButtonDown and MultiActionButtonDown, which are the native binding commands. Any key the action bars route through the button with SetOverrideBindingClick never reaches them, so the mirror never saw the press.

Per the useClick decision in EllesmereUIActionBars.lua, that is not just Bars 9 and 10, which have no native command at all:

  • Bars 9 and 10, always (info.customPage ~= nil)
  • every empower spell, on any bar
  • any bar with user-configured paging
  • MainBar when form or skyriding paging is opted out

So an empowered spell sitting on bar 1 had the same missing press flash, with no extra bar involved. Found while fixing the Bar 9 keybind report in #1230, not separately reported.

Why not hook UseAction

The tempting fix is hooksecurefunc("UseAction", ...), which catches every route at once: SECURE_ACTIONS.action calls the global from Lua at SecureTemplates.lua:349.

It is not safe here. OnActionButtonClick keeps going after that handler returns and calls protected C_Container.UseContainerItem, UseInventoryItem, and SpellTargetItem on the spell-targets-item path, which is what runs when you apply an oil, enchant, or poison. A hook on UseAction sits upstream of those. The two existing native hooks are safe precisely because ActionButtonDown calls TryUseActionButton, which completes that whole protected block before the post-hook runs.

What it does instead

The action bars module already hooks PostClick on every button for its own press-time GCD paint, added for this exact reason ("keybinds arrive as clicks too"). PostClick runs after Blizzard's click handler has fully returned, so it keeps the same taint posture the native hooks already have. The bars module publishes the press through a global, matching _EAB_UpdateKeybinds, so it costs one nil check when the CDM module is off.

The binding command is captured at hook-install time from BINDING_MAP[info.key] .. index. It is a property of the button rather than of the slot the button currently shows, so it stays correct across every page swap. Deriving it from the live action slot instead would be ambiguous exactly where it matters, since Bar 9 is action page 2 and its slots collide with MainBar's whenever MainBar is paged there.

Keeping the existing semantics

Two gates, so nothing changes for anyone not on a click-routed key.

It acts on the down edge only, because buttons register for both edges and the key is already released by the up click in key-up mode.

It requires evidence the press was from the keyboard, because by PostClick a click-routed keypress and a real mouse click are indistinguishable, as SecureActionButton_OnClick's own comment says. That needs two tests rather than one: a held binding key proves keyboard, but IsKeyDown is never true for mousewheel binds, and gating on it alone would silently drop every wheel-bound button that the native path still mirrors. The cursor-rect test covers those. Together they only miss a wheel bind pressed while the cursor happens to rest on that same button.

Notes

Two files, 56 lines. Read-only plus Show/Hide on our own overlay textures. No new events, no new strings, no options changes, no saved-variable changes. Zero cost when the mirror is off, which is an O(1) early-out on an already-cached flag.

Independent of #1230 despite being found alongside it: no shared files.

Tested in game: a bar 2 key flashes exactly once (native-routed presses do not reach PostClick, since TryUseActionButton calls SecureActionButton_OnClick as a plain Lua call rather than a widget click), Bars 9 and 10 now mirror, and empower spells now mirror where they previously did not.

The CDM press mirror hangs off ActionButtonDown and MultiActionButtonDown,
which are the NATIVE binding commands. Any key the action bars route through
the button with SetOverrideBindingClick never reaches them, so the mirror
never saw it. That is not just Bars 9 and 10, which have no native command at
all: useClick is also true for every empower spell on any bar, for any bar
with user-configured paging, and for MainBar when form or skyriding paging is
opted out. An empowered spell on bar 1 had the same missing press flash with
no extra bar involved.

Rather than hook UseAction, which sits upstream of the protected
UseContainerItem/UseInventoryItem/SpellTargetItem calls that
OnActionButtonClick makes on the spell-targets-item path, ride the PostClick
hook the action bars module already installs on every button for its own
press-time GCD paint. PostClick runs after Blizzard's click handler has fully
returned, so this keeps the same taint posture the two native hooks already
have. The action bars module publishes the press through a global, matching
_EAB_UpdateKeybinds, so it costs one nil check when the CDM module is off.

The subscriber needs the binding command back from the button to poll
IsKeyDown for the release, so the keybind cache now also records
slot -> command as it walks the bars. Built in that same loop deliberately: it
shares the page resolution and the bar priority order, and cannot drift from
the keybind cache the way the bar list itself had drifted.

Two gates keep this to the existing feature's semantics. It acts on the down
edge only, because buttons register for both edges and the key is already
released by the up click in key-up mode. And it requires one of the slot's
binding keys to be held, because by PostClick a click-routed keypress and a
real mouse click are indistinguishable, as Blizzard's own comment in
SecureTemplates says. Without that second gate, mouse clicks would start
mirroring, which is a behaviour change no one asked for.
…eel binds

Two defects from a review of the previous commit.

The slot to binding-command map was both ambiguous and unnecessary. Bar 9 IS
action page 2, so its slots collide with MainBar's whenever MainBar is paged
there, and a slot-keyed lookup then hands back the wrong bar's command. It was
also stale for the 0.5s debounce after any page change. The action bars module
already knows the exact command at hook-install time, and that command is a
property of the BUTTON rather than of the slot it currently shows, so it stays
correct across every page swap. Capture it there and pass it through. This
deletes the map, and with it the only change to
EllesmereUICooldownManager.lua.

The keyboard-versus-mouse gate dropped mousewheel binds entirely.
IsKeyDown is never true for MOUSEWHEELUP/DOWN, so requiring a held key
suppressed the mirror for every wheel-bound button, where the native path
still shows it and simply releases on the MIN_VISIBLE floor. A held key still
proves keyboard; the cursor-rect test now covers the binds it cannot see. The
pair only misses a wheel bind pressed while the cursor happens to rest on that
same button.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant