From b5da3017f2171480d1e263ba6b9562eb3b8229dd Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Mon, 30 Mar 2026 13:39:16 -0400 Subject: [PATCH 1/7] feat: keybind mode overlay + spec-based keybind profile switching - Add EUI_KeybindMode.lua: full keybind mode overlay with ToggleKeybindMode, SnapshotKeybinds, and RestoreKeybinds - Add /eui keybind (or /eui kb) slash command to toggle keybind mode - Add SwitchKeybindProfile for automatic keybind switching on spec change - Add EUI_KeybindDebugLog SavedVariable for debug logging - Hide profile rename button in options (name set at creation) Co-Authored-By: Claude Opus 4.6 (1M context) --- EUI_KeybindMode.lua | 882 +++++++++++++++++++++++++++++++++++++++ EUI__General_Options.lua | 21 +- EllesmereUI.lua | 7 +- EllesmereUI.toc | 1 + EllesmereUI_Profiles.lua | 60 +++ 5 files changed, 955 insertions(+), 16 deletions(-) create mode 100644 EUI_KeybindMode.lua diff --git a/EUI_KeybindMode.lua b/EUI_KeybindMode.lua new file mode 100644 index 00000000..6f8682fb --- /dev/null +++ b/EUI_KeybindMode.lua @@ -0,0 +1,882 @@ +-------------------------------------------------------------------------------- +-- EUI_KeybindMode.lua +-- Fast hover-to-bind keybinding mode for EllesmereUI action bars +-------------------------------------------------------------------------------- +local _, ns = ... + +local EllesmereUI = EllesmereUI +local PP = nil -- resolved on first use (EllesmereUI.PP) + +-- Lua APIs +local pairs, ipairs, type = pairs, ipairs, type +local CreateFrame = CreateFrame +local InCombatLockdown = InCombatLockdown +local SetBinding, GetBindingKey, SaveBindings = SetBinding, GetBindingKey, SaveBindings +local GetBindingAction = GetBindingAction + +-- Debug log (saved to EUI_KeybindDebugLog saved variable) +EUI_KeybindDebugLog = EUI_KeybindDebugLog or {} +local function DebugLog(msg) + EUI_KeybindDebugLog[#EUI_KeybindDebugLog + 1] = date("%H:%M:%S") .. " " .. msg + print("|cff0cd29f[KB]|r " .. msg) +end + +-- State +local isActive = false +local keybindFrame = nil -- main overlay frame +local buttonOverlays = {} -- array of overlay frames +local filterPills = {} -- bar filter pill frames +local dimmedBars = {} -- [barKey] = true if dimmed +local hoveredOverlay = nil -- currently hovered button overlay +local pendingConflict = nil -- { overlay, keyCombo, existingAction } awaiting confirm + +-- Bar config: maps barKey to WoW binding command prefix +local BINDING_MAP = { + MainBar = "ACTIONBUTTON", + Bar2 = "MULTIACTIONBAR1BUTTON", + Bar3 = "MULTIACTIONBAR2BUTTON", + Bar4 = "MULTIACTIONBAR3BUTTON", + Bar5 = "MULTIACTIONBAR4BUTTON", + Bar6 = "MULTIACTIONBAR5BUTTON", + Bar7 = "MULTIACTIONBAR6BUTTON", + Bar8 = "MULTIACTIONBAR7BUTTON", + StanceBar = "SHAPESHIFTBUTTON", + PetBar = "BONUSACTIONBUTTON", +} + +-- Ordered bar keys for filter pills +local BAR_ORDER = { + "MainBar", "Bar2", "Bar3", "Bar4", "Bar5", + "Bar6", "Bar7", "Bar8", "StanceBar", "PetBar", +} + +-- Display names for filter pills +local BAR_LABELS = { + MainBar = "Main Bar", + Bar2 = "Bar 2", + Bar3 = "Bar 3", + Bar4 = "Bar 4", + Bar5 = "Bar 5", + Bar6 = "Bar 6", + Bar7 = "Bar 7", + Bar8 = "Bar 8", + StanceBar = "Stance", + PetBar = "Pet", +} + +-- Colors +local ACCENT_R, ACCENT_G, ACCENT_B = 0.047, 0.824, 0.624 +local WARN_R, WARN_G, WARN_B = 1.0, 0.706, 0.235 +local FONT_PATH = "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" + +-------------------------------------------------------------------------------- +-- Keybind Profile Commands +-- All action bar binding commands we track for profile snapshots +-------------------------------------------------------------------------------- +local TRACKED_COMMANDS = {} +do + local barCounts = { + MainBar = 12, Bar2 = 12, Bar3 = 12, Bar4 = 12, + Bar5 = 12, Bar6 = 12, Bar7 = 12, Bar8 = 12, + StanceBar = 10, PetBar = 10, + } + for _, barKey in ipairs(BAR_ORDER) do + local prefix = BINDING_MAP[barKey] + local count = barCounts[barKey] or 12 + for i = 1, count do + TRACKED_COMMANDS[#TRACKED_COMMANDS + 1] = prefix .. i + end + end +end + +-------------------------------------------------------------------------------- +-- Keybind Profiles — Snapshot / Restore +-------------------------------------------------------------------------------- + +function EllesmereUI:SnapshotKeybinds(profileName) + if not profileName or profileName == "" then return end + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.keybindProfiles then EllesmereUIDB.keybindProfiles = {} end + + local binds = {} + for _, cmd in ipairs(TRACKED_COMMANDS) do + local key1, key2 = GetBindingKey(cmd) + if key1 then + binds[cmd] = key1 + if key2 then + binds[cmd .. "_2"] = key2 + end + end + end + EllesmereUIDB.keybindProfiles[profileName] = { binds = binds } +end + +function EllesmereUI:RestoreKeybinds(profileName) + if not profileName or profileName == "" then return end + if not EllesmereUIDB or not EllesmereUIDB.keybindProfiles then return end + local profile = EllesmereUIDB.keybindProfiles[profileName] + if not profile or not profile.binds then return end + + for _, cmd in ipairs(TRACKED_COMMANDS) do + local key1, key2 = GetBindingKey(cmd) + if key1 then SetBinding(key1, nil) end + if key2 then SetBinding(key2, nil) end + end + + for cmd, key in pairs(profile.binds) do + if not cmd:match("_2$") then + SetBinding(key, cmd) + local key2 = profile.binds[cmd .. "_2"] + if key2 then + SetBinding(key2, cmd) + end + end + end + + SaveBindings(2) +end + +function EllesmereUI:GetCurrentSpecKeybindProfile() + local specIdx = GetSpecialization and GetSpecialization() + if not specIdx or specIdx < 1 then return nil end + local specID = GetSpecializationInfo(specIdx) + if not specID then return nil end + + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end + + local profileName = EllesmereUIDB.specKeybindProfiles[specID] + return profileName, specID +end + +function EllesmereUI:AssignKeybindProfileToSpec(profileName, specID) + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end + EllesmereUIDB.specKeybindProfiles[specID] = profileName +end + +-------------------------------------------------------------------------------- +-- Forward declarations +-------------------------------------------------------------------------------- +local OpenKeybindMode, CloseKeybindMode + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function EllesmereUI:ToggleKeybindMode() + if isActive then + CloseKeybindMode() + else + OpenKeybindMode() + end +end + +-------------------------------------------------------------------------------- +-- Slash command +-------------------------------------------------------------------------------- +SLASH_EUIKEYBIND1 = "/euikeybind" +SlashCmdList.EUIKEYBIND = function() + if InCombatLockdown() then + print("|cffff6060[EllesmereUI]|r Cannot enter Keybind Mode during combat.") + return + end + EllesmereUI:ToggleKeybindMode() +end + +-------------------------------------------------------------------------------- +-- Overlay Frame +-------------------------------------------------------------------------------- +local function CreateOverlayFrame() + if keybindFrame then return end + PP = PP or EllesmereUI.PP + + keybindFrame = CreateFrame("Frame", "EllesmereKeybindMode", UIParent) + keybindFrame:SetFrameStrata("FULLSCREEN_DIALOG") + keybindFrame:SetAllPoints(UIParent) + keybindFrame:EnableMouse(false) + keybindFrame:SetAlpha(0) + + local overlay = keybindFrame:CreateTexture(nil, "BACKGROUND") + overlay:SetAllPoints() + overlay:SetColorTexture(0.02, 0.03, 0.04, 0.80) + keybindFrame._overlay = overlay +end + +local function DestroyOverlayFrame() + if not keybindFrame then return end + keybindFrame:Hide() + keybindFrame:SetParent(nil) + keybindFrame = nil +end + +-------------------------------------------------------------------------------- +-- Fade Animations +-------------------------------------------------------------------------------- +local function FadeIn(onComplete) + if not keybindFrame then return end + keybindFrame:Show() + local elapsed = 0 + local duration = 0.3 + keybindFrame:SetScript("OnUpdate", function(self, dt) + elapsed = elapsed + dt + if elapsed >= duration then + self:SetAlpha(1) + self:SetScript("OnUpdate", nil) + if onComplete then onComplete() end + return + end + self:SetAlpha(elapsed / duration) + end) +end + +local function FadeOut(onComplete) + if not keybindFrame then return end + local elapsed = 0 + local duration = 0.2 + keybindFrame:SetScript("OnUpdate", function(self, dt) + elapsed = elapsed + dt + if elapsed >= duration then + self:SetAlpha(0) + self:SetScript("OnUpdate", nil) + if onComplete then onComplete() end + return + end + self:SetAlpha(1 - (elapsed / duration)) + end) +end + +-------------------------------------------------------------------------------- +-- HUD Bar +-------------------------------------------------------------------------------- +local hudFrame = nil + +local function CreateHUD() + if hudFrame then hudFrame:Show(); return end + + hudFrame = CreateFrame("Frame", nil, keybindFrame) + hudFrame:SetSize(620, 36) + hudFrame:SetPoint("TOP", UIParent, "TOP", 0, -20) + hudFrame:SetFrameLevel(keybindFrame:GetFrameLevel() + 10) + + local bg = hudFrame:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.047, 0.071, 0.094, 0.95) + + EllesmereUI.MakeBorder(hudFrame, ACCENT_R, ACCENT_G, ACCENT_B, 0.4) + + local label = hudFrame:CreateFontString(nil, "OVERLAY") + label:SetFont(FONT_PATH, 13, "") + label:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + label:SetPoint("LEFT", hudFrame, "LEFT", 16, 0) + label:SetText("KEYBIND MODE") + + local sep1 = hudFrame:CreateFontString(nil, "OVERLAY") + sep1:SetFont(FONT_PATH, 11, "") + sep1:SetTextColor(1, 1, 1, 0.2) + sep1:SetPoint("LEFT", label, "RIGHT", 12, 0) + sep1:SetText("|") + + local instr = hudFrame:CreateFontString(nil, "OVERLAY") + instr:SetFont(FONT_PATH, 11, "") + instr:SetTextColor(1, 1, 1, 0.5) + instr:SetPoint("LEFT", sep1, "RIGHT", 12, 0) + instr:SetText("Hover + press key to bind | ESC to unbind | ESC (no hover) to exit") + + local closeBtn = CreateFrame("Button", nil, hudFrame) + closeBtn:SetSize(20, 20) + closeBtn:SetPoint("RIGHT", hudFrame, "RIGHT", -10, 0) + closeBtn:SetFrameLevel(hudFrame:GetFrameLevel() + 2) + + local closeTex = closeBtn:CreateFontString(nil, "OVERLAY") + closeTex:SetFont(FONT_PATH, 14, "") + closeTex:SetTextColor(1, 1, 1, 0.4) + closeTex:SetAllPoints() + closeTex:SetText("X") + + closeBtn:SetScript("OnEnter", function() closeTex:SetTextColor(1, 1, 1, 0.8) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetTextColor(1, 1, 1, 0.4) end) + closeBtn:SetScript("OnClick", function() CloseKeybindMode() end) + + -- Profile name indicator + local profLabel = hudFrame:CreateFontString(nil, "OVERLAY") + profLabel:SetFont(FONT_PATH, 11, "") + profLabel:SetPoint("RIGHT", closeBtn, "LEFT", -12, 0) + + local profileName = EllesmereUI:GetCurrentSpecKeybindProfile() + if profileName then + profLabel:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.8) + profLabel:SetText(profileName) + else + local specIdx = GetSpecialization and GetSpecialization() + if specIdx and specIdx > 0 then + local _, specName = GetSpecializationInfo(specIdx) + profLabel:SetTextColor(1, 1, 1, 0.4) + profLabel:SetText(specName or "No Profile") + end + end +end + +local function DestroyHUD() + if hudFrame then + hudFrame:Hide() + hudFrame:SetParent(nil) + hudFrame = nil + end +end + +-------------------------------------------------------------------------------- +-- Filter Pills +-------------------------------------------------------------------------------- +local pillContainer = nil + +local function UpdatePillVisual(pill, active) + if active then + pill._bg:SetColorTexture(ACCENT_R, ACCENT_G, ACCENT_B, 0.15) + pill._label:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + pill._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.5) + -- Show glow + if pill._glow then pill._glow:Show() end + -- Start subtle pulse + if not pill._pulsing then + pill._pulsing = true + pill._pulseElapsed = 0 + pill:SetScript("OnUpdate", function(self, dt) + self._pulseElapsed = (self._pulseElapsed or 0) + dt + -- Gentle sine wave: border alpha oscillates 0.35 to 0.65 + local t = math.sin(self._pulseElapsed * 2.2) * 0.5 + 0.5 + local alpha = 0.35 + t * 0.30 + self._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, alpha) + if self._glow then + self._glow:SetAlpha(0.15 + t * 0.15) + end + end) + end + else + pill._bg:SetColorTexture(1, 1, 1, 0.03) + pill._label:SetTextColor(1, 1, 1, 0.3) + pill._border:SetColor(1, 1, 1, 0.12) + -- Hide glow, stop pulse + if pill._glow then pill._glow:Hide() end + pill._pulsing = false + pill:SetScript("OnUpdate", nil) + end +end + +local function UpdateBarDimming(barKey) + local isDimmed = dimmedBars[barKey] + for _, ov in ipairs(buttonOverlays) do + if ov._barKey == barKey then + if isDimmed then + ov:SetAlpha(0.25) + ov:EnableMouse(false) + else + ov:SetAlpha(1) + ov:EnableMouse(true) + end + end + end +end + +local function CreateFilterPills() + if pillContainer then pillContainer:Show(); return end + + pillContainer = CreateFrame("Frame", nil, keybindFrame) + pillContainer:SetSize(1, 28) + pillContainer:SetPoint("TOP", hudFrame, "BOTTOM", 0, -10) + pillContainer:SetFrameLevel(keybindFrame:GetFrameLevel() + 10) + + local EAB = EllesmereUI.Lite.GetAddon("EllesmereUIActionBars", true) + + local pills = {} + local totalWidth = 0 + + for _, barKey in ipairs(BAR_ORDER) do + local barFrame + if EAB and EAB._barFrames then + barFrame = EAB._barFrames[barKey] + end + if barFrame and barFrame:IsShown() then + local pill = CreateFrame("Button", nil, pillContainer) + pill:SetSize(70, 24) + pill:SetFrameLevel(pillContainer:GetFrameLevel() + 1) + pill._barKey = barKey + + local bg = pill:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + pill._bg = bg + + local label = pill:CreateFontString(nil, "OVERLAY") + label:SetFont(FONT_PATH, 11, "") + label:SetPoint("CENTER") + label:SetText(BAR_LABELS[barKey] or barKey) + pill._label = label + + pill._border = EllesmereUI.MakeBorder(pill, ACCENT_R, ACCENT_G, ACCENT_B, 0.5) + + -- Soft glow behind pill (slightly larger, blurred look) + local glow = pill:CreateTexture(nil, "BACKGROUND", nil, -1) + glow:SetPoint("TOPLEFT", pill, "TOPLEFT", -4, 4) + glow:SetPoint("BOTTOMRIGHT", pill, "BOTTOMRIGHT", 4, -4) + glow:SetColorTexture(ACCENT_R, ACCENT_G, ACCENT_B, 0.20) + pill._glow = glow + + dimmedBars[barKey] = false + + pill:SetScript("OnClick", function(self) + dimmedBars[barKey] = not dimmedBars[barKey] + UpdatePillVisual(self, not dimmedBars[barKey]) + UpdateBarDimming(barKey) + end) + + pills[#pills + 1] = pill + totalWidth = totalWidth + 70 + 6 + end + end + + totalWidth = totalWidth - 6 + local startX = -totalWidth / 2 + for i, pill in ipairs(pills) do + pill:SetPoint("LEFT", pillContainer, "CENTER", startX + (i - 1) * 76, 0) + end + pillContainer:SetSize(totalWidth, 28) + + filterPills = pills +end + +local function DestroyFilterPills() + if pillContainer then + pillContainer:Hide() + pillContainer:SetParent(nil) + pillContainer = nil + end + wipe(filterPills) + wipe(dimmedBars) +end + +-------------------------------------------------------------------------------- +-- Button Overlays +-------------------------------------------------------------------------------- + +local function GetBindingCommand(barKey, buttonIndex) + local prefix = BINDING_MAP[barKey] + if not prefix then return nil end + return prefix .. buttonIndex +end + +local function GetKeybindText(barKey, buttonIndex) + local cmd = GetBindingCommand(barKey, buttonIndex) + if not cmd then return "" end + local key1 = GetBindingKey(cmd) + if key1 then + key1 = key1:gsub("SHIFT%-", "S-") + key1 = key1:gsub("CTRL%-", "C-") + key1 = key1:gsub("ALT%-", "A-") + return key1 + end + return "" +end + +-------------------------------------------------------------------------------- +-- Keybind Application +-------------------------------------------------------------------------------- + +local function RefreshOverlayText(ov) + local txt = GetKeybindText(ov._barKey, ov._buttonIndex) + ov._keyText:SetText(txt) + if ov._dash then + ov._dash:SetShown(txt == "") + end +end + +local function ClearPendingConflict(ov) + if pendingConflict and pendingConflict.overlay == ov then + pendingConflict = nil + end + -- Reset tooltip to default + if ov and ov._ttText then + ov._ttText:SetText("Press a key...") + ov._ttText:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + ov._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.8) + end +end + +local function FlashFeedback(ov, text, r, g, b) + -- Show feedback text + ov._ttText:SetText(text) + ov._ttText:SetTextColor(r, g, b, 1) + -- Flash border bright + ov._border:SetColor(r, g, b, 1) + ov._bg:SetColorTexture(r, g, b, 0.20) + -- Fade back after delay + C_Timer.After(0.8, function() + if ov and ov._ttText then + if hoveredOverlay == ov then + ov._ttText:SetText("Press a key...") + ov._ttText:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + ov._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.8) + ov._bg:SetColorTexture(ACCENT_R, ACCENT_G, ACCENT_B, 0.12) + else + ov._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.3) + ov._bg:SetColorTexture(0.05, 0.07, 0.09, 0.55) + end + end + end) +end + +local function DoBindKey(ov, keyCombo) + local cmd = GetBindingCommand(ov._barKey, ov._buttonIndex) + if not cmd then return end + SetBinding(keyCombo, nil) + SetBinding(keyCombo, cmd) + SaveBindings(2) + -- Refresh all overlays + for _, other in ipairs(buttonOverlays) do + RefreshOverlayText(other) + end + -- Shorten the display of the key for feedback + local shortKey = keyCombo:gsub("SHIFT%-", "S-"):gsub("CTRL%-", "C-"):gsub("ALT%-", "A-") + FlashFeedback(ov, shortKey .. " Bound!", 0.2, 0.9, 0.4) +end + +local function ApplyKeybind(ov, keyCombo) + local cmd = GetBindingCommand(ov._barKey, ov._buttonIndex) + if not cmd then return end + + -- If we have a pending conflict for THIS overlay with THIS key, confirm it + if pendingConflict and pendingConflict.overlay == ov and pendingConflict.keyCombo == keyCombo then + pendingConflict = nil + DoBindKey(ov, keyCombo) + return + end + + -- Check for conflict + local existingAction = GetBindingAction(keyCombo) + if existingAction and existingAction ~= "" and existingAction ~= cmd then + -- Show warning inline on the tooltip — press same key again to confirm + pendingConflict = { overlay = ov, keyCombo = keyCombo, existingAction = existingAction } + ov._ttText:SetText(keyCombo .. " = " .. existingAction .. "\nPress again to override") + ov._ttText:SetTextColor(WARN_R, WARN_G, WARN_B, 1) + ov._border:SetColor(WARN_R, WARN_G, WARN_B, 0.8) + -- Auto-clear after 3 seconds if they don't confirm + C_Timer.After(3, function() + if pendingConflict and pendingConflict.overlay == ov and pendingConflict.keyCombo == keyCombo then + ClearPendingConflict(ov) + end + end) + return + end + + -- No conflict — bind directly + DoBindKey(ov, keyCombo) +end + +local function ClearKeybind(ov) + local cmd = GetBindingCommand(ov._barKey, ov._buttonIndex) + if not cmd then return end + -- Clear ALL bindings for this command (a button can have multiple keys) + local key1, key2 = GetBindingKey(cmd) + local cleared = false + if key1 then + SetBinding(key1, nil) + cleared = true + end + if key2 then + SetBinding(key2, nil) + cleared = true + end + if cleared then + SaveBindings(2) + RefreshOverlayText(ov) + FlashFeedback(ov, "Cleared!", 1, 0.35, 0.35) + end +end + +-------------------------------------------------------------------------------- +-- Button Overlay Creation +-------------------------------------------------------------------------------- + +local function CreateButtonOverlay(btn, barKey, buttonIndex) + local ov = CreateFrame("Frame", nil, keybindFrame) + ov:SetFrameLevel(keybindFrame:GetFrameLevel() + 20) + ov:SetSize(btn:GetWidth(), btn:GetHeight()) + ov:SetPoint("CENTER", btn, "CENTER", 0, 0) + ov:EnableMouse(true) + ov._barKey = barKey + ov._buttonIndex = buttonIndex + ov._actionBtn = btn + + local bg = ov:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.05, 0.07, 0.09, 0.55) + ov._bg = bg + + ov._border = EllesmereUI.MakeBorder(ov, ACCENT_R, ACCENT_G, ACCENT_B, 0.3) + + local keyText = ov:CreateFontString(nil, "OVERLAY") + keyText:SetFont(FONT_PATH, 10, "OUTLINE") + keyText:SetPoint("TOPRIGHT", ov, "TOPRIGHT", -2, -2) + keyText:SetTextColor(1, 1, 1, 0.9) + ov._keyText = keyText + + -- Dash placeholder (always created, shown when unbound) + local dash = ov:CreateFontString(nil, "OVERLAY") + dash:SetFont(FONT_PATH, 10, "") + dash:SetPoint("CENTER") + dash:SetTextColor(1, 1, 1, 0.2) + dash:SetText("\226\128\148") + ov._dash = dash + + local currentBind = GetKeybindText(barKey, buttonIndex) + if currentBind ~= "" then + keyText:SetText(currentBind) + dash:Hide() + else + keyText:SetText("") + dash:Show() + end + + local tooltip = CreateFrame("Frame", nil, ov) + tooltip:SetSize(200, 40) + tooltip:SetPoint("TOP", ov, "BOTTOM", 0, -4) + tooltip:SetFrameLevel(ov:GetFrameLevel() + 5) + tooltip:Hide() + + local ttBg = tooltip:CreateTexture(nil, "BACKGROUND") + ttBg:SetAllPoints() + ttBg:SetColorTexture(0.031, 0.047, 0.063, 0.95) + EllesmereUI.MakeBorder(tooltip, ACCENT_R, ACCENT_G, ACCENT_B, 0.5) + + local ttText = tooltip:CreateFontString(nil, "OVERLAY") + ttText:SetFont(FONT_PATH, 10, "") + ttText:SetPoint("CENTER") + ttText:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + ttText:SetText("Press a key...") + ov._tooltip = tooltip + ov._ttText = ttText + + -- Register all mouse buttons for binding + ov:RegisterForDrag("LeftButton", "RightButton", "MiddleButton", "Button4", "Button5") + ov:SetScript("OnMouseDown", function(self, button) + if not hoveredOverlay or hoveredOverlay ~= self then return end + -- Map WoW button names to binding format + local btnMap = { + LeftButton = "BUTTON1", + RightButton = "BUTTON2", + MiddleButton = "BUTTON3", + Button4 = "BUTTON4", + Button5 = "BUTTON5", + } + local bindBtn = btnMap[button] + if not bindBtn then return end + + -- Build combo with modifiers + local combo = "" + if IsShiftKeyDown() then combo = combo .. "SHIFT-" end + if IsControlKeyDown() then combo = combo .. "CTRL-" end + if IsAltKeyDown() then combo = combo .. "ALT-" end + combo = combo .. bindBtn + + DebugLog("MOUSE=" .. combo .. " bar=" .. self._barKey .. " idx=" .. self._buttonIndex) + ApplyKeybind(self, combo) + end) + + ov:SetScript("OnEnter", function(self) + if dimmedBars[self._barKey] then return end + hoveredOverlay = self + self._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.8) + self._bg:SetColorTexture(ACCENT_R, ACCENT_G, ACCENT_B, 0.12) + self._tooltip:Show() + DebugLog("HOVER bar=" .. self._barKey .. " idx=" .. self._buttonIndex .. " cmd=" .. (GetBindingCommand(self._barKey, self._buttonIndex) or "nil") .. " btnName=" .. (self._actionBtn:GetName() or "anon") .. " btnID=" .. (self._actionBtn:GetID() or "?")) + end) + + ov:SetScript("OnLeave", function(self) + if hoveredOverlay == self then + hoveredOverlay = nil + end + ClearPendingConflict(self) + self._border:SetColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.3) + self._bg:SetColorTexture(0.05, 0.07, 0.09, 0.55) + self._tooltip:Hide() + self._ttText:SetText("Press a key...") + self._ttText:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) + end) + + return ov +end + +local function CreateAllButtonOverlays() + local EAB = EllesmereUI.Lite.GetAddon("EllesmereUIActionBars", true) + if not EAB or not EAB._barButtons then return end + + for _, barKey in ipairs(BAR_ORDER) do + local buttons = EAB._barButtons[barKey] + local barFrame = EAB._barFrames and EAB._barFrames[barKey] + if buttons and barFrame and barFrame:IsShown() then + for i, btn in ipairs(buttons) do + if btn and btn:IsShown() then + local ov = CreateButtonOverlay(btn, barKey, i) + buttonOverlays[#buttonOverlays + 1] = ov + DebugLog("OVERLAY bar=" .. barKey .. " idx=" .. i .. " btn=" .. (btn:GetName() or "anon") .. " btnID=" .. (btn:GetID() or "?") .. " cmd=" .. (GetBindingCommand(barKey, i) or "nil")) + end + end + end + end +end + +local function DestroyAllButtonOverlays() + for _, ov in ipairs(buttonOverlays) do + ov:Hide() + ov:SetParent(nil) + end + wipe(buttonOverlays) + hoveredOverlay = nil +end + +-------------------------------------------------------------------------------- +-- Combat auto-exit +-------------------------------------------------------------------------------- +local combatFrame = CreateFrame("Frame") +combatFrame:RegisterEvent("PLAYER_REGEN_DISABLED") +combatFrame:SetScript("OnEvent", function() + if isActive then + CloseKeybindMode() + print("|cffff6060[EllesmereUI]|r Keybind Mode closed \226\128\148 entering combat.") + end +end) + +-------------------------------------------------------------------------------- +-- Login: restore keybind profile for current spec +-------------------------------------------------------------------------------- +local loginFrame = CreateFrame("Frame") +loginFrame:RegisterEvent("PLAYER_LOGIN") +loginFrame:SetScript("OnEvent", function() + C_Timer.After(1, function() + local profileName, specID = EllesmereUI:GetCurrentSpecKeybindProfile() + if profileName and EllesmereUIDB.keybindProfiles + and EllesmereUIDB.keybindProfiles[profileName] then + EllesmereUI:RestoreKeybinds(profileName) + elseif specID then + local specIdx = GetSpecialization and GetSpecialization() + if specIdx and specIdx > 0 then + local _, specName = GetSpecializationInfo(specIdx) + if specName then + EllesmereUI:SnapshotKeybinds(specName) + EllesmereUI:AssignKeybindProfileToSpec(specName, specID) + end + end + end + end) +end) + +-------------------------------------------------------------------------------- +-- Open / Close +-------------------------------------------------------------------------------- +OpenKeybindMode = function() + if isActive then return end + if InCombatLockdown() then + print("|cffff6060[EllesmereUI]|r Cannot enter Keybind Mode during combat.") + return + end + + isActive = true + + if EllesmereUI.IsShown and EllesmereUI:IsShown() then + EllesmereUI:Toggle() + end + + CreateOverlayFrame() + CreateHUD() + CreateFilterPills() + CreateAllButtonOverlays() + + -- Register with WoW's ESC-to-close system + tinsert(UISpecialFrames, "EllesmereKeybindMode") + + -- Enable mouse to block clicks from reaching the game world + keybindFrame:EnableMouse(true) + + -- Single keyboard handler on the main frame — routes to hoveredOverlay + keybindFrame:EnableKeyboard(true) + keybindFrame:SetScript("OnKeyDown", function(self, key) + -- Ignore lone modifier keys + if key == "LSHIFT" or key == "RSHIFT" + or key == "LCTRL" or key == "RCTRL" + or key == "LALT" or key == "RALT" then + self:SetPropagateKeyboardInput(true) + return + end + + -- ESC: if hovering, unbind; if not, exit + if key == "ESCAPE" then + self:SetPropagateKeyboardInput(false) + if hoveredOverlay then + DebugLog("ESC unbind bar=" .. hoveredOverlay._barKey .. " idx=" .. hoveredOverlay._buttonIndex) + ClearKeybind(hoveredOverlay) + else + CloseKeybindMode() + end + return + end + + -- No overlay hovered — let key pass through to game + if not hoveredOverlay then + self:SetPropagateKeyboardInput(true) + return + end + + -- Consume the key + self:SetPropagateKeyboardInput(false) + + -- Build key combo + local combo = "" + if IsShiftKeyDown() then combo = combo .. "SHIFT-" end + if IsControlKeyDown() then combo = combo .. "CTRL-" end + if IsAltKeyDown() then combo = combo .. "ALT-" end + combo = combo .. key + + DebugLog("KEY=" .. combo .. " bar=" .. hoveredOverlay._barKey .. " idx=" .. hoveredOverlay._buttonIndex .. " cmd=" .. (GetBindingCommand(hoveredOverlay._barKey, hoveredOverlay._buttonIndex) or "nil")) + ApplyKeybind(hoveredOverlay, combo) + end) + + FadeIn() +end + +CloseKeybindMode = function() + if not isActive then return end + isActive = false + pendingConflict = nil + + -- Auto-save keybinds to current spec's profile + local profileName, specID = EllesmereUI:GetCurrentSpecKeybindProfile() + if profileName then + EllesmereUI:SnapshotKeybinds(profileName) + elseif specID then + local _, specName = GetSpecializationInfo(GetSpecialization()) + if specName then + EllesmereUI:SnapshotKeybinds(specName) + EllesmereUI:AssignKeybindProfileToSpec(specName, specID) + end + end + + -- Remove from UISpecialFrames + for i = #UISpecialFrames, 1, -1 do + if UISpecialFrames[i] == "EllesmereKeybindMode" then + table.remove(UISpecialFrames, i) + end + end + + if keybindFrame and keybindFrame:IsShown() then + FadeOut(function() + DestroyAllButtonOverlays() + DestroyFilterPills() + DestroyHUD() + DestroyOverlayFrame() + end) + else + -- Frame already hidden by ESC/UISpecialFrames — just clean up + DestroyAllButtonOverlays() + DestroyFilterPills() + DestroyHUD() + DestroyOverlayFrame() + end +end diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index 7ecab160..e024441f 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -4350,7 +4350,7 @@ initFrame:SetScript("OnEvent", function(self) local kbBtn = CreateFrame("Button", nil, itm) kbBtn:SetSize(X_SZ, X_SZ) - kbBtn:SetPoint("RIGHT", editBtn, "LEFT", -4, 0) + kbBtn:SetPoint("RIGHT", xBtn, "LEFT", -4, 0) kbBtn:SetFrameLevel(itm:GetFrameLevel() + 2) local kbIcon = kbBtn:CreateTexture(nil, "OVERLAY") kbIcon:SetAllPoints() @@ -4360,11 +4360,11 @@ initFrame:SetScript("OnEvent", function(self) itm._kbBtn = kbBtn local function IsOverInlineBtn() - return xBtn:IsMouseOver() or editBtn:IsMouseOver() or kbBtn:IsMouseOver() + return xBtn:IsMouseOver() or kbBtn:IsMouseOver() end local function SetAllInlineAlpha(a) - xBtn:SetAlpha(a); editBtn:SetAlpha(a); kbBtn:SetAlpha(a) + xBtn:SetAlpha(a); kbBtn:SetAlpha(a) end itm:SetScript("OnEnter", function() @@ -4403,14 +4403,6 @@ initFrame:SetScript("OnEvent", function(self) InlineBtnLeave(self) EllesmereUI.HideWidgetTooltip() end) - editBtn:SetScript("OnEnter", function(self) - InlineBtnEnter(self) - EllesmereUI.ShowWidgetTooltip(self, "Rename") - end) - editBtn:SetScript("OnLeave", function(self) - InlineBtnLeave(self) - EllesmereUI.HideWidgetTooltip() - end) kbBtn:SetScript("OnEnter", function(self) InlineBtnEnter(self) EllesmereUI.ShowWidgetTooltip(self, "Keybind") @@ -4447,20 +4439,19 @@ initFrame:SetScript("OnEvent", function(self) else local iLbl, iHl, iXBtn, iEditBtn, iKbBtn = itm._lbl, itm._hl, itm._xBtn, itm._editBtn, itm._kbBtn iLbl:SetTextColor(1, 1, 1, EllesmereUI.TEXT_DIM_A) + iEditBtn:Hide() -- rename disabled; name is set at creation if capName == "Default" then iXBtn:Hide() - iEditBtn:Hide() iKbBtn:Hide() else iXBtn:Show() - iEditBtn:Show() iKbBtn:Show() end local function IsOverInline() - return iXBtn:IsMouseOver() or iEditBtn:IsMouseOver() or iKbBtn:IsMouseOver() + return iXBtn:IsMouseOver() or iKbBtn:IsMouseOver() end local function SetAllAlpha(a) - iXBtn:SetAlpha(a); iEditBtn:SetAlpha(a); iKbBtn:SetAlpha(a) + iXBtn:SetAlpha(a); iKbBtn:SetAlpha(a) end itm:SetScript("OnEnter", function() iLbl:SetTextColor(1, 1, 1, 1) diff --git a/EllesmereUI.lua b/EllesmereUI.lua index 0551a6fd..537815ce 100644 --- a/EllesmereUI.lua +++ b/EllesmereUI.lua @@ -6485,11 +6485,16 @@ end SLASH_EUIOPTIONS1 = "/eui" SLASH_EUIOPTIONS2 = "/ellesmere" SLASH_EUIOPTIONS3 = "/ellesmereui" -SlashCmdList.EUIOPTIONS = function() +SlashCmdList.EUIOPTIONS = function(msg) if InCombatLockdown() then print("|cffff6060[EllesmereUI]|r Cannot open options during combat.") return end + local cmd = msg and msg:lower():trim() or "" + if cmd == "keybind" or cmd == "kb" then + EllesmereUI:ToggleKeybindMode() + return + end EllesmereUI:Toggle() end diff --git a/EllesmereUI.toc b/EllesmereUI.toc index ee096aa3..80e9649c 100644 --- a/EllesmereUI.toc +++ b/EllesmereUI.toc @@ -35,3 +35,4 @@ EUI__General_Options.lua EUI_UnlockMode.lua EUI_PartyMode_Options.lua EllesmereUI_Glows.lua +EUI_KeybindMode.lua diff --git a/EllesmereUI_Profiles.lua b/EllesmereUI_Profiles.lua index 73303292..db68d7be 100644 --- a/EllesmereUI_Profiles.lua +++ b/EllesmereUI_Profiles.lua @@ -1377,6 +1377,48 @@ function EllesmereUI.SwitchProfile(name) RepointAllDBs(name) end +------------------------------------------------------------------------------- +-- SwitchKeybindProfile +-- +-- Snapshots the outgoing spec's keybinds and restores the incoming spec's +-- keybind profile. If no profile exists for the incoming spec, one is +-- created automatically from the current keybinds. +------------------------------------------------------------------------------- +local function SwitchKeybindProfile(outgoingSpecID, incomingSpecID) + if not EllesmereUIDB then return end + if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end + if not EllesmereUIDB.keybindProfiles then EllesmereUIDB.keybindProfiles = {} end + + -- Snapshot outgoing spec's keybinds + if outgoingSpecID then + local outName = EllesmereUIDB.specKeybindProfiles[outgoingSpecID] + if outName then + EllesmereUI:SnapshotKeybinds(outName) + end + end + + -- Resolve incoming profile + local inName = EllesmereUIDB.specKeybindProfiles[incomingSpecID] + if not inName then + -- First time on this spec: create profile from current keybinds + local specIdx = GetSpecialization and GetSpecialization() + local specName + if specIdx and specIdx > 0 then + _, specName = GetSpecializationInfo(specIdx) + end + inName = specName or ("Spec " .. incomingSpecID) + EllesmereUI:SnapshotKeybinds(inName) + EllesmereUIDB.specKeybindProfiles[incomingSpecID] = inName + end + + -- Restore incoming keybinds + if EllesmereUIDB.keybindProfiles[inName] then + EllesmereUI:RestoreKeybinds(inName) + end +end + +EllesmereUI._SwitchKeybindProfile = SwitchKeybindProfile + function EllesmereUI.GetActiveProfileName() local db = GetProfilesDB() return db.activeProfile or "Default" @@ -1457,6 +1499,11 @@ do end end end + -- Always switch keybind profile on deferred spec change + local _, deferredSpecID = ResolveSpecProfile() + if deferredSpecID then + SwitchKeybindProfile(nil, deferredSpecID) + end end return end @@ -1549,6 +1596,7 @@ do return -- same char, same spec, nothing to do end end + local oldSpecID = lastKnownSpecID -- capture before overwrite for keybind switching lastKnownSpecID = specID lastKnownCharKey = charKey @@ -1659,6 +1707,18 @@ do end end end + + -- Switch keybind profile on spec change, independent of UI profile. + -- Delay slightly so Blizzard's action bar texture rebuild finishes first. + if specID and not InCombatLockdown() then + local capturedOld = oldSpecID + local capturedNew = specID + C_Timer.After(0.5, function() + if not InCombatLockdown() then + SwitchKeybindProfile(capturedOld, capturedNew) + end + end) + end end) end From 435b5f689ca95c137ba23d31d1bc8f0248c3b79d Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Mon, 30 Mar 2026 14:22:26 -0400 Subject: [PATCH 2/7] feat: add keybind mode button + profile info to ActionBars options - Expose barFrames/barButtons so keybind mode can discover action buttons - Add Keybind Mode button to ActionBars options panel - Add Keybind Profiles section showing active profile and per-spec assignments Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EUI_ActionBars_Options.lua | 121 ++++++++++++++++++ .../EllesmereUIActionBars.lua | 4 + 2 files changed, 125 insertions(+) diff --git a/EllesmereUIActionBars/EUI_ActionBars_Options.lua b/EllesmereUIActionBars/EUI_ActionBars_Options.lua index d0621382..892acea1 100644 --- a/EllesmereUIActionBars/EUI_ActionBars_Options.lua +++ b/EllesmereUIActionBars/EUI_ActionBars_Options.lua @@ -1365,6 +1365,127 @@ initFrame:SetScript("OnEvent", function(self) return not SB().bgEnabled end + ----------------------------------------------------------------------- + -- Keybind Mode button + ----------------------------------------------------------------------- + do + local FONT = (EllesmereUI and EllesmereUI.GetFontPath and EllesmereUI.GetFontPath("actionBars")) + or "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" + local kbBtn = CreateFrame("Button", nil, parent) + kbBtn:SetSize(160, 32) + kbBtn:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, y) + kbBtn:SetFrameLevel(parent:GetFrameLevel() + 3) + + local btnBg = kbBtn:CreateTexture(nil, "BACKGROUND") + btnBg:SetAllPoints() + btnBg:SetColorTexture(0.061, 0.095, 0.120, 0.6) + + EllesmereUI.MakeBorder(kbBtn, 1, 1, 1, 0.15) + + local icon = kbBtn:CreateTexture(nil, "OVERLAY") + icon:SetSize(18, 18) + icon:SetPoint("LEFT", kbBtn, "LEFT", 10, 0) + icon:SetTexture("Interface\\AddOns\\EllesmereUI\\media\\icons\\eui-keybind-2.png") + icon:SetAlpha(0.7) + + local label = kbBtn:CreateFontString(nil, "OVERLAY") + label:SetFont(FONT, 12, "") + label:SetPoint("LEFT", icon, "RIGHT", 8, 0) + label:SetTextColor(1, 1, 1, 0.7) + label:SetText("Keybind Mode") + + kbBtn:SetScript("OnEnter", function(self) + btnBg:SetColorTexture(0.071, 0.110, 0.140, 0.8) + EllesmereUI.MakeBorder(self, 0.047, 0.824, 0.624, 0.4) + label:SetTextColor(1, 1, 1, 0.9) + icon:SetAlpha(0.9) + end) + kbBtn:SetScript("OnLeave", function(self) + btnBg:SetColorTexture(0.061, 0.095, 0.120, 0.6) + EllesmereUI.MakeBorder(self, 1, 1, 1, 0.15) + label:SetTextColor(1, 1, 1, 0.7) + icon:SetAlpha(0.7) + end) + kbBtn:SetScript("OnClick", function() + EllesmereUI:ToggleKeybindMode() + end) + + y = y - 42 + end + + ----------------------------------------------------------------------- + -- Keybind Profile Info + ----------------------------------------------------------------------- + do + local FONT = (EllesmereUI and EllesmereUI.GetFontPath and EllesmereUI.GetFontPath("actionBars")) + or "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" + + -- Section label + local secLabel = parent:CreateFontString(nil, "OVERLAY") + secLabel:SetFont(FONT, 11, "") + secLabel:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, y - 4) + secLabel:SetTextColor(1, 1, 1, 0.4) + secLabel:SetText("KEYBIND PROFILES") + y = y - 22 + + -- Current profile display + local curLabel = parent:CreateFontString(nil, "OVERLAY") + curLabel:SetFont(FONT, 11, "") + curLabel:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, y) + curLabel:SetTextColor(1, 1, 1, 0.5) + curLabel:SetText("Active:") + + local curValue = parent:CreateFontString(nil, "OVERLAY") + curValue:SetFont(FONT, 12, "") + curValue:SetPoint("LEFT", curLabel, "RIGHT", 6, 0) + local profileName = EllesmereUI:GetCurrentSpecKeybindProfile() + if profileName then + curValue:SetTextColor(0.047, 0.824, 0.624, 1) + curValue:SetText(profileName) + else + curValue:SetTextColor(1, 1, 1, 0.3) + curValue:SetText("None") + end + y = y - 20 + + -- Per-spec assignment display + local numSpecs = GetNumSpecializations and GetNumSpecializations() or 0 + if not EllesmereUIDB then EllesmereUIDB = {} end + if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end + + for i = 1, numSpecs do + local specID, specName, _, specIcon = GetSpecializationInfo(i) + if specID and specName then + local row = CreateFrame("Frame", nil, parent) + row:SetSize(300, 18) + row:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, y) + + -- Spec icon + local sIcon = row:CreateTexture(nil, "OVERLAY") + sIcon:SetSize(14, 14) + sIcon:SetPoint("LEFT", row, "LEFT", 0, 0) + sIcon:SetTexture(specIcon) + + -- Spec name and assigned profile + local sLabel = row:CreateFontString(nil, "OVERLAY") + sLabel:SetFont(FONT, 11, "") + sLabel:SetPoint("LEFT", sIcon, "RIGHT", 6, 0) + sLabel:SetTextColor(1, 1, 1, 0.6) + + local assignedProfile = EllesmereUIDB.specKeybindProfiles[specID] + if assignedProfile then + sLabel:SetText(specName .. ": |cff0cd29f" .. assignedProfile .. "|r") + else + sLabel:SetText(specName .. ": |cff666666not set|r") + end + + y = y - 20 + end + end + + y = y - 10 + end + ----------------------------------------------------------------------- -- VISIBILITY ----------------------------------------------------------------------- diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.lua b/EllesmereUIActionBars/EllesmereUIActionBars.lua index ad63846b..9206942a 100644 --- a/EllesmereUIActionBars/EllesmereUIActionBars.lua +++ b/EllesmereUIActionBars/EllesmereUIActionBars.lua @@ -7398,6 +7398,10 @@ function EAB:FinishSetup() -- Register with unlock mode (deferred to ensure EllesmereUI is loaded) C_Timer_After(0.5, RegisterWithUnlockMode) + + -- Expose bar tables so keybind mode (and other modules) can access them + EAB._barFrames = barFrames + EAB._barButtons = barButtons end ------------------------------------------------------------------------------- From 1fd0114ba46dc3069ac8c9c5e171375258a343a1 Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Tue, 31 Mar 2026 01:49:22 -0400 Subject: [PATCH 3/7] style: polish keybind mode UI with tooltips and EllesmereUI styling ActionBars Options: - Replace custom keybind button with proper KEYBINDS section header - Accent-colored wide button with icon, shortcut hint, and detailed tooltip - Styled profile rows with spec icons, active-spec indicator, alternating backgrounds, and per-row hover tooltips explaining profile status Keybind Mode HUD: - Wider HUD (680px) with accent glow top edge and keybind icon - Profile indicator shows spec icon + profile name with hover tooltip - Instructions text is hoverable with full usage guide tooltip - Close button shows red on hover with auto-save reminder tooltip - Filter pills show bar state tooltip on hover - Dynamic accent color via GetAccentColor() to match active theme Co-Authored-By: Claude Opus 4.6 (1M context) --- EUI_KeybindMode.lua | 166 ++++++++++-- .../EUI_ActionBars_Options.lua | 236 +++++++++++++----- 2 files changed, 313 insertions(+), 89 deletions(-) diff --git a/EUI_KeybindMode.lua b/EUI_KeybindMode.lua index 6f8682fb..0c065096 100644 --- a/EUI_KeybindMode.lua +++ b/EUI_KeybindMode.lua @@ -64,10 +64,17 @@ local BAR_LABELS = { PetBar = "Pet", } --- Colors +-- Colors (resolve dynamically; fall back to teal if GetAccentColor not yet available) local ACCENT_R, ACCENT_G, ACCENT_B = 0.047, 0.824, 0.624 local WARN_R, WARN_G, WARN_B = 1.0, 0.706, 0.235 local FONT_PATH = "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" +local MEDIA_PATH = "Interface\\AddOns\\EllesmereUI\\media\\" + +local function RefreshAccent() + if EllesmereUI.GetAccentColor then + ACCENT_R, ACCENT_G, ACCENT_B = EllesmereUI.GetAccentColor() + end +end -------------------------------------------------------------------------------- -- Keybind Profile Commands @@ -252,68 +259,166 @@ local hudFrame = nil local function CreateHUD() if hudFrame then hudFrame:Show(); return end + RefreshAccent() hudFrame = CreateFrame("Frame", nil, keybindFrame) - hudFrame:SetSize(620, 36) - hudFrame:SetPoint("TOP", UIParent, "TOP", 0, -20) + hudFrame:SetSize(680, 40) + hudFrame:SetPoint("TOP", UIParent, "TOP", 0, -18) hudFrame:SetFrameLevel(keybindFrame:GetFrameLevel() + 10) local bg = hudFrame:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints() - bg:SetColorTexture(0.047, 0.071, 0.094, 0.95) + bg:SetColorTexture(0.035, 0.055, 0.075, 0.95) - EllesmereUI.MakeBorder(hudFrame, ACCENT_R, ACCENT_G, ACCENT_B, 0.4) + EllesmereUI.MakeBorder(hudFrame, ACCENT_R, ACCENT_G, ACCENT_B, 0.35) + -- Accent glow along top edge + local topGlow = hudFrame:CreateTexture(nil, "BACKGROUND", nil, 1) + topGlow:SetHeight(1) + topGlow:SetPoint("TOPLEFT", hudFrame, "TOPLEFT", 1, -1) + topGlow:SetPoint("TOPRIGHT", hudFrame, "TOPRIGHT", -1, -1) + topGlow:SetColorTexture(ACCENT_R, ACCENT_G, ACCENT_B, 0.40) + + -- Keybind icon (left side) + local hudIcon = hudFrame:CreateTexture(nil, "OVERLAY") + hudIcon:SetSize(16, 16) + hudIcon:SetPoint("LEFT", hudFrame, "LEFT", 14, 0) + hudIcon:SetTexture(MEDIA_PATH .. "icons\\eui-keybind-2.png") + hudIcon:SetVertexColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.9) + + -- Title local label = hudFrame:CreateFontString(nil, "OVERLAY") label:SetFont(FONT_PATH, 13, "") label:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 1) - label:SetPoint("LEFT", hudFrame, "LEFT", 16, 0) + label:SetPoint("LEFT", hudIcon, "RIGHT", 8, 0) label:SetText("KEYBIND MODE") + -- Separator local sep1 = hudFrame:CreateFontString(nil, "OVERLAY") sep1:SetFont(FONT_PATH, 11, "") - sep1:SetTextColor(1, 1, 1, 0.2) + sep1:SetTextColor(1, 1, 1, 0.15) sep1:SetPoint("LEFT", label, "RIGHT", 12, 0) sep1:SetText("|") + -- Instructions (hoverable) local instr = hudFrame:CreateFontString(nil, "OVERLAY") - instr:SetFont(FONT_PATH, 11, "") - instr:SetTextColor(1, 1, 1, 0.5) + instr:SetFont(FONT_PATH, 10, "") + instr:SetTextColor(1, 1, 1, 0.45) instr:SetPoint("LEFT", sep1, "RIGHT", 12, 0) - instr:SetText("Hover + press key to bind | ESC to unbind | ESC (no hover) to exit") + instr:SetText("Hover + press key | ESC unbind | ESC exit") + + local instrHit = CreateFrame("Frame", nil, hudFrame) + instrHit:SetPoint("TOPLEFT", instr, "TOPLEFT", -4, 4) + instrHit:SetPoint("BOTTOMRIGHT", instr, "BOTTOMRIGHT", 4, -4) + instrHit:SetScript("OnEnter", function() + instr:SetTextColor(1, 1, 1, 0.7) + EllesmereUI.ShowWidgetTooltip(instr, + "How to use Keybind Mode:\n\n" + .. "1. Hover over any action button\n" + .. "2. Press a key (or key combo) to bind it\n" + .. "3. Press ESC while hovering to clear a binding\n" + .. "4. Press ESC with nothing hovered to exit\n\n" + .. "Mouse buttons (including Button4/5) also work.\n" + .. "Conflicts are shown in orange \226\128\148 press the same key again to override.", + { anchor = "below" }) + end) + instrHit:SetScript("OnLeave", function() + instr:SetTextColor(1, 1, 1, 0.45) + EllesmereUI.HideWidgetTooltip() + end) + instrHit:SetMouseClickEnabled(false) + -- Close button (right side) local closeBtn = CreateFrame("Button", nil, hudFrame) - closeBtn:SetSize(20, 20) + closeBtn:SetSize(24, 24) closeBtn:SetPoint("RIGHT", hudFrame, "RIGHT", -10, 0) closeBtn:SetFrameLevel(hudFrame:GetFrameLevel() + 2) local closeTex = closeBtn:CreateFontString(nil, "OVERLAY") closeTex:SetFont(FONT_PATH, 14, "") - closeTex:SetTextColor(1, 1, 1, 0.4) + closeTex:SetTextColor(1, 1, 1, 0.35) closeTex:SetAllPoints() closeTex:SetText("X") - closeBtn:SetScript("OnEnter", function() closeTex:SetTextColor(1, 1, 1, 0.8) end) - closeBtn:SetScript("OnLeave", function() closeTex:SetTextColor(1, 1, 1, 0.4) end) + closeBtn:SetScript("OnEnter", function(self) + closeTex:SetTextColor(1, 0.35, 0.35, 0.9) + EllesmereUI.ShowWidgetTooltip(self, "Close Keybind Mode\nBindings are saved automatically.", { anchor = "below" }) + end) + closeBtn:SetScript("OnLeave", function() + closeTex:SetTextColor(1, 1, 1, 0.35) + EllesmereUI.HideWidgetTooltip() + end) closeBtn:SetScript("OnClick", function() CloseKeybindMode() end) - -- Profile name indicator - local profLabel = hudFrame:CreateFontString(nil, "OVERLAY") + -- Separator before profile + local sep2 = hudFrame:CreateFontString(nil, "OVERLAY") + sep2:SetFont(FONT_PATH, 11, "") + sep2:SetTextColor(1, 1, 1, 0.15) + sep2:SetPoint("RIGHT", closeBtn, "LEFT", -10, 0) + sep2:SetText("|") + + -- Profile indicator (hoverable) + local profFrame = CreateFrame("Frame", nil, hudFrame) + profFrame:SetSize(140, 24) + profFrame:SetPoint("RIGHT", sep2, "LEFT", -8, 0) + profFrame:SetFrameLevel(hudFrame:GetFrameLevel() + 2) + + local profIcon = profFrame:CreateTexture(nil, "OVERLAY") + profIcon:SetSize(14, 14) + profIcon:SetPoint("LEFT", profFrame, "LEFT", 0, 0) + + local profLabel = profFrame:CreateFontString(nil, "OVERLAY") profLabel:SetFont(FONT_PATH, 11, "") - profLabel:SetPoint("RIGHT", closeBtn, "LEFT", -12, 0) + profLabel:SetPoint("LEFT", profIcon, "RIGHT", 6, 0) + profLabel:SetPoint("RIGHT", profFrame, "RIGHT", 0, 0) + profLabel:SetJustifyH("LEFT") + profLabel:SetWordWrap(false) + + -- Resolve spec info for profile display + local specIdx = GetSpecialization and GetSpecialization() + local specName, specIconPath + if specIdx and specIdx > 0 then + local _, sName, _, sIcon = GetSpecializationInfo(specIdx) + specName = sName + specIconPath = sIcon + end + + if specIconPath then + profIcon:SetTexture(specIconPath) + profIcon:SetAlpha(0.8) + else + profIcon:SetTexture(MEDIA_PATH .. "icons\\eui-keybind-2.png") + profIcon:SetVertexColor(1, 1, 1, 0.4) + end local profileName = EllesmereUI:GetCurrentSpecKeybindProfile() if profileName then - profLabel:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.8) + profLabel:SetTextColor(ACCENT_R, ACCENT_G, ACCENT_B, 0.9) profLabel:SetText(profileName) else - local specIdx = GetSpecialization and GetSpecialization() - if specIdx and specIdx > 0 then - local _, specName = GetSpecializationInfo(specIdx) - profLabel:SetTextColor(1, 1, 1, 0.4) - profLabel:SetText(specName or "No Profile") + profLabel:SetTextColor(1, 1, 1, 0.35) + profLabel:SetText(specName or "No Profile") + end + + profFrame:SetScript("OnEnter", function(self) + profLabel:SetAlpha(1) + profIcon:SetAlpha(1) + local tip + if profileName then + tip = "Keybind Profile: |cff0cd29f" .. profileName .. "|r\n\n" + .. "Bindings will be saved to this profile when you close Keybind Mode." + else + tip = "No keybind profile exists for " .. (specName or "this spec") .. " yet.\n\n" + .. "Your current bindings will be saved as a new profile when you close Keybind Mode." end - end + EllesmereUI.ShowWidgetTooltip(self, tip, { anchor = "below" }) + end) + profFrame:SetScript("OnLeave", function() + profLabel:SetAlpha(0.9) + profIcon:SetAlpha(0.8) + EllesmereUI.HideWidgetTooltip() + end) + profFrame:SetMouseClickEnabled(false) end local function DestroyHUD() @@ -428,6 +533,18 @@ local function CreateFilterPills() UpdateBarDimming(barKey) end) + pill:SetScript("OnEnter", function(self) + local barLabel = BAR_LABELS[barKey] or barKey + local state = dimmedBars[barKey] and "dimmed (click to show)" or "visible (click to dim)" + EllesmereUI.ShowWidgetTooltip(self, + barLabel .. " \226\128\148 " .. state .. "\n\n" + .. "Toggle bars to focus on the ones you want to rebind.", + { anchor = "below" }) + end) + pill:SetScript("OnLeave", function() + EllesmereUI.HideWidgetTooltip() + end) + pills[#pills + 1] = pill totalWidth = totalWidth + 70 + 6 end @@ -778,6 +895,7 @@ OpenKeybindMode = function() return end + RefreshAccent() isActive = true if EllesmereUI.IsShown and EllesmereUI:IsShown() then diff --git a/EllesmereUIActionBars/EUI_ActionBars_Options.lua b/EllesmereUIActionBars/EUI_ActionBars_Options.lua index 892acea1..66da359e 100644 --- a/EllesmereUIActionBars/EUI_ActionBars_Options.lua +++ b/EllesmereUIActionBars/EUI_ActionBars_Options.lua @@ -1366,126 +1366,232 @@ initFrame:SetScript("OnEvent", function(self) end ----------------------------------------------------------------------- - -- Keybind Mode button + -- KEYBINDS section + ----------------------------------------------------------------------- + _, h = W:SectionHeader(parent, "KEYBINDS", y); y = y - h + + ----------------------------------------------------------------------- + -- Keybind Mode — wide accent button with icon ----------------------------------------------------------------------- do + local CONTENT_PAD = 45 local FONT = (EllesmereUI and EllesmereUI.GetFontPath and EllesmereUI.GetFontPath("actionBars")) or "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" - local kbBtn = CreateFrame("Button", nil, parent) - kbBtn:SetSize(160, 32) - kbBtn:SetPoint("TOPLEFT", parent, "TOPLEFT", 10, y) - kbBtn:SetFrameLevel(parent:GetFrameLevel() + 3) + local MEDIA = "Interface\\AddOns\\EllesmereUI\\media\\" + local aR, aG, aB = EllesmereUI.GetAccentColor() + + local BTN_W, BTN_H = 280, 36 + local ROW_H = BTN_H + 20 + + local row = CreateFrame("Frame", nil, parent) + row:SetSize(parent:GetWidth() - CONTENT_PAD * 2, ROW_H) + row:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) + + local kbBtn = CreateFrame("Button", nil, row) + kbBtn:SetSize(BTN_W, BTN_H) + kbBtn:SetPoint("LEFT", row, "LEFT", 20, 0) + kbBtn:SetFrameLevel(row:GetFrameLevel() + 2) local btnBg = kbBtn:CreateTexture(nil, "BACKGROUND") btnBg:SetAllPoints() - btnBg:SetColorTexture(0.061, 0.095, 0.120, 0.6) + btnBg:SetColorTexture(aR, aG, aB, 0.08) - EllesmereUI.MakeBorder(kbBtn, 1, 1, 1, 0.15) + local brd = EllesmereUI.MakeBorder(kbBtn, aR, aG, aB, 0.30) local icon = kbBtn:CreateTexture(nil, "OVERLAY") icon:SetSize(18, 18) - icon:SetPoint("LEFT", kbBtn, "LEFT", 10, 0) - icon:SetTexture("Interface\\AddOns\\EllesmereUI\\media\\icons\\eui-keybind-2.png") - icon:SetAlpha(0.7) + icon:SetPoint("LEFT", kbBtn, "LEFT", 14, 0) + icon:SetTexture(MEDIA .. "icons\\eui-keybind-2.png") + icon:SetVertexColor(aR, aG, aB, 0.9) - local label = kbBtn:CreateFontString(nil, "OVERLAY") - label:SetFont(FONT, 12, "") - label:SetPoint("LEFT", icon, "RIGHT", 8, 0) - label:SetTextColor(1, 1, 1, 0.7) - label:SetText("Keybind Mode") + local label = EllesmereUI.MakeFont(kbBtn, 13, nil, 1, 1, 1) + label:SetAlpha(0.85) + label:SetPoint("LEFT", icon, "RIGHT", 10, 0) + label:SetText("Enter Keybind Mode") + + local hint = EllesmereUI.MakeFont(kbBtn, 10, nil, 1, 1, 1) + hint:SetAlpha(0.35) + hint:SetPoint("RIGHT", kbBtn, "RIGHT", -14, 0) + hint:SetText("/eui kb") kbBtn:SetScript("OnEnter", function(self) - btnBg:SetColorTexture(0.071, 0.110, 0.140, 0.8) - EllesmereUI.MakeBorder(self, 0.047, 0.824, 0.624, 0.4) - label:SetTextColor(1, 1, 1, 0.9) - icon:SetAlpha(0.9) + btnBg:SetColorTexture(aR, aG, aB, 0.15) + brd:SetColor(aR, aG, aB, 0.55) + label:SetAlpha(1) + icon:SetVertexColor(aR, aG, aB, 1) + EllesmereUI.ShowWidgetTooltip(self, + "Opens the fast keybind overlay. Hover over any action button and " + .. "press a key to bind it instantly.\n\n" + .. "Bindings are saved per-spec automatically when you close the overlay.\n\n" + .. "Shortcut: /eui kb", { anchor = "below" }) end) kbBtn:SetScript("OnLeave", function(self) - btnBg:SetColorTexture(0.061, 0.095, 0.120, 0.6) - EllesmereUI.MakeBorder(self, 1, 1, 1, 0.15) - label:SetTextColor(1, 1, 1, 0.7) - icon:SetAlpha(0.7) + btnBg:SetColorTexture(aR, aG, aB, 0.08) + brd:SetColor(aR, aG, aB, 0.30) + label:SetAlpha(0.85) + icon:SetVertexColor(aR, aG, aB, 0.9) + EllesmereUI.HideWidgetTooltip() end) kbBtn:SetScript("OnClick", function() EllesmereUI:ToggleKeybindMode() end) - y = y - 42 + y = y - ROW_H end ----------------------------------------------------------------------- - -- Keybind Profile Info + -- Keybind Profiles — active profile + per-spec assignments ----------------------------------------------------------------------- do + local CONTENT_PAD = 45 + local SIDE_PAD = 20 local FONT = (EllesmereUI and EllesmereUI.GetFontPath and EllesmereUI.GetFontPath("actionBars")) or "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" - - -- Section label - local secLabel = parent:CreateFontString(nil, "OVERLAY") - secLabel:SetFont(FONT, 11, "") - secLabel:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, y - 4) - secLabel:SetTextColor(1, 1, 1, 0.4) - secLabel:SetText("KEYBIND PROFILES") - y = y - 22 - - -- Current profile display - local curLabel = parent:CreateFontString(nil, "OVERLAY") - curLabel:SetFont(FONT, 11, "") - curLabel:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, y) - curLabel:SetTextColor(1, 1, 1, 0.5) - curLabel:SetText("Active:") - - local curValue = parent:CreateFontString(nil, "OVERLAY") - curValue:SetFont(FONT, 12, "") - curValue:SetPoint("LEFT", curLabel, "RIGHT", 6, 0) + local aR, aG, aB = EllesmereUI.GetAccentColor() + + -- Active profile row + local activeRow = CreateFrame("Frame", nil, parent) + local rowW = parent:GetWidth() - CONTENT_PAD * 2 + activeRow:SetSize(rowW, 40) + activeRow:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) + + -- Alternating row bg + local arBg = activeRow:CreateTexture(nil, "BACKGROUND") + arBg:SetAllPoints() + arBg:SetColorTexture(0, 0, 0, 0.10) + + local arLabel = EllesmereUI.MakeFont(activeRow, 13, nil, 1, 1, 1) + arLabel:SetAlpha(0.53) + arLabel:SetPoint("LEFT", activeRow, "LEFT", SIDE_PAD, 0) + arLabel:SetText("Active Keybind Profile") + + local arValue = EllesmereUI.MakeFont(activeRow, 13, nil, 1, 1, 1) + arValue:SetPoint("RIGHT", activeRow, "RIGHT", -SIDE_PAD, 0) local profileName = EllesmereUI:GetCurrentSpecKeybindProfile() if profileName then - curValue:SetTextColor(0.047, 0.824, 0.624, 1) - curValue:SetText(profileName) + arValue:SetTextColor(aR, aG, aB, 1) + arValue:SetText(profileName) else - curValue:SetTextColor(1, 1, 1, 0.3) - curValue:SetText("None") + arValue:SetTextColor(1, 1, 1, 0.3) + arValue:SetText("None") end - y = y - 20 - -- Per-spec assignment display + activeRow:SetScript("OnEnter", function(self) + arBg:SetColorTexture(0, 0, 0, 0.18) + EllesmereUI.ShowWidgetTooltip(self, + "The keybind profile currently loaded for your active specialization.\n\n" + .. "Profiles are created and saved automatically the first time you " + .. "enter Keybind Mode on a spec. Switching specs restores each spec's bindings.", + { anchor = "below" }) + end) + activeRow:SetScript("OnLeave", function() + arBg:SetColorTexture(0, 0, 0, 0.10) + EllesmereUI.HideWidgetTooltip() + end) + + y = y - 40 + + -- Per-spec rows local numSpecs = GetNumSpecializations and GetNumSpecializations() or 0 if not EllesmereUIDB then EllesmereUIDB = {} end if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end + local currentSpecIdx = GetSpecialization and GetSpecialization() or 0 + local currentSpecID = currentSpecIdx > 0 and GetSpecializationInfo(currentSpecIdx) or nil + for i = 1, numSpecs do local specID, specName, _, specIcon = GetSpecializationInfo(i) if specID and specName then - local row = CreateFrame("Frame", nil, parent) - row:SetSize(300, 18) - row:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, y) + local isActive = (specID == currentSpecID) + local sRow = CreateFrame("Frame", nil, parent) + sRow:SetSize(rowW, 34) + sRow:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) + + -- Alternating bg + local sBg = sRow:CreateTexture(nil, "BACKGROUND") + sBg:SetAllPoints() + sBg:SetColorTexture(0, 0, 0, (i % 2 == 0) and 0.10 or 0.05) + local bgAlphaBase = (i % 2 == 0) and 0.10 or 0.05 + + -- Active spec indicator (thin accent bar on left edge) + if isActive then + local activeMark = sRow:CreateTexture(nil, "OVERLAY") + activeMark:SetSize(2, 20) + activeMark:SetPoint("LEFT", sRow, "LEFT", 4, 0) + activeMark:SetColorTexture(aR, aG, aB, 0.7) + end -- Spec icon - local sIcon = row:CreateTexture(nil, "OVERLAY") - sIcon:SetSize(14, 14) - sIcon:SetPoint("LEFT", row, "LEFT", 0, 0) + local sIcon = sRow:CreateTexture(nil, "OVERLAY") + sIcon:SetSize(16, 16) + sIcon:SetPoint("LEFT", sRow, "LEFT", SIDE_PAD, 0) sIcon:SetTexture(specIcon) + if not isActive then sIcon:SetDesaturated(true); sIcon:SetAlpha(0.5) end - -- Spec name and assigned profile - local sLabel = row:CreateFontString(nil, "OVERLAY") - sLabel:SetFont(FONT, 11, "") - sLabel:SetPoint("LEFT", sIcon, "RIGHT", 6, 0) - sLabel:SetTextColor(1, 1, 1, 0.6) + -- Spec name + local sLabel = EllesmereUI.MakeFont(sRow, 12, nil, 1, 1, 1) + sLabel:SetPoint("LEFT", sIcon, "RIGHT", 8, 0) + if isActive then + sLabel:SetTextColor(1, 1, 1, 0.85) + else + sLabel:SetTextColor(1, 1, 1, 0.45) + end + sLabel:SetText(specName) + -- Assigned profile value (right-aligned) + local sVal = EllesmereUI.MakeFont(sRow, 12, nil, 1, 1, 1) + sVal:SetPoint("RIGHT", sRow, "RIGHT", -SIDE_PAD, 0) local assignedProfile = EllesmereUIDB.specKeybindProfiles[specID] if assignedProfile then - sLabel:SetText(specName .. ": |cff0cd29f" .. assignedProfile .. "|r") + if isActive then + sVal:SetTextColor(aR, aG, aB, 1) + else + sVal:SetTextColor(aR, aG, aB, 0.5) + end + sVal:SetText(assignedProfile) else - sLabel:SetText(specName .. ": |cff666666not set|r") + sVal:SetTextColor(1, 1, 1, 0.2) + sVal:SetText("not set") end - y = y - 20 + -- Hover effect + tooltip + sRow:SetScript("OnEnter", function(self) + sBg:SetColorTexture(0, 0, 0, bgAlphaBase + 0.08) + sLabel:SetTextColor(1, 1, 1, 1) + if not isActive then sIcon:SetAlpha(0.8) end + local tip + if assignedProfile then + tip = specName .. " uses the \"" .. assignedProfile .. "\" keybind profile.\n\n" + .. "This profile was auto-saved the last time you used Keybind Mode on this spec." + else + tip = specName .. " has no keybind profile yet.\n\n" + .. "Enter Keybind Mode while this spec is active to create one automatically." + end + if isActive then + tip = tip .. "\n\n|cff0cd29fThis is your current spec.|r" + end + EllesmereUI.ShowWidgetTooltip(self, tip, { anchor = "below" }) + end) + sRow:SetScript("OnLeave", function() + sBg:SetColorTexture(0, 0, 0, bgAlphaBase) + if isActive then + sLabel:SetTextColor(1, 1, 1, 0.85) + else + sLabel:SetTextColor(1, 1, 1, 0.45) + sIcon:SetAlpha(0.5) + end + EllesmereUI.HideWidgetTooltip() + end) + + y = y - 34 end end - y = y - 10 + y = y - 8 end + _, h = W:Spacer(parent, y, 8); y = y - h + ----------------------------------------------------------------------- -- VISIBILITY ----------------------------------------------------------------------- From 294ff663eec86bd3fa6ab267aa346694c61e6e9f Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Tue, 31 Mar 2026 17:20:25 -0400 Subject: [PATCH 4/7] fix: address code review issues before PR Critical: - Remove EUI_KeybindDebugLog SavedVariable (unbounded growth on disk) - Gate DebugLog behind EllesmereUIDB.keybindDebug flag (silent by default) - Guard login RestoreKeybinds against combat lockdown with PLAYER_REGEN_ENABLED defer Important: - Revert all editBtn changes in EUI__General_Options.lua (preserve author's rename behavior) - Add isActive guard to FlashFeedback and conflict timer callbacks (prevent orphan frame ops) Optimization: - Defer SaveBindings(2) to CloseKeybindMode instead of per-bind disk writes Co-Authored-By: Claude Opus 4.6 (1M context) --- EUI_KeybindMode.lua | 28 ++++++++++++++++++++++------ EUI__General_Options.lua | 21 +++++++++++++++------ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/EUI_KeybindMode.lua b/EUI_KeybindMode.lua index 0c065096..0ce7270b 100644 --- a/EUI_KeybindMode.lua +++ b/EUI_KeybindMode.lua @@ -14,10 +14,9 @@ local InCombatLockdown = InCombatLockdown local SetBinding, GetBindingKey, SaveBindings = SetBinding, GetBindingKey, SaveBindings local GetBindingAction = GetBindingAction --- Debug log (saved to EUI_KeybindDebugLog saved variable) -EUI_KeybindDebugLog = EUI_KeybindDebugLog or {} +-- Debug log (silent by default; set EllesmereUIDB.keybindDebug = true to enable) local function DebugLog(msg) - EUI_KeybindDebugLog[#EUI_KeybindDebugLog + 1] = date("%H:%M:%S") .. " " .. msg + if not (EllesmereUIDB and EllesmereUIDB.keybindDebug) then return end print("|cff0cd29f[KB]|r " .. msg) end @@ -626,6 +625,7 @@ local function FlashFeedback(ov, text, r, g, b) ov._bg:SetColorTexture(r, g, b, 0.20) -- Fade back after delay C_Timer.After(0.8, function() + if not isActive then return end if ov and ov._ttText then if hoveredOverlay == ov then ov._ttText:SetText("Press a key...") @@ -645,7 +645,6 @@ local function DoBindKey(ov, keyCombo) if not cmd then return end SetBinding(keyCombo, nil) SetBinding(keyCombo, cmd) - SaveBindings(2) -- Refresh all overlays for _, other in ipairs(buttonOverlays) do RefreshOverlayText(other) @@ -676,6 +675,7 @@ local function ApplyKeybind(ov, keyCombo) ov._border:SetColor(WARN_R, WARN_G, WARN_B, 0.8) -- Auto-clear after 3 seconds if they don't confirm C_Timer.After(3, function() + if not isActive then return end if pendingConflict and pendingConflict.overlay == ov and pendingConflict.keyCombo == keyCombo then ClearPendingConflict(ov) end @@ -702,7 +702,6 @@ local function ClearKeybind(ov) cleared = true end if cleared then - SaveBindings(2) RefreshOverlayText(ov) FlashFeedback(ov, "Cleared!", 1, 0.35, 0.35) end @@ -866,8 +865,22 @@ end) -------------------------------------------------------------------------------- local loginFrame = CreateFrame("Frame") loginFrame:RegisterEvent("PLAYER_LOGIN") -loginFrame:SetScript("OnEvent", function() +loginFrame:RegisterEvent("PLAYER_REGEN_ENABLED") +loginFrame:SetScript("OnEvent", function(self, event) + if event == "PLAYER_REGEN_ENABLED" then + -- Deferred restore: combat ended, retry if pending + if not self._pendingRestore then return end + self._pendingRestore = false + self:UnregisterEvent("PLAYER_REGEN_ENABLED") + end + C_Timer.After(1, function() + if InCombatLockdown() then + -- Defer until combat ends — SetBinding is protected + self._pendingRestore = true + return + end + local profileName, specID = EllesmereUI:GetCurrentSpecKeybindProfile() if profileName and EllesmereUIDB.keybindProfiles and EllesmereUIDB.keybindProfiles[profileName] then @@ -964,6 +977,9 @@ CloseKeybindMode = function() isActive = false pendingConflict = nil + -- Persist all binding changes made during this session (single disk write) + SaveBindings(2) + -- Auto-save keybinds to current spec's profile local profileName, specID = EllesmereUI:GetCurrentSpecKeybindProfile() if profileName then diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index e024441f..7ecab160 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -4350,7 +4350,7 @@ initFrame:SetScript("OnEvent", function(self) local kbBtn = CreateFrame("Button", nil, itm) kbBtn:SetSize(X_SZ, X_SZ) - kbBtn:SetPoint("RIGHT", xBtn, "LEFT", -4, 0) + kbBtn:SetPoint("RIGHT", editBtn, "LEFT", -4, 0) kbBtn:SetFrameLevel(itm:GetFrameLevel() + 2) local kbIcon = kbBtn:CreateTexture(nil, "OVERLAY") kbIcon:SetAllPoints() @@ -4360,11 +4360,11 @@ initFrame:SetScript("OnEvent", function(self) itm._kbBtn = kbBtn local function IsOverInlineBtn() - return xBtn:IsMouseOver() or kbBtn:IsMouseOver() + return xBtn:IsMouseOver() or editBtn:IsMouseOver() or kbBtn:IsMouseOver() end local function SetAllInlineAlpha(a) - xBtn:SetAlpha(a); kbBtn:SetAlpha(a) + xBtn:SetAlpha(a); editBtn:SetAlpha(a); kbBtn:SetAlpha(a) end itm:SetScript("OnEnter", function() @@ -4403,6 +4403,14 @@ initFrame:SetScript("OnEvent", function(self) InlineBtnLeave(self) EllesmereUI.HideWidgetTooltip() end) + editBtn:SetScript("OnEnter", function(self) + InlineBtnEnter(self) + EllesmereUI.ShowWidgetTooltip(self, "Rename") + end) + editBtn:SetScript("OnLeave", function(self) + InlineBtnLeave(self) + EllesmereUI.HideWidgetTooltip() + end) kbBtn:SetScript("OnEnter", function(self) InlineBtnEnter(self) EllesmereUI.ShowWidgetTooltip(self, "Keybind") @@ -4439,19 +4447,20 @@ initFrame:SetScript("OnEvent", function(self) else local iLbl, iHl, iXBtn, iEditBtn, iKbBtn = itm._lbl, itm._hl, itm._xBtn, itm._editBtn, itm._kbBtn iLbl:SetTextColor(1, 1, 1, EllesmereUI.TEXT_DIM_A) - iEditBtn:Hide() -- rename disabled; name is set at creation if capName == "Default" then iXBtn:Hide() + iEditBtn:Hide() iKbBtn:Hide() else iXBtn:Show() + iEditBtn:Show() iKbBtn:Show() end local function IsOverInline() - return iXBtn:IsMouseOver() or iKbBtn:IsMouseOver() + return iXBtn:IsMouseOver() or iEditBtn:IsMouseOver() or iKbBtn:IsMouseOver() end local function SetAllAlpha(a) - iXBtn:SetAlpha(a); iKbBtn:SetAlpha(a) + iXBtn:SetAlpha(a); iEditBtn:SetAlpha(a); iKbBtn:SetAlpha(a) end itm:SetScript("OnEnter", function() iLbl:SetTextColor(1, 1, 1, 1) From d52782d66b4ce620e0f23e7d1034d8e3d352a8d1 Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Tue, 31 Mar 2026 17:44:16 -0400 Subject: [PATCH 5/7] fix: keybind profile UI updates live on spec switch The keybind profile rows and active profile display were built once and never refreshed. Now uses RegisterWidgetRefresh callback to update all dynamic elements (active indicator, spec icons, profile names, colors) when RefreshPage is called. Adds PLAYER_SPECIALIZATION_CHANGED listener to trigger RefreshPage so the keybind section updates immediately when switching specs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EUI_ActionBars_Options.lua | 147 ++++++++++++------ 1 file changed, 97 insertions(+), 50 deletions(-) diff --git a/EllesmereUIActionBars/EUI_ActionBars_Options.lua b/EllesmereUIActionBars/EUI_ActionBars_Options.lua index 66da359e..a9a82af9 100644 --- a/EllesmereUIActionBars/EUI_ActionBars_Options.lua +++ b/EllesmereUIActionBars/EUI_ActionBars_Options.lua @@ -1441,6 +1441,8 @@ initFrame:SetScript("OnEvent", function(self) ----------------------------------------------------------------------- -- Keybind Profiles — active profile + per-spec assignments + -- All dynamic elements refresh via RegisterWidgetRefresh so + -- spec switches update the display without a full page rebuild. ----------------------------------------------------------------------- do local CONTENT_PAD = 45 @@ -1455,7 +1457,6 @@ initFrame:SetScript("OnEvent", function(self) activeRow:SetSize(rowW, 40) activeRow:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) - -- Alternating row bg local arBg = activeRow:CreateTexture(nil, "BACKGROUND") arBg:SetAllPoints() arBg:SetColorTexture(0, 0, 0, 0.10) @@ -1467,14 +1468,6 @@ initFrame:SetScript("OnEvent", function(self) local arValue = EllesmereUI.MakeFont(activeRow, 13, nil, 1, 1, 1) arValue:SetPoint("RIGHT", activeRow, "RIGHT", -SIDE_PAD, 0) - local profileName = EllesmereUI:GetCurrentSpecKeybindProfile() - if profileName then - arValue:SetTextColor(aR, aG, aB, 1) - arValue:SetText(profileName) - else - arValue:SetTextColor(1, 1, 1, 0.3) - arValue:SetText("None") - end activeRow:SetScript("OnEnter", function(self) arBg:SetColorTexture(0, 0, 0, 0.18) @@ -1491,102 +1484,156 @@ initFrame:SetScript("OnEvent", function(self) y = y - 40 - -- Per-spec rows + -- Per-spec rows — build once, update via refresh callback local numSpecs = GetNumSpecializations and GetNumSpecializations() or 0 if not EllesmereUIDB then EllesmereUIDB = {} end if not EllesmereUIDB.specKeybindProfiles then EllesmereUIDB.specKeybindProfiles = {} end - local currentSpecIdx = GetSpecialization and GetSpecialization() or 0 - local currentSpecID = currentSpecIdx > 0 and GetSpecializationInfo(currentSpecIdx) or nil + -- Store references for refresh + local specRows = {} -- { specID, specName, sIcon, sLabel, sVal, activeMark, sRow } for i = 1, numSpecs do local specID, specName, _, specIcon = GetSpecializationInfo(i) if specID and specName then - local isActive = (specID == currentSpecID) local sRow = CreateFrame("Frame", nil, parent) sRow:SetSize(rowW, 34) sRow:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) - -- Alternating bg local sBg = sRow:CreateTexture(nil, "BACKGROUND") sBg:SetAllPoints() - sBg:SetColorTexture(0, 0, 0, (i % 2 == 0) and 0.10 or 0.05) local bgAlphaBase = (i % 2 == 0) and 0.10 or 0.05 + sBg:SetColorTexture(0, 0, 0, bgAlphaBase) - -- Active spec indicator (thin accent bar on left edge) - if isActive then - local activeMark = sRow:CreateTexture(nil, "OVERLAY") - activeMark:SetSize(2, 20) - activeMark:SetPoint("LEFT", sRow, "LEFT", 4, 0) - activeMark:SetColorTexture(aR, aG, aB, 0.7) - end + -- Active spec indicator (always created, shown/hidden by refresh) + local activeMark = sRow:CreateTexture(nil, "OVERLAY") + activeMark:SetSize(2, 20) + activeMark:SetPoint("LEFT", sRow, "LEFT", 4, 0) + activeMark:SetColorTexture(aR, aG, aB, 0.7) + activeMark:Hide() - -- Spec icon local sIcon = sRow:CreateTexture(nil, "OVERLAY") sIcon:SetSize(16, 16) sIcon:SetPoint("LEFT", sRow, "LEFT", SIDE_PAD, 0) sIcon:SetTexture(specIcon) - if not isActive then sIcon:SetDesaturated(true); sIcon:SetAlpha(0.5) end - -- Spec name local sLabel = EllesmereUI.MakeFont(sRow, 12, nil, 1, 1, 1) sLabel:SetPoint("LEFT", sIcon, "RIGHT", 8, 0) - if isActive then - sLabel:SetTextColor(1, 1, 1, 0.85) - else - sLabel:SetTextColor(1, 1, 1, 0.45) - end sLabel:SetText(specName) - -- Assigned profile value (right-aligned) local sVal = EllesmereUI.MakeFont(sRow, 12, nil, 1, 1, 1) sVal:SetPoint("RIGHT", sRow, "RIGHT", -SIDE_PAD, 0) - local assignedProfile = EllesmereUIDB.specKeybindProfiles[specID] - if assignedProfile then - if isActive then - sVal:SetTextColor(aR, aG, aB, 1) - else - sVal:SetTextColor(aR, aG, aB, 0.5) - end - sVal:SetText(assignedProfile) - else - sVal:SetTextColor(1, 1, 1, 0.2) - sVal:SetText("not set") - end - -- Hover effect + tooltip + -- Hover (tooltip is rebuilt dynamically via current state) sRow:SetScript("OnEnter", function(self) sBg:SetColorTexture(0, 0, 0, bgAlphaBase + 0.08) sLabel:SetTextColor(1, 1, 1, 1) - if not isActive then sIcon:SetAlpha(0.8) end + sIcon:SetDesaturated(false); sIcon:SetAlpha(1) + + local curIdx = GetSpecialization and GetSpecialization() or 0 + local curSID = curIdx > 0 and GetSpecializationInfo(curIdx) or nil + local isNowActive = (specID == curSID) + local ap = EllesmereUIDB.specKeybindProfiles and EllesmereUIDB.specKeybindProfiles[specID] local tip - if assignedProfile then - tip = specName .. " uses the \"" .. assignedProfile .. "\" keybind profile.\n\n" + if ap then + tip = specName .. " uses the \"" .. ap .. "\" keybind profile.\n\n" .. "This profile was auto-saved the last time you used Keybind Mode on this spec." else tip = specName .. " has no keybind profile yet.\n\n" .. "Enter Keybind Mode while this spec is active to create one automatically." end - if isActive then + if isNowActive then tip = tip .. "\n\n|cff0cd29fThis is your current spec.|r" end EllesmereUI.ShowWidgetTooltip(self, tip, { anchor = "below" }) end) sRow:SetScript("OnLeave", function() sBg:SetColorTexture(0, 0, 0, bgAlphaBase) - if isActive then + -- Restore correct state via the same logic as refresh + local curIdx = GetSpecialization and GetSpecialization() or 0 + local curSID = curIdx > 0 and GetSpecializationInfo(curIdx) or nil + local isNowActive = (specID == curSID) + if isNowActive then sLabel:SetTextColor(1, 1, 1, 0.85) + sIcon:SetDesaturated(false); sIcon:SetAlpha(1) else sLabel:SetTextColor(1, 1, 1, 0.45) - sIcon:SetAlpha(0.5) + sIcon:SetDesaturated(true); sIcon:SetAlpha(0.5) end EllesmereUI.HideWidgetTooltip() end) + specRows[#specRows + 1] = { + specID = specID, specName = specName, + sIcon = sIcon, sLabel = sLabel, sVal = sVal, + activeMark = activeMark, sRow = sRow, + } + y = y - 34 end end + -- Refresh callback: updates all keybind profile display elements + EllesmereUI.RegisterWidgetRefresh(function() + local curIdx = GetSpecialization and GetSpecialization() or 0 + local curSID = curIdx > 0 and GetSpecializationInfo(curIdx) or nil + + -- Update active profile header + local pName = EllesmereUI:GetCurrentSpecKeybindProfile() + if pName then + arValue:SetTextColor(aR, aG, aB, 1) + arValue:SetText(pName) + else + arValue:SetTextColor(1, 1, 1, 0.3) + arValue:SetText("None") + end + + -- Update per-spec rows + for _, sr in ipairs(specRows) do + local isNowActive = (sr.specID == curSID) + local ap = EllesmereUIDB.specKeybindProfiles + and EllesmereUIDB.specKeybindProfiles[sr.specID] + + -- Active indicator + if isNowActive then + sr.activeMark:Show() + else + sr.activeMark:Hide() + end + + -- Icon + sr.sIcon:SetDesaturated(not isNowActive) + sr.sIcon:SetAlpha(isNowActive and 1 or 0.5) + + -- Label + sr.sLabel:SetTextColor(1, 1, 1, isNowActive and 0.85 or 0.45) + + -- Profile value + if ap then + sr.sVal:SetTextColor(aR, aG, aB, isNowActive and 1 or 0.5) + sr.sVal:SetText(ap) + else + sr.sVal:SetTextColor(1, 1, 1, 0.2) + sr.sVal:SetText("not set") + end + end + end) + + -- Listen for spec changes to trigger a widget refresh + local specWatcher = CreateFrame("Frame") + specWatcher:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED") + specWatcher:SetScript("OnEvent", function(_, _, unit) + if unit ~= "player" then return end + -- Defer slightly so WoW's spec data is fully updated + C_Timer.After(0.2, function() + if EllesmereUI.RefreshPage then + EllesmereUI:RefreshPage() + end + end) + end) + + -- Run refresh once now to set initial state + EllesmereUI:RefreshPage() + y = y - 8 end From 0a6bc4af47b8107bc2f281dbb2a397df1173249f Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Tue, 31 Mar 2026 20:12:50 -0400 Subject: [PATCH 6/7] docs: add EllesmereUIPreyHunt design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-31-prey-hunt-tracker-design.md | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-31-prey-hunt-tracker-design.md diff --git a/docs/superpowers/specs/2026-03-31-prey-hunt-tracker-design.md b/docs/superpowers/specs/2026-03-31-prey-hunt-tracker-design.md new file mode 100644 index 00000000..a5259acd --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-prey-hunt-tracker-design.md @@ -0,0 +1,196 @@ +# EllesmereUIPreyHunt — Design Spec + +## Overview + +A child addon module that tracks Prey Hunt progress in EllesmereUI style. Displays hunt stage progression as a sleek bar, auto-shows only when the player is in active prey content, integrates with unlock mode for positioning, and can be toggled on/off from the EllesmereUI module system. + +## Goals + +- Track prey hunt stage progression via `C_UIWidgetManager` +- Display progress in three selectable modes: stage segments, smooth bar, compact indicator +- Auto-show when in prey hunt content, auto-hide when not +- Fully movable via EllesmereUI unlock mode +- Toggleable as a module inside EllesmereUI options +- Zero impact on existing EllesmereUI features when disabled + +## Architecture + +### Module Pattern + +- **Addon**: `EllesmereUIPreyHunt/` — independent child addon folder +- **Framework**: `EUILite.NewAddon("EllesmereUIPreyHunt")` + - `OnInitialize`: fires on ADDON_LOADED, loads DB + - `OnEnable`: fires on PLAYER_LOGIN, builds frames, starts listening +- **Database**: `EUILite.NewDB("EllesmereUIDB", defaults)` — settings stored at `EllesmereUIDB.profiles[name].addons.EllesmereUIPreyHunt` +- **Module toggle**: Registers with EllesmereUI module system so it appears in the modules list and can be enabled/disabled from options +- **Unlock mode**: Registers via `EllesmereUI:RegisterUnlockElements()` so the bar is draggable/positionable + +### Files + +| File | Purpose | +|------|---------| +| `EllesmereUIPreyHunt.toc` | TOC with `## Dependencies: EllesmereUI` | +| `EllesmereUIPreyHunt.lua` | Core addon: data layer, frame creation, 3 display modes, auto-show/hide, unlock registration | +| `EUI_PreyHunt_Options.lua` | Options page in EllesmereUI panel: display mode, bar size, opacity | + +### No existing files modified + +This is a fully additive module. No changes to any core EllesmereUI file or any other child addon. + +## Data Layer + +### Source: C_UIWidgetManager + +The Prey Hunt system exposes progress as **stage transitions** through Blizzard's widget system, not raw percentages. + +- **Detection**: Listen to `UPDATE_UI_WIDGET` event, filter for prey hunt widget IDs +- **Stage data**: `C_UIWidgetManager.GetFillUpFramesWidgetVisualizationInfo(widgetID)` returns fill-up frame data with stage/segment info +- **Fallback**: Also check `C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo()` and `C_UIWidgetManager.GetIconAndTextWidgetVisualizationInfo()` as Blizzard may expose hunt data through multiple widget types +- **Discovery**: On zone enter, iterate `C_UIWidgetManager.GetAllWidgetsBySetID(C_UIWidgetManager.GetTopCenterWidgetSetID())` to find active prey hunt widgets + +### Zone Awareness + +- Listen to `ZONE_CHANGED_NEW_AREA`, `ZONE_CHANGED`, `PLAYER_ENTERING_WORLD` +- Query `C_Map.GetBestMapForUnit("player")` to determine if player is in a Midnight prey hunt zone +- Auto-show the bar when: active hunt detected AND in relevant zone +- Auto-hide when: hunt completes, player leaves zone, or no active hunt widget found + +### Hunt State + +``` +state = { + active = bool, -- is a hunt in progress + stage = number, -- current stage (1-based) + maxStages = number, -- total stages in this hunt + zoneName = string, -- zone where hunt is active + difficulty = string, -- "Normal" / "Heroic" / "Nightmare" + widgetID = number, -- tracked widget ID +} +``` + +## Display Modes + +User selects one mode in options. Default: **Smooth Bar**. + +### 1. Smooth Bar (default) + +- Horizontal bar matching EllesmereUI XP/Rep bar style +- Background: dark (`0.05, 0.07, 0.09, 0.80`) +- Fill: accent color via `EllesmereUI.GetAccentColor()` +- Animated fill transitions (lerp over 0.3s when stage changes) +- Fill percentage: `currentStage / maxStages` +- Stage text right-aligned inside bar: "Stage 3/5" +- Bar dimensions default: 220px wide, 16px tall +- Border: 1px, white, 0.10 alpha (EllesmereUI standard) +- Hunt info label above bar: zone name + difficulty in dim text + +### 2. Stage Segments + +- Same bar frame, but divided into `maxStages` equal segments with 1px gaps +- Completed segments: filled with accent color +- Incomplete segments: dark background +- Current segment: partial fill or pulsing accent glow +- No text overlay (clean visual) +- Small stage count below: "3 / 5" in dim text + +### 3. Compact Indicator + +- Small frame: ~32px icon + text beside it +- Icon: prey hunt themed (use Blizzard's hunt texture or a generic eye/crosshair) +- Text: "3/5" in accent color, spec name in dim text below +- Hover tooltip: full hunt details (zone, difficulty, stage, description) +- Tooltip uses `EllesmereUI.ShowWidgetTooltip()` + +## Frame Hierarchy + +``` +EllesmereUIPreyHuntFrame (main anchor, movable via unlock mode) + ├── background texture + ├── fill texture (smooth bar mode) + ├── segment frames[] (stage segment mode) + ├── icon texture (compact mode) + ├── stage label (FontString) + ├── info label (FontString — zone + difficulty) + └── border (via MakeBorder) +``` + +Only the active mode's elements are shown; others are hidden. + +## Options Page + +Registered as a module page in EllesmereUI options panel. + +### Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `enabled` | toggle | true | Module enabled/disabled | +| `displayMode` | dropdown | "bar" | "bar" / "segments" / "compact" | +| `barWidth` | slider | 220 | Bar width in pixels (100-400) | +| `barHeight` | slider | 16 | Bar height in pixels (8-32) | +| `opacity` | slider | 1.0 | Overall frame opacity (0.3-1.0) | +| `showLabel` | toggle | true | Show zone + difficulty label above bar | +| `animateFill` | toggle | true | Animate bar fill transitions | + +### Layout + +Uses `W:SectionHeader`, `W:DualRow`, `W:Dropdown`, `W:Slider`, `W:Toggle` — standard EllesmereUI widget factory. + +## Unlock Mode Integration + +Register with unlock mode so the bar is movable: + +```lua +EllesmereUI:RegisterUnlockElements({ + { + key = "PreyHunt", + label = "Prey Hunt", + frame = mainFrame, + savePosition = function() ... end, + loadPosition = function() ... end, + clearPosition = function() ... end, + } +}) +``` + +Position saved to `EllesmereUIDB.unlockAnchors["PreyHunt"]`. + +## Auto-Show/Hide Logic + +``` +on ZONE_CHANGED / PLAYER_ENTERING_WORLD / UPDATE_UI_WIDGET: + if module disabled → hide, return + scan widgets for prey hunt data + if active hunt found: + update state + show frame + refresh display + else: + hide frame +``` + +No polling — purely event-driven. + +## Visual Style + +- All colors via `EllesmereUI.GetAccentColor()` — matches active theme +- Font: Expressway (via `EllesmereUI.GetFontPath()` or hardcoded path) +- Border: `EllesmereUI.MakeBorder()` at 1px, white, 0.10 alpha +- Tooltips: `EllesmereUI.ShowWidgetTooltip()` / `HideWidgetTooltip()` +- Text labels: `EllesmereUI.MakeFont()` +- Background: dark flat, consistent with action bars / resource bars + +## Edge Cases + +- **No active hunt**: Frame stays hidden. No errors. +- **Hunt completes**: Widget update fires, state clears, frame hides with fade-out. +- **Zone transition during hunt**: Frame hides on zone leave, re-shows on zone return (hunt is still active server-side). +- **Combat**: Bar is display-only (no protected API calls), safe during combat. +- **Module disabled**: All frames hidden, events unregistered, zero overhead. +- **Widget ID changes between patches**: Discovery loop re-scans on each zone enter rather than hardcoding IDs. + +## Branch Strategy + +- New branch `prey-hunt` from `keybind-system` (or from `main` if we want it independent) +- Fully isolated — can be deleted/disabled without affecting keybind work +- If successful, can be proposed as a separate PR to the author From aac451ebcf1b8841e598a1a0db2040e818229ce6 Mon Sep 17 00:00:00 2001 From: Daniel_ALD Date: Wed, 1 Apr 2026 16:20:39 -0400 Subject: [PATCH 7/7] docs: add EllesmereUIPreyHunt implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-31-prey-hunt-tracker.md | 796 ++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-31-prey-hunt-tracker.md diff --git a/docs/superpowers/plans/2026-03-31-prey-hunt-tracker.md b/docs/superpowers/plans/2026-03-31-prey-hunt-tracker.md new file mode 100644 index 00000000..1ada0a19 --- /dev/null +++ b/docs/superpowers/plans/2026-03-31-prey-hunt-tracker.md @@ -0,0 +1,796 @@ +# EllesmereUIPreyHunt Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build an EllesmereUI child addon that tracks Prey Hunt progress with three display modes (smooth bar, stage segments, compact indicator), auto-shows during active hunts, and integrates with unlock mode. + +**Architecture:** Isolated child addon `EllesmereUIPreyHunt/` using `EUILite.NewAddon()`. Data layer reads `C_UIWidgetManager` for hunt stages (with stub detection until Midnight ships). Display frame auto-shows in prey zones, registers with unlock mode for positioning. Options page in EllesmereUI panel. + +**Tech Stack:** WoW Lua, EllesmereUI framework (EUILite, Widgets, PP, MakeBorder, MakeFont, ShowWidgetTooltip, RegisterUnlockElements, RegisterModule) + +--- + +## File Structure + +| File | Responsibility | +|------|---------------| +| `EllesmereUIPreyHunt/EllesmereUIPreyHunt.toc` | TOC, dependencies, load order | +| `EllesmereUIPreyHunt/EllesmereUIPreyHunt.lua` | Core: data layer, frame, 3 display modes, auto-show, unlock registration | +| `EllesmereUIPreyHunt/EUI_PreyHunt_Options.lua` | Options page: mode picker, size, opacity, label toggle | + +--- + +### Task 1: Create branch and TOC file + +**Files:** +- Create: `EllesmereUIPreyHunt/EllesmereUIPreyHunt.toc` + +- [ ] **Step 1: Create feature branch from main** + +```bash +cd c:/Users/danie/Documents/GitHub/EllesmereUI +git checkout main +git checkout -b prey-hunt +``` + +- [ ] **Step 2: Create the TOC file** + +Create `EllesmereUIPreyHunt/EllesmereUIPreyHunt.toc`: + +``` +## Interface: 120000, 120001 +## Title: |cff0cd29fEllesmereUI|r Prey Hunt +## Category: |cff0cd29fEllesmere|rUI +## Group: EllesmereUI +## Notes: Prey Hunt progress tracker in EllesmereUI style +## Author: bulshack +## Version: 1.0 +## Dependencies: EllesmereUI +## SavedVariables: EllesmereUIPreyHuntDB +## IconTexture: Interface\AddOns\EllesmereUI\media\eg-logo.tga + +EllesmereUIPreyHunt.lua +EUI_PreyHunt_Options.lua +``` + +- [ ] **Step 3: Add to .pkgmeta move-folders** + +Add this line to the `move-folders:` section in `.pkgmeta`: + +```yaml + EllesmereUI/EllesmereUIPreyHunt: EllesmereUIPreyHunt +``` + +- [ ] **Step 4: Commit** + +```bash +git add EllesmereUIPreyHunt/EllesmereUIPreyHunt.toc .pkgmeta +git commit -m "feat(prey-hunt): add TOC and pkgmeta entry" +``` + +--- + +### Task 2: Core addon scaffold — data layer and state + +**Files:** +- Create: `EllesmereUIPreyHunt/EllesmereUIPreyHunt.lua` + +- [ ] **Step 1: Write the full core addon file** + +Create `EllesmereUIPreyHunt/EllesmereUIPreyHunt.lua`: + +```lua +-------------------------------------------------------------------------------- +-- EllesmereUIPreyHunt.lua +-- Prey Hunt progress tracker for EllesmereUI +-------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... +local EPH = EllesmereUI.Lite.NewAddon(ADDON_NAME) +ns.EPH = EPH + +local PP = EllesmereUI.PP +local floor = math.floor + +-------------------------------------------------------------------------------- +-- Constants +-------------------------------------------------------------------------------- +local FONT_PATH = "Interface\\AddOns\\EllesmereUI\\media\\fonts\\Expressway.ttf" +local MEDIA_PATH = "Interface\\AddOns\\EllesmereUI\\media\\" + +-------------------------------------------------------------------------------- +-- Defaults +-------------------------------------------------------------------------------- +local DEFAULTS = { + profile = { + enabled = true, + displayMode = "bar", -- "bar" | "segments" | "compact" + barWidth = 220, + barHeight = 16, + opacity = 1.0, + showLabel = true, + animateFill = true, + }, +} + +-------------------------------------------------------------------------------- +-- Hunt State +-------------------------------------------------------------------------------- +local huntState = { + active = false, + stage = 0, + maxStages = 5, + zoneName = "", + difficulty = "", + widgetID = nil, +} + +-------------------------------------------------------------------------------- +-- Widget Detection (stub — will be wired to real IDs on Midnight PTR/launch) +-- +-- The Prey Hunt system uses C_UIWidgetManager. Until Midnight ships, we +-- provide a stub that can be tested with /ephtest. Replace the body of +-- ScanForPreyHuntWidget() once real widget IDs are known. +-------------------------------------------------------------------------------- + +-- TODO: Replace with real widget type + ID detection when Midnight is live. +-- Expected approach: +-- 1. On zone enter, iterate C_UIWidgetManager.GetAllWidgetsBySetID( +-- C_UIWidgetManager.GetTopCenterWidgetSetID()) +-- 2. Check each widget for prey-hunt-specific visualization type +-- (likely GetStatusBarWidgetVisualizationInfo or +-- GetFillUpFramesWidgetVisualizationInfo) +-- 3. Store the widgetID and read stage/max from the widget data + +local function ScanForPreyHuntWidget() + -- STUB: Always returns nil until we have real widget IDs. + -- When Midnight ships, this will scan C_UIWidgetManager for the + -- prey hunt widget and return { widgetID, stage, maxStages, zoneName, difficulty }. + return nil +end + +local function UpdateHuntStateFromWidget() + local data = ScanForPreyHuntWidget() + if data then + huntState.active = true + huntState.stage = data.stage or 0 + huntState.maxStages = data.maxStages or 5 + huntState.zoneName = data.zoneName or "" + huntState.difficulty = data.difficulty or "" + huntState.widgetID = data.widgetID + return true + end + huntState.active = false + return false +end + +-------------------------------------------------------------------------------- +-- Debug / Test Commands +-------------------------------------------------------------------------------- +local testMode = false +local testStage = 0 +local testMax = 5 + +local function SetTestHunt(stage, maxStages, zone, diff) + testMode = true + huntState.active = true + huntState.stage = stage or 3 + huntState.maxStages = maxStages or 5 + huntState.zoneName = zone or "Dawnbreaker Crest" + huntState.difficulty = diff or "Heroic" +end + +local function ClearTestHunt() + testMode = false + huntState.active = false + huntState.stage = 0 +end + +SLASH_EPHTEST1 = "/ephtest" +SlashCmdList.EPHTEST = function(msg) + local args = msg and msg:trim():lower() or "" + if args == "off" or args == "clear" then + ClearTestHunt() + EPH:Refresh() + print("|cff0cd29f[PreyHunt]|r Test mode off.") + return + end + local stage = tonumber(args) + if stage then + SetTestHunt(stage, testMax) + else + -- Toggle: advance stage or start at 1 + testStage = testStage + 1 + if testStage > testMax then testStage = 1 end + SetTestHunt(testStage, testMax) + end + EPH:Refresh() + print("|cff0cd29f[PreyHunt]|r Test: stage " .. huntState.stage .. "/" .. huntState.maxStages) +end + +-------------------------------------------------------------------------------- +-- Accent Color Helper +-------------------------------------------------------------------------------- +local function GetAccent() + if EllesmereUI.GetAccentColor then + return EllesmereUI.GetAccentColor() + end + return 0.047, 0.824, 0.624 +end + +-------------------------------------------------------------------------------- +-- Main Frame +-------------------------------------------------------------------------------- +local mainFrame = nil +local fillBar, fillAnim +local segmentFrames = {} +local compactIcon, compactText +local stageLabel, infoLabel + +local function GetDB() + return EPH.db and EPH.db.profile +end + +local function CreateMainFrame() + if mainFrame then return end + local db = GetDB() + if not db then return end + + mainFrame = CreateFrame("Frame", "EllesmereUIPreyHuntFrame", UIParent) + mainFrame:SetSize(db.barWidth, db.barHeight) + mainFrame:SetPoint("CENTER", UIParent, "CENTER", 0, -200) + mainFrame:SetFrameStrata("MEDIUM") + mainFrame:SetFrameLevel(5) + mainFrame:Hide() + + -- Background + local bg = mainFrame:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.05, 0.07, 0.09, 0.80) + mainFrame._bg = bg + + -- Border + mainFrame._border = EllesmereUI.MakeBorder(mainFrame, 1, 1, 1, 0.10) + + -- Fill bar (smooth bar mode) + fillBar = mainFrame:CreateTexture(nil, "ARTWORK") + fillBar:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 1, -1) + fillBar:SetPoint("BOTTOMLEFT", mainFrame, "BOTTOMLEFT", 1, 1) + local aR, aG, aB = GetAccent() + fillBar:SetColorTexture(aR, aG, aB, 0.85) + fillBar:SetWidth(0) + mainFrame._fill = fillBar + + -- Stage label (inside bar, right side) + stageLabel = mainFrame:CreateFontString(nil, "OVERLAY") + stageLabel:SetFont(FONT_PATH, 10, "OUTLINE") + stageLabel:SetPoint("RIGHT", mainFrame, "RIGHT", -6, 0) + stageLabel:SetTextColor(1, 1, 1, 0.9) + mainFrame._stageLabel = stageLabel + + -- Info label (above bar — zone + difficulty) + infoLabel = mainFrame:CreateFontString(nil, "OVERLAY") + infoLabel:SetFont(FONT_PATH, 10, "") + infoLabel:SetPoint("BOTTOMLEFT", mainFrame, "TOPLEFT", 0, 4) + infoLabel:SetTextColor(1, 1, 1, 0.45) + mainFrame._infoLabel = infoLabel + + -- Compact mode: icon + text (hidden by default) + compactIcon = mainFrame:CreateTexture(nil, "OVERLAY") + compactIcon:SetSize(20, 20) + compactIcon:SetPoint("LEFT", mainFrame, "LEFT", 4, 0) + compactIcon:SetTexture(136814) -- generic crosshair icon; replace with prey icon later + compactIcon:SetVertexColor(aR, aG, aB, 0.9) + compactIcon:Hide() + mainFrame._compactIcon = compactIcon + + compactText = mainFrame:CreateFontString(nil, "OVERLAY") + compactText:SetFont(FONT_PATH, 13, "") + compactText:SetPoint("LEFT", compactIcon, "RIGHT", 6, 0) + compactText:SetTextColor(aR, aG, aB, 1) + compactText:Hide() + mainFrame._compactText = compactText + + -- Tooltip on hover + mainFrame:SetScript("OnEnter", function(self) + if not huntState.active then return end + local tip = "Prey Hunt" + if huntState.zoneName ~= "" then + tip = tip .. "\n" .. huntState.zoneName + end + if huntState.difficulty ~= "" then + tip = tip .. " |cff0cd29f(" .. huntState.difficulty .. ")|r" + end + tip = tip .. "\n\nProgress: Stage " .. huntState.stage .. " / " .. huntState.maxStages + if huntState.stage >= huntState.maxStages then + tip = tip .. "\n\n|cff0cd29fPrey location revealed!|r" + else + tip = tip .. "\n\nComplete quests, kill rares, loot treasures,\nand find traps to fill the bar." + end + EllesmereUI.ShowWidgetTooltip(self, tip, { anchor = "below" }) + end) + mainFrame:SetScript("OnLeave", function() + EllesmereUI.HideWidgetTooltip() + end) +end + +-------------------------------------------------------------------------------- +-- Segment Frames (stage segment mode) +-------------------------------------------------------------------------------- +local function CreateOrUpdateSegments() + local db = GetDB() + if not db or not mainFrame then return end + local aR, aG, aB = GetAccent() + local max = huntState.maxStages + if max < 1 then max = 5 end + local gap = 1 + local totalGaps = (max - 1) * gap + 2 -- 1px border each side + local segW = (db.barWidth - totalGaps) / max + local segH = db.barHeight - 2 -- 1px border top/bottom + + -- Hide extras + for i = max + 1, #segmentFrames do + segmentFrames[i]:Hide() + end + + for i = 1, max do + local seg = segmentFrames[i] + if not seg then + seg = mainFrame:CreateTexture(nil, "ARTWORK") + segmentFrames[i] = seg + end + seg:ClearAllPoints() + seg:SetSize(segW, segH) + local xOff = 1 + (i - 1) * (segW + gap) + seg:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", xOff, -1) + + if i <= huntState.stage then + seg:SetColorTexture(aR, aG, aB, 0.85) + else + seg:SetColorTexture(1, 1, 1, 0.04) + end + seg:Show() + end +end + +-------------------------------------------------------------------------------- +-- Display Refresh +-------------------------------------------------------------------------------- +local targetFillWidth = 0 +local currentFillWidth = 0 + +local function RefreshDisplay() + local db = GetDB() + if not db or not mainFrame then return end + if not huntState.active then + mainFrame:Hide() + return + end + + local aR, aG, aB = GetAccent() + local mode = db.displayMode + local barW = db.barWidth + local barH = db.barHeight + + mainFrame:SetSize(barW, barH) + mainFrame:SetAlpha(db.opacity) + + -- Info label + if db.showLabel and huntState.zoneName ~= "" then + local info = huntState.zoneName + if huntState.difficulty ~= "" then + info = info .. " |cff0cd29f" .. huntState.difficulty .. "|r" + end + infoLabel:SetText(info) + infoLabel:Show() + else + infoLabel:Hide() + end + + -- Hide all mode-specific elements first + fillBar:Hide() + stageLabel:Hide() + compactIcon:Hide() + compactText:Hide() + for _, seg in ipairs(segmentFrames) do seg:Hide() end + mainFrame._bg:Show() + mainFrame._border:Show() + + local pct = huntState.maxStages > 0 and (huntState.stage / huntState.maxStages) or 0 + + if mode == "bar" then + -- Smooth bar + fillBar:SetColorTexture(aR, aG, aB, 0.85) + targetFillWidth = (barW - 2) * pct + if not db.animateFill then + currentFillWidth = targetFillWidth + end + fillBar:SetWidth(math.max(currentFillWidth, 0.01)) + fillBar:Show() + + stageLabel:SetText(huntState.stage .. " / " .. huntState.maxStages) + stageLabel:Show() + + elseif mode == "segments" then + -- Stage segments + CreateOrUpdateSegments() + stageLabel:SetText(huntState.stage .. " / " .. huntState.maxStages) + stageLabel:SetPoint("RIGHT", mainFrame, "RIGHT", -6, 0) + stageLabel:Show() + + elseif mode == "compact" then + -- Compact: smaller frame, icon + text + mainFrame:SetSize(100, 28) + mainFrame._bg:SetColorTexture(0.05, 0.07, 0.09, 0.70) + + compactIcon:SetVertexColor(aR, aG, aB, 0.9) + compactIcon:Show() + + compactText:SetTextColor(aR, aG, aB, 1) + compactText:SetText(huntState.stage .. " / " .. huntState.maxStages) + compactText:Show() + + infoLabel:Hide() -- too small for label in compact + end + + mainFrame:Show() +end + +-- Smooth fill animation +local function OnUpdateFill(self, dt) + if not mainFrame or not mainFrame:IsShown() then return end + local db = GetDB() + if not db or db.displayMode ~= "bar" or not db.animateFill then return end + + if math.abs(currentFillWidth - targetFillWidth) > 0.5 then + local speed = 8 -- pixels per second multiplier + local delta = (targetFillWidth - currentFillWidth) * math.min(dt * speed, 1) + currentFillWidth = currentFillWidth + delta + fillBar:SetWidth(math.max(currentFillWidth, 0.01)) + end +end + +-------------------------------------------------------------------------------- +-- Public API +-------------------------------------------------------------------------------- +function EPH:Refresh() + if not testMode then + UpdateHuntStateFromWidget() + end + RefreshDisplay() +end + +-------------------------------------------------------------------------------- +-- Event Handling +-------------------------------------------------------------------------------- +local eventFrame = CreateFrame("Frame") + +local function OnEvent(self, event, ...) + local db = GetDB() + if not db or not db.enabled then + if mainFrame then mainFrame:Hide() end + return + end + + if event == "UPDATE_UI_WIDGET" then + local widgetInfo = ... + -- If we're tracking a specific widget, only refresh on that one + if huntState.widgetID and widgetInfo and widgetInfo.widgetID ~= huntState.widgetID then + return + end + end + + EPH:Refresh() +end + +-------------------------------------------------------------------------------- +-- Unlock Mode Registration +-------------------------------------------------------------------------------- +local function RegisterWithUnlockMode() + if not EllesmereUI or not EllesmereUI.RegisterUnlockElements then return end + + local function S() return GetDB() or {} end + + local function savePos(key, point, relPoint, x, y) + if not point then return end + local db = S() + db.unlockPos = { point = point, relPoint = relPoint or point, x = x, y = y } + if not EllesmereUI._unlockActive and mainFrame then + mainFrame:ClearAllPoints() + mainFrame:SetPoint(point, UIParent, relPoint or point, x, y) + end + end + + local function loadPos() + local pos = S().unlockPos + if not pos then return nil end + return { point = pos.point, relPoint = pos.relPoint or pos.point, x = pos.x, y = pos.y } + end + + local function clearPos() + local db = S() + db.unlockPos = nil + end + + local function applyPos() + local pos = S().unlockPos + if not pos or not mainFrame then return end + mainFrame:ClearAllPoints() + mainFrame:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) + end + + EllesmereUI:RegisterUnlockElements({ + { + key = "PreyHunt", + label = "Prey Hunt", + group = "Prey Hunt", + order = 600, + getFrame = function() return mainFrame end, + getSize = function() + local db = S() + return db.barWidth or 220, db.barHeight or 16 + end, + setWidth = function(_, w) + local db = S() + db.barWidth = floor(w + 0.5) + EPH:Refresh() + end, + setHeight = function(_, h) + local db = S() + db.barHeight = floor(h + 0.5) + EPH:Refresh() + end, + savePos = savePos, + loadPos = loadPos, + clearPos = clearPos, + applyPos = applyPos, + }, + }) +end + +-------------------------------------------------------------------------------- +-- Lifecycle +-------------------------------------------------------------------------------- +function EPH:OnInitialize() + self.db = EllesmereUI.Lite.NewDB("EllesmereUIPreyHuntDB", DEFAULTS, true) + + _G._EPH_AceDB = self.db + _G._EPH_Apply = function() EPH:Refresh() end + _G._EPH_RegisterUnlock = RegisterWithUnlockMode +end + +function EPH:OnEnable() + CreateMainFrame() + + -- Apply saved position + local db = GetDB() + if db and db.unlockPos then + local pos = db.unlockPos + mainFrame:ClearAllPoints() + mainFrame:SetPoint(pos.point, UIParent, pos.relPoint or pos.point, pos.x, pos.y) + end + + -- Fill animation ticker + mainFrame:SetScript("OnUpdate", OnUpdateFill) + + -- Events + eventFrame:RegisterEvent("UPDATE_UI_WIDGET") + eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA") + eventFrame:RegisterEvent("ZONE_CHANGED") + eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + eventFrame:SetScript("OnEvent", OnEvent) + + -- Register with unlock mode (deferred) + C_Timer.After(0.5, RegisterWithUnlockMode) + + -- Initial scan + C_Timer.After(1, function() EPH:Refresh() end) +end +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIPreyHunt/EllesmereUIPreyHunt.lua +git commit -m "feat(prey-hunt): core addon with data layer, 3 display modes, unlock registration" +``` + +--- + +### Task 3: Options page + +**Files:** +- Create: `EllesmereUIPreyHunt/EUI_PreyHunt_Options.lua` + +- [ ] **Step 1: Write the options page** + +Create `EllesmereUIPreyHunt/EUI_PreyHunt_Options.lua`: + +```lua +-------------------------------------------------------------------------------- +-- EUI_PreyHunt_Options.lua +-- Options page for EllesmereUIPreyHunt +-------------------------------------------------------------------------------- +local ADDON_NAME, ns = ... + +local PAGE_SETTINGS = "Settings" + +local initFrame = CreateFrame("Frame") +initFrame:RegisterEvent("PLAYER_LOGIN") +initFrame:SetScript("OnEvent", function(self) + self:UnregisterEvent("PLAYER_LOGIN") + + if not EllesmereUI or not EllesmereUI.RegisterModule then return end + + local db + C_Timer.After(0, function() db = _G._EPH_AceDB end) + + local function DB() + if not db then db = _G._EPH_AceDB end + return db and db.profile + end + + local function Refresh() + if _G._EPH_Apply then _G._EPH_Apply() end + end + + --------------------------------------------------------------------------- + -- Page Builder + --------------------------------------------------------------------------- + local function BuildSettingsPage(pageName, parent, yOffset) + local W = EllesmereUI.Widgets + local _, h + local y = yOffset + + --------------------------------------------------------------- + -- DISPLAY section + --------------------------------------------------------------- + _, h = W:SectionHeader(parent, "DISPLAY", y); y = y - h + + -- Row 1: Display Mode | Bar Opacity + _, h = W:DualRow(parent, y, + { type = "dropdown", text = "Display Mode", + values = { bar = "Smooth Bar", segments = "Stage Segments", compact = "Compact" }, + order = { "bar", "segments", "compact" }, + tooltip = "Smooth Bar: animated fill bar.\nStage Segments: divided chunks per stage.\nCompact: small icon with stage count.", + getValue = function() return DB().displayMode end, + setValue = function(v) DB().displayMode = v; Refresh(); EllesmereUI:RefreshPage() end }, + { type = "slider", text = "Opacity", min = 0.3, max = 1.0, step = 0.05, + tooltip = "Overall opacity of the prey hunt tracker.", + getValue = function() return DB().opacity end, + setValue = function(v) DB().opacity = v; Refresh() end } + ); y = y - h + + -- Row 2: Bar Width | Bar Height + _, h = W:DualRow(parent, y, + { type = "slider", text = "Bar Width", min = 100, max = 400, step = 1, + tooltip = "Width of the prey hunt bar in pixels.", + getValue = function() return DB().barWidth end, + setValue = function(v) DB().barWidth = v; Refresh() end }, + { type = "slider", text = "Bar Height", min = 8, max = 32, step = 1, + tooltip = "Height of the prey hunt bar in pixels.", + getValue = function() return DB().barHeight end, + setValue = function(v) DB().barHeight = v; Refresh() end } + ); y = y - h + + --------------------------------------------------------------- + -- OPTIONS section + --------------------------------------------------------------- + _, h = W:SectionHeader(parent, "OPTIONS", y); y = y - h + + -- Row 3: Show Label | Animate Fill + _, h = W:DualRow(parent, y, + { type = "toggle", text = "Show Zone Label", + tooltip = "Display the zone name and difficulty above the bar.", + getValue = function() return DB().showLabel end, + setValue = function(v) DB().showLabel = v; Refresh() end }, + { type = "toggle", text = "Animate Fill", + tooltip = "Smoothly animate the bar fill when the stage changes.", + getValue = function() return DB().animateFill end, + setValue = function(v) DB().animateFill = v; Refresh() end } + ); y = y - h + + --------------------------------------------------------------- + -- TEST section + --------------------------------------------------------------- + _, h = W:SectionHeader(parent, "TESTING", y); y = y - h + + -- Info text + do + local CONTENT_PAD = 45 + local infoRow = CreateFrame("Frame", nil, parent) + infoRow:SetSize(parent:GetWidth() - CONTENT_PAD * 2, 40) + infoRow:SetPoint("TOPLEFT", parent, "TOPLEFT", CONTENT_PAD, y) + + local infoText = EllesmereUI.MakeFont(infoRow, 11, nil, 1, 1, 1) + infoText:SetAlpha(0.45) + infoText:SetPoint("LEFT", infoRow, "LEFT", 20, 0) + infoText:SetText("Type /ephtest to simulate hunt progress. /ephtest off to stop.") + y = y - 40 + end + + return y + end + + --------------------------------------------------------------------------- + -- Register Module + --------------------------------------------------------------------------- + EllesmereUI:RegisterModule("EllesmereUIPreyHunt", { + title = "Prey Hunt", + description = "Track Prey Hunt progress in EllesmereUI style.", + pages = { PAGE_SETTINGS }, + buildPage = function(pageName, parent, yOffset) + if pageName == PAGE_SETTINGS then + return BuildSettingsPage(pageName, parent, yOffset) + end + end, + }) +end) +``` + +- [ ] **Step 2: Commit** + +```bash +git add EllesmereUIPreyHunt/EUI_PreyHunt_Options.lua +git commit -m "feat(prey-hunt): options page with display mode, size, opacity, labels" +``` + +--- + +### Task 4: Deploy to live WoW and test + +**Files:** +- No new files — copy to AddOns folder + +- [ ] **Step 1: Copy addon to WoW AddOns** + +```bash +mkdir -p "D:/World of Warcraft/_retail_/Interface/AddOns/EllesmereUIPreyHunt" +cp "c:/Users/danie/Documents/GitHub/EllesmereUI/EllesmereUIPreyHunt/"* "D:/World of Warcraft/_retail_/Interface/AddOns/EllesmereUIPreyHunt/" +``` + +- [ ] **Step 2: Verify files are in place** + +```bash +ls "D:/World of Warcraft/_retail_/Interface/AddOns/EllesmereUIPreyHunt/" +``` + +Expected output: `EllesmereUIPreyHunt.lua EllesmereUIPreyHunt.toc EUI_PreyHunt_Options.lua` + +- [ ] **Step 3: Test in-game** + +1. `/reload` in WoW +2. Open EllesmereUI options — "Prey Hunt" should appear as a module +3. Type `/ephtest` — the bar should appear with stage 1/5 +4. Type `/ephtest` again — advances to 2/5, 3/5, etc. +5. Type `/ephtest off` — bar hides +6. Change display mode in options — bar style should change +7. Enter unlock mode — "Prey Hunt" element should be draggable +8. Adjust sliders for width/height/opacity — bar updates live + +- [ ] **Step 4: Commit any fixes needed** + +```bash +git add -A +git commit -m "fix(prey-hunt): adjustments from in-game testing" +``` + +--- + +### Task 5: Push and final verification + +- [ ] **Step 1: Push branch** + +```bash +cd c:/Users/danie/Documents/GitHub/EllesmereUI +git push -u origin prey-hunt +``` + +- [ ] **Step 2: Verify branch on GitHub** + +```bash +gh browse -b prey-hunt +``` + +- [ ] **Step 3: Verify module can be disabled** + +In-game: disable "Prey Hunt" in EllesmereUI module list. Confirm the bar disappears and no Lua errors. Re-enable and confirm it comes back.