Skip to content

feat(raidframes): per-spec Target Unit / Context Menu via drag - #1243

Open
wanghwplus wants to merge 2 commits into
EllesmereGaming:mainfrom
wanghwplus:hovercast-spec-target-menu-override
Open

feat(raidframes): per-spec Target Unit / Context Menu via drag#1243
wanghwplus wants to merge 2 commits into
EllesmereGaming:mainfrom
wanghwplus:hovercast-spec-target-menu-override

Conversation

@wanghwplus

@wanghwplus wanghwplus commented Aug 7, 2026

Copy link
Copy Markdown

What does this PR do?

HoverCast's Target Unit and Context Menu are seeded into cc.globals when the DB is created and cannot be deleted there. cc lives at EllesmereUIDB.clickCast, which is account-global and deliberately stripped from profiles, so those two keys were shared by every character on the account with no way to vary them per spec - while every other binding type already had a per-spec path.

Dragging a Target Unit / Context Menu tile from Global Bindings onto the Spec Bindings sidebar now copies it into the active spec. No new popup, no new button - if you never drag, the page is exactly what it was.

  • The copy is seeded from the global, so it starts behaving identically and only has to be rebound.
  • Deleting the copy falls straight back to the global.
  • The global tile dims while a spec copy shadows it, so it is never ambiguous which key is live.

Resolution had to grow a type rule

GetActiveBindings de-duped on key alone. Moving a spec's Target Unit off Left Click therefore left the global's Left Click still targeting, so the spec entry read as an addition rather than an override. Overridable types now shadow by type as well.

Two predicates that this implies are single functions the whole file shares:

Function Owns
BindingIsLive(b) "is this binding in force" (enabled ~= false and key)
GetShadowedTypes() which overridable types the active spec shadows

Resolution, FindKeyConflicts and the options page all go through them, so a shadowed global can no longer be reported as a live keybind conflict, nor drawn as active while the spec copy is what fires. A spec copy whose key was cleared shadows nothing and every surface agrees on that.

Drag mechanics

  • Cursor tracking rides EllesmereUI.Mouse.SubscribeFrame (motionOnly), unsubscribed on drop - no bespoke OnUpdate.
  • The click the engine delivers with the drag's mouse-up is suppressed for 0.2s, the same reason the CDM icon reorder keeps a dragEndTime. Without it the drop re-selected the source tile and fired a second page rebuild from a tile the first rebuild had already orphaned.
  • The rebuild is deferred a frame and dispatched through ns._ccRebuild (latest-wins), so the page is never torn down from inside the handler the engine is still dispatching on, and a stale closure can never drive the page singleton.
  • Seeding the copy carries the global's key across, which can collide with a spell already bound to it in that spec. That path now raises the same Duplicate Keybind warning every other key assignment in the file uses, instead of silently winning the attribute and killing the existing binding.

Scope

Only target and menu are overridable. dispel / external / trinket1 / trinket2 / dynamicrez are the same kind of non-deletable global singleton and the machinery is general enough to cover them - happy to extend if you want it, but I kept the diff to what was actually asked for.

How was it tested?

Live retail 8.7.6, on the HoverCast page:

  • Dragged Target Unit across; the copy appears in Spec Bindings, is selected, and the global tile dims.
  • Rebound the copy's key; the global's old key stops targeting (the point of the type rule) and is then free to bind elsewhere without a phantom conflict.
  • Deleted the copy; the global goes back to full alpha and fires again.
  • Cleared the copy's key instead of deleting it; the global goes live again and both tooltips say so.
  • Dropped outside the sidebar, and dragged with a copy already present - both are no-ops with the hint text saying which.

12.1 PTR: not tested. The change adds no IS_121-gated paths and touches no aura rendering - it is plain Lua plus options-page UI - but I am not claiming PTR verification I did not do.

Screenshots

1 2

Checklist

  • New settings default OFF (no behavior change without opt-in) - N/A, no new setting: this is a gesture. With no spec copies present the shadow set is empty and resolution is byte-for-byte what it was.
  • Zero cost while disabled: no events registered, no polling, no hooks doing work, no frames built - all of it lives in the options page, built only when that page opens. The drag ghost is created lazily on the first drag and reused.
  • Cheap while enabled: event-driven (no polling, no timer-based logic, no per-frame allocations) - cursor tracking is the shared Mouse service, unsubscribed on drop; the one deferred rebuild is a single C_Timer.After(0).
  • No writes onto Blizzard-owned frames (weak-table pattern used); HookScript/hooksecurefunc only, never SetScript on Blizzard frames - N/A, nothing here touches a Blizzard frame. SetScript is used only on frames this file creates.
  • Tested in-game, works on live retail; no load errors on the 12.1 PTR client - live retail 8.7.6 verified; 12.1 PTR not tested (see above).

Both are seeded into cc.globals at DB creation and cannot be deleted there,
so their key was shared by every character on the account with no way to vary
it per spec. Dragging one onto the Spec Bindings sidebar now copies it into
the active spec, seeded from the global so it starts behaving identically and
only has to be rebound. Deleting the copy falls straight back to the global.

Resolution had to grow a type rule to match: GetActiveBindings de-duped on key
alone, so moving a spec's Target Unit off Left Click left the global's Left
Click still targeting -- the copy read as an addition, not an override. Both
the shadow rule and the "is this binding in force" predicate are now single
functions (GetShadowedTypes / BindingIsLive) that resolution, FindKeyConflicts
and the options page all share, so a shadowed global can no longer be reported
as a live keybind conflict or drawn as active while the spec copy fires.

The drop suppresses the click the engine delivers with the drag's mouse-up
(same reason the CDM icon reorder keeps a dragEndTime) and defers its rebuild
a frame through ns._ccRebuild, so the page is never torn down from inside the
handler the engine is still dispatching on.

Nothing changes for anyone who does not drag: with no spec copies present the
shadow set is empty and resolution is byte-for-byte what it was.
@wanghwplus
wanghwplus force-pushed the hovercast-spec-target-menu-override branch from ac84570 to 206aef2 Compare August 7, 2026 07:50
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