diff --git a/WoWUI.toc b/WoWUI.toc new file mode 100644 index 0000000..481bcdd --- /dev/null +++ b/WoWUI.toc @@ -0,0 +1,18 @@ +## Interface: 100206 +## Title: WoWUI +## Notes: Minimalist unit frames and action bars using oUF and LibActionButton +## Author: Jules +## Version: 1.0.0 +## SavedVariables: WoWUI_DB + +libs/LibActionButton-1.0/LibStub/LibStub.lua +libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua +libs/LibActionButton-1.0/LibButtonGlow-1.0/LibButtonGlow-1.0.lua +libs/LibActionButton-1.0/LibActionButton-1.0.lua + +libs/oUF/oUF.xml + +core.lua +settings.lua +unitframes.lua +actionbars.lua diff --git a/actionbars.lua b/actionbars.lua new file mode 100644 index 0000000..e4bc5bc --- /dev/null +++ b/actionbars.lua @@ -0,0 +1,130 @@ +local addonName, ns = ... +local LAB = LibStub("LibActionButton-1.0") + +ns.WoWUI.actionBars = {} +local buttonSpacing = 4 + +local function CreateBar(name, numButtons, point, x, y, rows) + -- Inherit SecureHandlerStateTemplate to use FrameRefs, WrapScript, and StateDrivers safely + local bar = CreateFrame("Frame", "WoWUIActionBar_" .. name, UIParent, "SecureHandlerStateTemplate") + bar.numButtons = numButtons + bar.rows = rows + table.insert(ns.WoWUI.actionBars, bar) + + local cols = math.ceil(numButtons / rows) + + bar:SetPoint(point, x, y) + + bar.buttons = {} + + for i = 1, numButtons do + local btn = LAB:CreateButton(i, "WoWUIButton_" .. name .. "_" .. i, bar, nil) + + + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + + + + table.insert(bar.buttons, btn) + end + + ns.WoWUI:RegisterMovable(bar, "ActionBar_" .. name) + + return bar +end + +local MainBar = CreateBar("Main", 12, "BOTTOM", 0, 50, 1) +local BottomLeftBar = CreateBar("BottomLeft", 12, "BOTTOM", 0, 90, 1) +local BottomRightBar = CreateBar("BottomRight", 12, "BOTTOM", 0, 130, 1) +local RightBar1 = CreateBar("Right1", 12, "RIGHT", -50, 0, 12) +local RightBar2 = CreateBar("Right2", 12, "RIGHT", -10, 0, 12) + +function ns.WoWUI:UpdateActionBarSizes() + local w = WoWUI_DB.settings.abWidth + local h = WoWUI_DB.settings.abHeight + for _, bar in ipairs(ns.WoWUI.actionBars) do + local cols = math.ceil(bar.numButtons / bar.rows) + bar:SetSize((w + buttonSpacing) * cols - buttonSpacing, (h + buttonSpacing) * bar.rows - buttonSpacing) + for i, btn in ipairs(bar.buttons) do + btn:SetSize(w, h) + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", bar, "TOPLEFT", col * (w + buttonSpacing), -row * (h + buttonSpacing)) + end + end +end + + + +-- Setup State Drivers for paging (Stance/Form/Stealth/Vehicle) +local pageDriver = "[vehicleui] 12; [possessbar] 12; [overridebar] 14; [shapeshift] 13; [bar:2] 2; [bar:3] 3; [bar:4] 4; [bar:5] 5; [bar:6] 6; [bonusbar:1] 7; [bonusbar:2] 8; [bonusbar:3] 9; [bonusbar:4] 10; 1" + +MainBar:RegisterEvent("PLAYER_LOGIN") +MainBar:SetScript("OnEvent", function(self, event) + if event == "PLAYER_LOGIN" then + -- Apply state driver to the main bar + for i, btn in ipairs(MainBar.buttons) do + btn:SetState(0, "action", i) + for state = 1, 14 do + btn:SetState(state, "action", (state - 1) * 12 + i) + end + end + RegisterStateDriver(MainBar, "page", pageDriver) + MainBar:SetAttribute("_onstate-page", [[ + self:ChildUpdate("state", newstate) + ]]) + + -- Setup child updates + for i, btn in ipairs(MainBar.buttons) do + MainBar:SetFrameRef("Button"..i, btn) + btn:SetAttribute("frameref-MainBar", MainBar) + end + + -- Add button state script mapping (so they react to ChildUpdate) + for i, btn in ipairs(MainBar.buttons) do + SecureHandlerWrapScript(btn, "OnAttributeChanged", btn, [[ + if name == "state-page" then + self:SetAttribute("state", value) + end + ]]) + end + + -- Static Action Bars + local staticOffset = { + [BottomLeftBar] = 6, -- Action Page 6 (BottomLeft) + [BottomRightBar] = 5, -- Action Page 5 (BottomRight) + [RightBar1] = 3, -- Action Page 3 (Right 1) + [RightBar2] = 4, -- Action Page 4 (Right 2) + } + + for bar, page in pairs(staticOffset) do + for i, btn in ipairs(bar.buttons) do + btn:SetState(0, "action", (page - 1) * 12 + i) + end + end + + -- Hide default Blizzard action bars robustly + local hiddenFrame = CreateFrame("Frame") + hiddenFrame:Hide() + + local framesToHide = { + MainMenuBar, + MultiBarBottomLeft, + MultiBarBottomRight, + MultiBarLeft, + MultiBarRight, + StanceBarFrame, + PossessBarFrame, + PetActionBarFrame, + } + + for _, frame in ipairs(framesToHide) do + if frame then + frame:SetParent(hiddenFrame) + frame:UnregisterAllEvents() + end + end + end +end) diff --git a/core.lua b/core.lua new file mode 100644 index 0000000..d570333 --- /dev/null +++ b/core.lua @@ -0,0 +1,110 @@ +local addonName, ns = ... + +-- Global table for saving positions +WoWUI_DB = WoWUI_DB or {} + + +local WoWUI = CreateFrame("Frame") +ns.WoWUI = WoWUI + +local movableFrames = {} + +WoWUI:RegisterEvent("ADDON_LOADED") +WoWUI:SetScript("OnEvent", function(self, event, loadedAddon) + if loadedAddon == addonName then + WoWUI_DB.positions = WoWUI_DB.positions or {} + WoWUI_DB.settings = WoWUI_DB.settings or { abWidth = 36, abHeight = 36, ufWidth = 200, ufHeight = 40 } + + self:UnregisterEvent("ADDON_LOADED") + + -- Restore saved positions now that the DB is loaded + if ns.WoWUI.UpdateActionBarSizes then ns.WoWUI:UpdateActionBarSizes() end + if ns.WoWUI.UpdateUnitFrameSizes then ns.WoWUI:UpdateUnitFrameSizes() end + + for _, frame in ipairs(movableFrames) do + local name = frame.dbKey + if WoWUI_DB.positions[name] then + local pos = WoWUI_DB.positions[name] + frame:ClearAllPoints() + frame:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4]) + end + end + end +end) + +-- Movability system +local unlocked = false + +function WoWUI:RegisterMovable(frame, name) + frame.dbKey = name + table.insert(movableFrames, frame) + + local mover = CreateFrame("Frame", nil, frame, "BackdropTemplate") + mover:SetAllPoints(frame) + mover:SetFrameStrata("TOOLTIP") + mover:EnableMouse(true) + mover:RegisterForDrag("LeftButton") + + mover:SetScript("OnDragStart", function(self) + frame:StartMoving() + end) + + mover:SetScript("OnDragStop", function(self) + frame:StopMovingOrSizing() + local point, _, relativePoint, x, y = frame:GetPoint() + WoWUI_DB.positions[frame.dbKey] = { point, relativePoint, x, y } + end) + + -- Simple background to show it's unlocked + local tex = mover:CreateTexture(nil, "BACKGROUND") + tex:SetAllPoints() + tex:SetColorTexture(0, 1, 0, 0.4) + + local text = mover:CreateFontString(nil, "OVERLAY", "GameFontNormal") + text:SetPoint("CENTER") + text:SetText(name) + + mover:Hide() + frame.mover = mover + + -- If the frame is registered after ADDON_LOADED has fired, try to load it now + if WoWUI_DB and WoWUI_DB.positions and WoWUI_DB.positions[name] then + local pos = WoWUI_DB.positions[name] + frame:ClearAllPoints() + frame:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4]) + end +end + +SLASH_WOWUI1 = "/wowui" +SlashCmdList["WOWUI"] = function(msg) + if msg == "unlock" then + unlocked = true + for _, frame in ipairs(movableFrames) do + frame:SetMovable(true) + frame:SetUserPlaced(true) + frame.mover:Show() + end + print("WoWUI: Frames unlocked.") + elseif msg == "settings" then + if not WoWUI.settingsFrame then + WoWUI:CreateSettingsFrame() + end + if WoWUI.settingsFrame:IsShown() then + WoWUI.settingsFrame:Hide() + else + WoWUI.settingsFrame:Show() + end + elseif msg == "lock" then + unlocked = false + for _, frame in ipairs(movableFrames) do + frame:SetMovable(false) + frame.mover:Hide() + end + print("WoWUI: Frames locked.") + else + print("WoWUI Commands:") + print(" /wowui unlock - Unlock frames to move them") + print(" /wowui settings - Open settings menu") + print(" /wowui lock - Lock frames") + end +end diff --git a/libs/LibActionButton-1.0/CHANGES.txt b/libs/LibActionButton-1.0/CHANGES.txt new file mode 100644 index 0000000..10d1174 --- /dev/null +++ b/libs/LibActionButton-1.0/CHANGES.txt @@ -0,0 +1,109 @@ +tag 69904d18f5834df9ee3d7bc820348cf19cfa750e 0.60 +Author: Hendrik Leppkes +Date: Tue Mar 24 19:44:34 2026 +0100 + +Tag as 0.60 + +commit 4b6b13af72da5eca2e7784b0e8eb614648e8b31f +Author: Hendrik Leppkes +Date: Tue Mar 24 16:13:43 2026 +0100 + + Update cooldown handling for 12.0.1.66562 hotfix + +commit fbcc6ec6ad744df7e3436773c39fadbf13e5f9c3 +Author: Hendrik Leppkes +Date: Tue Mar 24 15:34:27 2026 +0100 + + Update level link lock handling to match latest Blizzard changes + +commit 004f030840fb367f05a458e4e3d3619f02df2186 +Author: Hendrik Leppkes +Date: Thu Jan 22 20:48:48 2026 +0100 + + Use C_Spell.GetSpellDisplayCount when appropriate + + Fixes a secret comparison error for spell buttons + +commit d437febf91703c7e9817af1c564d39232c06b28e +Author: Hendrik Leppkes +Date: Thu Jan 22 20:48:07 2026 +0100 + + Factor display count computation into the button type prototype + +commit ef7d8c7cf98c0a221a16974eb917dbf963221124 +Author: Hendrik Leppkes +Date: Tue Jan 20 16:59:05 2026 +0100 + + Check cooldown info for sanity before using them with ApplyCooldown + +commit c2cf2990ed1ddc907ddfa80a046ff60f859b3261 +Author: Hendrik Leppkes +Date: Sat Jan 17 15:55:02 2026 +0100 + + Remove button state override on mouse up + + The issue this was designed to fix has seemingly been fixed in the game, + and it ended up causing issues with button clicks when keybindings and + mouse clicks are combined in certain ways. + +commit 4fc8d919def3fd96d26c6a248e1c28d16f825ba4 +Author: Hendrik Leppkes +Date: Fri Jan 16 09:49:45 2026 +0100 + + Disable use of vanilla overlay glow for now + +commit 90c63511a0bcb28364219a61492f7e9a098d353a +Author: Hendrik Leppkes +Date: Fri Jan 16 00:11:44 2026 +0100 + + Limit using ActionButtonSpellAlertManager to retail + + Its broken, but present, on TBC Anniversary. Thanks Blizzard. + +commit 72510e09490fa2aa7c4f19a720d77e85452e84fd +Author: Hendrik Leppkes +Date: Wed Jan 14 15:18:07 2026 +0100 + + Update click handling for BCC Annivesary + +commit d0baa26997e9b3b85e96fdba3971cdfd07569f97 +Author: Hendrik Leppkes +Date: Mon Dec 15 09:53:27 2025 +0100 + + Update event registration for TBC Classic 2.5.5 + +commit 11d3e2b6a424a1114080cd4db78f1b2e88c79516 +Author: Hendrik Leppkes +Date: Mon Dec 15 09:27:24 2025 +0100 + + Use ActionButtonSpellAlertManager for proc glow + +commit dad6203f1295bc5ebf60c7ae7dad86f8dd312f4d +Author: Hendrik Leppkes +Date: Mon Dec 15 08:41:21 2025 +0100 + + Fix pre-12 charge info handling + +commit f600a42d4ad3a2d9e00ec4f94232a2a603c0254e +Author: Hendrik Leppkes +Date: Fri Dec 12 14:03:55 2025 +0100 + + Update TOC for recent releases and Midnight + +commit 50fabf9e37377b3ace8c7e4566b0e4a337428e56 +Author: Hendrik Leppkes +Date: Fri Dec 12 14:01:13 2025 +0100 + + Use GetActionDisplayCount if available in Midnight + +commit 046186d19b24fe93b2a72b09a7f35f9498d5768b +Author: Hendrik Leppkes +Date: Fri Dec 12 14:00:39 2025 +0100 + + Update Cooldown API for Midnight + +commit d3ac3d3d324c7cb9b8dab17ea555aab5da7e33d0 +Author: Hendrik Leppkes +Date: Fri Dec 12 13:58:26 2025 +0100 + + Update event registration for Midnight diff --git a/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua b/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua new file mode 100644 index 0000000..ad4a89e --- /dev/null +++ b/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua @@ -0,0 +1,201 @@ +--[[ $Id: CallbackHandler-1.0.lua 26 2022-12-12 15:09:39Z nevcairiel $ ]] +local MAJOR, MINOR = "CallbackHandler-1.0", 8 +local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR) + +if not CallbackHandler then return end -- No upgrade needed + +local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end} + +-- Lua APIs +local securecallfunction, error = securecallfunction, error +local setmetatable, rawget = setmetatable, rawget +local next, select, pairs, type, tostring = next, select, pairs, type, tostring + + +local function Dispatch(handlers, ...) + local index, method = next(handlers) + if not method then return end + repeat + securecallfunction(method, ...) + index, method = next(handlers, index) + until not method +end + +-------------------------------------------------------------------------- +-- CallbackHandler:New +-- +-- target - target object to embed public APIs in +-- RegisterName - name of the callback registration API, default "RegisterCallback" +-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback" +-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API. + +function CallbackHandler.New(_self, target, RegisterName, UnregisterName, UnregisterAllName) + + RegisterName = RegisterName or "RegisterCallback" + UnregisterName = UnregisterName or "UnregisterCallback" + if UnregisterAllName==nil then -- false is used to indicate "don't want this method" + UnregisterAllName = "UnregisterAllCallbacks" + end + + -- we declare all objects and exported APIs inside this closure to quickly gain access + -- to e.g. function names, the "target" parameter, etc + + + -- Create the registry object + local events = setmetatable({}, meta) + local registry = { recurse=0, events=events } + + -- registry:Fire() - fires the given event/message into the registry + function registry:Fire(eventname, ...) + if not rawget(events, eventname) or not next(events[eventname]) then return end + local oldrecurse = registry.recurse + registry.recurse = oldrecurse + 1 + + Dispatch(events[eventname], eventname, ...) + + registry.recurse = oldrecurse + + if registry.insertQueue and oldrecurse==0 then + -- Something in one of our callbacks wanted to register more callbacks; they got queued + for event,callbacks in pairs(registry.insertQueue) do + local first = not rawget(events, event) or not next(events[event]) -- test for empty before. not test for one member after. that one member may have been overwritten. + for object,func in pairs(callbacks) do + events[event][object] = func + -- fire OnUsed callback? + if first and registry.OnUsed then + registry.OnUsed(registry, target, event) + first = nil + end + end + end + registry.insertQueue = nil + end + end + + -- Registration of a callback, handles: + -- self["method"], leads to self["method"](self, ...) + -- self with function ref, leads to functionref(...) + -- "addonId" (instead of self) with function ref, leads to functionref(...) + -- all with an optional arg, which, if present, gets passed as first argument (after self if present) + target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]]) + if type(eventname) ~= "string" then + error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2) + end + + method = method or eventname + + local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten. + + if type(method) ~= "string" and type(method) ~= "function" then + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2) + end + + local regfunc + + if type(method) == "string" then + -- self["method"] calling style + if type(self) ~= "table" then + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2) + elseif self==target then + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2) + elseif type(self[method]) ~= "function" then + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2) + end + + if select("#",...)>=1 then -- this is not the same as testing for arg==nil! + local arg=select(1,...) + regfunc = function(...) self[method](self,arg,...) end + else + regfunc = function(...) self[method](self,...) end + end + else + -- function ref with self=object or self="addonId" or self=thread + if type(self)~="table" and type(self)~="string" and type(self)~="thread" then + error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2) + end + + if select("#",...)>=1 then -- this is not the same as testing for arg==nil! + local arg=select(1,...) + regfunc = function(...) method(arg,...) end + else + regfunc = method + end + end + + + if events[eventname][self] or registry.recurse<1 then + -- if registry.recurse<1 then + -- we're overwriting an existing entry, or not currently recursing. just set it. + events[eventname][self] = regfunc + -- fire OnUsed callback? + if registry.OnUsed and first then + registry.OnUsed(registry, target, eventname) + end + else + -- we're currently processing a callback in this registry, so delay the registration of this new entry! + -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency + registry.insertQueue = registry.insertQueue or setmetatable({},meta) + registry.insertQueue[eventname][self] = regfunc + end + end + + -- Unregister a callback + target[UnregisterName] = function(self, eventname) + if not self or self==target then + error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2) + end + if type(eventname) ~= "string" then + error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2) + end + if rawget(events, eventname) and events[eventname][self] then + events[eventname][self] = nil + -- Fire OnUnused callback? + if registry.OnUnused and not next(events[eventname]) then + registry.OnUnused(registry, target, eventname) + end + end + if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then + registry.insertQueue[eventname][self] = nil + end + end + + -- OPTIONAL: Unregister all callbacks for given selfs/addonIds + if UnregisterAllName then + target[UnregisterAllName] = function(...) + if select("#",...)<1 then + error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2) + end + if select("#",...)==1 and ...==target then + error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2) + end + + + for i=1,select("#",...) do + local self = select(i,...) + if registry.insertQueue then + for eventname, callbacks in pairs(registry.insertQueue) do + if callbacks[self] then + callbacks[self] = nil + end + end + end + for eventname, callbacks in pairs(events) do + if callbacks[self] then + callbacks[self] = nil + -- Fire OnUnused callback? + if registry.OnUnused and not next(callbacks) then + registry.OnUnused(registry, target, eventname) + end + end + end + end + end + end + + return registry +end + + +-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it +-- try to upgrade old implicit embeds since the system is selfcontained and +-- relies on closures to work. diff --git a/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml b/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml new file mode 100644 index 0000000..876df83 --- /dev/null +++ b/libs/LibActionButton-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml @@ -0,0 +1,4 @@ + +