From 676a4909473435e1ea66b5e2a80e52727e33f486 Mon Sep 17 00:00:00 2001 From: Sebastian Tannert Date: Sat, 1 Aug 2026 12:45:44 +0200 Subject: [PATCH] feat(cdm): customize rotation assist highlight --- .../EUI_CooldownManager_Options.lua | 200 ++++++++++++++++++ .../EllesmereUICooldownManager.lua | 161 ++++++++++++-- 2 files changed, 349 insertions(+), 12 deletions(-) diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index 6e6f3703..1ae7f1ac 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -19294,6 +19294,206 @@ initFrame:SetScript("OnEvent", function(self) end end }); y = y - h + -- Rotation Assist styling is profile-wide (not tied to the selected + -- bar). Keeping it on this override-eligible page lets the existing + -- spec/conditional override system capture every scalar below. + -- Read and write the runtime addon's authoritative profile. The options + -- DB reference can lag behind a profile/override proxy swap, which made + -- the swatches display Class while the renderer still read Custom. + local function RotationBars() + local runtime = ns.ECME and ns.ECME.db and ns.ECME.db.profile + local fallback = DB() + return (runtime and runtime.cdmBars) or (fallback and fallback.cdmBars) + end + + local rotStyleRow + rotStyleRow, h = W:DualRow(parent, y, + { type="dropdown", text="Rotation Assist Style", + values={ + blizzard="Blizzard Default", solid="Solid Border", + pixel="Pixel Glow", shape="Shape Glow", + button="Action Button Glow", autocast="Auto-Cast Shine", + gcd="GCD", modern="Modern WoW Glow", classic="Classic WoW Glow", + }, + order={ "blizzard", "solid", "pixel", "shape", "button", "autocast", "gcd", "modern", "classic" }, + tooltip="Choose the profile-wide border or glow used for Blizzard's Assisted Combat suggestion.", + getValue=function() + local c = RotationBars(); return (c and c.rotationAssistStyle) or "blizzard" + end, + setValue=function(v) + local c = RotationBars() + if c then + c.rotationAssistStyle = v + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + EllesmereUI:RefreshPage() + end + end }, + { type="label", text="Rotation Assist Color" }); y = y - h + + do + local colorRgn = rotStyleRow._rightRegion + if colorRgn and EllesmereUI.BuildColorSwatch then + -- Keep mode selection separate from custom RGB changes. The shared + -- trio helper intentionally selects "custom" from its color setter, + -- but color-picker cancellation and panel teardown may replay that + -- setter. These independent swatches make an explicit custom-swatch + -- click the only path which can select custom mode. + colorRgn._noCapture = true + local function getMode() + local c = RotationBars() + return (c and c.rotationAssistColorMode) or "default" + end + local refreshRotColors + local function setMode(mode) + local c = RotationBars() + if not c or c.rotationAssistStyle == "blizzard" then return end + c.rotationAssistColorMode = mode + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + if refreshRotColors then refreshRotColors() end + if EllesmereUI._NotifySettingWrite then + EllesmereUI._NotifySettingWrite(colorRgn) + end + end + local custom, refreshCustom = EllesmereUI.BuildColorSwatch( + colorRgn, rotStyleRow:GetFrameLevel() + 3, + function() + local c = RotationBars() + return (c and c.rotationAssistColorR) or 1, + (c and c.rotationAssistColorG) or 0, + (c and c.rotationAssistColorB) or 0, 1 + end, + function(r, g, b) + local c = RotationBars() + if c then + c.rotationAssistColorR = r + c.rotationAssistColorG = g + c.rotationAssistColorB = b + end + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + end) + local default = EllesmereUI.BuildColorSwatch( + colorRgn, rotStyleRow:GetFrameLevel() + 3, + function() return 1.0, 0.788, 0.137, 1 end, + function() end) + local classSwatch = EllesmereUI.BuildColorSwatch( + colorRgn, rotStyleRow:GetFrameLevel() + 3, + function() + local cc = EllesmereUI.GetClassColor(EllesmereUI._playerClass) + return cc.r, cc.g, cc.b, 1 + end, + function() end) + + local customPickerClick = custom:GetScript("OnClick") + custom:SetScript("OnClick", function(self) + if getMode() ~= "custom" then + setMode("custom") + elseif customPickerClick then + customPickerClick(self) + end + end) + default:SetScript("OnClick", function() setMode("default") end) + classSwatch:SetScript("OnClick", function() setMode("class") end) + custom:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(custom, "Custom Color") + end) + default:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(default, "Default") + end) + classSwatch:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(classSwatch, "Class Colored") + end) + custom:SetScript("OnLeave", EllesmereUI.HideWidgetTooltip) + default:SetScript("OnLeave", EllesmereUI.HideWidgetTooltip) + classSwatch:SetScript("OnLeave", EllesmereUI.HideWidgetTooltip) + + refreshRotColors = function() + local c = RotationBars() + local disabled = not c or c.rotationAssistStyle == "blizzard" + local mode = getMode() + custom:SetAlpha(disabled and 0.15 or (mode == "custom" and 1 or 0.3)) + default:SetAlpha(disabled and 0.15 or (mode == "default" and 1 or 0.3)) + classSwatch:SetAlpha(disabled and 0.15 or (mode == "class" and 1 or 0.3)) + refreshCustom() + end + refreshRotColors() + if EllesmereUI.RegisterWidgetRefresh then + EllesmereUI.RegisterWidgetRefresh(refreshRotColors) + end + PP.Point(classSwatch, "RIGHT", colorRgn, "RIGHT", -20, 0) + PP.Point(custom, "RIGHT", classSwatch, "LEFT", -8, 0) + PP.Point(default, "RIGHT", custom, "LEFT", -8, 0) + colorRgn._noCapture = nil + colorRgn._captureCfg = { + type = "multi", text = "Rotation Assist Color", + accessors = { + { + type = "dropdown", text = "Rotation Assist Color Mode", + values = { default = "Default", custom = "Custom", class = "Class Color" }, + order = { "default", "custom", "class" }, + getValue = function() + local c = RotationBars() + return (c and c.rotationAssistColorMode) or "default" + end, + setValue = function(mode) + local c = RotationBars() + if c then c.rotationAssistColorMode = mode end + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + end, + }, + { + type = "colorpicker", text = "Rotation Assist Custom Color", + getValue = function() + local c = RotationBars() + return (c and c.rotationAssistColorR) or 1, + (c and c.rotationAssistColorG) or 0, + (c and c.rotationAssistColorB) or 0, 1 + end, + setValue = function(r, g, b) + local c = RotationBars() + if c then + c.rotationAssistColorR = r + c.rotationAssistColorG = g + c.rotationAssistColorB = b + end + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + end, + }, + }, + } + end + end + + _, h = W:DualRow(parent, y, + { type="slider", text="Rotation Assist Thickness", min=1, max=8, step=1, trackWidth=120, + tooltip="Thickness in physical pixels for Solid Border and Pixel Glow.", + disabled=function() + local c = RotationBars(); local s = c and c.rotationAssistStyle + return s ~= "solid" and s ~= "pixel" + end, + disabledTooltip="Thickness applies to Solid Border and Pixel Glow", + getValue=function() + local c = RotationBars(); return (c and c.rotationAssistThickness) or 3 + end, + setValue=function(v) + local c = RotationBars() + if c then c.rotationAssistThickness = v end + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + end }, + { type="slider", text="Rotation Assist Outset", min=0, max=12, step=1, trackWidth=120, + tooltip="How many pixels the custom effect extends beyond the icon.", + disabled=function() + local c = RotationBars(); return not c or c.rotationAssistStyle == "blizzard" + end, + disabledTooltip="Blizzard Default uses Blizzard's native size", + getValue=function() + local c = RotationBars(); return (c and c.rotationAssistOutset) or 1 + end, + setValue=function(v) + local c = RotationBars() + if c then c.rotationAssistOutset = v end + if ns.UpdateRotationHighlights then ns.UpdateRotationHighlights() end + end }); y = y - h + -- Hide Items if Missing | Mirror Key Presses -- CD/utility bars only. -- Buff bars host Hide Items if Missing in the tooltip row above (their -- copy of this row would be empty), and Mirror Key Presses is not for diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index a823ab10..6baa8add 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -539,6 +539,15 @@ local DEFAULTS = { hideBuffsWhenInactive = true, showInactiveBuffIcons = false, desaturateInactiveBuffs = true, + -- Rotation Assist uses Blizzard's native highlight until the user + -- explicitly opts into a custom style. + rotationAssistStyle = "blizzard", + rotationAssistColorMode = "default", + rotationAssistColorR = 1, + rotationAssistColorG = 0, + rotationAssistColorB = 0, + rotationAssistThickness = 3, + rotationAssistOutset = 1, -- The 3 default bars (match Blizzard CDM) bars = { { @@ -2445,7 +2454,8 @@ StartNativeGlow = function(overlay, style, cr, cg, cb, opts) local parent = overlay:GetParent() if not parent then return end - local pW, pH = parent:GetWidth(), parent:GetHeight() + local pW = (opts and opts.width) or parent:GetWidth() + local pH = (opts and opts.height) or parent:GetHeight() if pW < 5 then pW = 36 end if pH < 5 then pH = 36 end local noColor = (cr == nil) @@ -8991,21 +9001,36 @@ end ------------------------------------------------------------------------------- -- Rotation Helper Integration (Blizzard C_AssistedCombat) --- Highlights the currently suggested spell on its CDM icon using Blizzard's --- native ActionBarButtonAssistedCombatHighlightTemplate -- same shine as the --- stock action bars. Gated purely by Blizzard's "assistedCombatHighlight" --- CVar; we don't carry a second toggle of our own. +-- Highlights the currently suggested spell on its CDM icon. The default is +-- Blizzard's ActionBarButtonAssistedCombatHighlightTemplate; profile settings +-- can opt into a custom glow or a static solid border. ------------------------------------------------------------------------------- +do ns._rotationGlowedIcons = {} ns._rotationHookInstalled = false ns._rotationInCombat = false local ROT_GLOW_RATIO = 0.33 +local ROT_STYLE_TO_GLOW = { + pixel = 1, + shape = 2, + button = 3, + autocast = 4, + gcd = 5, + modern = 6, + classic = 7, +} +local ROT_RESTRICTED_STYLE = { [1] = 7, [2] = 6, [3] = 6, [4] = 6 } + +local function _rotConfig() + local p = ECME.db and ECME.db.profile + return p and p.cdmBars +end local function _rotCVarOn() -- User can force-hide via our own toggle, overriding Blizzard's CVar - local p = ECME.db and ECME.db.profile - if p and p.cdmBars and p.cdmBars.hideRotationHelper then return false end + local cfg = _rotConfig() + if cfg and cfg.hideRotationHelper then return false end return GetCVarBool and GetCVarBool("assistedCombatHighlight") end @@ -9025,17 +9050,128 @@ local function _rotCreateHighlight(icon) return hf end +local function _rotCreateCustom(icon) + local overlay = CreateFrame("Frame", nil, icon) + overlay:SetFrameLevel(icon:GetFrameLevel() + 15) + overlay:SetAlpha(0) + return overlay +end + +local function _rotSolidBorder(overlay, thickness, r, g, b) + local t = overlay._rotSolid + if not t then + t = {} + for i = 1, 4 do + t[i] = overlay:CreateTexture(nil, "OVERLAY", nil, 7) + end + t[1]:SetPoint("TOPLEFT"); t[1]:SetPoint("TOPRIGHT") + t[2]:SetPoint("BOTTOMLEFT"); t[2]:SetPoint("BOTTOMRIGHT") + t[3]:SetPoint("TOPLEFT"); t[3]:SetPoint("BOTTOMLEFT") + t[4]:SetPoint("TOPRIGHT"); t[4]:SetPoint("BOTTOMRIGHT") + overlay._rotSolid = t + end + local px = SnapForScale(thickness) + t[1]:SetHeight(px); t[2]:SetHeight(px) + t[3]:SetWidth(px); t[4]:SetWidth(px) + for i = 1, 4 do + t[i]:SetColorTexture(r, g, b, 1) + t[i]:Show() + end +end + +local function _rotHideSolid(overlay) + local t = overlay and overlay._rotSolid + if not t then return end + for i = 1, 4 do t[i]:Hide() end +end + +local function _rotResolveColor(cfg) + local mode = cfg and cfg.rotationAssistColorMode or "default" + if mode == "class" then + local c = EllesmereUI.GetClassColor and EllesmereUI.GetClassColor(EllesmereUI._playerClass) + if c then return c.r, c.g, c.b end + elseif mode == "custom" then + return cfg.rotationAssistColorR or 1, + cfg.rotationAssistColorG or 0, + cfg.rotationAssistColorB or 0 + end + return 1.0, 0.788, 0.137 +end + local function _rotHide(icon) local rfc = icon and _ecmeFC[icon] local hf = rfc and rfc.rotationHighlight - if not hf then return end - if hf.Flipbook and hf.Flipbook.Anim then hf.Flipbook.Anim:Stop() end - hf:Hide() + if hf then + if hf.Flipbook and hf.Flipbook.Anim then hf.Flipbook.Anim:Stop() end + hf:Hide() + end + local custom = rfc and rfc.rotationCustomHighlight + if custom then + StopNativeGlow(custom) + _rotHideSolid(custom) + custom._rotCfgKey = nil + end end local function _rotShow(icon) if not icon then return end local rfc = FC(icon) + local cfg = _rotConfig() + local style = cfg and cfg.rotationAssistStyle or "blizzard" + if style ~= "solid" and not ROT_STYLE_TO_GLOW[style] then style = "blizzard" end + + if style ~= "blizzard" then + local hf0 = rfc.rotationHighlight + if hf0 then + if hf0.Flipbook and hf0.Flipbook.Anim then hf0.Flipbook.Anim:Stop() end + hf0:Hide() + end + local overlay = rfc.rotationCustomHighlight + if not overlay then + overlay = _rotCreateCustom(icon) + rfc.rotationCustomHighlight = overlay + end + local outset = cfg.rotationAssistOutset or 1 + if outset < 0 then outset = 0 elseif outset > 12 then outset = 12 end + overlay:ClearAllPoints() + overlay:SetPoint("TOPLEFT", icon, "TOPLEFT", -outset, outset) + overlay:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", outset, -outset) + overlay:SetFrameLevel(icon:GetFrameLevel() + 15) + local thickness = cfg.rotationAssistThickness or 3 + if thickness < 1 then thickness = 1 elseif thickness > 8 then thickness = 8 end + local cr, cg, cb = _rotResolveColor(cfg) + local glowStyle = ROT_STYLE_TO_GLOW[style] + if glowStyle and rfc.isReplacementBuff then + glowStyle = ROT_RESTRICTED_STYLE[glowStyle] or glowStyle + end + local cfgKey = table.concat({ style, cr, cg, cb, thickness, outset, glowStyle or 0 }, ":") + if overlay._rotCfgKey ~= cfgKey or not overlay._glowActive then + overlay._rotCfgKey = cfgKey + if style == "solid" then + StopNativeGlow(overlay) + _rotSolidBorder(overlay, thickness, cr, cg, cb) + overlay._glowActive = true + overlay:SetAlpha(1) + else + _rotHideSolid(overlay) + local w = (icon:GetWidth() or 36) + outset * 2 + local h = (icon:GetHeight() or 36) + outset * 2 + StartNativeGlow(overlay, glowStyle, cr, cg, cb, { + th = thickness, + width = w, + height = h, + }) + end + end + return + end + + local custom = rfc.rotationCustomHighlight + if custom then + StopNativeGlow(custom) + _rotHideSolid(custom) + custom._rotCfgKey = nil + end local hf = rfc.rotationHighlight if not hf then hf = _rotCreateHighlight(icon) @@ -9137,7 +9273,7 @@ local function _rotSyncCombat() end ns._syncRotationCombatState = _rotSyncCombat -local function InstallRotationHook() +function ns.InstallRotationHook() if ns._rotationHookInstalled then return end ns._rotationHookInstalled = true @@ -9166,6 +9302,7 @@ local function InstallRotationHook() UpdateRotationHighlights() end +end -- Show Item Count "Out of Combat" mode: re-run the icon restyle for bars -- using it whenever combat starts or ends (the gate inside the restyle reads @@ -9490,7 +9627,7 @@ eventFrame:SetScript("OnEvent", function(_, event, unit, updateInfo, arg3) end -- Install rotation helper hook after CDM frames have been built C_Timer.After(1, function() - InstallRotationHook() + ns.InstallRotationHook() end) -- Safety: re-apply visibility after loading screen settles. -- Two passes to catch both fast and late viewer pool rebuilds.