Mercurial > wow > reaction
diff Bar.lua @ 245:65f2805957a0
No real reason to store some of the code in a subdirectory.
author | Flick |
---|---|
date | Sat, 26 Mar 2011 12:35:08 -0700 |
parents | classes/Bar.lua@b56cff349bd6 |
children | 36a29870bf34 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Bar.lua Sat Mar 26 12:35:08 2011 -0700 @@ -0,0 +1,854 @@ +local addonName, addonTable = ... +local ReAction = addonTable.ReAction +local L = ReAction.L +local LKB = ReAction.LKB +local _G = _G +local CreateFrame = CreateFrame +local floor = math.floor +local fmod = math.fmod +local format = string.format +local tfetch = addonTable.tfetch +local tbuild = addonTable.tbuild +local fieldsort = addonTable.fieldsort + +local LSG = LibStub("ReAction-LibShowActionGrid-1.0") + +---- Secure snippets ---- +local _reaction_init = +[[ + anchorKeys = newtable("point","relPoint","x","y") + + state = nil + set_state = nil + state_override = nil + unit_exists = nil + + showAll = false + hidden = false + + defaultAlpha = 1.0 + defaultScale = 1.0 + defaultAnchor = newtable() + + activeStates = newtable() + settings = newtable() + extensions = newtable() +]] + +local _reaction_refresh = +[[ + local oldState = state + state = state_override or set_state or state + + local hide = nil + if state then + local settings = settings[state] + if settings then + -- show/hide + hide = settings.hide + -- re-anchor + local old_anchor = activeStates.anchor + activeStates.anchor = settings.anchorEnable and state + if old_anchor ~= activeStates.anchor or not set_state then + if activeStates.anchor then + if settings.anchorPoint then + self:ClearAllPoints() + local f = self:GetAttribute("frameref-anchor-"..state) + if f then + self:SetPoint(settings.anchorPoint, f, settings.anchorRelPoint, settings.anchorX, settings.anchorY) + end + end + elseif defaultAnchor.point then + self:ClearAllPoints() + self:SetPoint(defaultAnchor.point, defaultAnchor.frame, + defaultAnchor.relPoint, defaultAnchor.x, defaultAnchor.y) + end + end + -- re-scale + local old_scale = activeStates.scale + activeStates.scale = settings.enableScale and state + if old_scale ~= activeStates.scale or not set_state then + self:SetScale(activeStates.scale and settings.scale or defaultScale) + end + -- alpha + local old_alpha = activeStates.alpha + activeStates.alpha = settings.enableAlpha and state + if old_alpha ~= activeStates.alpha or not set_state then + self:SetAlpha(activeStates.alpha and settings.alpha or defaultAlpha) + end + end + end + + -- hide if state or unit_exists says to + hide = not showAll and (hide or unithide) + if hide ~= hidden then + hidden = hide + if hide then + self:Hide() + else + self:Show() + end + end + + for _, attr in pairs(extensions) do + control:RunAttribute(attr) + end + + control:ChildUpdate() + + if showAll then + control:CallMethod("UpdateHiddenLabel", state and settings[state] and settings[state].hide) + end + + if oldState ~= state then + control:CallMethod("StateRefresh", state) + end +]] + +local _onstate_reaction = -- function( self, stateid, newstate ) +[[ + set_state = newstate +]] .. _reaction_refresh + +local _onstate_showgrid = -- function( self, stateid, newstate ) +[[ + control:ChildUpdate(stateid,newstate) + control:CallMethod("UpdateShowGrid") +]] + +local _onstate_unitexists = -- function( self, stateid, newstate ) +[[ + unithide = not newstate or newstate == "hide" +]] .. _reaction_refresh + +local _onclick = -- function( self, button, down ) +[[ + if state_override == button then + state_override = nil -- toggle + else + state_override = button + end +]] .. _reaction_refresh + +-- For reference +-- the option field names must match the field names of the options table, below +local stateProperties = { + hide = true, + --keybindState = true, TODO: broken + anchorEnable = true, + anchorFrame = true, + anchorPoint = true, + anchorRelPoint = true, + anchorX = true, + anchorY = true, + enableScale = true, + scale = true, + enableAlpha = true, + alpha = true, +} + + + +---- Bar class ---- +local Bar = { } +local frameList = { } + +ReAction.Bar = Bar -- export to ReAction + +function Bar:New( name, config, buttonClass ) + if type(config) ~= "table" then + error("ReAction.Bar: config table required") + end + + -- create new self + self = setmetatable( + { + config = config, + name = name, + buttons = { }, + buttonClass = buttonClass, + width = config.width or 480, + height = config.height or 40, + }, + {__index = self} ) + + -- The frame type is 'Button' in order to have an OnClick handler. However, the frame itself is + -- not mouse-clickable by the user. + local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent + name = name and "ReAction-"..name + local f = name and frameList[name] + if not f then + f = CreateFrame("Button", name, parent, "SecureHandlerStateTemplate, SecureHandlerClickTemplate") + if name then + frameList[name] = f + end + end + f:SetFrameStrata("MEDIUM") + f:SetWidth(self.width) + f:SetHeight(self.height) + f:SetAlpha(config.alpha or 1.0) + f:Show() + f:EnableMouse(false) + f:SetClampedToScreen(true) + LSG:AddFrame(f) + + -- secure handlers + f:Execute(_reaction_init) + f:SetAttribute("_onstate-reaction", _onstate_reaction) + f:SetAttribute("_onstate-showgrid", _onstate_showgrid) + f:SetAttribute("_onstate-unitexists", _onstate_unitexists) + f:SetAttribute("_onclick", _onclick) + + -- secure handler CallMethod()s + f.UpdateShowGrid = function() self:UpdateShowGrid() end + f.StateRefresh = function() self:RefreshControls() end + f.UpdateHiddenLabel = function(f,hidden) self:SetLabelSubtext(hidden and L["Hidden"]) end + + -- Override the default frame accessor to provide strict read-only access + function self:GetFrame() + return f + end + + self:ApplyAnchor() + self:SetConfigMode(ReAction:GetConfigMode()) + self:SetKeybindMode(ReAction:GetKeybindMode()) + + if ReAction.LBF then + local g = ReAction.LBF:Group(L["ReAction"], self.name) + self.config.ButtonFacade = self.config.ButtonFacade or { + skinID = "Blizzard", + backdrop = true, + gloss = 0, + colors = {}, + } + local c = self.config.ButtonFacade + g:Skin(c.skinID, c.gloss, c.backdrop, c.colors) + self.LBFGroup = g + end + + ReAction.RegisterCallback(self, "OnConfigModeChanged") + + buttonClass:SetupBar(self) + self:ApplyStates() + + return self +end + +function Bar:Destroy() + local f = self:GetFrame() + self:CleanupStates() + for idx, b in self:IterateButtons() do + b:Destroy() + end + f:UnregisterAllEvents() + self:ShowControls(false) + ReAction.UnregisterAllCallbacks(self) + LKB.UnregisterAllCallbacks(self) + if self.LBFGroup then + self.LBFGroup:Delete(true) + end + LSG:RemoveFrame(f) + f:SetParent(UIParent) + f:ClearAllPoints() + f:Hide() +end + +-- +-- Events +-- + +function Bar:OnConfigModeChanged(event, mode) + self:SetConfigMode(mode) +end + +-- +-- Accessors +-- + +function Bar:GetName() + return self.name +end + +-- only ReAction:RenameBar() should call this function. Calling from any other +-- context will desync the bar list in the ReAction class. +function Bar:SetName(name) + if self.LBFGroup then + -- LBF doesn't offer a method of renaming a group, so delete and remake the group. + local c = self.config.ButtonFacade + local g = ReAction.LBF:Group(L["ReAction"], name) + for idx, b in self:IterateButtons() do + self.LBFGroup:RemoveButton(b:GetFrame(), true) + g:AddButton(b:GetFrame()) + end + self.LBFGroup:Delete(true) + self.LBFGroup = g + self.LBFGroup:Skin(c.skinID, c.gloss, c.backdrop, c.colors) + end + self.name = name + if self.overlay then + self.overlay:SetLabel(self.name) + end +end + +function Bar:GetFrame() + -- this method is included for documentation purposes. It is overridden + -- for each object in the :New() method. + error("Invalid Bar object: used without initialization") +end + +function Bar:GetButton(idx) + return self.buttons[idx] +end + +function Bar:GetButtonClass() + return self.buttonClass +end + +function Bar:GetConfig() + return self.config +end + +function Bar:GetAnchor() + local c = self.config + return (c.point or "CENTER"), + (c.anchor or self:GetFrame():GetParent():GetName()), + (c.relpoint or c.point or "CENTER"), + (c.x or 0), + (c.y or 0) +end + +function Bar:SetAnchor(point, frame, relativePoint, x, y) + local c = self.config + c.point = point or c.point + c.anchor = frame or c.anchor + c.relpoint = relativePoint or c.relpoint + c.x = x or c.x + c.y = y or c.y + self:ApplyAnchor() +end + +function Bar:GetSize() + local f = self:GetFrame() + return f:GetWidth(), f:GetHeight() +end + +function Bar:SetSize(w,h) + local f = self:GetFrame() + self.config.width = w + self.config.height = h + f:SetWidth(w) + f:SetHeight(h) +end + +function Bar:GetButtonSize() + local w = self.config.btnWidth or 32 + local h = self.config.btnHeight or 32 + -- TODO: get from modules? + return w,h +end + +function Bar:SetButtonSize(w,h) + if w > 0 and h > 0 then + self.config.btnWidth = w + self.config.btnHeight = h + end +end + +function Bar:GetNumButtons() + local r,c = self:GetButtonGrid() + return r*c +end + +function Bar:GetButtonGrid() + local cfg = self.config + local r = cfg.btnRows or 1 + local c = cfg.btnColumns or 1 + local s = cfg.spacing or 4 + return r,c,s +end + +function Bar:SetButtonGrid(r,c,s) + if r > 0 and c > 0 and s > 0 then + local cfg = self.config + cfg.btnRows = r + cfg.btnColumns = c + cfg.spacing = s + end + self.buttonClass:SetupBar(self) +end + +function Bar:GetAlpha() + return self.config.alpha or 1.0 +end + +function Bar:SetAlpha(value) + self.config.alpha = value + self:GetFrame():SetAlpha(value or 1.0) + self:UpdateDefaultStateAlpha() +end + +function Bar:IterateButtons() + -- iterator returns idx, button, but does NOT iterate in index order + return pairs(self.buttons) +end + +-- +-- Methods +-- + +function Bar:SetConfigMode(mode) + self:SetSecureData("showAll",mode) + self:ShowControls(mode) + for idx, b in self:IterateButtons() do + b:ShowGridTemp(mode) + b:UpdateActionIDLabel(mode) + end +end + +function Bar:SetKeybindMode(mode) + self:SetSecureData("showAll",mode) + for idx, b in self:IterateButtons() do + b:SetKeybindMode(mode) + end +end + +function Bar:ApplyAnchor() + local f = self:GetFrame() + local c = self.config + local p = c.point + + f:SetWidth(c.width) + f:SetHeight(c.height) + f:ClearAllPoints() + + if p then + local a = f:GetParent() + if c.anchor then + local bar = ReAction:GetBar(c.anchor) + if bar then + a = bar:GetFrame() + else + a = _G[c.anchor] + end + end + local fr = a or f:GetParent() + f:SetPoint(p, a or f:GetParent(), c.relpoint, c.x or 0, c.y or 0) + else + f:SetPoint("CENTER") + end + + self:UpdateDefaultStateAnchor() +end + +function Bar:ClipNButtons( n ) + local cfg = self.config + local r = cfg.btnRows or 1 + local c = cfg.btnColumns or 1 + + cfg.btnRows = ceil(n/c) + cfg.btnColumns = min(n,c) +end + +function Bar:AddButton(idx, button) + local f = self:GetFrame() + + self.buttons[idx] = button + + -- Store a properly wrapped reference to the child frame as an attribute + -- (accessible via "frameref-btn#") + f:SetFrameRef(format("btn%d",idx), button:GetFrame()) + + -- button constructors are responsible for calling SkinButton +end + +function Bar:RemoveButton(button) + local idx = button:GetIndex() + if idx then + self:GetFrame():SetAttribute(format("frameref-btn%d",idx),nil) + self.buttons[idx] = nil + end + if self.LBFGroup then + self.LBFGroup:RemoveButton(button:GetFrame(),true) + end +end + +function Bar:PlaceButton(button, baseW, baseH) + local idx = button:GetIndex() + if idx then + local r, c, s = self:GetButtonGrid() + local bh, bw = self:GetButtonSize() + local row, col = floor((idx-1)/c), fmod((idx-1),c) -- zero-based + local x, y = col*bw + (col+0.5)*s, -(row*bh + (row+0.5)*s) + local scale = bw/baseW + local b = button:GetFrame() + + b:ClearAllPoints() + b:SetPoint("TOPLEFT",x/scale,y/scale) + b:SetScale(scale) + end +end + +function Bar:SkinButton( button, data ) + if self.LBFGroup then + self.LBFGroup:AddButton(button:GetFrame(), data) + end +end + +function Bar:UpdateShowGrid() + for idx, button in self:IterateButtons() do + button:UpdateShowGrid() + end +end + +function Bar:ShowControls(show) + if show then + if not self.overlay then + self.overlay = Bar.Overlay:New(self) -- see Overlay.lua + end + self.overlay:Show() + self:RefreshSecureState() + elseif self.overlay then + self.overlay:Hide() + end +end + +function Bar:RefreshControls() + if self.overlay and self.overlay:IsShown() then + self.overlay:RefreshControls() + end +end + +function Bar:SetLabelSubtext(text) + if self.overlay then + self.overlay:SetLabelSubtext(text) + end +end + +-- +-- Secure state functions +-- + +function Bar:GetSecureState() + local env = GetManagedEnvironment(self:GetFrame()) + return env and env.state +end + +function Bar:GetStateProperty(state, propname) + return tfetch(self:GetConfig(), "states", state, propname) +end + +function Bar:SetStateProperty(state, propname, value) + local s = tbuild(self:GetConfig(), "states", state) + s[propname] = value + self:SetSecureStateData(state, propname, value) +end + +function Bar:ApplyStates() + local states = tfetch(self:GetConfig(), "states") + if states then + self:SetStateDriver(states) + end +end + +function Bar:CleanupStates() + self:SetStateDriver(nil) +end + +function Bar:RefreshSecureState() + self:GetFrame():Execute(_reaction_refresh) +end + +-- usage: SetSecureData(globalname, [tblkey1, tblkey2, ...], value) +function Bar:SetSecureData( ... ) + local n = select('#',...) + if n < 2 then + error("ReAction.Bar:SetSecureData() requires at least 2 arguments") + end + local f = self:GetFrame() + f:SetAttribute("data-depth",n-1) + f:SetAttribute("data-value",select(n,...)) + for i = 1, n-1 do + local key = select(i,...) + if key == nil then + error("ReAction.Bar:SetSecureData() - nil table key in argument list (#"..i..")") + end + f:SetAttribute("data-key-"..i, key) + end + f:Execute( + [[ + local n = self:GetAttribute("data-depth") + if n > 0 then + local value = self:GetAttribute("data-value") + local t = _G + for i = 1, n do + local key = self:GetAttribute("data-key-"..i) + if not key then return end + if not t[key] then + t[key] = newtable() + end + if i == n then + t[key] = value + else + t = t[key] + end + end + end + ]]) + self:RefreshSecureState() +end + +function Bar:SetSecureStateData( state, key, value ) + self:SetSecureData("settings",state,key,value) +end + +-- sets a snippet to be run as an extension to _onstate-reaction +function Bar:SetSecureStateExtension( id, snippet ) + if id == nil then + error("ReAction.Bar:SetSecureStateExtension() requires an id") + end + local f = self:GetFrame() + f:SetAttribute("input-secure-ext-id",id) + f:SetAttribute("secure-ext-"..id,snippet) + f:Execute( + [[ + local id = self:GetAttribute("input-secure-ext-id") + if id then + extensions[id] = self:GetAttribute("secure-ext-"..id) or nil + end + ]]) + self:RefreshSecureState() +end + +function Bar:SetFrameRef( name, refFrame ) + if refFrame then + local _, explicit = refFrame:IsProtected() + if not explicit then + refFrame = nil + end + end + if refFrame then + self:GetFrame():SetFrameRef(name,refFrame) + else + self:GetFrame():SetAttribute("frameref-"..name,nil) + end +end + +function Bar:SetStateDriver( states ) + if states then + for state, props in pairs(states) do + self:SetSecureStateData(state, "active_", true) -- make sure there's a 'settings' field for this state + for propname, value in pairs(props) do + if propname == "anchorFrame" then + self:SetFrameRef("anchor-"..state, _G[value]) + elseif propname == "rule" then + -- do nothing + else + self:SetSecureStateData(state, propname, value) + end + end + end + end + local rule = states and self:BuildStateRule(states) + if rule then + RegisterStateDriver(self:GetFrame(),"reaction",rule) + elseif self.statedriver then + UnregisterStateDriver(self:GetFrame(),"reaction") + end + self.statedriver = rule + self:BuildStateKeybinds(states) + self:RefreshSecureState() +end + +-- pass unit=nil to set up the unit elsewhere, if you want something more complex +function Bar:RegisterUnitWatch( unit, enable ) + local f = self:GetFrame() + if unit then + f:SetAttribute("unit",unit) + end + if enable then + if not self.unitwatch then + RegisterUnitWatch(self:GetFrame(),true) + end + elseif self.unitwatch then + UnregisterUnitWatch(self:GetFrame()) + end + self.unitwatch = enable + self:RefreshSecureState() +end + +function Bar:SetStateKeybind( key, state ) + local f = self:GetFrame() + local binds = self.statebinds + if not binds then + binds = { } + self.statebinds = binds + end + + -- clear the old binding, if any + if binds[state] then + SetOverrideBinding(f, false, binds[state], nil) + end + + if key then + SetOverrideBindingClick(f, false, key, f:GetName(), state) -- state name is virtual mouse button + end + binds[state] = key +end + +function Bar:GetStateKeybind( state ) + if self.statebinds and state then + return self.statebinds[state] + end +end + +function Bar:UpdateDefaultStateAnchor() + local point, frame, relPoint, x, y = self:GetAnchor() + local f = self:GetFrame() + f:SetAttribute("defaultAnchor-point",point) + f:SetAttribute("defaultAnchor-relPoint",relPoint) + f:SetAttribute("defaultAnchor-x",x) + f:SetAttribute("defaultAnchor-y",y) + self:SetFrameRef("defaultAnchor",_G[frame or "UIParent"]) + f:Execute([[ + for _, k in pairs(anchorKeys) do + defaultAnchor[k] = self:GetAttribute("defaultAnchor-"..k) + end + defaultAnchor.frame = self:GetAttribute("frameref-defaultAnchor") + ]]) +end + +function Bar:UpdateDefaultStateAlpha() + local f = self:GetFrame() + f:SetAttribute("defaultAlpha",self:GetAlpha()) + f:Execute([[ + defaultAlpha = self:GetAttribute("defaultAlpha") + ]]) +end + +---- secure state driver rules ---- + +local playerClass = select(2, UnitClass("player")) +local function ClassFilter(...) + for i = 1, select('#',...) do + if playerClass == select(i,...) then + return false + end + end + return true +end + +local ruleformats = { + stealth = { format = "stealth", filter = ClassFilter("ROGUE","DRUID") }, + nostealth = { format = "nostealth", filter = ClassFilter("ROGUE","DRUID") }, + shadowdance = { format = "bonusbar:2", filter = ClassFilter("ROGUE") }, + shadowform = { format = "form:1", filter = ClassFilter("PRIEST") }, + noshadowform = { format = "noform", filter = ClassFilter("PRIEST") }, + battle = { format = "stance:1", filter = ClassFilter("WARRIOR") }, + defensive = { format = "stance:2", filter = ClassFilter("WARRIOR") }, + berserker = { format = "stance:3", filter = ClassFilter("WARRIOR") }, + caster = { format = "form:0/2/4/5/6", filter = ClassFilter("DRUID") }, + bear = { format = "form:1", filter = ClassFilter("DRUID") }, + cat = { format = "form:3", filter = ClassFilter("DRUID") }, + tree = { format = "form:5", filter = ClassFilter("DRUID") }, + moonkin = { format = "form:5", filter = ClassFilter("DRUID") }, + demon = { format = "form:2", filter = ClassFilter("WARLOCK") }, + nodemon = { format = "noform", filter = ClassFilter("WARLOCK") }, + pet = { format = "pet" }, + nopet = { format = "nopet" }, + harm = { format = "@target,harm" }, + help = { format = "@target,help" }, + notarget = { format = "@target,noexists" }, + focusharm = { format = "@focus,harm" }, + focushelp = { format = "@focus,help" }, + nofocus = { format = "@focus,noexists" }, + raid = { format = "group:raid" }, + party = { format = "group:party" }, + solo = { format = "nogroup" }, + combat = { format = "combat" }, + nocombat = { format = "nocombat" }, + possess = { format = "@vehicle,noexists,bonusbar:5" }, + vehicle = { format = "@vehicle,exists,bonusbar:5" }, +} + +function Bar.InitRuleFormats() + local forms = { } + for i = 1, GetNumShapeshiftForms() do + local _, name = GetShapeshiftFormInfo(i) + forms[name] = i; + end + -- use 9 if not found since 9 is never a valid stance/form + local defensive = forms[GetSpellInfo(71)] or 9 + local berserker = forms[GetSpellInfo(2458)] or 9 + local bear = forms[GetSpellInfo(5487)] or 9 + local aquatic = forms[GetSpellInfo(1066)] or 9 + local cat = forms[GetSpellInfo(768)] or 9 + local travel = forms[GetSpellInfo(783)] or 9 + local tree = forms[GetSpellInfo(33891)] or 9 + local moonkin = forms[GetSpellInfo(24858)] or 9 + local flight = forms[GetSpellInfo(40120)] or forms[GetSpellInfo(33943)] or 9 + + ruleformats.defensive.format = "stance:"..defensive + ruleformats.berserker.format = "stance:"..berserker + ruleformats.caster.format = format("form:0/%d/%d/%d", aquatic, travel, flight) + ruleformats.bear.format = "form:"..bear + ruleformats.cat.format = "form:"..cat + ruleformats.tree.format = "form:"..tree + ruleformats.moonkin.format = "form:"..moonkin +end + +function Bar:BuildStateRule(states) + -- states is a table : + -- states[statename].rule = { + -- order = #, + -- type = "default"/"custom"/"any"/"all", + -- values = { ... }, -- keys of ruleformats[] + -- custom = "...", + -- } + local rules = { } + local default + + for idx, state in ipairs(fieldsort(states, "rule", "order")) do + local c = states[state].rule + local type = c.type + if type == "default" then + default = default or state + elseif type == "custom" then + if c.custom then + -- strip out all spaces from the custom rule + table.insert(rules, format("%s %s", c.custom:gsub("%s",""), state)) + end + elseif type == "any" or type == "all" then + if c.values then + local clauses = { } + for key, value in pairs(c.values) do + if ruleformats[key] and not ruleformats[key].filter then + table.insert(clauses, ruleformats[key].format) + end + end + if #clauses > 0 then + local sep = (type == "any") and "][" or "," + table.insert(rules, format("[%s] %s", table.concat(clauses,sep), state)) + end + end + end + end + -- make sure that the default, if any, is last + if default then + table.insert(rules, default) + end + return table.concat(rules,";") +end + +function Bar:BuildStateKeybinds( states ) + if states then + for name, state in pairs(states) do + local rule = tfetch(state, "rule") + if rule and rule.type == "keybind" then + self:SetStateKeybind(rule.keybind, name) + else + self:SetStateKeybind(nil, name) -- this clears an existing keybind + end + end + end +end +