Mercurial > wow > reaction
diff modules/ReAction_Action/ReAction_Action.lua @ 87:3499ac7c3a9b
Implemented paged actions and mind control actions, and config menus to suit.
There's still some sort of bug in the actionID-selection routine, it doesn't always auto-select the IDs that I think it's going to if you resize a bar several times (especially in the presence of multiple pages).
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Sat, 28 Jun 2008 00:54:21 +0000 |
parents | 502cdb5666e2 |
children | fc83b3f5b322 |
line wrap: on
line diff
--- a/modules/ReAction_Action/ReAction_Action.lua Fri Jun 27 18:40:40 2008 +0000 +++ b/modules/ReAction_Action/ReAction_Action.lua Sat Jun 28 00:54:21 2008 +0000 @@ -2,11 +2,12 @@ ReAction Action button module. The button module implements standard action button functionality by wrapping Blizzard's - ActionButton frame and associated functions. + ActionBarButtonTemplate frame and associated functions. - It also provides support for multiple pages (interacting with the State module) as well - as optional action remapping for possessed targets (mind control). - + 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 @@ -15,7 +16,7 @@ local _G = _G local CreateFrame = CreateFrame -ReAction:UpdateRevision("$Revision: 103 $") +ReAction:UpdateRevision("$Revision$") -- module declaration local moduleID = "Action" @@ -24,7 +25,7 @@ -- Button class declaration local Button = { } --- private -- +-- private utility -- local function RefreshLite(bar) local btns = module.buttons[bar] if btns then @@ -34,6 +35,11 @@ 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, @@ -90,22 +96,24 @@ local r, c = bar:GetButtonGrid() local n = r*c - for i = 1, n do - if btnCfg[i] == nil then - btnCfg[i] = {} + 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 - 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 + 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 @@ -149,35 +157,116 @@ ---- Options ---- -local Handler = { } +do + local Handler = { } -local options = { - hideEmpty = { - name = L["Hide Empty Buttons"], - desc = L["Hide buttons when empty. This option is not supported for multi-state bars"], - order = 1, - type = "toggle", - get = "GetHideEmpty", - set = "SetHideEmpty", - }, -} + 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 -do - local function GetBarConfig( bar ) - return module.db.profile.bars[bar:GetName()] + 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 @@ -199,64 +288,407 @@ 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 ------- Button class ------ +------ 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", + }, + } --- use-count of action IDs -local nActionIDs = 120 -local ActionIDList = setmetatable( {}, { - __index = function(self, idx) - if idx == nil then - for i = 1, nActionIDs do - if rawget(self,i) == nil then - rawset(self,i,1) - return i + 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 - error("ran out of action IDs") - else - local c = rawget(self,idx) or 0 - rawset(self,idx,c+1) - return idx end - end, - __newindex = function(self,idx,value) - if value == nil then - value = rawget(self,idx) - if value == 1 then - value = nil - elseif value then - value = value - 1 + 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 - rawset(self,idx,value) + 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 + +local frameRecycler = { } + +------ Button class ------ 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 - config.name = config.name or ("ReAction_%s_%d"):format(bar:GetName(),idx) - self.name = config.name - config.actionID = ActionIDList[config.actionID] -- gets a free one if none configured + 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 - local f = CreateFrame("CheckButton", self.name, bar:GetButtonFrame(), "ActionBarButtonTemplate") + -- 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") + end 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) + 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) @@ -276,14 +708,14 @@ f:SetParent(UIParent) f:ClearAllPoints() if self.name then - _G[self.name] = nil + frameRecycler[self.name] = f end if self.config.actionID then - ActionIDList[self.config.actionID] = nil + IDAlloc:Release(self.config.actionID) end if self.config.pages then - for _, id in ipairs(self.config.pages) do - ActionIDList[id] = nil + for _, id in ipairs(self.config.pageactions) do + IDAlloc:Release(id) end end self.frame = nil @@ -308,31 +740,70 @@ return self.name end -function Button:GetActionID() - return SecureButton_GetModifiedAttribute(self.frame, "action") +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:RefreshPages() - local nPages = 1 --self.bar:GetNumPages() - if nPages ~= self.nPages then +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.pages + local c = self.config.pageactions if nPages > 1 and not c then c = { } - self.config.pages = c + self.config.pageactions = c end for i = 1, nPages do - c[i] = ActionIDList[c[i]] -- gets a free one if none configured - f:SetAttribute(("action-page%d"):format(i)) + 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 - ActionIDList[c[i]] = nil + IDAlloc:Release(c[i]) c[i] = nil - f:SetAttribute(("action-page%d"):format(i)) + f:SetAttribute(("*action-page%d"):format(i),nil) end - - -- TODO: - -- apply next-page, prev-page, and direct-page keybinds (via bar:SetStateKeybind abstraction) + self.nPages = nPages end end @@ -361,27 +832,27 @@ end function Button:ShowActionIDLabel( show ) + local f = self:GetFrame() if show then local id = self:GetActionID() - if not self.actionIDLabel and id and id ~= 0 then - local f = self:GetFrame() + 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) - label:SetText(tostring(id)) - self.actionIDLabel = label + f.actionIDLabel = label -- store the label with the frame for recycling f:HookScript("OnAttributeChanged", function(frame, attr, value) - if attr == "state-parent" then - label:SetText(tostring(self:GetActionID())) + if label:IsVisible() and (attr == "state-parent" or attr:match("action")) then + label:SetText(tostring(frame.action)) end end) end - self.actionIDLabel:Show() - elseif self.actionIDLabel then - self.actionIDLabel:Hide() + f.actionIDLabel:SetText(tostring(id)) + f.actionIDLabel:Show() + elseif f.actionIDLabel then + f.actionIDLabel:Hide() end end