Skip to content

Commit 69df579

Browse files
committed
frame: extend attribute click dispatcher (one-verb-per-click, spell/macro/menu)
The `type*` click path now mirrors retail's SecureActionButton_OnClick: resolve ONE verb per click from the modifier/button-qualified `type` attribute and perform it, instead of a fixed `type1=target`. - Modifier/button resolution: `[alt-/ctrl-/shift-]type[1..5]` with prefix..name..suffix -> name..suffix -> name precedence, read from the live key state and the click's button. - Verbs: target (with the engine's default-interaction precedence - cast a pending spell / drop a cursor item on the unit instead of switching target), assist, focus, spell, stopcasting, macro, and menu. - Own-the-click: when a verb resolves we perform it and do NOT chain the frame's previous OnClick, so a configured `type1` no longer double- dispatches alongside an addon's own conditional click handler. - Self-healing wire: addons that re-SetScript OnClick (pfUI on every raid relayout) clobber our closure; WireOnClick re-wraps a clobbered Lua handler but skips its own C closure, so re-setting `type*` recovers without double-chaining. Dispatch goes straight to C++ where we own the code - no Lua round-trip through the global table: spell -> Spell::AtUnit::CastByName (a native unit-targeted cast; the unit's GUID is fed to the engine's dispatcher so there's no target juggling, and ground-target spells land at the unit's feet), focus -> Unit::Focus::Set, and the target verb's predicates -> Spell::AtCursor::IsPlacementActive / Cursor::Info::HasItem. Only genuine engine entries (TargetUnit/AssistUnit/SpellTargetUnit/DropItemOnUnit/ SpellStopCasting) and addon-provided functions still go through Lua globals. Spell::AtUnit gains a C++ header sharing its CastCore with the C_Spell.CastAtUnit Lua entry. macro verb: takes the content from the `macrotext`/`macro` attribute and prefers an addon-provided RunMacro (SuperCleveRoidMacros etc. - handles named macros and extended macro text), falling back to running the text natively when no RunMacro global is present. The native path runs each line through the stock FrameXML ChatEdit_ParseText via a throwaway edit-box (GetText closure + no-op __index), so it needs no addon. menu/togglemenu verb: pops the standard unit dropdown at the cursor via the new ClassicAPI_ToggleUnitMenu (Util/SecureUnitMenu.lua), resolving the unit -> menu type with Blizzard's own generic resolver (self->SELF, pet->PET, grouped player->PARTY, other player->PLAYER, NPC->RAID_TARGET_ICON). Keeps the FrameXML dropdown logic in Lua with the C side as a thin call.
1 parent 40d85d9 commit 69df579

6 files changed

Lines changed: 499 additions & 144 deletions

File tree

AddOns/!!!ClassicAPI/!!!ClassicAPI.toc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Util\Vector3D.lua
4444
Util\Vector4D.lua
4545
Util\UIParent.lua
4646
Util\AuraDurationModifiers.lua
47+
Util\SecureUnitMenu.lua
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
-- Unit right-click menu for attribute-driven unit frames.
2+
--
3+
-- Backs the `menu` / `togglemenu` click verb of the Frame:SetAttribute
4+
-- backport (see src/frame/Attributes.cpp). When a frame with a `unit`
5+
-- attribute resolves a click to the `menu` verb, the DLL calls
6+
-- ClassicAPI_ToggleUnitMenu(unit), which pops the standard unit dropdown at
7+
-- the cursor -- the same UnitPopup menu Blizzard's PlayerFrame / TargetFrame /
8+
-- PartyMemberFrame show on right-click. Lets a unit frame express its
9+
-- right-click menu purely as an attribute (e.g. type2 = "menu") instead of a
10+
-- hand-rolled OnClick handler.
11+
--
12+
-- The unit -> menu-type resolution mirrors Blizzard's own generic resolver
13+
-- (TargetFrameDropDown_Initialize in FrameXML): self -> SELF, pet -> PET, a
14+
-- grouped player -> PARTY (whisper / inspect / trade / follow / promote / ...),
15+
-- any other player -> PLAYER (adds INVITE), anything else (NPC) ->
16+
-- RAID_TARGET_ICON. Vanilla's stock UI never wires a unit to the "RAID" menu
17+
-- (there are no clickable raid unit frames in 1.12), and PARTY carries the
18+
-- options players actually want on a grouped member, so grouped players (party
19+
-- or raid) use PARTY -- matching how pfUI drives its raid menu.
20+
21+
local dropdown;
22+
23+
local function EnsureDropdown()
24+
if not dropdown then
25+
dropdown = CreateFrame("Frame", "ClassicAPIUnitMenuDropDown", UIParent, "UIDropDownMenuTemplate");
26+
dropdown.displayMode = "MENU";
27+
end
28+
return dropdown;
29+
end
30+
31+
local function ResolveMenu(unit)
32+
if UnitIsUnit(unit, "player") then
33+
return "SELF";
34+
elseif UnitIsUnit(unit, "pet") then
35+
return "PET";
36+
elseif UnitIsPlayer(unit) then
37+
if UnitInParty(unit) or UnitInRaid(unit) then
38+
return "PARTY";
39+
end
40+
return "PLAYER";
41+
end
42+
return "RAID_TARGET_ICON";
43+
end
44+
45+
function ClassicAPI_ToggleUnitMenu(unit)
46+
if not unit or not UnitExists(unit) then
47+
return;
48+
end
49+
local which = ResolveMenu(unit);
50+
local name;
51+
if which == "RAID_TARGET_ICON" then
52+
name = RAID_TARGET_ICON;
53+
end
54+
local dd = EnsureDropdown();
55+
dd.initialize = function()
56+
UnitPopup_ShowMenu(dd, which, unit, name);
57+
end
58+
ToggleDropDownMenu(1, nil, dd, "cursor");
59+
end

docs/API.md

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3844,25 +3844,53 @@ may be any token the resolver understands — `"party1"`, `"target"`, `"focus"`,
38443844
`"nameplateN"`, or a raw GUID literal. Set `unit` to a non-string (e.g. `nil`)
38453845
to stop the binding.
38463846

3847-
**Click actions (`type1` / `type2`).** A `type` attribute makes clicking the
3848-
frame act on its `unit`:
3849-
3850-
```lua
3851-
f:SetAttribute("type1", "target") -- left-click targets the unit
3852-
f:SetAttribute("type2", "focus") -- right-click sets ClassicAPI focus to it
3853-
```
3854-
3855-
Left-click reads `type1`, right-click reads `type2`, and both fall back to a
3856-
plain `type` attribute. Supported verbs: `"target"`, `"assist"` (target the
3857-
unit's target), `"focus"`. Setting a `type*` attribute installs a **chained
3858-
`OnClick` on that frame only** (nothing global) — it reads the attributes at
3859-
click time and runs *after* any handler the frame already had. Because it chains
3860-
whatever `OnClick` is present when the `type` attribute is set, set `type*`
3861-
**after** the frame's own scripts (real addons configure attributes after
3862-
building the widget). The frame must be a **Button** registered for the click:
3863-
left is the Button default; right needs `RegisterForClicks("RightButtonUp")`,
3864-
which real unit frames already call. Other verbs (`togglemenu`, `spell`,
3865-
`macro`) aren't backported yet.
3847+
**Click actions (`type1` / `type2` / …).** A `type` attribute makes clicking the
3848+
frame perform one action on its `unit` — the retail secure-button model: exactly
3849+
**one verb per click**, resolved from the attributes.
3850+
3851+
```lua
3852+
f:SetAttribute("type1", "target") -- left-click targets the unit
3853+
f:SetAttribute("type2", "focus") -- right-click sets ClassicAPI focus to it
3854+
-- click-casting, expressed purely as attributes:
3855+
f:SetAttribute("shift-type1", "spell")
3856+
f:SetAttribute("shift-spell1", "Flash Heal") -- shift-left-click heals the unit
3857+
```
3858+
3859+
**Resolution.** The verb is read from `[prefix]type[suffix]`, where the prefix is
3860+
the held modifiers (`alt-`, `ctrl-`, `shift-`, in that order) and the suffix is
3861+
the button number (`1`=Left, `2`=Right, `3`=Middle, `4`/`5`=side). Precedence is
3862+
`prefix..type..suffix` → `type..suffix` → `type`, so `type1` applies under any
3863+
modifier unless a modifier-specific attribute (`shift-type1`) overrides it — and
3864+
`type` (no suffix) is the catch-all.
3865+
3866+
**Verbs:** `target`, `assist`, `focus`, `spell` (reads the modifier-qualified
3867+
`spell` attribute and casts it on the unit via the native
3868+
[`C_Spell.CastAtUnit`](#c_spellcastatunitspellidorname-unit) — the unit's GUID is fed straight to
3869+
the cast dispatcher, so there's no target juggling, and ground-target spells
3870+
land at the unit's feet), `macro` (takes the
3871+
`macrotext`/`macro` attribute and prefers an addon-provided `RunMacro` — e.g.
3872+
SuperCleveRoidMacros, which handles named macros and extended macro text —
3873+
falling back to running the text natively, line by line through the stock
3874+
`ChatEdit_ParseText`, when no `RunMacro` global is present), `stopcasting`, and `menu` /
3875+
`togglemenu` (pops the standard unit dropdown — whisper/inspect/trade/invite/…,
3876+
the same menu `PlayerFrame`/`TargetFrame`/`PartyMemberFrame` show — at the
3877+
cursor). `target` respects the engine's default-interaction precedence: with a
3878+
spell on the cursor it casts on the unit, with an item on the cursor it drops it
3879+
on the unit, instead of switching target.
3880+
3881+
**One verb per click.** Setting a `type*` attribute installs a **chained
3882+
`OnClick` on that frame only** (nothing global). When a verb resolves, our
3883+
handler *owns* the click and does **not** run the frame's previous `OnClick` —
3884+
so a configured `type1` never fires alongside the addon's own click handler.
3885+
Unconfigured clicks (no matching `type`, or an unrecognized verb) fall through
3886+
to the frame's own `OnClick`, so an addon can keep custom behavior there. The
3887+
frame must be a **Button** registered for the click
3888+
(left is the Button default; right needs `RegisterForClicks("RightButtonUp")`).
3889+
3890+
**Clobbering.** The handler self-heals: addons that re-`SetScript("OnClick", …)`
3891+
(pfUI on every raid relayout) replace our closure, so re-set a `type*` attribute
3892+
afterward to reinstall — re-wiring detects and skips its own closure, so it never
3893+
double-chains.
38663894

38673895
**`OnAttributeChanged` is not fired.** Retail fires this script from
38683896
`SetAttribute` (and `SetAttributeNoHandler` suppresses it). Making it a real

0 commit comments

Comments
 (0)