Mercurial > wow > reaction
view modules/ReAction_Action/ReAction_Action.lua @ 88:fc83b3f5b322
Added keybindings using LibKeyBound-1.0, with modifications for Override bindings instead of standard bindings.
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Sun, 31 Aug 2008 06:02:18 +0000 |
parents | 3499ac7c3a9b |
children | 7cabc8ac6c16 |
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). This is done by interacting with the built-in State module to map these features to states via the "statebutton" attribute. --]] -- 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 ) -- Button class declaration local Button = { } -- private utility -- local function RefreshLite(bar) local btns = module.buttons[bar] if btns then for _, b in ipairs(btns) do b:Refresh() end end end local function GetBarConfig(bar) return module.db.profile.bars[bar:GetName()] end -- Event handlers function module:OnInitialize() self.db = ReAction.db:RegisterNamespace( moduleID, { profile = { buttons = { }, bars = { }, } } ) self.buttons = { } ReAction:RegisterBarOptionGenerator(self, "GetBarOptions") ReAction.RegisterCallback(self, "OnCreateBar", "OnRefreshBar") ReAction.RegisterCallback(self, "OnDestroyBar") ReAction.RegisterCallback(self, "OnRefreshBar") 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) end function module:OnDisable() ReAction:UnregisterBarType(L["Action Bar"]) end function module:OnRefreshBar(event, bar, name) if bar.config.type == moduleID then if self.buttons[bar] == nil then self.buttons[bar] = { } end local btns = self.buttons[bar] local profile = self.db.profile if profile.buttons[name] == nil then profile.buttons[name] = {} end if profile.bars[name] == nil then profile.bars[name] = {} end local btnCfg = profile.buttons[name] local barCfg = profile.bars[name] local r, c = bar:GetButtonGrid() local n = r*c if n ~= #btns then for i = 1, n do if btnCfg[i] == nil then btnCfg[i] = {} end if btns[i] == nil then local b = Button:New(bar, i, btnCfg[i], barCfg) btns[i] = b bar:AddButton(i,b) end end for i = n+1, #btns do if btns[i] then bar:RemoveButton(btns[i]) btns[i] = btns[i]:Destroy() if btnCfg[i] then btnCfg[i] = nil end end end end RefreshLite(bar) end end function module:OnDestroyBar(event, bar, name) if self.buttons[bar] then local btns = self.buttons[bar] for _,b in pairs(btns) do if b then b:Destroy() end end self.buttons[bar] = nil end end function module:OnEraseBar(event, bar, name) self.db.profile.buttons[name] = nil self.db.profile.bars[name] = nil end function module:OnRenameBar(event, bar, oldname, newname) local b = self.db.profile.buttons b[newname], b[oldname] = b[oldname], nil b = self.db.profile.bars b[newname], b[oldname] = b[oldname], nil end function module:OnConfigModeChanged(event, mode) for _, bar in pairs(self.buttons) do for _, b in pairs(bar) do b:ShowGrid(mode) b:ShowActionIDLabel(mode) end end end function module:LIBKEYBOUND_ENABLED(evt) -- set the border for all buttons to the keybind-enable color local r,g,b,a = KB:GetColorKeyBoundMode() for _, bar in pairs(self.buttons) do for _, b in pairs(bar) do b.border:SetVertexColor(r,g,b,a) b.border:Show() end end end function module:LIBKEYBOUND_DISABLED(evt) for _, bar in pairs(self.buttons) do for _, b in pairs(bar) do b.border:Hide() end end end ---- Options ---- do local Handler = { } 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", }, actions = { name = L["Edit Action IDs"], order = 13, 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", } } }, } function module:GetBarOptions(bar) return { type = "group", name = L["Action Buttons"], handler = Handler:New(bar), hidden = "Hidden", args = options } end -- options handler private function Handler:New(bar) return setmetatable( { bar = bar }, { __index = Handler } ) end function Handler:Hidden() return self.bar.config.type ~= moduleID end function Handler:SetHideEmpty(info, value) local c = GetBarConfig(self.bar) if value ~= c.hideEmpty then for b in self.bar:IterateButtons() do b:ShowGrid(not value) end c.hideEmpty = value end end function Handler:GetHideEmpty() return GetBarConfig(self.bar).hideEmpty end function Handler:GetNumPages() return GetBarConfig(self.bar).nPages end function Handler:SetNumPages(info, value) GetBarConfig(self.bar).nPages = value RefreshLite(self.bar) end function Handler:GetActionEditMethod() return self.editMethod or 0 end function Handler:SetActionEditMethod(info, value) self.editMethod = value end function Handler:IsButtonSelectHidden() return self.editMethod ~= 1 end function Handler: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 Handler: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 Handler:SetSelectedRow(info, value) self.selectedRow = value end function Handler: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 Handler: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 Handler:SetSelectedColumn(info, value) self.selectedColumn = value end function Handler:IsPageSelectHidden() return self.editMethod ~= 1 or (GetBarConfig(self.bar).nPages or 1) < 2 end function Handler:GetPageList() local n = GetBarConfig(self.bar).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 Handler:GetSelectedPage() local p = self.selectedPage or 1 if p > (GetBarConfig(self.bar).nPages or 1) then p = 1 end self.selectedPage = p return p end function Handler:SetSelectedPage(info, value) self.selectedPage = value end function Handler: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 = module.buttons[self.bar][n] if btn then return tostring(btn:GetActionID(self.selectedPage or 1)) end end function Handler: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 = module.buttons[self.bar][n] if btn then btn:SetActionID(tonumber(value), self.selectedPage or 1) end end function Handler: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 Handler:IsMultiIDHidden() return self.editMethod ~= 2 end function Handler:GetMultiID() local p = { } for i = 1, GetBarConfig(self.bar).nPages or 1 do local b = { } for _, btn in ipairs(module.buttons[self.bar]) 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 Handler:SetMultiID(info, value) local btns = module.buttons[self.bar] local p = ParseMultiID(#btns, GetBarConfig(self.bar).nPages or 1, value) for page, b in ipairs(p) do for button, id in ipairs(b) do btns[button]:SetActionID(id, page) end end end function Handler:ValidateMultiID(info, value) local bad = L["Invalid action ID list string"] if value == nil or ParseMultiID(#module.buttons[self.bar], GetBarConfig(self.bar).nPages or 1, value) == nil then return bad end return true end end ------ State property options ------ do local pageOptions = { 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. Select the 'Mind Control' option for the rule type to enable."], order = 11, type = "toggle", disabled = "IsMCDisabled", hidden = "IsPageHidden", width = "double", set = "SetProp", get = "GetProp", }, page = { name = L["Show Page #"], order = 12, type = "select", width = "half", disabled = "IsPageDisabled", hidden = "IsPageHidden", values = "GetPageValues", set = "SetProp", get = "GetPage", }, } local function pageImpl( bar, states ) local map = { } for state, c in pairs(states) do if c.mindcontrol then map[state] = "mc" elseif c.page then map[state] = ("page%d"):format(c.page) end end bar:SetStateAttribute("statebutton", map, 1, true) end local PageOptsHandler = { } -- will inherit properties and methods via State:RegisterStateProperty function PageOptsHandler:IsMCDisabled(info) if self:IsPageHidden() then return true end -- only allow this if the mind-control selector or custom/keybind is chosen -- see State.lua for the structure of the 'rule' config element local rule = self.states[self:GetName()].rule if rule then if rule.type == "custom" or rule.type == "keybind" then return false else if rule.values and rule.values.possess then return false end end end return true end function PageOptsHandler:IsPageDisabled() -- disabled if not an action button return not GetBarConfig(self.bar) or -- OR mind-control remapping is enabled self.states[self:GetName()].mindcontrol or -- OR only one page is enabled (GetBarConfig(self.bar).nPages and GetBarConfig(self.bar).nPages < 2) end function PageOptsHandler:IsPageHidden() return not GetBarConfig(self.bar) end function PageOptsHandler: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[i] = i end end return self._pagevalues end end function PageOptsHandler:GetPage(info) return self:GetProp(info) or 1 end ReAction:GetModule("State"):RegisterStateProperty("page", pageImpl, pageOptions, PageOptsHandler) 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 ------ 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() 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 function Button:New( bar, idx, config, barConfig ) -- create new self self = setmetatable( { }, {__index = Button} ) self.bar, self.idx, self.config, self.barConfig = bar, idx, config, barConfig local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx) self.name = name config.name = name local lastButton = module.buttons[bar][#module.buttons[bar]] 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 (below). Can't set to nil in the global -- table because you end up getting taint local parent = bar:GetButtonFrame() 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 action support for all buttons here just for simplicity if self.idx <= 12 then f:SetAttribute("*action-mc", 120 + self.idx) end 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.pages 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) if self.barConfig.mckeybinds then f:SetAttribute("bindings-mc", self.barConfig.mckeybinds[self.idx]) end 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 -- new in 2.4.1: can't call ActionButton_ShowGrid/HideGrid because they won't update the attribute 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 == "state-parent" or 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