A World of Warcraft Experience Bar addon
worldofwarcraft
wow
addon
midnight
1local _, F = ...
2
3F.Options = F.Options or {}
4
5local panel
6local content
7local controls = {}
8local questRelatedKeys = {
9 trackedOnly = true,
10 showQuestRestedText = true,
11 showIncompleteQuestXPText = true,
12 showCompletedQuestXPText = true,
13 showIncompleteQuestBar = true,
14 colorQuest = true,
15 colorQuestComplete = true,
16 barTextureQuest = true,
17 barTextureQuestComplete = true,
18}
19
20local TICK_OPACITY_KEY = "tickOpacity"
21local DEFAULT_STATUSBAR = "Interface\\Buttons\\WHITE8x8"
22local DEFAULT_FONT = "Fonts\\FRIZQT__.TTF"
23
24local function GetLibSharedMedia()
25 if LibStub then
26 return LibStub("LibSharedMedia-3.0", true)
27 end
28 return nil
29end
30
31local function SetOption(key, value)
32 if F.InitDB then F.InitDB() end
33 if not F.DB then return end
34
35 F.DB[key] = value and true or false
36
37 if key == "enabled" then
38 if value then
39 if F.UpdateUI then F.UpdateUI() end
40 else
41 if F.Frame and F.Frame.Hide then F.Frame:Hide() end
42 end
43 return
44 end
45
46 if key == "hideBlizzardXPBar" and F.UI and F.UI.HideBlizzardBars then
47 F.UI.HideBlizzardBars()
48 end
49
50 if F.UpdateUI then F.UpdateUI() end
51
52 if key == "questTrackingEnabled" or key == "trackedOnly" then
53 if F.State and F.State.UpdateQuestXP then
54 F.State:UpdateQuestXP()
55 end
56 if key == "questTrackingEnabled" and panel and panel:IsShown() then
57 F.Options.Refresh()
58 end
59 end
60end
61
62local function SetNumericOption(key, value)
63 if F.InitDB then F.InitDB() end
64 if not F.DB then return end
65 if key == "barWidth" then
66 local width = tonumber(value) or 0
67 local scale = UIParent and UIParent:GetEffectiveScale() or 1
68 local stepPixels = math.floor(((width / 20) * scale) + 0.5)
69 local snapped = (stepPixels / scale) * 20
70 if snapped < 200 then snapped = 200 end
71 F.DB[key] = snapped
72 elseif key == TICK_OPACITY_KEY then
73 F.DB[key] = (tonumber(value) or 0) / 100
74 else
75 F.DB[key] = value
76 end
77 if F.UpdateUI then F.UpdateUI() end
78end
79
80local function SetStringOption(key, value)
81 if F.InitDB then F.InitDB() end
82 if not F.DB then return end
83 F.DB[key] = value
84 if F.UpdateUI then F.UpdateUI() end
85end
86
87local function SetColorOption(key, r, g, b, a)
88 if F.InitDB then F.InitDB() end
89 if not F.DB then return end
90 F.DB[key] = { r = r, g = g, b = b, a = a }
91 if F.UpdateUI then F.UpdateUI() end
92end
93
94local function GetColorOption(key, fallback)
95 if F.InitDB then F.InitDB() end
96 if not F.DB then return fallback[1], fallback[2], fallback[3], fallback[4] end
97 local color = F.DB[key]
98 if type(color) == "table" then
99 if color.r then
100 return color.r or 1, color.g or 1, color.b or 1, color.a or 1
101 end
102 if color[1] then
103 return color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1
104 end
105 end
106 return fallback[1], fallback[2], fallback[3], fallback[4]
107end
108
109local function CreateHeader(parent, text, y)
110 local fs = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
111 fs:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, y)
112 fs:SetText(text)
113 return y - 20
114end
115
116local function CreateCheckbox(parent, key, label, y)
117 local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
118 cb.Text:SetText(label)
119 cb:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, y)
120 cb:SetScript("OnClick", function(self)
121 SetOption(key, self:GetChecked())
122 end)
123
124 controls[key] = { type = "checkbox", widget = cb }
125 return y - 28
126end
127
128local function CreateSlider(parent, key, label, y, minValue, maxValue, step)
129 local name = "NXP_Options_" .. key .. "Slider"
130 local slider = CreateFrame("Slider", name, parent, "OptionsSliderTemplate")
131 slider:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, y)
132 slider:SetMinMaxValues(minValue, maxValue)
133 slider:SetValueStep(step)
134 slider:SetObeyStepOnDrag(true)
135
136 local labelText = _G[name .. "Text"]
137 local lowText = _G[name .. "Low"]
138 local highText = _G[name .. "High"]
139 if labelText then labelText:SetText(label) end
140 if lowText then lowText:SetText(tostring(minValue)) end
141 if highText then highText:SetText(tostring(maxValue)) end
142
143 slider:SetScript("OnValueChanged", function(self, value)
144 if self._updating then return end
145 local rounded = math.floor(value + 0.5)
146 if step and step < 1 then
147 rounded = value
148 end
149 self._updating = true
150 self:SetValue(rounded)
151 self._updating = false
152 SetNumericOption(key, rounded)
153 end)
154
155 controls[key] = { type = "slider", widget = slider }
156 return y - 52
157end
158
159local function CreateSliderWithInput(parent, key, label, y, minValue, maxValue, step, inputWidth)
160 local name = "NXP_Options_" .. key .. "Slider"
161 local slider = CreateFrame("Slider", name, parent, "OptionsSliderTemplate")
162 slider:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, y)
163 slider:SetMinMaxValues(minValue, maxValue)
164 slider:SetValueStep(step)
165 slider:SetObeyStepOnDrag(true)
166
167 local labelText = _G[name .. "Text"]
168 local lowText = _G[name .. "Low"]
169 local highText = _G[name .. "High"]
170 if labelText then labelText:SetText(label) end
171 if lowText then lowText:SetText(tostring(minValue)) end
172 if highText then highText:SetText(tostring(maxValue)) end
173
174 local edit = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
175 edit:SetAutoFocus(false)
176 edit:SetSize(inputWidth or 60, 20)
177 edit:SetPoint("LEFT", slider, "RIGHT", 12, 0)
178 edit:SetNumeric(true)
179
180 slider:SetScript("OnValueChanged", function(self, value)
181 if self._updating then return end
182 local rounded = math.floor(value + 0.5)
183 if step and step < 1 then
184 rounded = value
185 end
186 self._updating = true
187 self:SetValue(rounded)
188 self._updating = false
189 edit:SetNumber(rounded)
190 SetNumericOption(key, rounded)
191 end)
192
193 local function commitEdit()
194 local value = tonumber(edit:GetText())
195 if not value then return end
196 if value < minValue then value = minValue end
197 if value > maxValue then value = maxValue end
198 slider._updating = true
199 slider:SetValue(value)
200 slider._updating = false
201 edit:SetNumber(value)
202 SetNumericOption(key, value)
203 end
204
205 edit:SetScript("OnEnterPressed", function(self)
206 commitEdit()
207 self:ClearFocus()
208 end)
209
210 edit:SetScript("OnEditFocusLost", function()
211 commitEdit()
212 end)
213
214 controls[key] = { type = "offset", slider = slider, edit = edit }
215 return y - 52
216end
217
218local function BuildMediaList(kind, defaultLabel, defaultValue)
219 local list = {}
220 local order = {}
221
222 list[defaultValue] = defaultLabel
223 table.insert(order, defaultValue)
224
225 local LSM = GetLibSharedMedia()
226 if LSM and LSM.HashTable then
227 local entries = {}
228 for name, path in pairs(LSM:HashTable(kind) or {}) do
229 if type(path) == "string" and path ~= "" then
230 table.insert(entries, { name = tostring(name), path = path })
231 end
232 end
233 table.sort(entries, function(a, b) return a.name < b.name end)
234 for _, entry in ipairs(entries) do
235 list[entry.path] = entry.name
236 table.insert(order, entry.path)
237 end
238 end
239
240 return list, order
241end
242
243local function CreateDropdown(parent, key, label, y, list, order, width)
244 local name = "NXP_Options_" .. key .. "Dropdown"
245 local dropdown = CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate")
246 dropdown:SetPoint("TOPLEFT", parent, "TOPLEFT", -6, y)
247
248 local labelText = dropdown:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
249 labelText:SetPoint("TOPLEFT", dropdown, "TOPLEFT", 16, 14)
250 labelText:SetText(label)
251
252 UIDropDownMenu_SetWidth(dropdown, width or 220)
253 UIDropDownMenu_Initialize(dropdown, function(self)
254 for _, value in ipairs(order or {}) do
255 local info = UIDropDownMenu_CreateInfo()
256 info.text = list[value] or value
257 info.value = value
258 info.func = function()
259 UIDropDownMenu_SetSelectedValue(dropdown, value)
260 SetStringOption(key, value)
261 end
262 UIDropDownMenu_AddButton(info)
263 end
264 end)
265
266 controls[key] = { type = "dropdown", widget = dropdown, list = list, label = labelText }
267 return y - 50
268end
269
270local function CreateColorPicker(parent, key, label, y, fallback)
271 local labelText = parent:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
272 labelText:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, y)
273 labelText:SetText(label)
274
275 local swatch = CreateFrame("Button", nil, parent)
276 swatch:SetSize(16, 16)
277 swatch:SetPoint("LEFT", labelText, "RIGHT", 8, 0)
278 swatch:SetNormalTexture("Interface\\Buttons\\WHITE8x8")
279 swatch:SetHighlightTexture("Interface\\Buttons\\WHITE8x8", "ADD")
280 local swatchTex = swatch:GetNormalTexture()
281
282 local border = swatch:CreateTexture(nil, "BORDER")
283 border:SetPoint("TOPLEFT", swatch, "TOPLEFT", -1, 1)
284 border:SetPoint("BOTTOMRIGHT", swatch, "BOTTOMRIGHT", 1, -1)
285 border:SetColorTexture(0, 0, 0, 1)
286 if swatchTex then
287 swatchTex:SetPoint("TOPLEFT", swatch, "TOPLEFT", 1, -1)
288 swatchTex:SetPoint("BOTTOMRIGHT", swatch, "BOTTOMRIGHT", -1, 1)
289 end
290
291 swatch:SetScript("OnClick", function()
292 local r, g, b, a = GetColorOption(key, fallback)
293 local previous = { r = r, g = g, b = b, a = a }
294
295 local function getPickerAlpha()
296 if ColorPickerFrame.GetColorAlpha then
297 return ColorPickerFrame:GetColorAlpha()
298 end
299 return 1 - (ColorPickerFrame.opacity or 0)
300 end
301
302 local function applyColor()
303 local nr, ng, nb = ColorPickerFrame:GetColorRGB()
304 local na = getPickerAlpha()
305 SetColorOption(key, nr, ng, nb, na)
306 F.Options.Refresh()
307 end
308
309 local function cancelColor()
310 local pv = previous
311 if pv then
312 SetColorOption(key, pv.r, pv.g, pv.b, pv.a)
313 F.Options.Refresh()
314 end
315 end
316
317 if ColorPickerFrame.SetupColorPickerAndShow then
318 ColorPickerFrame:SetupColorPickerAndShow({
319 r = r,
320 g = g,
321 b = b,
322 opacity = 1 - (a or 1),
323 hasOpacity = true,
324 swatchFunc = applyColor,
325 opacityFunc = applyColor,
326 cancelFunc = cancelColor,
327 })
328 else
329 ColorPickerFrame.hasOpacity = true
330 ColorPickerFrame.opacity = 1 - (a or 1)
331 ColorPickerFrame.previousValues = previous
332 ColorPickerFrame.func = applyColor
333 ColorPickerFrame.opacityFunc = applyColor
334 ColorPickerFrame.cancelFunc = cancelColor
335 ColorPickerFrame:SetColorRGB(r, g, b)
336 ColorPickerFrame:Hide()
337 ColorPickerFrame:Show()
338 end
339 end)
340
341 controls[key] = { type = "color", widget = swatch, label = labelText, fallback = fallback }
342 return y - 24
343end
344
345function F.Options.Refresh()
346 if F.InitDB then F.InitDB() end
347 if not F.DB then return end
348
349 local defaults = F.Config or {}
350
351 for key, control in pairs(controls) do
352 if control.type == "checkbox" then
353 control.widget:SetChecked(F.DB[key] and true or false)
354 elseif control.type == "slider" then
355 local value = F.DB[key]
356 if value == nil then value = defaults[key] end
357 if value == nil then value = 0 end
358 if key == TICK_OPACITY_KEY then
359 value = (tonumber(value) or 0) * 100
360 end
361 control.widget._updating = true
362 control.widget:SetValue(tonumber(value) or 0)
363 control.widget._updating = false
364 elseif control.type == "color" then
365 local r, g, b, a = GetColorOption(key, control.fallback)
366 local tex = control.widget:GetNormalTexture()
367 if tex then tex:SetColorTexture(r, g, b, a) end
368 elseif control.type == "offset" then
369 local value = F.DB[key]
370 if value == nil then value = defaults[key] end
371 if value == nil then value = 0 end
372 control.slider._updating = true
373 control.slider:SetValue(tonumber(value) or 0)
374 control.slider._updating = false
375 control.edit:SetNumber(tonumber(value) or 0)
376 elseif control.type == "dropdown" then
377 local defaults = F.Config or {}
378 local value = F.DB[key]
379 if value == nil then value = defaults[key] end
380 UIDropDownMenu_SetSelectedValue(control.widget, value)
381 UIDropDownMenu_SetText(control.widget, control.list and control.list[value] or value or "")
382 end
383 end
384
385 local questEnabled = F.DB.questTrackingEnabled ~= false
386 for key, _ in pairs(questRelatedKeys) do
387 local control = controls[key]
388 if control then
389 if control.type == "checkbox" or control.type == "slider" then
390 control.widget:SetEnabled(questEnabled)
391 if control.widget.Text then
392 local color = questEnabled and NORMAL_FONT_COLOR or GRAY_FONT_COLOR
393 control.widget.Text:SetTextColor(color.r, color.g, color.b)
394 end
395 elseif control.type == "color" then
396 if questEnabled then
397 control.widget:Enable()
398 control.label:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
399 else
400 control.widget:Disable()
401 control.label:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
402 end
403 end
404 end
405 end
406end
407
408function F.Options.Open()
409 if not panel then return end
410
411 if Settings and Settings.OpenToCategory then
412 Settings.OpenToCategory(panel.name)
413 elseif InterfaceOptionsFrame_OpenToCategory then
414 InterfaceOptionsFrame_OpenToCategory(panel)
415 InterfaceOptionsFrame_OpenToCategory(panel)
416 end
417end
418
419function F.Options.Create()
420 if panel then return end
421
422 panel = CreateFrame("Frame", "NXP_OptionsPanel", UIParent)
423 panel.name = "NixxnuxXPBar"
424
425 local scroll = CreateFrame("ScrollFrame", "NXP_OptionsScrollFrame", panel, "UIPanelScrollFrameTemplate")
426 scroll:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, -8)
427 scroll:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -30, 8)
428
429 content = CreateFrame("Frame", nil, scroll)
430 content:SetSize(1, 1)
431 scroll:SetScrollChild(content)
432
433 local title = content:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
434 title:SetPoint("TOPLEFT", content, "TOPLEFT", 16, -8)
435 title:SetText("NixxnuxXPBar")
436
437 local y = -40
438 y = CreateHeader(content, "General", y)
439 y = CreateCheckbox(content, "enabled", "Enable addon", y)
440 y = CreateCheckbox(content, "questTrackingEnabled", "Enable quest tracking", y)
441 y = CreateCheckbox(content, "trackedOnly", "Count tracked (pinned) quests only", y)
442 y = CreateCheckbox(content, "showIncompleteQuestBar", "Show uncompleted quest XP bar", y)
443 y = CreateCheckbox(content, "showTicks", "Show XP ticks", y)
444 y = y - 6
445 y = CreateSlider(content, "tickOpacity", "Tick opacity", y, 10, 100, 1)
446
447 y = y - 10
448 y = CreateHeader(content, "Position", y)
449 y = y - 6
450 local anchorList = {
451 TOP = "Top",
452 BOTTOM = "Bottom",
453 LEFT = "Left",
454 RIGHT = "Right",
455 CENTER = "Center",
456 }
457 local anchorOrder = { "TOP", "BOTTOM", "LEFT", "RIGHT", "CENTER" }
458 y = CreateDropdown(content, "anchorPoint", "Anchor position", y, anchorList, anchorOrder)
459 y = CreateSliderWithInput(content, "offsetX", "X offset", y, -1000, 1000, 1, 60)
460 y = CreateSliderWithInput(content, "offsetY", "Y offset", y, -1000, 1000, 1, 60)
461
462 y = y - 10
463 y = CreateHeader(content, "Bar Size", y)
464 y = y - 6
465 y = CreateSlider(content, "barWidth", "Bar width", y, 200, 900, 1)
466 y = CreateSlider(content, "barHeight", "Bar height", y, 8, 60, 1)
467
468 y = y - 6
469 y = CreateHeader(content, "Bar Colors", y)
470 y = CreateColorPicker(content, "colorMain", "Normal XP", y, { 0.76, 0.38, 1, 1 })
471 y = CreateColorPicker(content, "colorRested", "Rested XP", y, { 0.34, 0.61, 0.99, 0.8 })
472 y = CreateColorPicker(content, "colorQuest", "Uncompleted Quest XP", y, { 1, 0.64, 0.0078, 0.25 })
473 y = CreateColorPicker(content, "colorQuestComplete", "Completed Quest XP", y, { 1, 0.64, 0.0078, 0.8 })
474
475 y = y - 6
476 y = CreateHeader(content, "Textures", y)
477 y = y - 6
478 local statusbarList, statusbarOrder = BuildMediaList("statusbar", "Default (Solid)", DEFAULT_STATUSBAR)
479 y = CreateDropdown(content, "barTextureMain", "Normal XP texture", y, statusbarList, statusbarOrder)
480 y = CreateDropdown(content, "barTextureRested", "Rested XP texture", y, statusbarList, statusbarOrder)
481 y = CreateDropdown(content, "barTextureQuest", "Uncompleted quest XP texture", y, statusbarList, statusbarOrder)
482 y = CreateDropdown(content, "barTextureQuestComplete", "Completed quest XP texture", y, statusbarList, statusbarOrder)
483
484 y = y - 6
485 y = CreateHeader(content, "Font", y)
486 y = y - 6
487 local fontList, fontOrder = BuildMediaList("font", "Default", DEFAULT_FONT)
488 y = CreateDropdown(content, "fontPath", "Font", y, fontList, fontOrder)
489
490 y = y - 10
491 y = CreateHeader(content, "Text Labels", y)
492 y = CreateCheckbox(content, "showLevelText", "Level text", y)
493 y = CreateCheckbox(content, "showXPText", "XP text", y)
494 y = CreateCheckbox(content, "showPercentText", "Percent text", y)
495 y = CreateCheckbox(content, "showQuestRestedText", "Quest/rested percent text", y)
496 y = CreateCheckbox(content, "showLevelingText", "Leveling info text", y)
497 y = CreateCheckbox(content, "showIncompleteQuestXPText", "Uncompleted quest XP text", y)
498 y = CreateCheckbox(content, "showCompletedQuestXPText", "Completed quest XP text", y)
499 y = CreateCheckbox(content, "showRestedXPText", "Rested XP text", y)
500
501 content:SetHeight(-y + 20)
502
503 panel:SetScript("OnShow", function()
504 F.Options.Refresh()
505 end)
506
507 if Settings and Settings.RegisterCanvasLayoutCategory then
508 local category = Settings.RegisterCanvasLayoutCategory(panel, panel.name)
509 Settings.RegisterAddOnCategory(category)
510 elseif InterfaceOptions_AddCategory then
511 InterfaceOptions_AddCategory(panel)
512 end
513end
514
515F.Options.Create()