Mercurial > wow > reaction
view classes/Bar.lua @ 208:443d2ea4be86
ReAction.lua cleanup: Fixed calls to DestroyBar()
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Thu, 18 Nov 2010 13:06:14 -0800 |
parents | 576c50e51fc5 |
children | c24ac8ee1e45 |
line wrap: on
line source
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 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, } ---- Utility functions ---- -- traverse a table tree by key list and fetch the result or first nil local function tfetch(t, ...) for i = 1, select('#', ...) do t = t and t[select(i, ...)] end return t end -- traverse a table tree by key list and build tree as necessary local function tbuild(t, ...) for i = 1, select('#', ...) do local key = select(i, ...) if not t[key] then t[key] = { } end t = t[key] end return t end -- return a new array of keys of table 't', sorted by comparing -- sub-fields (obtained via tfetch) of the table values local function fieldsort( t, ... ) local r = { } for k in pairs(t) do table.insert(r,k) end local path = { ... } table.sort(r, function(lhs, rhs) local olhs = tfetch(t[lhs], unpack(path)) or 0 local orhs = tfetch(t[rhs], unpack(path)) or 0 return olhs < orhs end) return r end ---- Bar class ---- local Bar = { } local weak = { __mode = "k" } local frameList = { } ReAction.Bar = Bar -- export to ReAction function Bar:New( name, config ) if type(config) ~= "table" then error("ReAction.Bar: config table required") end -- create new self self = setmetatable( { config = config, name = name, buttons = setmetatable( { }, weak ), 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()) ReAction.RegisterCallback(self, "OnConfigModeChanged") LKB.RegisterCallback(self, "LIBKEYBOUND_ENABLED") LKB.RegisterCallback(self, "LIBKEYBOUND_DISABLED") LKB.RegisterCallback(self, "LIBKEYBOUND_MODE_COLOR_CHANGED","LIBKEYBOUND_ENABLED") return self end function Bar:Destroy() local f = self:GetFrame() f:UnregisterAllEvents() self:ShowControls(false) ReAction.UnregisterAllCallbacks(self) LKB.UnregisterAllCallbacks(self) LSG:RemoveFrame(f) f:SetParent(UIParent) f:ClearAllPoints() f:Hide() end -- -- Events -- function Bar:OnConfigModeChanged(event, mode) self:SetConfigMode(mode) end function Bar:LIBKEYBOUND_ENABLED(evt) self:SetKeybindMode(true) end function Bar:LIBKEYBOUND_DISABLED(evt) self:SetKeybindMode(false) 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) 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: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() ReAction:RefreshBar(self) 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 ReAction:RefreshBar(self) 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 ReAction:RefreshBar(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() ReAction:RefreshBar(self) end function Bar:IterateButtons() -- iterator returns button, idx and does NOT iterate in index order return pairs(self.buttons) end -- -- Methods -- function Bar:SetConfigMode(mode) self:SetSecureData("showAll",mode) self:ShowControls(mode) for b in self:IterateButtons() do b:ShowGridTemp(mode) b:UpdateActionIDLabel(mode) end end function Bar:SetKeybindMode(mode) self:SetSecureData("showAll",mode) for 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() -- store in a weak reverse-index array self.buttons[button] = idx -- Store a properly wrapped reference to the child frame as an attribute -- (accessible via "frameref-btn#") f:SetFrameRef(format("btn%d",idx), button:GetFrame()) end function Bar:RemoveButton(button) local idx = self.buttons[button] if idx then self:GetFrame():SetAttribute(format("frameref-btn%d",idx),nil) self.buttons[button] = nil end end function Bar:PlaceButton(button, baseW, baseH) local idx = self.buttons[button] 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() -- does nothing by default end function Bar:UpdateShowGrid() for 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) -- override in modules/State.lua for now end function Bar:SetStateProperty(state, propname, value) -- override in modules/State.lua for now 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 RegisterUnitWatch(self:GetFrame(),true) 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