-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWidgets.lua
More file actions
342 lines (300 loc) · 12.9 KB
/
Copy pathWidgets.lua
File metadata and controls
342 lines (300 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
-- Widgets.lua - CheckBox / Slider / Dropdown / EditBox / Button factories.
-- Author: Serv
-- Source: https://github.com/powerfulqa/BarWarden
-- License: see LICENSE; attribution preservation is required.
local addonName, ns = ...
local widgetCount = 0
-- Guard flag: when true, slider/checkbox OnValueChanged callbacks are
-- suppressed. Set during programmatic SetValue calls (e.g. Refresh)
-- so that restoring UI state doesn't write back to the DB and overwrite
-- per-group settings with global defaults.
ns.suppressCallbacks = false
local function NextName(prefix)
widgetCount = widgetCount + 1
return "BarWarden" .. prefix .. widgetCount
end
-- ----------------------------------------------------------------------------
-- Tooltip helper for widgets whose frame templates don't have built-in
-- tooltip support (sliders, editboxes). Checkboxes use the template-provided
-- `tooltipText` field instead. Uses HookScript so we don't clobber any
-- existing OnEnter/OnLeave handlers the widget already has.
-- ----------------------------------------------------------------------------
local function AttachTooltip(widget, tooltipText)
if not tooltipText or tooltipText == "" then return end
widget:HookScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltipText, 1, 1, 1, 1, true) -- trailing true = wrap
GameTooltip:Show()
end)
widget:HookScript("OnLeave", function()
GameTooltip:Hide()
end)
end
-- ----------------------------------------------------------------------------
-- DB-path helpers
--
-- Walk a dotted path on a root table, returning (parentTable, lastKey) so
-- the caller can read parent[key] and write parent[key] = v.
-- Returns (nil, nil) if any intermediate segment is missing.
-- ----------------------------------------------------------------------------
local function ResolvePath(root, path)
local parent, key = root, nil
for segment in path:gmatch("[^.]+") do
if key then
if type(parent[key]) ~= "table" then return nil, nil end
parent = parent[key]
end
key = segment
end
return parent, key
end
-- Returns a widget-shaped callback `(self, value)` that writes `value`
-- into BarWardenDB.<path> and optionally calls ns:<refreshMethod>() after.
--
-- Validates strictly at registration time:
-- 1. the intermediate path must resolve under BarWardenDB
-- 2. the leaf must already exist (i.e. be declared in ns.DEFAULTS)
-- 3. the refresh method (if given) must be defined on ns
--
-- This makes ns.DEFAULTS the single source of truth for every option, and
-- surfaces typos at addon load instead of as a silent no-op when the user
-- clicks the control. By the time any Options_*.lua file constructs its
-- tab, ns:OnInitialize has run InitDB (populating defaults) and
-- Core/MinimapButton.lua have defined their refresh methods.
function ns:DBSet(path, refreshMethod)
local parent, key = ResolvePath(BarWardenDB, path)
if not parent then
error(string.format(
"ns:DBSet: path %q has an unresolved intermediate segment", path), 2)
end
if parent[key] == nil then
error(string.format(
"ns:DBSet: leaf %q is not declared in ns.DEFAULTS; add it there first",
path), 2)
end
if refreshMethod and not ns[refreshMethod] then
error(string.format(
"ns:DBSet: refresh method ns:%s is not defined", refreshMethod), 2)
end
return function(_, value)
local p, k = ResolvePath(BarWardenDB, path)
if p then p[k] = value end
if refreshMethod and ns[refreshMethod] then
ns[refreshMethod](ns)
end
end
end
-- Read BarWardenDB.<path>, returning `default` if any segment is missing
-- or the leaf is nil. For use in Refresh handlers.
--
-- Unlike DBSet, DBGet does NOT validate at call time; its whole purpose
-- is to gracefully fall back when a path is absent (e.g. before MergeDefaults
-- has populated a new field on a pre-existing save).
function ns:DBGet(path, default)
local parent, key = ResolvePath(BarWardenDB, path)
if parent and parent[key] ~= nil then return parent[key] end
return default
end
function ns:CreateCheckbox(parent, label, tooltip, onClick)
local name = NextName("CB")
local cb = CreateFrame("CheckButton", name, parent, "InterfaceOptionsCheckButtonTemplate")
_G[name .. "Text"]:SetText(label)
cb.tooltipText = tooltip
cb:HookScript("OnClick", function(self)
if ns.suppressCallbacks then return end
if onClick then onClick(self, self:GetChecked() == 1) end
end)
return cb
end
function ns:CreateSlider(parent, label, min, max, step, onChange, tooltip)
local name = NextName("SL")
local slider = CreateFrame("Slider", name, parent, "OptionsSliderTemplate")
local labelText = _G[name .. "Text"]
labelText:SetText(label)
_G[name .. "Low"]:SetText(tostring(min))
_G[name .. "High"]:SetText(tostring(max))
slider:SetMinMaxValues(min, max)
slider:SetValueStep(step)
slider.label = label -- store for value display updates
slider:HookScript("OnValueChanged", function(self, value)
-- Always update the displayed value, even during suppressed refreshes
if step >= 1 then
labelText:SetText(label .. ": " .. string.format("%d", value))
else
labelText:SetText(label .. ": " .. string.format("%.2f", value))
end
if ns.suppressCallbacks then return end
if onChange then onChange(self, value) end
end)
AttachTooltip(slider, tooltip)
return slider
end
function ns:CreateDropdown(parent, label, items, onSelect, tooltip)
local name = NextName("DD")
local dd = CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate")
local lbl = dd:CreateFontString(nil, "OVERLAY", "GameFontNormal")
lbl:SetPoint("BOTTOMLEFT", dd, "TOPLEFT", 16, 3)
lbl:SetText(label)
-- Hover tooltip attaches to the clickable button, so it fires reliably.
if tooltip and tooltip ~= "" then
local button = _G[name .. "Button"]
if button then AttachTooltip(button, tooltip) end
end
UIDropDownMenu_SetWidth(dd, 150)
local function Initialize(self, level)
for i, item in ipairs(items) do
local info = UIDropDownMenu_CreateInfo()
info.text = item.text or item
info.value = item.value or item
info.func = function(self)
UIDropDownMenu_SetSelectedID(dd, i)
if onSelect then onSelect(dd, self.value, i) end
end
UIDropDownMenu_AddButton(info, level)
end
end
UIDropDownMenu_Initialize(dd, Initialize)
return dd
end
function ns:CreateEditBox(parent, label, width, onChange, tooltip)
local name = NextName("EB")
local eb = CreateFrame("EditBox", name, parent, "InputBoxTemplate")
eb:SetSize(width or 150, 20)
eb:SetAutoFocus(false)
local lbl = eb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
lbl:SetPoint("BOTTOMLEFT", eb, "TOPLEFT", 0, 3)
lbl:SetText(label)
-- `snapshot` records the text at the moment the field gained focus.
-- Used to detect whether an edit actually changed the value, so Commit
-- doesn't fire onChange redundantly for unchanged text (avoids churn
-- on every focus loss).
local snapshot
local function Commit(self)
local text = self:GetText()
if snapshot == nil or text ~= snapshot then
snapshot = text
if onChange then onChange(self, text) end
end
end
eb:HookScript("OnEditFocusGained", function(self)
snapshot = self:GetText()
end)
eb:HookScript("OnEnterPressed", function(self)
Commit(self)
self:ClearFocus() -- fires OnEditFocusLost below; snapshot now
-- equals current text so the second Commit no-ops
end)
eb:HookScript("OnEditFocusLost", function(self)
-- Ignore focus loss triggered by Refresh's SetText-driven updates.
if ns.suppressCallbacks then return end
Commit(self)
end)
-- NOTE: OnEscapePressed intentionally NOT hooked. InputBoxTemplate's
-- default handler fires `self:ClearFocus()`, which triggers our
-- OnEditFocusLost → Commit. Net result: Escape commits whatever text
-- is currently in the field, same as Enter and click-away. Consistent
-- "any exit commits" UX.
--
-- Historical note: v1.5.x tried to make Escape revert via
-- HookScript("OnEscapePressed", ...) that restored the snapshot before
-- ClearFocus. But in WoW 3.3.5a, HookScript runs AFTER the template's
-- default OnEscapePressed, so the template's ClearFocus fires
-- OnEditFocusLost (committing the in-progress text) before the hook
-- gets a chance to restore the snapshot. Making Escape revert properly
-- would require SetScript-overriding the template default.
AttachTooltip(eb, tooltip)
return eb
end
function ns:CreateButton(parent, label, width, onClick)
local name = NextName("BT")
local btn = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
btn:SetSize(width or 100, 22)
btn:SetText(label)
btn:SetScript("OnClick", function(self)
if onClick then onClick(self) end
end)
return btn
end
-- Small clickable [?] anchored next to a section header. Clicking deep-links
-- into the Help tab via ns:OpenHelpEntry(entryId), which switches to the Help
-- tab, expands the owning section, and scrolls to + flashes the entry.
-- Mirrors EbonClearance's MakeHelpIcon. anchorWidget/anchorPoint/relPoint
-- position it; pass an entryId that exists in Options_Help's HELP_ENTRIES.
function ns:CreateHelpIcon(parent, anchorWidget, anchorPoint, relPoint, xOff, yOff, entryId)
local name = NextName("HELP")
local btn = CreateFrame("Button", name, parent)
btn:SetSize(22, 16)
if anchorWidget then
btn:SetPoint(anchorPoint or "LEFT", anchorWidget, relPoint or "RIGHT",
xOff or 4, yOff or 0)
end
local txt = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
txt:SetAllPoints()
txt:SetText(ns.COLORS.emphasis .. "[?]|r")
btn.text = txt
btn:SetScript("OnEnter", function(self)
txt:SetText("|cffffffaa[?]|r")
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText("Open help", 1, 1, 1, 1, true)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
txt:SetText(ns.COLORS.emphasis .. "[?]|r")
GameTooltip:Hide()
end)
btn:SetScript("OnClick", function()
-- Remember the section we came from (the one currently on screen) so the
-- Help tab can offer a Back button. [?] icons never live on Help itself.
ns.helpReturnTab = ns.currentOptionsTab
if ns.OpenHelpEntry then ns:OpenHelpEntry(entryId) end
end)
return btn
end
function ns:CreateColorSwatch(parent, label, initialColor, onChange)
local name = NextName("CS")
local frame = CreateFrame("Frame", name, parent)
frame:SetSize(20, 20)
local swatch = frame:CreateTexture(nil, "ARTWORK")
swatch:SetAllPoints()
local c = initialColor or { r = 1, g = 1, b = 1, a = 1 }
swatch:SetTexture(c.r, c.g, c.b, c.a or 1)
local border = frame:CreateTexture(nil, "BORDER")
border:SetPoint("TOPLEFT", -1, 1)
border:SetPoint("BOTTOMRIGHT", 1, -1)
border:SetTexture(0.5, 0.5, 0.5, 1)
local lbl = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
lbl:SetPoint("LEFT", frame, "RIGHT", 5, 0)
lbl:SetText(label)
frame:EnableMouse(true)
frame:SetScript("OnMouseUp", function(self)
local prev = { r = c.r, g = c.g, b = c.b, a = c.a or 1 }
local function SetColor()
local r, g, b = ColorPickerFrame:GetColorRGB()
-- Opacity is inverted in 3.3.5a: 0 = opaque, 1 = transparent
local a = 1 - OpacitySliderFrame:GetValue()
c.r, c.g, c.b, c.a = r, g, b, a
swatch:SetTexture(r, g, b, a)
if onChange then onChange(self, c) end
end
local function CancelColor()
c.r, c.g, c.b, c.a = prev.r, prev.g, prev.b, prev.a
swatch:SetTexture(prev.r, prev.g, prev.b, prev.a)
if onChange then onChange(self, c) end
end
ColorPickerFrame:Hide()
ColorPickerFrame.hasOpacity = true
ColorPickerFrame.opacity = 1 - (c.a or 1) -- invert for display
ColorPickerFrame.previousValues = { c.r, c.g, c.b, 1 - (c.a or 1) }
ColorPickerFrame.func = SetColor
ColorPickerFrame.opacityFunc = SetColor
ColorPickerFrame.cancelFunc = CancelColor
ColorPickerFrame:SetColorRGB(c.r, c.g, c.b)
ColorPickerFrame:Show()
-- The picker opens at DIALOG strata and can land behind the Interface
-- Options window; lift it above (restored on hide).
if ns.RaiseFrameAboveOptions then ns:RaiseFrameAboveOptions(ColorPickerFrame) end
end)
frame.swatch = swatch
frame.color = c
return frame
end