From e0110805c7a561c4fd136de39d352d4413e1fad9 Mon Sep 17 00:00:00 2001 From: James Gregory Date: Tue, 4 Aug 2026 08:54:03 +1000 Subject: [PATCH 1/2] feat: add content type trigger to hovercast. add option for both units and frames --- .../EUI_RaidFrames_ClickCast.lua | 218 ++++++++++++++---- Locales/_keys.txt | 2 + 2 files changed, 169 insertions(+), 51 deletions(-) diff --git a/EllesmereUIRaidFrames/EUI_RaidFrames_ClickCast.lua b/EllesmereUIRaidFrames/EUI_RaidFrames_ClickCast.lua index 96ab6b1f..38cc41db 100644 --- a/EllesmereUIRaidFrames/EUI_RaidFrames_ClickCast.lua +++ b/EllesmereUIRaidFrames/EUI_RaidFrames_ClickCast.lua @@ -25,6 +25,9 @@ local floor = math.floor local max = math.max local CreateFrame = CreateFrame local InCombatLockdown = InCombatLockdown +local IsInGroup = IsInGroup +local IsInRaid = IsInRaid +local IsInInstance = IsInInstance local IsShiftKeyDown = IsShiftKeyDown local IsControlKeyDown = IsControlKeyDown local IsAltKeyDown = IsAltKeyDown @@ -180,6 +183,7 @@ local originalTargetAttrs = setmetatable({}, { __mode = "k" }) local regQueue = {} local unregQueue = {} local pendingApply = false +local lastRosterCtx = nil -- content gate: last context a roster update saw local ccInitialized = false local ccEventFrame = nil local lastBindingCount = 0 @@ -249,6 +253,42 @@ local function GetGlobalBindings() return cc and cc.globals or {} end +-- Content gate. binding.groupCtx is the set of contexts a binding is active in. +-- In a context that is switched off the binding is never applied so the key falls +-- through to whatever the player normally has bound. +local CC_CTX_ORDER = { "solo", "party", "raid", "pvp" } + +local function CtxEnabled(binding, ctx) + local set = binding.groupCtx + if not set then return true end + return set[ctx] == true +end + +-- The context the player is in right now. +local function CurrentCtx() + local _, instType = IsInInstance() + if instType == "pvp" or instType == "arena" then return "pvp" end + if IsInRaid() then return "raid" end + if IsInGroup() then return "party" end + return "solo" +end + +-- True when a binding's content gate matches the context the player is in now. +local function MatchesGroupCtx(binding) + return CtxEnabled(binding, CurrentCtx()) +end + +-- Hovercast mode. binding.hovercast is: +-- false / nil -> frame clicks only (attributes live on the unit frames) +-- true -> the global @mouseover override button only +-- "both" -> applied through BOTH paths +local function IsHoverBinding(binding) + return binding.hovercast and true or false +end +local function IsFrameBinding(binding) + return (not binding.hovercast) or binding.hovercast == "both" +end + -- Merge globals + current spec. Spec overrides globals on key conflict. -- Only includes enabled bindings. Respects master enable toggle. local function GetActiveBindings() @@ -256,13 +296,13 @@ local function GetActiveBindings() if not cc or not cc.enabled then return {} end local result, usedKeys = {}, {} for _, b in ipairs(GetSpecBindings()) do - if b.enabled ~= false and b.key then + if b.enabled ~= false and b.key and MatchesGroupCtx(b) then result[#result + 1] = b usedKeys[b.key] = true end end for _, b in ipairs(cc.globals) do - if b.enabled ~= false and b.key and not usedKeys[b.key] then + if b.enabled ~= false and b.key and not usedKeys[b.key] and MatchesGroupCtx(b) then result[#result + 1] = b end end @@ -989,7 +1029,7 @@ local function GenerateKeyBindSnippets(bindings) local enter, leave, selfClear = {}, {}, {} local kbBindings = {} for i, b in ipairs(bindings) do - if not b.hovercast then + if IsFrameBinding(b) then local parsed = ParseKeyString(b.key) if not parsed.isMouseButton or not parsed.buttonNum or parsed.buttonNum > 5 then kbBindings[#kbBindings + 1] = { binding = b, index = i, parsed = parsed } @@ -1070,7 +1110,7 @@ end local function NeutralizeDefaultClicks(frame, bindings) local b1, b2 = false, false for _, b in ipairs(bindings) do - if not b.hovercast and b.key then + if IsFrameBinding(b) and b.key then local parsed = ParseKeyString(b.key) if parsed.isMouseButton and parsed.modifiers == "" then if parsed.buttonNum == 1 then b1 = true @@ -1141,7 +1181,7 @@ local function DoRegisterFrame(frame) -- Apply current bindings to this frame immediately local bindings = GetActiveBindings() for i, b in ipairs(bindings) do - if not b.hovercast and b.key then + if IsFrameBinding(b) and b.key then local parsed = ParseKeyString(b.key) local aType, spellName, macrotext = ResolveBinding(b) if aType then @@ -1168,7 +1208,7 @@ local function DoUnregisterFrame(frame) -- Clear all click-cast attributes from this frame local bindings = GetActiveBindings() for i, b in ipairs(bindings) do - if not b.hovercast and b.key then + if IsFrameBinding(b) and b.key then local parsed = ParseKeyString(b.key) if parsed.isMouseButton and parsed.buttonNum and parsed.buttonNum <= 5 then ClearClickAttr(frame, parsed) @@ -1338,10 +1378,13 @@ function ns.CC_ApplyBindings() -- Split into frame-based and hovercast local frameBindings = {} local hoverBindings = {} + -- A "both" binding lands in BOTH lists: frame attributes for clicks on the + -- frames, plus the hover override for nameplates / world units. for i, b in ipairs(bindings) do - if b.hovercast then + if IsHoverBinding(b) then hoverBindings[#hoverBindings + 1] = { b = b, idx = i } - else + end + if IsFrameBinding(b) then frameBindings[#frameBindings + 1] = { b = b, idx = i } end end @@ -1352,7 +1395,7 @@ function ns.CC_ApplyBindings() -- Clear old frame attributes for frame in pairs(registeredFrames) do for _, pb in ipairs(prevBindings) do - if not pb.b.hovercast then + if IsFrameBinding(pb.b) then local parsed = ParseKeyString(pb.b.key) if parsed.isMouseButton and parsed.buttonNum and parsed.buttonNum <= 5 then ClearClickAttr(frame, parsed) @@ -1764,6 +1807,15 @@ local function OnCCEvent(self, event) if pendingApply then pendingApply = false; ns.CC_ApplyBindings() end elseif event == "PLAYER_SPECIALIZATION_CHANGED" then if not InCombatLockdown() then ns.CC_ApplyBindings() else pendingApply = true end + elseif event == "GROUP_ROSTER_UPDATE" then + -- Solo <-> party <-> raid transitions change which bindings are active. + -- GROUP_ROSTER_UPDATE fires every join, leave, promote and zone-in, so + -- only act when the context changed. + local ctx = CurrentCtx() + if ctx ~= lastRosterCtx then + lastRosterCtx = ctx + if not InCombatLockdown() then ns.CC_ApplyBindings() else pendingApply = true end + end elseif event == "PLAYER_ENTERING_WORLD" then -- Reapply bindings after zone/loading screen to clear any stuck -- frame-based bindings (OnLeave may not fire during transitions). @@ -1820,6 +1872,7 @@ function ns.CC_Init() ccEventFrame = (ns.TakeShell and ns.TakeShell()) or CreateFrame("Frame") ccEventFrame:RegisterEvent("PLAYER_REGEN_ENABLED") ccEventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") + ccEventFrame:RegisterEvent("GROUP_ROSTER_UPDATE") ccEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") ccEventFrame:SetScript("OnEvent", OnCCEvent) @@ -3515,65 +3568,128 @@ function ns.CC_BuildPage(pageName, parent, yOffset) centerY = centerY - ROW_H end + -- Content gate row: which content this binding is active in. + -- Outside the chosen contexts the binding is simply not applied, so + -- its key keeps doing whatever the player normally has bound. + do + local row = MakeRow(centerY) + RowLabel(row, "Active In") + -- Same checkbox dropdown the buff manager's filter pickers use, so + -- any combination of Solo / Party / Raid can be ticked. It renders + -- the summary itself ("All" when everything is on, "None" when + -- nothing is). groupCtx stays nil while all three are on -- that is + -- the default, so untouched bindings store nothing. + local ctxItems = { + { key = "solo", label = "Solo", + tooltip = "Active while you are not in a group." }, + { key = "party", label = "Party", + tooltip = "Active in a PvE party." }, + { key = "raid", label = "Raid", + tooltip = "Active in a PvE raid." }, + { key = "pvp", label = "PvP", + tooltip = "Active in battlegrounds and arenas." }, + } + local cbDD = EllesmereUI.BuildVisOptsCBDropdown( + row, 160, row:GetFrameLevel() + 2, + ctxItems, + function(key) return CtxEnabled(selectedBinding, key) end, + function(key, v) + -- nil means all-on, so materialize the full set before the + -- first tick turns one context off; collapse back to nil + -- once everything is on again. + local set = selectedBinding.groupCtx + if not set then + set = {} + for _, c in ipairs(CC_CTX_ORDER) do set[c] = true end + selectedBinding.groupCtx = set + end + set[key] = v and true or false + local n = 0 + for _, c in ipairs(CC_CTX_ORDER) do + if set[c] == true then n = n + 1 end + end + if n == #CC_CTX_ORDER then selectedBinding.groupCtx = nil end + ns.CC_ApplyBindings() + end, + nil, #CC_CTX_ORDER) + PP.Point(cbDD, "RIGHT", row, "RIGHT", -SIDE_PAD, 0) + centerY = centerY - ROW_H + end + if hasAdvancedOpts then - -- Hovercast row (disabled for bare left/right click) + -- Hovercast row: a dropdown rather than a third toggle so the row + -- count (and the page height) is unchanged -- the center column has + -- no spare row. + -- Hovercast is unavailable for bare left/right click, so those two + -- entries are shown disabled with the reason as a tooltip. do local row = MakeRow(centerY) local isBareMouseBtn = selectedBinding.key == "BUTTON1" or selectedBinding.key == "BUTTON2" - RowLabel(row, "Only Cast on Actual Units (Not Frames)") - if isBareMouseBtn then - -- Force off and show disabled state - if selectedBinding.hovercast then - selectedBinding.hovercast = false - ns.CC_ApplyBindings() - end - local pill, _ = RowToggle(row, - function() return false end, - function() end) - pill:SetAlpha(0.35) - pill:EnableMouse(false) - if EllesmereUI.ShowWidgetTooltip then - row:SetScript("OnEnter", function(self) - EllesmereUI.ShowWidgetTooltip(self, EllesmereUI.L("Hovercast is not available for unmodified left/right click")) - end) - row:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) - end - else - RowToggle(row, - function() return selectedBinding.hovercast end, - function(v) - selectedBinding.hovercast = v - ns.CC_ApplyBindings() - RebuildPage() - end) + RowLabel(row, "Cast On") + if isBareMouseBtn and selectedBinding.hovercast then + selectedBinding.hovercast = false + ns.CC_ApplyBindings() end + local hcValues = { + frames = "Frames", + units = "Mouseover", + both = "Frames and Mouseover", + } + local hcOrder = { "frames", "units", "both" } + local ddCtrl = EllesmereUI.BuildDropdownControl( + row, 160, row:GetFrameLevel() + 2, + hcValues, hcOrder, + function() + if selectedBinding.hovercast == "both" then return "both" end + return selectedBinding.hovercast and "units" or "frames" + end, + function(v) + selectedBinding.hovercast = (v == "both" and "both") + or (v == "units" and true) or false + ns.CC_ApplyBindings() + RebuildPage() + end, + function(key) + if isBareMouseBtn and key ~= "frames" then + return EllesmereUI.L("Hovercast is not available for unmodified left/right click") + end + return false + end) + PP.Point(ddCtrl, "RIGHT", row, "RIGHT", -SIDE_PAD, 0) centerY = centerY - ROW_H end - -- Hovercast targets (only when hovercast is on) + -- Hovercast targets (only when the mouseover path is involved) if selectedBinding.hovercast then - -- Friendly row + -- Friendly + Enemy share ONE row. They are a single "which units" + -- choice, and the center column is a fixed-height, non-scrolling + -- panel that was already close to full: a row each would push the + -- last row past the bottom of the scroll viewport, where it is + -- clipped and invisible. do local row = MakeRow(centerY) - RowLabel(row, " Friendly Units") - RowToggle(row, - function() return selectedBinding.hoverFriendly ~= false end, + RowLabel(row, " Unit Types") + local ePill = RowToggle(row, + function() return selectedBinding.hoverEnemy == true end, function(v) - selectedBinding.hoverFriendly = v + selectedBinding.hoverEnemy = v ns.CC_ApplyBindings() end) - centerY = centerY - ROW_H - end - -- Enemy row - do - local row = MakeRow(centerY) - RowLabel(row, " Enemy Units") - RowToggle(row, - function() return selectedBinding.hoverEnemy == true end, + local eLbl = MakeFont(row, 13, 1, 1, 1, 0.8) + eLbl:SetPoint("RIGHT", ePill, "LEFT", -8, 0) + eLbl:SetText(EllesmereUI.L("Enemy")) + + local fPill = RowToggle(row, + function() return selectedBinding.hoverFriendly ~= false end, function(v) - selectedBinding.hoverEnemy = v + selectedBinding.hoverFriendly = v ns.CC_ApplyBindings() end) + fPill:ClearAllPoints() + PP.Point(fPill, "RIGHT", eLbl, "LEFT", -18, 0) + local fLbl = MakeFont(row, 13, 1, 1, 1, 0.8) + fLbl:SetPoint("RIGHT", fPill, "LEFT", -8, 0) + fLbl:SetText(EllesmereUI.L("Friendly")) centerY = centerY - ROW_H end end diff --git a/Locales/_keys.txt b/Locales/_keys.txt index 1a5ad812..8a8a0615 100644 --- a/Locales/_keys.txt +++ b/Locales/_keys.txt @@ -243,6 +243,7 @@ Enable Enable hash lines first Enable/Disable Bar per Form Enable/Disable per Form +Enemy Enemy Name Text Enhance 5 Bar minimum is %d (if you want less just change 5 bar color) Enter a name for the new preset: @@ -278,6 +279,7 @@ Font changed. A UI reload is needed to apply the new font. Food For Icons: Left click to edit group, Right click to custom size individual For player frame, this provides a simple, mini castbar below player frame. To edit the main player cast bar, %sclick here|r +Friendly From Full Preview GLOBAL From 048e9418fc835e5cbded6981bb071b082ca797f8 Mon Sep 17 00:00:00 2001 From: James Gregory Date: Wed, 5 Aug 2026 11:14:34 +1000 Subject: [PATCH 2/2] update keys --- Locales/_keys.txt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Locales/_keys.txt b/Locales/_keys.txt index 8a8a0615..2e8bf755 100644 --- a/Locales/_keys.txt +++ b/Locales/_keys.txt @@ -1,6 +1,6 @@ # Auto-generated by .tools/extract-locale-keys.sh -- do not edit by hand. # Canonical list of translatable English keys passed as string literals -# (666 unique). Regenerate after wrapping new strings. Keys passed as +# (660 unique). Regenerate after wrapping new strings. Keys passed as # variables are not listed here -- use the in-game /euiloc harvester for # the complete runtime set. (Raid) @@ -98,7 +98,6 @@ Anchor Anchored Apply Apply All Settings To -Apply the sharer's Blizz UI Enhanced Window Skins and Tooltips, Menus & Popups settings. These are account-wide and will overwrite yours across ALL profiles. Off = keep your own. Apply to Bar Apply to Bar (All Specs) Apply to This Spell @@ -320,17 +319,12 @@ Import Full Account Data Import Profile Import Selected Addons Import a profile from string. -Import the anchor & size-match relationships from this profile. Off = keep your own layout; only the selected modules' own positions/settings come in. -Import the sharer's complete override setup: spec and conditional override values, groups, their custom Unlock Mode layouts, and Buff Manager overrides. This replaces ALL of your own overrides. Off = keep yours untouched. Import will include %1$s of %2$s addons. Importing %1$s In-game countdown unavailable in combat; the boss mod pull timer still started. Include Include CDM Spell Layout? -Include Overrides Include This Spell -Include Window Skins -Include layout Include your Cooldown Manager spell layout (which spells sit on which bars) plus all per-spell settings for any specs you choose. Include: Independent @@ -587,8 +581,6 @@ This is where you can control the settings of Unlock Mode.\n\nElements can be re This option requires %1$s to be %2$s This option requires Subtitle Text to include the Guild Name This preset -This profile string does not carry any Window & Tooltip Skins settings. -This profile string does not carry any override data. This profile was made at %1$d%% UI scale; yours is %2$d%%. Change your UI scale to match the imported profile? This will show all profiles at this scale as UI Scale is not a per-profile setting, but can be changed at any time back to your original value. This setting's active Apply to Bar (All Specs) value will be replaced. This setting's active Apply to Bar value will be replaced.