--------------------------------------------------------------- -- Input handler --------------------------------------------------------------- -- Provides widgets and methods for performing secure actions, -- mapped by various parts of the interface to override the -- default actions of gamepad inputs. local _, db = ...; local Intellisense = db.Intellisense; local InputMixin, InputAPI = {}, CPAPI.CreateEventHandler({'Frame', '$parentInputHandler', ConsolePort, 'SecureHandlerStateTemplate'}, { 'PLAYER_REGEN_DISABLED'; -- enter combat 'PLAYER_REGEN_ENABLED'; -- leave combat }, { Widgets = {}; }); --------------------------------------------------------------- db:Register('Input', InputAPI) RegisterStateDriver(InputAPI, 'combat', '[combat] true; nil') InputAPI:SetAttribute('_onstate-combat', [[ control:ChildUpdate('combat', newstate) ]]) --------------------------------------------------------------- function InputAPI:GetWidget(id, owner) id = tostring(id):upper(); assert(not InCombatLockdown(), 'Attempted to get input widget in combat.') local widget = self.Widgets[id]; if not widget then widget = CreateFrame( 'Button', ('CP-Input-%s'):format(id), self, 'SecureActionButtonTemplate, SecureHandlerBaseTemplate' ); widget:Hide() db.table.mixin(widget, InputMixin) widget:OnLoad(id) self.Widgets[id] = widget; end widget:SetAttribute('owner', owner) return widget; end function InputAPI:Release(owner) for id, widget in pairs(self.Widgets) do if ( widget:HasOwner(owner) ) then widget:ClearOverride(owner) end end end function InputAPI:ReleaseAll() for id, widget in pairs(self.Widgets) do widget:ClearOverride() end end function InputAPI:GetActiveWidget(id, owner) local widget = self.Widgets[id]; if (widget and widget:GetAttribute('owner') == owner) then return widget; end end function InputAPI:IsOverrideActive(id, isPriority) local widget = self.Widgets[id]; if not widget then return false; elseif isPriority then return widget[1] and true or false; end return widget[1] or widget[2] and true or false; end --------------------------------------------------------------- -- Supported functions --------------------------------------------------------------- function InputAPI:SetButton(id, owner, ...) return self:GetWidget(id, owner):Button(...) end function InputAPI:SetMacro(id, owner, ...) return self:GetWidget(id, owner):Macro(...) end function InputAPI:SetGlobal(id, owner, ...) return self:GetWidget(id, owner):Global(...) end function InputAPI:SetCommand(id, owner, ...) return self:GetWidget(id, owner):Command(...) end --------------------------------------------------------------- -- Common args: -- @value : value for the configured action -- @isPriority : whether this binding should be prioritized -- @click : (optional) emulated mouse button function InputMixin:Button(value, isPriority, click) return self:SetOverride({ owner = self:GetAttribute('owner'); button = click; isPriority = isPriority; attributes = { type = 'click'; clickbutton = value; } }) end function InputMixin:Macro(value, isPriority, click) return self:SetOverride({ owner = self:GetAttribute('owner'); button = click; isPriority = isPriority; attributes = { type = 'macro'; macrotext = value; } }) end function InputMixin:Global(value, isPriority, click) return self:SetOverride({ owner = self:GetAttribute('owner'); button = click; isPriority = isPriority; target = value; attributes = { type = 'none'; } }) end -- Creates a new command: -- @name : name of the function to add -- @func : lambda function to call -- @init : (optional) function to set up properties -- @clear: (optional) function to run when clearing -- @args : (optional) properties for initialization function InputMixin:Command(isPriority, click, name, func, init, clear, ...) self[name] = func if init then init(self, ...) end return self:SetOverride({ owner = self:GetAttribute('owner'); clear = clear; button = click; isPriority = isPriority; attributes = { type = name; } }) end --------------------------------------------------------------- -- InputMixin override handler, supports 2 layers of overrides --------------------------------------------------------------- function InputMixin:SetOverride(data) self[data.isPriority and 1 or 2] = data if data.attributes then for attribute, value in pairs(data.attributes) do self:SetAttribute(attribute, value) end end self:SetOverrideBinding( data.isPriority, self:GetAttribute('id'), data.target or self:GetName(), data.button or 'LeftButton' ); return self end function InputMixin:HasOwner(owner) for i=1, 2 do local data = self[i]; if ( data and data.owner == owner ) then return i, data; end end end function InputMixin:GetOverride(isPriority) if isPriority then return self[1]; end return self[2]; end function InputMixin:ClearOverride(owner) if owner then local i = self:HasOwner(owner) if i then self:ClearDataAndBinding(i) local other = self[i % 2 + 1]; if other then -- reinstate other return self:SetOverride(other) end end return -- do nothing if owner is faulty end self:ClearDataAndBinding(1, 2) end function InputMixin:ClearDataAndBinding(...) for i=1, select('#', ...) do local idx = select(i, ...) local data = self[idx] if data and data.clear then data.clear(self) end self[idx] = nil; end self:ClearOverrideBinding() end --------------------------------------------------------------- -- InputMixin --------------------------------------------------------------- InputMixin.timer = 0; function InputMixin:OnLoad(id) self:SetAttribute('id', id) self:SetAttribute('_childupdate-combat', [[ if message then self:SetAttribute('clickbutton', nil) self:Hide() self:CallMethod('Clear') end ]]) end function InputMixin:OnMouseDown() local func = self:GetAttribute('type') local click = self:GetAttribute('clickbutton') self.state, self.timer = true, 0; if self:IsSecureAction(func, click) then return self:EmulateFrontend(click, 'PUSHED', 'OnMouseDown') end return self:CallFunc(func) end function InputMixin:OnMouseUp() local func = self:GetAttribute('type') local click = self:GetAttribute('clickbutton') self.state = false; if self:IsSecureAction(func, click) then return self:EmulateFrontend(click, 'NORMAL', 'OnMouseUp') end return self:CallFunc(func) end function InputMixin:IsSecureAction(func, click) return (func == 'click' or func == 'action') and click; end function InputMixin:EmulateFrontend(click, state, script) if click:IsEnabled() then if Intellisense:ProcessInterfaceClickEvent(script, click, state) then self.postreset = self:GetAttribute('type') self:SetAttribute('type', nil) end ExecuteFrameScript(click, script) return click:SetButtonState(state) end end function InputMixin:PostClick(...) if self.postreset then self:SetAttribute('type', self.postreset) self.postreset = nil; end end function InputMixin:CallFunc(func) if not self[func] then return end return self[func](self, self.state, self:GetAttribute('id')) end function InputMixin:ClearClickButton() assert(not InCombatLockdown(), 'Attempted to insecurely clear click action in combat.') self:SetAttribute('clickbutton', nil) end function InputMixin:HasClickButton() return self:GetAttribute('clickbutton') end function InputMixin:Clear(manual) self.timer, self.state = 0, false; if manual then self:ClearClickButton() end end --------------------------------------------------------------- -- Conflict handler --------------------------------------------------------------- -- Handles a specific case when external overrides knock input -- handling into a lower priority, because of the chronological -- stack. Might need a var for this assertion if other addons -- leverage custom gamepad controls outside combat. InputMixin.SetOverrideBinding = SetOverrideBindingClick; InputMixin.ClearOverrideBinding = ClearOverrideBindings; do local function HandleConflict(owner, isPriority, key) local formattedKey = tostring(key):upper() if IsBindingForGamePad(formattedKey) then local widget = InputAPI.Widgets[formattedKey]; local override = widget and widget:GetOverride(isPriority) if override then widget:SetOverride(override) end end end hooksecurefunc('SetOverrideBinding', HandleConflict) hooksecurefunc('SetOverrideBindingClick', HandleConflict) hooksecurefunc('SetOverrideBindingItem', HandleConflict) hooksecurefunc('SetOverrideBindingMacro', HandleConflict) hooksecurefunc('SetOverrideBindingSpell', HandleConflict) function InputAPI:HandleConflict(...) if not InCombatLockdown() then HandleConflict(...) end end end