Mercurial > wow > reaction
view modules/ReAction_Action/ReAction_Action.lua @ 91:c2504a8b996c
Bug fixes
- action buttons resetting to 6 pixels
- stray prints
- config panels for action buttons not showing all panels
- fixed multi-ID typo
- fixed autocast model on pet buttons
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Fri, 17 Oct 2008 03:59:55 +0000 |
parents | 7cabc8ac6c16 |
children | 5f1d7a81317c |
line wrap: on
line source
--[[ ReAction Action button module. The button module implements standard action button functionality by wrapping Blizzard's ActionBarButtonTemplate frame and associated functions. It also provides action remapping support for multiple pages and possessed targets (Mind Control, Eyes of the Beast, Karazhan Chess event, various quests, etc). --]] -- local imports local ReAction = ReAction local L = ReAction.L local _G = _G local CreateFrame = CreateFrame local format = string.format ReAction:UpdateRevision("$Revision$") -- libraries local KB = LibStub("LibKeyBound-1.0") -- module declaration local moduleID = "Action" local module = ReAction:NewModule( moduleID ) -- Class declarations local Button = { } local Handle = { } local PropHandler = { } -- Event handlers function module:OnInitialize() self.db = ReAction.db:RegisterNamespace( moduleID, { profile = { bars = { }, } } ) self.handles = setmetatable({ }, weak) ReAction:RegisterBarOptionGenerator(self, "GetBarOptions") ReAction.RegisterCallback(self, "OnCreateBar") ReAction.RegisterCallback(self, "OnRefreshBar") ReAction.RegisterCallback(self, "OnDestroyBar") ReAction.RegisterCallback(self, "OnEraseBar") ReAction.RegisterCallback(self, "OnRenameBar") ReAction.RegisterCallback(self, "OnConfigModeChanged") KB.RegisterCallback(self, "LIBKEYBOUND_ENABLED") KB.RegisterCallback(self, "LIBKEYBOUND_DISABLED") KB.RegisterCallback(self, "LIBKEYBOUND_MODE_COLOR_CHANGED","LIBKEYBOUND_ENABLED") end function module:OnEnable() ReAction:RegisterBarType(L["Action Bar"], { type = moduleID, defaultButtonSize = 36, defaultBarRows = 1, defaultBarCols = 12, defaultBarSpacing = 3 }, true) ReAction:GetModule("State"):RegisterStateProperty("page", nil, PropHandler.GetOptions(), PropHandler) end function module:OnDisable() ReAction:UnregisterBarType(L["Action Bar"]) ReAction:GetModule("State"):UnregisterStateProperty("page") end function module:OnCreateBar(event, bar, name) if bar.config.type == moduleID then local profile = self.db.profile if profile.bars[name] == nil then profile.bars[name] = { buttons = { } } end if self.handles[bar] == nil then self.handles[bar] = Handle:New(bar, profile.bars[name]) end end end function module:OnRefreshBar(event, bar, name) if self.handles[bar] then self.handles[bar]:Refresh() end end function module:OnDestroyBar(event, bar, name) if self.handles[bar] then self.handles[bar]:Destroy() self.handles[bar] = nil end end function module:OnEraseBar(event, bar, name) self.db.profile.bars[name] = nil end function module:OnRenameBar(event, bar, oldname, newname) b = self.db.profile.bars b[newname], b[oldname] = b[oldname], nil end function module:OnConfigModeChanged(event, mode) for _, h in pairs(self.handles) do h:SetConfigMode(mode) end end function module:LIBKEYBOUND_ENABLED(evt) for _, h in pairs(self.handles) do h:SetKeybindMode(true) end end function module:LIBKEYBOUND_DISABLED(evt) for _, h in pairs(self.handles) do h:SetKeybindMode(false) end end ---- Interface ---- function module:GetBarOptions(bar) local h = self.handles[bar] if h then return h:GetOptions() end end ---- Bar Handle ---- do local options = { hideEmpty = { name = L["Hide Empty Buttons"], order = 1, type = "toggle", width = "double", get = "GetHideEmpty", set = "SetHideEmpty", }, pages = { name = L["# Pages"], desc = L["Use the Dynamic State tab to specify page transitions"], order = 2, type = "range", min = 1, max = 10, step = 1, get = "GetNumPages", set = "SetNumPages", }, mindcontrol = { name = L["Mind Control Support"], desc = L["When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions."], order = 3, type = "toggle", width = "double", set = "SetMindControl", get = "GetMindControl", }, actions = { name = L["Edit Action IDs"], order = 4, type = "group", inline = true, args = { method = { name = L["Assign"], order = 1, type = "select", width = "double", values = { [0] = L["Choose Method..."], [1] = L["Individually"], [2] = L["All at Once"], }, get = "GetActionEditMethod", set = "SetActionEditMethod", }, rowSelect = { name = L["Row"], desc = L["Rows are numbered top to bottom"], order = 2, type = "select", width = "half", hidden = "IsButtonSelectHidden", values = "GetRowList", get = "GetSelectedRow", set = "SetSelectedRow", }, colSelect = { name = L["Col"], desc = L["Columns are numbered left to right"], order = 3, type = "select", width = "half", hidden = "IsButtonSelectHidden", values = "GetColumnList", get = "GetSelectedColumn", set = "SetSelectedColumn", }, pageSelect = { name = L["Page"], order = 4, type = "select", width = "half", hidden = "IsPageSelectHidden", values = "GetPageList", get = "GetSelectedPage", set = "SetSelectedPage", }, single = { name = L["Action ID"], usage = L["Specify ID 1-120"], order = 5, type = "input", width = "half", hidden = "IsButtonSelectHidden", get = "GetActionID", set = "SetActionID", validate = "ValidateActionID", }, multi = { name = L["ID List"], usage = L["Specify a comma-separated list of IDs for each button in the bar (in order). Separate multiple pages with semicolons (;)"], order = 6, type = "input", multiline = true, width = "double", hidden = "IsMultiIDHidden", get = "GetMultiID", set = "SetMultiID", validate = "ValidateMultiID", }, }, }, } local weak = { __mode="k" } local meta = { __index = Handle } function Handle:New( bar, config ) local self = setmetatable( { bar = bar, config = config, btns = { } }, meta) if self.config.buttons == nil then self.config.buttons = { } end self:Refresh() return self end function Handle:Refresh() local r, c = self.bar:GetButtonGrid() local n = r*c local btnCfg = self.config.buttons if n ~= #self.btns then for i = 1, n do if btnCfg[i] == nil then btnCfg[i] = {} end if self.btns[i] == nil then local b = Button:New(self, i, btnCfg[i], self.config) self.btns[i] = b self.bar:AddButton(i,b) end end for i = n+1, #self.btns do if self.btns[i] then self.bar:RemoveButton(self.btns[i]) self.btns[i]:Destroy() self.btns[i] = nil btnCfg[i] = nil end end end for _, b in ipairs(self.btns) do b:Refresh() end local f = self.bar:GetFrame() f:SetAttribute("mindcontrol",self.config.mindcontrol) f:Execute( [[ doMindControl = self:GetAttribute("mindcontrol") control:ChildUpdate() ]]) f:SetAttribute("_onstate-mindcontrol", -- function _onstate-mindcontrol(self, stateid, newstate) [[ control:ChildUpdate() ]]) RegisterStateDriver(f, "mindcontrol", "[bonusbar:5] mc; none") end function Handle:Destroy() for _,b in pairs(self.btns) do if b then b:Destroy() end end end function Handle:SetConfigMode(mode) for _, b in pairs(self.btns) do b:ShowGrid(mode) b:ShowActionIDLabel(mode) end end function Handle:SetKeybindMode(mode) for _, b in pairs(self.btns) do if mode then -- set the border for all buttons to the keybind-enable color local r,g,b,a = KB:GetColorKeyBoundMode() b.border:SetVertexColor(r,g,b,a) b.border:Show() else b.border:Hide() end end end function Handle:GetLastButton() return self.btns[#self.btns] end -- options handlers function Handle:GetOptions() return { type = "group", name = L["Action Buttons"], handler = self, args = options } end function Handle:SetHideEmpty(info, value) if value ~= self.config.hideEmpty then for b in self.bar:IterateButtons() do b:ShowGrid(not value) end self.config.hideEmpty = value end end function Handle:GetHideEmpty() return self.config.hideEmpty end function Handle:GetNumPages() return self.config.nPages end function Handle:SetNumPages(info, value) self.config.nPages = value self:Refresh() end function Handle:GetMindControl() return self.config.mindcontrol end function Handle:SetMindControl(info, value) self.config.mindcontrol = value self:Refresh() end function Handle:GetActionEditMethod() return self.editMethod or 0 end function Handle:SetActionEditMethod(info, value) self.editMethod = value end function Handle:IsButtonSelectHidden() return self.editMethod ~= 1 end function Handle:GetRowList() local r,c = self.bar:GetButtonGrid() if self.rowList == nil or #self.rowList ~= r then local list = { } for i = 1, r do table.insert(list,i) end self.rowList = list end return self.rowList end function Handle:GetSelectedRow() local r, c = self.bar:GetButtonGrid() local row = self.selectedRow or 1 if row > r then row = 1 end self.selectedRow = row return row end function Handle:SetSelectedRow(info, value) self.selectedRow = value end function Handle:GetColumnList() local r,c = self.bar:GetButtonGrid() if self.columnList == nil or #self.columnList ~= c then local list = { } for i = 1, c do table.insert(list,i) end self.columnList = list end return self.columnList end function Handle:GetSelectedColumn() local r, c = self.bar:GetButtonGrid() local col = self.selectedColumn or 1 if col > c then col = 1 end self.selectedColumn = col return col end function Handle:SetSelectedColumn(info, value) self.selectedColumn = value end function Handle:IsPageSelectHidden() return self.editMethod ~= 1 or (self.config.nPages or 1) < 2 end function Handle:GetPageList() local n = self.config.nPages or 1 if self.pageList == nil or #self.pageList ~= n then local p = { } for i = 1, n do table.insert(p,i) end self.pageList = p end return self.pageList end function Handle:GetSelectedPage() local p = self.selectedPage or 1 if p > (self.config.nPages or 1) then p = 1 end self.selectedPage = p return p end function Handle:SetSelectedPage(info, value) self.selectedPage = value end function Handle:GetActionID() local row = self.selectedRow or 1 local col = self.selectedColumn or 1 local r, c = self.bar:GetButtonGrid() local n = (row-1) * c + col local btn = self.btns[n] if btn then return tostring(btn:GetActionID(self.selectedPage or 1)) end end function Handle:SetActionID(info, value) local row = self.selectedRow or 1 local col = self.selectedColumn or 1 local r, c = self.bar:GetButtonGrid() local n = (row-1) * c + col local btn = self.btns[n] if btn then btn:SetActionID(tonumber(value), self.selectedPage or 1) end end function Handle:ValidateActionID(info, value) value = tonumber(value) if value == nil or value < 1 or value > 120 then return L["Specify ID 1-120"] end return true end function Handle:IsMultiIDHidden() return self.editMethod ~= 2 end function Handle:GetMultiID() local p = { } for i = 1, self.config.nPages or 1 do local b = { } for _, btn in ipairs(self.btns) do table.insert(b, btn:GetActionID(i)) end table.insert(p, table.concat(b,",")) end return table.concat(p,";\n") end local function ParseMultiID(nBtns, nPages, s) if s:match("[^%d%s,;]") then ReAction:Print("items other than digits, spaces, commas, and semicolons in string",s) return nil end local p = { } for list in s:gmatch("[^;]+") do local pattern = ("^%s?$"):format(("%s*(%d+)%s*,"):rep(nBtns)) local ids = { list:match(pattern) } if #ids ~= nBtns then ReAction:Print("found",#ids,"buttons instead of",nBtns) return nil end table.insert(p,ids) end if #p ~= nPages then ReAction:Print("found",#p,"pages instead of",nPages) return nil end return p end function Handle:SetMultiID(info, value) local p = ParseMultiID(#self.btns, self.config.nPages or 1, value) for page, b in ipairs(p) do for button, id in ipairs(b) do self.btns[button]:SetActionID(id, page) end end end function Handle:ValidateMultiID(info, value) local bad = L["Invalid action ID list string"] if value == nil or ParseMultiID(#self.btns, self.config.nPages or 1, value) == nil then return bad end return true end end ------ State property options ------ do local pageOptions = { page = { name = L["Show Page #"], order = 11, type = "select", width = "half", disabled = "IsPageDisabled", hidden = "IsPageHidden", values = "GetPageValues", set = "SetProp", get = "GetPage", }, } local function GetBarConfig(bar) return module.db.profile.bars[bar:GetName()] end function PropHandler.GetOptions() return pageOptions end function PropHandler:IsPageDisabled() local c = GetBarConfig(self.bar) local n = c and c.nPages or 1 return not (n > 1) end function PropHandler:IsPageHidden() return not GetBarConfig(self.bar) end function PropHandler:GetPageValues() local c = GetBarConfig(self.bar) if c then local n = c.nPages if self._npages ~= n then self._pagevalues = { } self._npages = n -- cache the results for i = 1, n do self._pagevalues["page"..i] = i end end return self._pagevalues end end function PropHandler:GetPage(info) return self:GetProp(info) or 1 end end ------ ActionID allocation ------ -- this needs to be high performance when requesting new IDs, -- or certain controls will become sluggish. However, the new-request -- infrastructure can be built lazily the first time that a new request -- comes in (which will only happen at user config time: at static startup -- config time all actionIDs should already have been assigned and stored -- in the config file) local IDAlloc do local n = 120 IDAlloc = setmetatable({ wrap = 1, freecount = n }, {__index = function() return 0 end}) function IDAlloc:Acquire(id, hint) id = tonumber(id) hint = tonumber(hint) if id and (id < 1 or id > n) then id = nil end if hint and (hint < 1 or hint > n) then hint = nil end if id == nil then -- get a free ID if hint and self[hint] == 0 then -- use the hint if it's free id = hint elseif self.freecount > 0 then -- if neither the id nor the hint are defined or free, but -- the list is known to have free IDs, then start searching -- at the hint for a free one for i = hint or 1, n do if self[i] == 0 then id = i break end end -- self.wrap the search if id == nil and hint and hint > 1 then for i = 1, hint - 1 do if self[i] == 0 then id = i break end end end end if id == nil then -- if there are no free IDs, start wrapping at 1 id = self.wrap self.wrap = id + 1 if self.wrap > n then self.wrap = 1 end end end if self[id] == 0 then self.freecount = self.freecount - 1 end self[id] = self[id] + 1 return id end function IDAlloc:Release(id) id = tonumber(id) if id and (id >= 1 or id <= n) then self[id] = self[id] - 1 if self[id] == 0 then self.freecount = self.freecount + 1 self.wrap = 1 end end end end ------ Button class ------ do local frameRecycler = { } local trash = CreateFrame("Frame") local OnUpdate, KBAttach, GetActionName, GetHotkey, SetKey, FreeKey, ClearBindings, GetBindings do local ATTACK_BUTTON_FLASH_TIME = ATTACK_BUTTON_FLASH_TIME local IsActionInRange = IsActionInRange local buttonLookup = setmetatable({},{__mode="kv"}) function OnUpdate(frame, elapsed) -- note: This function taints frame.flashtime and frame.rangeTimer. Both of these -- are only read by ActionButton_OnUpdate (which this function replaces). In -- all other places they're just written, so it doesn't taint any secure code. if frame.flashing == 1 then frame.flashtime = frame.flashtime - elapsed if frame.flashtime <= 0 then local overtime = -frame.flashtime if overtime >= ATTACK_BUTTON_FLASH_TIME then overtime = 0 end frame.flashtime = ATTACK_BUTTON_FLASH_TIME - overtime local flashTexture = frame.flash if flashTexture:IsShown() then flashTexture:Hide() else flashTexture:Show() end end end if frame.rangeTimer then frame.rangeTimer = frame.rangeTimer - elapsed; if frame.rangeTimer <= 0 then if IsActionInRange(frame.action) == 0 then frame.icon:SetVertexColor(1.0,0.1,0.1) else ActionButton_UpdateUsable(frame) end frame.rangeTimer = 0.1 end end end -- Use KeyBound-1.0 for binding, but use Override bindings instead of -- regular bindings to support multiple profile use. This is a little -- weird with the KeyBound dialog box (which has per-char selector as well -- as an OK/Cancel box) but it's the least amount of effort to implement. function GetActionName(f) local b = buttonLookup[f] if b then return format("%s:%s", b.bar:GetName(), b.idx) end end function GetHotkey(f) local b = buttonLookup[f] if b then return KB:ToShortKey(b:GetConfig().hotkey) end end function SetKey(f, key) local b = buttonLookup[f] if b then local c = b:GetConfig() if c.hotkey then SetOverrideBinding(f, false, c.hotkey, nil) end if key then SetOverrideBindingClick(f, false, key, f:GetName(), nil) end c.hotkey = key b:DisplayHotkey(GetHotkey(f)) end end function FreeKey(f, key) local b = buttonLookup[f] if b then local c = b:GetConfig() if c.hotkey == key then local action = f:GetActionName() SetOverrideBinding(f, false, c.hotkey, nil) c.hotkey = nil b:DisplayHotkey(nil) return action end end return ReAction:FreeOverrideHotkey(key) end function ClearBindings(f) SetKey(f, nil) end function GetBindings(f) local b = buttonLookup[f] if b then return b:GetConfig().hotkey end end function KBAttach( button ) local f = button:GetFrame() f.GetActionName = GetActionName f.GetHotkey = GetHotkey f.SetKey = SetKey f.FreeKey = FreeKey f.ClearBindings = ClearBindings f.GetBindings = GetBindings buttonLookup[f] = button f:SetKey(button:GetConfig().hotkey) ReAction:RegisterKeybindFrame(f) end end local meta = {__index = Button} function Button:New( handle, idx, config, barConfig ) local bar = handle.bar -- create new self self = setmetatable( { bar = bar, idx = idx, config = config, barConfig = barConfig, }, meta ) local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx) self.name = name config.name = name local lastButton = handle:GetLastButton() config.actionID = IDAlloc:Acquire(config.actionID, lastButton and lastButton.config.actionID) -- gets a free one if none configured self.nPages = 1 -- have to recycle frames with the same name: CreateFrame() doesn't overwrite -- existing globals. Can't set to nil in the global because it's then tainted. local parent = bar:GetFrame() local f = frameRecycler[name] if f then f:SetParent(parent) else f = CreateFrame("CheckButton", name, parent, "ActionBarButtonTemplate") -- ditch the old hotkey text because it's tied in ActionButton_Update() to the -- standard binding. We use override bindings. local hotkey = _G[name.."HotKey"] hotkey:SetParent(trash) hotkey = f:CreateFontString(nil, "ARTWORK", "NumberFontNormalSmallGray") hotkey:SetWidth(36) hotkey:SetHeight(18) hotkey:SetJustifyH("RIGHT") hotkey:SetJustifyV("TOP") hotkey:SetPoint("TOPLEFT",f,"TOPLEFT",-2,-2) f.hotkey = hotkey f.icon = _G[name.."Icon"] f.flash = _G[name.."Flash"] f:SetScript("OnUpdate",OnUpdate) end self.hotkey = f.hotkey self.border = _G[name.."Border"] f:SetAttribute("action", config.actionID) -- install mind control actions for all buttons just for simplicity if self.idx <= 12 then f:SetAttribute("*action-mc", 120 + self.idx) end -- wrap the OnClick handler to use a pagemap from the header's context parent:WrapScript(f, "OnClick", -- function OnClick(self, button, down) [[ if doMindControl and GetBonusBarOffset() == 5 then return "mc" else return state and page and page[state] or button end ]]) -- set a _childupdate handler, called within the header's context -- SetAttribute() is a brute-force way to trigger ActionButton_UpdateAction(). Setting "*action1" -- will, in the absence of a useful replacement for SecureButton_GetEffectiveButton(), force -- ActionButton_CalculateAction() to use the new action-id for display purposes. It also -- sort of obviates the OnClick handler, but hopefully this is only temporary until -- SecureButton_GetEffectiveButton() gets fixed. f:SetAttribute("_childupdate", -- function _childupdate(self, snippetid, message) [[ local action = "action" if doMindControl and GetBonusBarOffset() == 5 then action = "*action-mc" elseif page and state and page[state] then action = "*action-"..page[state] end local value = self:GetAttribute(action) self:SetAttribute("*action1",value) ]]) self.frame = f self.normalTexture = getglobal(format("%sNormalTexture",f:GetName())) -- initialize the hide state f:SetAttribute("showgrid",0) self:ShowGrid(not barConfig.hideEmpty) if ReAction:GetConfigMode() then self:ShowGrid(true) end -- show the ID label if applicable self:ShowActionIDLabel(ReAction:GetConfigMode()) -- attach the keybinder KBAttach(self) self:Refresh() return self end function Button:Destroy() local f = self.frame f:UnregisterAllEvents() f:Hide() f:SetParent(UIParent) f:ClearAllPoints() if self.name then frameRecycler[self.name] = f end if self.config.actionID then IDAlloc:Release(self.config.actionID) end if self.config.pageactions then for _, id in ipairs(self.config.pageactions) do IDAlloc:Release(id) end end self.frame = nil self.config = nil self.bar = nil end function Button:Refresh() local f = self.frame self.bar:PlaceButton(self, 36, 36) self:RefreshPages() end function Button:GetFrame() return self.frame end function Button:GetName() return self.name end function Button:GetConfig() return self.config end function Button:GetActionID(page) if page == nil then -- get the effective ID return self.frame.action -- kept up-to-date by Blizzard's ActionButton_CalculateAction() else if page == 1 then return self.config.actionID else return self.config.pageactions and self.config.pageactions[page] or self.config.actionID end end end function Button:SetActionID( id, page ) id = tonumber(id) page = tonumber(page) if id == nil or id < 1 or id > 120 then error("Button:SetActionID - invalid action ID") end if page and page ~= 1 then if not self.config.pageactions then self.config.pageactions = { } end if self.config.pageactions[page] then IDAlloc:Release(self.config.pageactions[page]) end self.config.pageactions[page] = id IDAlloc:Acquire(self.config.pageactions[page]) self.frame:SetAttribute(("*action-page%d"):format(page),id) else IDAlloc:Release(self.config.actionID) self.config.actionID = id IDAlloc:Acquire(self.config.actionID) self.frame:SetAttribute("action",id) if self.config.pageactions then self.config.pageactions[1] = id self.frame:SetAttribute("*action-page1",id) end end end function Button:RefreshPages( force ) local nPages = self.barConfig.nPages if nPages and (nPages ~= self.nPages or force) then local f = self:GetFrame() local c = self.config.pageactions if nPages > 1 and not c then c = { } self.config.pageactions = c end for i = 1, nPages do if i > 1 then c[i] = IDAlloc:Acquire(c[i], self.config.actionID + (i-1)*self.bar:GetNumButtons()) else c[i] = self.config.actionID -- page 1 is the same as the base actionID end f:SetAttribute(("*action-page%d"):format(i),c[i]) end for i = nPages+1, #c do IDAlloc:Release(c[i]) c[i] = nil f:SetAttribute(("*action-page%d"):format(i),nil) end self.nPages = nPages end end function Button:ShowGrid( show ) if not InCombatLockdown() then local f = self.frame local count = f:GetAttribute("showgrid") if show then count = count + 1 else count = count - 1 end if count < 0 then count = 0 end f:SetAttribute("showgrid",count) if count >= 1 and not f:GetAttribute("statehidden") then self.normalTexture:SetVertexColor(1.0, 1.0, 1.0, 0.5); f:Show() elseif count < 1 and not HasAction(self:GetActionID()) then f:Hide() end end end function Button:ShowActionIDLabel( show ) local f = self:GetFrame() if show then local id = self:GetActionID() if not f.actionIDLabel then local label = f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge") label:SetAllPoints() label:SetJustifyH("CENTER") label:SetShadowColor(0,0,0,1) label:SetShadowOffset(2,-2) f.actionIDLabel = label -- store the label with the frame for recycling f:HookScript("OnAttributeChanged", function(frame, attr, value) if label:IsVisible() and attr:match("action") then label:SetText(tostring(frame.action)) end end) end f.actionIDLabel:SetText(tostring(id)) f.actionIDLabel:Show() elseif f.actionIDLabel then f.actionIDLabel:Hide() end end function Button:DisplayHotkey( key ) self.hotkey:SetText(key or "") end end