--[[-------------------------------------------------------------------------- ManiaxNamePlates - the settings panel. Laid out to match Ascension's: four tabs across the top (General, Friendly, Enemy, Personal), and within Friendly and Enemy a second row of tabs for Health, Cast Bar, Level Indicator and Objective Icons. Their fifth tab, Profiles, is not here - it was AceDBOptions, and this fork does not use Ace. BUILT FROM 3.3.5A'S OWN TEMPLATES, ON PURPOSE Everything below is InterfaceOptionsCheckButtonTemplate, OptionsSliderTemplate, UIDropDownMenuTemplate, UIPanelButtonTemplate and OptionsFrameTabButtonTemplate - all confirmed present in this client's FrameXML. No custom widget library. That is a direct response to how the TurboPlates attempt went: it shipped a hand-rolled dropdown, and every dropdown in it came up empty. Blizzard's UIDropDownMenu is native here, so it populates or it errors - it does not quietly render nothing. The one exception is the colour swatch. 3.3.5a has no ColorSwatchTemplate (checked), so that one is assembled by hand from a button and two textures. A NOTE ON REFRESH InterfaceOptions_AddCategory on 3.3.5a does not invoke panel.refresh when the panel is shown; that callback arrived in Cataclysm. Every control is therefore refreshed from an OnShow hook installed here rather than relying on the framework to do it, which is why opening the panel always shows current values instead of whatever was there when it was built. ----------------------------------------------------------------------------]] local addonName = ... local Addon = _G.ManiaxNamePlates --------------------------------------------------------------------------- -- Reading and writing a setting by path. --------------------------------------------------------------------------- local function getPath(path) local t = Addon.db for i = 1, #path - 1 do if type(t) ~= "table" then return nil end t = t[path[i]] end return type(t) == "table" and t[path[#path]] or nil end local function setPath(path, value) local t = Addon.db for i = 1, #path - 1 do if type(t[path[i]]) ~= "table" then t[path[i]] = {} end t = t[path[i]] end t[path[#path]] = value Addon:UpdateAll() end --------------------------------------------------------------------------- -- Media lists. -- -- LibSharedMedia is optional in this pack, so both lists fall back to what the -- client itself ships. An empty dropdown is the failure being designed out -- here, so the fallback is a real list rather than nothing. --------------------------------------------------------------------------- local LSM = LibStub and LibStub("LibSharedMedia-3.0", true) local function mediaList(kind, fallback) if LSM then local names = LSM:List(kind) if names and #names > 0 then local out = {} for i, n in ipairs(names) do out[i] = { value = n, text = n } end return out end end return fallback end local function fontList() return mediaList("font", { { value = "Friz Quadrata TT", text = "Friz Quadrata TT" }, { value = "Arial Narrow", text = "Arial Narrow" }, { value = "Skurri", text = "Skurri" }, { value = "Morpheus", text = "Morpheus" }, }) end local function statusBarList() return mediaList("statusbar", { { value = "Blizzard", text = "Blizzard" }, { value = "Blizzard2", text = "Blizzard2" }, }) end local FONT_FLAGS = { { value = "NONE", text = "None" }, { value = "OUTLINE", text = "Outline" }, { value = "THICKOUTLINE", text = "Thick Outline" }, { value = "MONOCHROME", text = "Monochrome" }, } local HEALTH_FORMATS = { { value = "losthealth", text = "Missing" }, { value = "health", text = "Current" }, { value = "health-full", text = "Current / Max" }, { value = "perc", text = "Current Percent" }, { value = "perc-full", text = "Current / Max - Percent" }, } local QUEST_ANCHORS = { { value = "TOP", text = "Top" }, { value = "LEFT", text = "Left" }, { value = "RIGHT", text = "Right" }, } --------------------------------------------------------------------------- -- Widgets. -- -- Each returns a frame carrying a Refresh method. The panel's OnShow walks -- every registered control and calls it, so there is one refresh path rather -- than a per-widget one that can be forgotten. --------------------------------------------------------------------------- local uid = 0 local function nextName(prefix) uid = uid + 1 return "ManiaxNPOpt" .. prefix .. uid end local function makeCheckbox(parent, label, tooltip, path, x, y) local name = nextName("Check") local cb = CreateFrame("CheckButton", name, parent, "InterfaceOptionsCheckButtonTemplate") cb:SetPoint("TOPLEFT", x, y) -- The stock template names its label $parentText, so it is reachable as a -- global. parentKey works on this client too, but the global is what this -- template has always used and it costs nothing to take the documented one. local text = _G[name .. "Text"] or cb.Text if text then text:SetText(label) end if tooltip then cb.tooltipText = label; cb.tooltipRequirement = tooltip end cb:SetScript("OnClick", function(self) setPath(path, self:GetChecked() and true or false) end) cb.Refresh = function(self) self:SetChecked(getPath(path) and true or false) end return cb end local function makeSlider(parent, label, path, minV, maxV, step, x, y) local name = nextName("Slider") local s = CreateFrame("Slider", name, parent, "OptionsSliderTemplate") s:SetPoint("TOPLEFT", x, y) s:SetWidth(180) s:SetMinMaxValues(minV, maxV) s:SetValueStep(step) _G[name .. "Text"]:SetText(label) _G[name .. "Low"]:SetText(tostring(minV)) _G[name .. "High"]:SetText(tostring(maxV)) -- The live number, which the stock template does not provide. local readout = s:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") readout:SetPoint("TOP", s, "BOTTOM", 0, 3) s:SetScript("OnValueChanged", function(self, value) -- Snap to the step. Sliders report continuous values, and writing 4.37 to -- a bar height that is meant to be whole pixels produces a bar that never -- quite matches what the number says. local snapped = math.floor((value / step) + 0.5) * step readout:SetText(tostring(snapped)) if self.loading then return end if getPath(path) ~= snapped then setPath(path, snapped) end end) s.Refresh = function(self) self.loading = true local v = getPath(path) or minV self:SetValue(v) readout:SetText(tostring(v)) self.loading = false end return s end local function makeDropdown(parent, label, path, itemsFunc, x, y) local name = nextName("Drop") local dd = CreateFrame("Frame", name, parent, "UIDropDownMenuTemplate") dd:SetPoint("TOPLEFT", x - 16, y) local title = dd:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") title:SetPoint("BOTTOMLEFT", dd, "TOPLEFT", 20, 2) title:SetText(label) local function textFor(value) for _, item in ipairs(itemsFunc()) do if item.value == value then return item.text end end return tostring(value) end UIDropDownMenu_Initialize(dd, function(self, level) for _, item in ipairs(itemsFunc()) do local info = UIDropDownMenu_CreateInfo() info.text = item.text info.value = item.value info.func = function() setPath(path, item.value) UIDropDownMenu_SetSelectedValue(dd, item.value) UIDropDownMenu_SetText(dd, item.text) end info.checked = (item.value == getPath(path)) UIDropDownMenu_AddButton(info, level) end end) UIDropDownMenu_SetWidth(dd, 150) dd.Refresh = function() local v = getPath(path) UIDropDownMenu_SetSelectedValue(dd, v) UIDropDownMenu_SetText(dd, textFor(v)) end return dd end local function makeColorSwatch(parent, label, path, x, y) -- No ColorSwatchTemplate on 3.3.5a, so this is assembled by hand. local btn = CreateFrame("Button", nextName("Color"), parent) btn:SetSize(18, 18) btn:SetPoint("TOPLEFT", x, y) local border = btn:CreateTexture(nil, "BACKGROUND") border:SetAllPoints() border:SetTexture(0, 0, 0) local swatch = btn:CreateTexture(nil, "ARTWORK") swatch:SetPoint("TOPLEFT", 2, -2) swatch:SetPoint("BOTTOMRIGHT", -2, 2) local title = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") title:SetPoint("LEFT", btn, "RIGHT", 6, 0) title:SetText(label) local function current() local c = getPath(path) if type(c) ~= "table" then return 0.2, 0.2, 0.2, 0.85 end return c[1] or 0, c[2] or 0, c[3] or 0, c[4] or 1 end btn:SetScript("OnClick", function() local r, g, b, a = current() local function apply() local nr, ng, nb = ColorPickerFrame:GetColorRGB() -- OpacitySliderFrame's value is inverted against alpha on 3.3.5a: -- the slider counts opacity downwards. local na = OpacitySliderFrame and (1 - OpacitySliderFrame:GetValue()) or a setPath(path, { nr, ng, nb, na }) swatch:SetTexture(nr, ng, nb) swatch:SetAlpha(na) end local function cancel(prev) setPath(path, { prev.r, prev.g, prev.b, prev.a }) swatch:SetTexture(prev.r, prev.g, prev.b) swatch:SetAlpha(prev.a) end ColorPickerFrame.func = apply ColorPickerFrame.opacityFunc = apply ColorPickerFrame.cancelFunc = cancel ColorPickerFrame.hasOpacity = true ColorPickerFrame.opacity = 1 - a ColorPickerFrame.previousValues = { r = r, g = g, b = b, a = a } ColorPickerFrame:SetColorRGB(r, g, b) ColorPickerFrame:Hide() -- forces OnShow to run even if already open ColorPickerFrame:Show() end) btn.Refresh = function() local r, g, b, a = current() swatch:SetTexture(r, g, b) swatch:SetAlpha(a) end return btn end local function makeButton(parent, label, onClick, x, y, width) local b = CreateFrame("Button", nextName("Btn"), parent, "UIPanelButtonTemplate") b:SetSize(width or 160, 22) b:SetPoint("TOPLEFT", x, y) b:SetText(label) b:SetScript("OnClick", onClick) b.Refresh = function() end return b end --------------------------------------------------------------------------- -- Page construction. --------------------------------------------------------------------------- local controls = {} local function track(page, widget) page.controls = page.controls or {} table.insert(page.controls, widget) table.insert(controls, widget) return widget end local function newPage(parent) local f = CreateFrame("Frame", nil, parent) f:SetAllPoints() f:Hide() return f end local function heading(page, text, x, y) local fs = page:CreateFontString(nil, "OVERLAY", "GameFontNormal") fs:SetPoint("TOPLEFT", x, y) fs:SetText(text) fs:SetTextColor(1, 0.82, 0) return fs end -- One unit page (Friendly or Enemy): a Name block plus four sub-tabs. local function buildUnitPage(parent, key) local page = newPage(parent) heading(page, "Name", 16, -8) track(page, makeCheckbox(page, "Display Name", nil, { key, "name", "displayName" }, 16, -28)) track(page, makeDropdown(page, "Font", { key, "name", "font" }, fontList, 200, -34)) track(page, makeSlider(page, "Font Size", { key, "name", "fontSize" }, 8, 18, 1, 380, -40)) track(page, makeDropdown(page, "Font Flags", { key, "name", "fontFlags" }, function() return FONT_FLAGS end, 16, -84)) local sub = {} local subNames = { "Health", "Cast Bar", "Level Indicator", "Objective Icons" } for i = 1, #subNames do sub[i] = newPage(page) end -- Health ----------------------------------------------------------------- local h = sub[1] if key == "friendly" then track(h, makeCheckbox(h, "Name Only (no health bar)", "Show only the name on friendly plates.", { key, "health", "nameOnly" }, 16, -150)) end track(h, makeSlider(h, "Bar Width", { key, "health", "width" }, 40, 200, 1, 16, -190)) track(h, makeSlider(h, "Bar Height", { key, "health", "height" }, 4, 60, 1, 210, -190)) track(h, makeDropdown(h, "Text Format", { key, "health", "textFormat" }, function() return HEALTH_FORMATS end, 404, -184)) track(h, makeCheckbox(h, "Show Health Text", nil, { key, "health", "showTextFormat" }, 16, -232)) track(h, makeDropdown(h, "Font", { key, "health", "font" }, fontList, 16, -290)) track(h, makeSlider(h, "Font Size", { key, "health", "fontSize" }, 8, 18, 1, 210, -290)) track(h, makeDropdown(h, "Font Flags", { key, "health", "fontFlags" }, function() return FONT_FLAGS end, 404, -290)) track(h, makeDropdown(h, "Bar Texture", { key, "health", "statusBar" }, statusBarList, 16, -350)) track(h, makeCheckbox(h, "Use Class Color", nil, { key, "health", "useClassColor" }, 210, -350)) track(h, makeColorSwatch(h, "Background Color", { key, "health", "backgroundColor" }, 16, -400)) track(h, makeButton(h, "Reset Background Color", function() setPath({ key, "health", "backgroundColor" }, { 0.2, 0.2, 0.2, 0.85 }) for _, c in ipairs(controls) do if c.Refresh then c:Refresh() end end end, 210, -402, 180)) -- Cast Bar --------------------------------------------------------------- local c = sub[2] track(c, makeCheckbox(c, "Enabled", nil, { key, "castBar", "enabled" }, 16, -150)) track(c, makeDropdown(c, "Font", { key, "castBar", "font" }, fontList, 16, -210)) track(c, makeSlider(c, "Font Size", { key, "castBar", "fontSize" }, 8, 18, 1, 210, -210)) track(c, makeDropdown(c, "Font Flags", { key, "castBar", "fontFlags" }, function() return FONT_FLAGS end, 404, -210)) track(c, makeDropdown(c, "Bar Texture", { key, "castBar", "statusBar" }, statusBarList, 16, -270)) track(c, makeSlider(c, "Height", { key, "castBar", "height" }, 4, 32, 1, 210, -270)) -- Level Indicator -------------------------------------------------------- local l = sub[3] track(l, makeCheckbox(l, "Show Player Level", nil, { key, "levelIndicator", "showPlayerLevel" }, 16, -150)) track(l, makeCheckbox(l, "Show NPC Level", nil, { key, "levelIndicator", "showNPCLevel" }, 16, -180)) -- Objective Icons -------------------------------------------------------- local o = sub[4] local note = o:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") note:SetPoint("TOPLEFT", 16, -150) note:SetWidth(420) note:SetJustifyH("LEFT") note:SetText("Quest icons need per-unit quest data, which this client does not " .. "provide. These settings are kept so profiles stay compatible, but " .. "nothing is drawn.") track(o, makeCheckbox(o, "Show Quest NPCs", nil, { key, "objectiveIcons", "showQuestNPCs" }, 16, -200)) track(o, makeCheckbox(o, "Show Quest Objectives", nil, { key, "objectiveIcons", "showQuestObjectives" }, 16, -230)) track(o, makeSlider(o, "Icon Scale", { key, "objectiveIcons", "iconScale" }, 0.5, 2, 0.05, 16, -280)) track(o, makeDropdown(o, "Icon Anchor", { key, "objectiveIcons", "anchor" }, function() return QUEST_ANCHORS end, 210, -274)) -- Sub-tab strip ---------------------------------------------------------- local buttons = {} local function selectSub(index) for i, p in ipairs(sub) do if i == index then p:Show() else p:Hide() end if buttons[i] then if i == index then buttons[i]:Disable() else buttons[i]:Enable() end end end end for i, label in ipairs(subNames) do local b = CreateFrame("Button", nil, page, "UIPanelButtonTemplate") b:SetSize(112, 22) b:SetPoint("TOPLEFT", 16 + (i - 1) * 116, -118) b:SetText(label) b:SetScript("OnClick", function() selectSub(i) end) buttons[i] = b end page.selectSub = selectSub page:SetScript("OnShow", function() selectSub(1) end) return page end local function buildGeneralPage(parent) local page = newPage(parent) heading(page, "General", 16, -8) track(page, makeCheckbox(page, "Classic Style", "Use fixed classic plate dimensions.", { "general", "useClassicStyle" }, 16, -32)) heading(page, "Clickable Area", 16, -76) track(page, makeSlider(page, "Width", { "general", "clickable", "width" }, 40, 200, 1, 16, -110)) track(page, makeSlider(page, "Height", { "general", "clickable", "height" }, 10, 100, 1, 210, -110)) track(page, makeSlider(page, "Target Scale", { "general", "clickable", "targetScale" }, 0.8, 1.4, 0.1, 404, -110)) local note = page:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") note:SetPoint("TOPLEFT", 16, -170) note:SetWidth(500) note:SetJustifyH("LEFT") -- Says plainly that two of these three do nothing here. -- -- Resizing the clickable box needs the nameplateWidth and nameplateHeight -- CVars, and this client has neither: zero hits for either name in Wow.exe -- and in realm.dll. C_NamePlateManager.SetNamePlateSize is correspondingly a -- no-op in !Maniax335Compat, so the two sliders write a saved value that -- nothing can apply. The box is the client's own plate frame and the client -- sizes it. -- -- Left on rather than removed, because the setting is real and the profile -- should keep it against the day the engine gains support - and because a -- control that silently does nothing is the exact failure this panel was -- built to avoid. Target Scale is unaffected: it is applied in Lua by -- UpdateTargetScale and works. note:SetText("The clickable area is the box the game uses to target a plate. " .. "It is separate from the health bar size on the Friendly and Enemy tabs.\n\n" .. "Width and Height need engine support this client does not have, so they " .. "are saved but not applied. Target Scale works.") heading(page, "Stacking (shared with Maniax CVar)", 16, -254) local status = page:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") status:SetPoint("TOPLEFT", 16, -278) status:SetWidth(580) status:SetJustifyH("LEFT") local hint = page:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") hint:SetPoint("TOPLEFT", 16, -335) hint:SetWidth(580) hint:SetJustifyH("LEFT") hint:SetText("Crowded packs uses enemy stacking with less vertical and horizontal movement. " .. "Very dense groups may still overlap. Changes also appear in Maniax CVar.") local buttons = {} local function refreshStacking() local text, supported = Addon:GetStackingStatus() status:SetText(text) if supported then buttons[1]:Enable() else buttons[1]:Disable() end if Addon.db and Addon.db.stackingRestore then buttons[2]:Enable() else buttons[2]:Disable() end end for i, def in ipairs({ { "Crowded packs", "ApplyCrowdedStacking" }, { "Restore previous", "RestoreStacking" }, { "Maniax CVar", "OpenStackingOptions" } }) do local method = def[2] local button = CreateFrame("Button", nil, page, "UIPanelButtonTemplate") button:SetWidth(150) button:SetHeight(22) button:SetPoint("TOPLEFT", 16 + (i - 1) * 166, -304) button:SetText(def[1]) button:SetScript("OnClick", function() local ok, reason = Addon[method](Addon) if not ok then DEFAULT_CHAT_FRAME:AddMessage("ManiaxNamePlates: " .. tostring(reason)) end refreshStacking() end) buttons[i] = button end page:HookScript("OnShow", refreshStacking) page:RegisterEvent("CVAR_UPDATE") page:SetScript("OnEvent", function() if page:IsShown() then refreshStacking() end end) return page end local function buildPersonalPage(parent) local page = newPage(parent) heading(page, "Personal Plate", 16, -8) track(page, makeSlider(page, "Bar Width", { "personal", "health", "width" }, 40, 200, 1, 16, -48)) track(page, makeSlider(page, "Bar Height", { "personal", "health", "height" }, 4, 60, 1, 210, -48)) track(page, makeDropdown(page, "Text Format", { "personal", "health", "textFormat" }, function() return HEALTH_FORMATS end, 404, -42)) track(page, makeCheckbox(page, "Show Health Text", nil, { "personal", "health", "showTextFormat" }, 16, -96)) track(page, makeDropdown(page, "Font", { "personal", "health", "font" }, fontList, 16, -150)) track(page, makeSlider(page, "Font Size", { "personal", "health", "fontSize" }, 8, 18, 1, 210, -150)) track(page, makeDropdown(page, "Font Flags", { "personal", "health", "fontFlags" }, function() return FONT_FLAGS end, 404, -150)) track(page, makeDropdown(page, "Bar Texture", { "personal", "health", "statusBar" }, statusBarList, 16, -210)) track(page, makeColorSwatch(page, "Background Color", { "personal", "health", "backgroundColor" }, 210, -212)) return page end --------------------------------------------------------------------------- -- The panel. -- -- Built straight into Interface -> AddOns rather than into a window of its -- own. That was not possible before: the frame is 648x520 on a stock 3.3.5a -- client, leaving about 620x470 of panel, and this layout did not fit - the -- Target Scale slider was clipped off the right edge. -- -- client/FrameXML/InterfaceOptionsFrame.xml enlarges the frame to 858x660, -- which takes the panel to roughly 830x610. The columns below are still -- placed to fit the smaller one, so nothing clips even if that override is -- not active; the extra width just becomes breathing room. -- -- InterfaceOptions_AddCategory on 3.3.5a does not invoke panel.refresh when -- the panel is shown - that callback arrived in Cataclysm - so the controls -- are refreshed from an OnShow hook here instead. --------------------------------------------------------------------------- local panel = CreateFrame("Frame", "ManiaxNamePlatesOptionsPanel", UIParent) panel.name = "Maniax NamePlates" local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") title:SetPoint("TOPLEFT", 16, -16) title:SetText("Maniax NamePlates") local subtitle = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6) subtitle:SetText("Compact nameplates.") local body = CreateFrame("Frame", nil, panel) body:SetPoint("TOPLEFT", 0, -78) body:SetPoint("BOTTOMRIGHT", 0, 0) local pages = { buildGeneralPage(body), buildUnitPage(body, "friendly"), buildUnitPage(body, "enemy"), buildPersonalPage(body), } local pageNames = { "General", "Friendly", "Enemy", "Personal" } local tabs = {} local function selectTab(index) for i, p in ipairs(pages) do if i == index then p:Show() else p:Hide() end end PanelTemplates_SetTab(panel, index) end for i, label in ipairs(pageNames) do -- Named off the panel: PanelTemplates_SetTab and PanelTemplates_SetNumTabs -- find tabs as _G[frame:GetName().."Tab"..i]. A tab named anything else is -- never found and the highlight never moves, with no error to say why. local tab = CreateFrame("Button", panel:GetName() .. "Tab" .. i, panel, "OptionsFrameTabButtonTemplate") tab:SetText(label) tab:SetID(i) if i == 1 then tab:SetPoint("TOPLEFT", 14, -56) else tab:SetPoint("LEFT", tabs[i - 1], "RIGHT", -14, 0) end tab:SetScript("OnClick", function(self) selectTab(self:GetID()) PlaySound("igCharacterInfoTab") end) PanelTemplates_TabResize(tab, 0) tabs[i] = tab end panel.numTabs = #tabs PanelTemplates_SetNumTabs(panel, #tabs) local function refreshAll() if not Addon.db then return end for _, c in ipairs(controls) do if c.Refresh then c:Refresh() end end end panel:SetScript("OnShow", function() refreshAll() selectTab(1) end) panel.refresh = refreshAll panel.okay = function() end panel.cancel = function() end panel.default = function() end InterfaceOptions_AddCategory(panel) Addon.optionsPanel = panel function Addon:OpenOptions() -- Called twice on purpose: on 3.3.5a the first call only opens the Interface -- window, and the second selects the category within it. InterfaceOptionsFrame_OpenToCategory(panel) InterfaceOptionsFrame_OpenToCategory(panel) end -- A slash command cannot contain a space, so "/mnp options" is handled by the -- /mnp handler in ManiaxNamePlates.lua rather than registered here. SLASH_MANIAXNAMEPLATESOPT1 = "/mnpconfig" SlashCmdList["MANIAXNAMEPLATESOPT"] = function() Addon:OpenOptions() end