Files
2026-08-05 15:54:45 +02:00

457 lines
19 KiB
Lua

-- TotemBar - options.lua
-- Standalone options panel (no Blizzard InterfaceOptions integration). Built
-- once (lazy) on first ToggleOptions; repopulated from TotemBarDB on show.
-- Opened by the minimap button's left-click and by /tb options. WoW-API
-- file (parse-checked only). Widget wiring uses 1.12 templates:
-- UICheckButtonTemplate (GetChecked() -> 1/nil) and OptionsSliderTemplate
-- (SetMinMaxValues before SetValue; $parentLow/$parentHigh/$parentText).
TotemBar = TotemBar or {}
local ChatOut = DEFAULT_CHAT_FRAME or ChatFrame1
local optionsFrame = nil
-- Applies pfUI's font to a FontString (the main "matches pfUI" tweak) at the
-- given size, or falls back to a small game font when pfUI is absent.
local function ApplyFont(fontString, size)
if not fontString then
return
end
if pfUI and pfUI.font_default then
local sz = size or (pfUI_config and pfUI_config.global and pfUI_config.global.font_size) or 12
fontString:SetFont(pfUI.font_default, sz)
else
fontString:SetFontObject(GameFontNormalSmall)
end
end
-- Adds a wrapped explanatory GameTooltip on hover. Chains any existing
-- OnEnter/OnLeave (e.g. pfUI's SkinButton hover-highlight) instead of
-- replacing it, so skinning still works. No HookScript on 1.12.
local function AddTooltip(widget, text)
if not widget or not text then
return
end
widget.tbTip = text
local oldEnter = widget:GetScript("OnEnter")
local oldLeave = widget:GetScript("OnLeave")
widget:SetScript("OnEnter", function()
if oldEnter then oldEnter() end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(this.tbTip, 1, 1, 1, 1, 1)
GameTooltip:Show()
end)
widget:SetScript("OnLeave", function()
if oldLeave then oldLeave() end
GameTooltip:Hide()
end)
end
-- Unique widget-name counters (template child labels resolve via getglobal,
-- so every widget needs a non-nil, unique frame name).
local cbIndex = 0
local slIndex = 0
-- Factory: a labeled checkbox wired to getter/setter. getter()->boolean,
-- setter(boolean). Returns the CheckButton.
local function CreateCheckbox(parent, label, getter, setter)
cbIndex = cbIndex + 1
local name = "TotemBarOptCheck" .. cbIndex
local cb = CreateFrame("CheckButton", name, parent, "UICheckButtonTemplate")
cb:SetWidth(24)
cb:SetHeight(24)
local lbl = getglobal(name .. "Text")
if lbl then
lbl:SetText(label)
ApplyFont(lbl)
end
cb.tbGet = getter
cb.tbSet = setter
cb:SetScript("OnClick", function()
this.tbSet(this:GetChecked() == 1)
end)
if pfUI and pfUI.api and pfUI.api.SkinCheckbox then
pfUI.api.SkinCheckbox(cb)
end
return cb
end
-- Factory: a labeled slider wired to getter/setter over [minVal,maxVal] in
-- `step` increments. `fmt` is a printf pattern for the live value text
-- (e.g. "UI size: %.2f"). Returns the Slider.
local function CreateSlider(parent, label, minVal, maxVal, step, fmt, getter, setter)
slIndex = slIndex + 1
local name = "TotemBarOptSlider" .. slIndex
local sl = CreateFrame("Slider", name, parent, "OptionsSliderTemplate")
sl:SetWidth(200)
sl:SetHeight(16)
sl:SetMinMaxValues(minVal, maxVal) -- BEFORE SetValue, else clamps to 0
sl:SetValueStep(step)
local low, high, txt = getglobal(name .. "Low"), getglobal(name .. "High"), getglobal(name .. "Text")
if low then low:SetText(tostring(minVal)); ApplyFont(low) end
if high then high:SetText(tostring(maxVal)); ApplyFont(high) end
if txt then
-- Left-align the value label above the slider (the template centers it),
-- so it lines up with the left-aligned checkbox labels. pfUI font.
txt:ClearAllPoints()
txt:SetPoint("BOTTOMLEFT", sl, "TOPLEFT", 0, 2)
txt:SetJustifyH("LEFT")
ApplyFont(txt)
end
sl.tbLabel = label
sl.tbFmt = fmt
sl.tbText = txt
sl.tbSet = setter
sl.tbGet = getter
sl:SetScript("OnValueChanged", function()
local v = this:GetValue()
if this.tbText then
this.tbText:SetText(string.format(this.tbFmt, v))
end
this.tbSet(v)
end)
if pfUI and pfUI.api and pfUI.api.SkinSlider then
pfUI.api.SkinSlider(sl)
end
return sl
end
-- Label text for the bar-layout cycle button, reflecting the current choice.
local function BarLayoutLabel()
return "Layout: " .. (TotemBarDB.barLayout or "1x6")
end
-- Factory: a labeled push button wired to an onClick. Returns the Button.
-- Named (project convention: name all frames) so the pfDebug profiler can attribute it.
local function CreateButton(parent, name, label, onClick)
local btn = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
btn:SetWidth(140)
btn:SetHeight(22)
btn:SetText(label)
btn:SetScript("OnClick", onClick)
if pfUI and pfUI.api and pfUI.api.SkinButton then
pfUI.api.SkinButton(btn)
end
return btn
end
-- Factory: a static hairline divider spanning the panel's content width at
-- cursor position y (Rev 4 UI chrome, section-break decoration). Anonymous
-- texture - it's a static decoration, not an interactive/named widget.
local function CreateDivider(parent, y)
local d = parent:CreateTexture(nil, "ARTWORK")
d:SetTexture(TotemBar.UI_TEXTURE)
local c = TotemBar.uiCoords[TotemBar.UI_DIVIDER_CELL]
d:SetTexCoord(c.l, c.r, c.t, c.b)
d:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, y)
d:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -14, y)
d:SetHeight(7)
return d
end
-- All widgets, stored so OnShow can repopulate them from TotemBarDB.
local widgets = {}
local function BuildOptionsFrame()
if optionsFrame then
return optionsFrame
end
local f = CreateFrame("Frame", "TotemBarOptionsFrame", UIParent)
f:SetWidth(280)
-- 472 + 44 + 28 + 28 + 44 + 28: room for the header emblem (+16, taller
-- than the old title-only header), two section dividers (+14 each), the
-- "Show pulse waves" checkbox row (+28, the pulse ring's countdown
-- readout sits alongside the ripple), the bar-layout cycle button row
-- (+28), the "Button spacing" slider row (+44, same slider-row
-- height as guard/gap/scale above it), and the "Show drop-all button"
-- checkbox row (+28) - see the layout-cursor deltas below (same
-- mechanism as the Task-5 height bump). Not cosmetic: without the last
-- +28 the "Key bind mode" button would land on the version footer.
f:SetHeight(628)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
f:SetFrameStrata("DIALOG")
-- Rev 4 UI chrome: shared bespoke panel skin (see ui.lua's PANEL_BACKDROP)
-- instead of the Blizzard dialog backdrop or pfUI's generic skin, so the
-- options panel matches the bar's own art (emblem/dividers below pair
-- with this border/bg specifically).
f:SetBackdrop(TotemBar.PANEL_BACKDROP)
f:SetBackdropColor(1, 1, 1, 0.97)
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetClampedToScreen(true)
-- Plain gold title, no emblem (the Rev-4 totem glyph was removed on
-- user request - the silhouette read, well, unfortunate).
local title = f:CreateFontString("TotemBarOptionsTitle", "OVERLAY", "GameFontNormal")
title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -16)
title:SetText("TotemBar Options")
ApplyFont(title, 14)
title:SetTextColor(0.85, 0.66, 0.31)
local close = CreateFrame("Button", "TotemBarOptionsClose", f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
if pfUI and pfUI.api and pfUI.api.SkinCloseButton then
pfUI.api.SkinCloseButton(close, f)
end
-- Layout cursor (back to the title-only header spacing).
local x = 24
local y = -44
local function place(w, dy)
w:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
y = y + dy
end
-- Checkboxes.
widgets.lock = CreateCheckbox(f, "Lock bar",
function() return TotemBarDB.locked end,
function(v) TotemBarDB.locked = v end)
place(widgets.lock, -28)
AddTooltip(widgets.lock, "Locks the bar in place so you can't drag it by accident. Uncheck to reposition it.")
widgets.autoRecall = CreateCheckbox(f, "Auto-recall before setting",
function() return TotemBarDB.autoRecall end,
function(v)
TotemBarDB.autoRecall = v
if TotemBar.RefreshRecallIndicator then TotemBar.RefreshRecallIndicator() end
end)
place(widgets.autoRecall, -28)
AddTooltip(widgets.autoRecall, "When on, the Totems macro casts Totemic Recall before re-dropping your totems (refunds some mana when relocating).")
widgets.show = CreateCheckbox(f, "Show bar",
function() return not TotemBarDB.hidden end,
function(v)
if v then
TotemBarDB.hidden = false
if TotemBarFrame then TotemBarFrame:Show() end
else
TotemBarDB.hidden = true
if TotemBarFrame then TotemBarFrame:Hide() end
end
end)
place(widgets.show, -28)
AddTooltip(widgets.show, "Shows or hides the totem bar. The choice is remembered between sessions.")
-- Same kind of switch as "Show bar" above (visibility only, not a
-- feature toggle), hence the same group. ApplyBarLayout hides/shows the
-- button AND re-lays out the grid, so the bar resizes immediately -
-- no /reload needed.
widgets.dropSet = CreateCheckbox(f, "Show drop-all button",
function() return TotemBarDB.showDropSetButton end,
function(v)
TotemBarDB.showDropSetButton = v
if TotemBar.ApplyBarLayout then TotemBar.ApplyBarLayout() end
end)
place(widgets.dropSet, -40)
AddTooltip(widgets.dropSet, "Shows or hides the 'Drop all totems' button on the bar. Its key binding keeps working while it is hidden - rebind it under Key Bindings in the game menu.")
widgets.ring = CreateCheckbox(f, "Show duration ring",
function() return TotemBarDB.showDurationRing end,
function(v)
TotemBarDB.showDurationRing = v
if TotemBar.RefreshPulseUI then TotemBar.RefreshPulseUI() end
end)
place(widgets.ring, -28)
AddTooltip(widgets.ring, "Shows a colored arc on each button's ring that empties as the totem's remaining time runs out. The ring frame itself always stays visible.")
widgets.pulse = CreateCheckbox(f, "Show pulse ring",
function() return TotemBarDB.showPulseBars end,
function(v)
TotemBarDB.showPulseBars = v
if TotemBar.RefreshPulseUI then TotemBar.RefreshPulseUI() end
end)
place(widgets.pulse, -28)
AddTooltip(widgets.pulse, "Shows a thin inner ring around the icon that fills up toward the totem's next pulse, then resets.")
widgets.pulseWaves = CreateCheckbox(f, "Show pulse waves",
function() return TotemBarDB.showPulseWaves end,
function(v)
TotemBarDB.showPulseWaves = v
if TotemBar.RefreshPulseUI then TotemBar.RefreshPulseUI() end
end)
place(widgets.pulseWaves, -28)
AddTooltip(widgets.pulseWaves, "Shows an expanding ripple in the totem's color each time a pulsing totem fires.")
widgets.timerText = CreateCheckbox(f, "Show countdown text",
function() return TotemBarDB.showTimerText end,
function(v)
TotemBarDB.showTimerText = v
if TotemBar.RefreshPulseUI then TotemBar.RefreshPulseUI() end
end)
place(widgets.timerText, -28)
AddTooltip(widgets.timerText, "Shows the remaining-seconds text under each element button (the ring shows the same information graphically).")
-- Bar-layout cycle button: click steps through TotemBar.BAR_LAYOUTS
-- (wrapping), saves the choice, and re-lays out the live bar.
widgets.layout = CreateButton(f, "TotemBarOptLayoutButton", BarLayoutLabel(), function()
local layouts = TotemBar.BAR_LAYOUTS
local n = table.getn(layouts)
local current = TotemBarDB.barLayout or "1x6"
local currentIdx = 1
for i = 1, n do
if layouts[i] == current then
currentIdx = i
end
end
local nextIdx = math.mod(currentIdx, n) + 1
TotemBarDB.barLayout = layouts[nextIdx]
this:SetText(BarLayoutLabel())
if TotemBar.ApplyBarLayout then
TotemBar.ApplyBarLayout()
end
end)
place(widgets.layout, -28)
AddTooltip(widgets.layout, "Cycles the bar arrangement: one row of six, two rows of three, or three rows of two.")
-- Section divider (Rev 4 UI chrome): checkboxes above, sliders below.
CreateDivider(f, y)
y = y - 14
-- Sliders (leave headroom below each for its low/high/value text).
widgets.guard = CreateSlider(f, "Recall guard (sec)", 0, 5, 0.5, "Recall guard: %.1fs",
function() return TotemBarDB.recallGuardSeconds end,
function(v) TotemBarDB.recallGuardSeconds = v end)
place(widgets.guard, -44)
AddTooltip(widgets.guard, "A second press of the Totems macro within this many seconds skips Totemic Recall, so a double-press won't pull the totems you just dropped. 0 = no guard.")
widgets.gap = CreateSlider(f, "Cycle reset gap (sec)", 0.5, 5, 0.5, "Cycle gap: %.1fs",
function() return TotemBarDB.gapSeconds end,
function(v) TotemBarDB.gapSeconds = v end)
place(widgets.gap, -44)
AddTooltip(widgets.gap, "How long a pause (in seconds) restarts the one-per-press cast cycle back at the first totem.")
widgets.scale = CreateSlider(f, "UI size", 0.5, 2.0, 0.05, "UI size: %.2f",
function() return TotemBarDB.scale end,
function(v)
if TotemBar.SetBarScale then
TotemBar.SetBarScale(v)
elseif TotemBarFrame then
TotemBarDB.scale = v
TotemBarFrame:SetScale(v)
end
end)
place(widgets.scale, -44)
AddTooltip(widgets.scale, "Scales the whole bar. It stays anchored at its top-left corner, and the flyout scales with it.")
widgets.buttonGap = CreateSlider(f, "Button spacing", 10, 30, 1, "Spacing: %.0fpx",
function() return TotemBarDB.buttonGap end,
function(v)
if TotemBar.SetButtonGap then
TotemBar.SetButtonGap(v)
else
TotemBarDB.buttonGap = v
end
end)
place(widgets.buttonGap, -44)
AddTooltip(widgets.buttonGap, "Distance between the bar buttons. Applies immediately.")
-- Section divider (Rev 4 UI chrome): sliders above, action buttons below.
CreateDivider(f, y)
y = y - 14
optionsFrame = f
-- Buttons (Reset position, Create macro) are added in Task 4.
if TotemBar.BuildOptionsButtons then
TotemBar.BuildOptionsButtons(f, x, y)
end
-- Repopulate every widget from TotemBarDB whenever the panel shows.
f:SetScript("OnShow", function()
widgets.lock:SetChecked(TotemBarDB.locked and 1 or nil)
widgets.autoRecall:SetChecked(TotemBarDB.autoRecall and 1 or nil)
widgets.show:SetChecked((not TotemBarDB.hidden) and 1 or nil)
widgets.dropSet:SetChecked(TotemBarDB.showDropSetButton and 1 or nil)
widgets.ring:SetChecked(TotemBarDB.showDurationRing and 1 or nil)
widgets.pulse:SetChecked(TotemBarDB.showPulseBars and 1 or nil)
widgets.pulseWaves:SetChecked(TotemBarDB.showPulseWaves and 1 or nil)
widgets.timerText:SetChecked(TotemBarDB.showTimerText and 1 or nil)
widgets.layout:SetText(BarLayoutLabel())
widgets.guard:SetValue(TotemBar.clampValue(TotemBarDB.recallGuardSeconds, 0, 5))
widgets.gap:SetValue(TotemBar.clampValue(TotemBarDB.gapSeconds, 0.5, 5))
widgets.scale:SetValue(TotemBar.clampValue(TotemBarDB.scale, 0.5, 2.0))
widgets.buttonGap:SetValue(TotemBar.clampValue(TotemBarDB.buttonGap, 10, 30))
end)
-- Version footer (bottom-centre), read from the .toc so it always matches
-- the real addon version instead of a hardcoded duplicate.
local ver = (GetAddOnMetadata and GetAddOnMetadata("TotemBar", "Version")) or "0.1.0"
local verFS = f:CreateFontString("TotemBarOptionsVersion", "OVERLAY")
ApplyFont(verFS, 10)
verFS:SetPoint("BOTTOM", f, "BOTTOM", 0, 6)
verFS:SetText("v" .. ver)
verFS:SetTextColor(0.6, 0.6, 0.6)
-- ESC closes it.
tinsert(UISpecialFrames, "TotemBarOptionsFrame")
f:Hide()
return f
end
-- Creates or updates the "Totems" convenience macro from TotemBar.macroSpec.
-- General (account) macro, capped at 18 in 1.12; if the cap is full and no
-- "Totems" macro exists yet, reports and does nothing. Never auto-places it
-- on the action bar (no 1.12 API for that - the player drags it).
local function ApplyTotemsMacro()
local name, body, icon = TotemBar.macroSpec()
local existing = GetMacroIndexByName(name)
if existing and existing > 0 then
EditMacro(existing, name, icon, body, 1, nil)
ChatOut:AddMessage("TotemBar: '" .. name .. "' macro updated - drag it to your action bar.")
return
end
local numGlobal = GetNumMacros() -- returns global, perChar in 1.12
if numGlobal and numGlobal >= 18 then
ChatOut:AddMessage("TotemBar: macro slots full (18) - free one and retry.")
return
end
CreateMacro(name, icon, body, nil)
ChatOut:AddMessage("TotemBar: '" .. name .. "' macro created - drag it to your action bar.")
end
-- Adds the Reset-position and Create-macro buttons to the options frame.
-- Called by BuildOptionsFrame (Task 3) after the sliders, at layout cursor
-- (x, yStart). Kept separate so the panel body and the action buttons are
-- two focused units.
function TotemBar.BuildOptionsButtons(f, x, yStart)
-- Full content width (frame width minus the left/right margin x), so the
-- buttons read as a balanced block instead of a narrow left-biased pair.
local w = f:GetWidth() - (x * 2)
local reset = CreateButton(f, "TotemBarOptResetButton", "Reset position", function()
if TotemBar.ResetPosition then TotemBar.ResetPosition() end
end)
reset:SetWidth(w)
reset:SetPoint("TOPLEFT", f, "TOPLEFT", x, yStart)
AddTooltip(reset, "Moves the bar back to the center of the screen.")
local macro = CreateButton(f, "TotemBarOptMacroButton", "Create 'Totems' macro", function()
ApplyTotemsMacro()
end)
macro:SetWidth(w)
macro:SetPoint("TOPLEFT", f, "TOPLEFT", x, yStart - 28)
AddTooltip(macro, "Creates or updates a 'Totems' macro that drops all four chosen totems in one press. Drag it from the macro window to your action bar.")
local bind = CreateButton(f, "TotemBarOptBindButton", "Key bind mode", function()
if TotemBar.ToggleBindMode then TotemBar.ToggleBindMode() end
end)
bind:SetWidth(w)
bind:SetPoint("TOPLEFT", f, "TOPLEFT", x, yStart - 56)
AddTooltip(bind, "Toggle bind mode, then hover any bar button or a flyout totem and press a key to bind it. ESC clears. Bindings are saved automatically.")
end
-- Show/hide the options panel (builds it lazily). Left-click on the minimap
-- button and /tb options both call this.
function TotemBar.ToggleOptions()
local f = BuildOptionsFrame()
if f:IsShown() then
f:Hide()
else
f:Show()
end
end