Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 167 additions & 51 deletions EllesmereUIRaidFrames/EUI_RaidFrames_ClickCast.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -249,20 +253,56 @@ 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()
local cc = GetClickCastDB()
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
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
12 changes: 3 additions & 9 deletions Locales/_keys.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -243,6 +242,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:
Expand Down Expand Up @@ -278,6 +278,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
Expand Down Expand Up @@ -318,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
Expand Down Expand Up @@ -585,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.
Expand Down