--------------------------------------------------------------- -- EasyMotion.lua: Combo shortcuts for targeting --------------------------------------------------------------- -- Creates a set of combinations for different pools of units, -- in order to alleviate targeting. A healer's delight. -- Thanks to Yoki for original concept! :) local _, db = ... --------------------------------------------------------------- -- Key sets and their integer identifiers for input processing local Key = { R = { PAD1 = 0x1; PAD2 = 0x2; PAD3 = 0x3; PAD4 = 0x4; }; L = { PADDDOWN = 0x5; PADDRIGHT = 0x6; PADDLEFT = 0x7; PADDUP = 0x8; }; } local Index = {} for side, set in pairs(Key) do for name, id in pairs(set) do Index[id] = name; end end local Actions = { plate = 'CLICK ConsolePortEasyMotionButton:RightButton'; frame = 'CLICK ConsolePortEasyMotionButton:LeftButton'; tab = 'CLICK ConsolePortEasyMotionButton:MiddleButton'; } --------------------------------------------------------------- -- Get action/input handlers, EasyMotion -> EM for brevity local EM, Input = CPAPI.EventHandler(ConsolePortEasyMotionButton), ConsolePortEasyMotionInput -- Link functions for world targeting EM.HighlightTarget = TargetPriorityHighlightStart EM.GetNamePlateForUnit = C_NamePlate.GetNamePlateForUnit -- Mixin functions for the hotkey display local HotkeyMixin, GroupMixin = {}, {} --------------------------------------------------------------- -- Initialize secure namespace --------------------------------------------------------------- EM:Execute([[ -- References to other frames headers = newtable() -- Unit tables units, ignore, plates, sorted = newtable(), newtable(), newtable(), newtable() -- Binding tables btns, bindings, lookup = newtable(), newtable(), newtable() -- Ignore mouseover/target ignore.mouseover = true; ignore.target = true; bindRef = 'ConsolePortEasyMotionInput'; MAX = self:GetAttribute('maxcombos') ]]) for side, set in pairs(Key) do EM:Execute('btns.'..side..' = newtable()') for name, keyID in pairs(set) do EM:Execute(format([[ btns.%s.%s = '%s' ]], side, name, keyID)) end end -- Run snippets --------------------------------------------------------------- for name, script in pairs({ -- Parse the input Input = [[ key = ... input = input and tonumber( input .. key ) or tonumber(key) self:CallMethod('Filter', tostring(input)) ]], -- Set the new target SetTarget = [[ local unit = lookup[input] if unit then self:SetAttribute('macrotext', '/target '..unit) elseif defaultToTab then self:SetAttribute('macrotext', '/targetenemy') end self:ClearBindings() if self:GetAttribute('ghostMode') then self:CallMethod('HideBindings', unit, true) self:RunAttribute('DisplayBindings', true) self:RunAttribute('Wipe') else self:RunAttribute('Wipe') self:CallMethod('HideBindings', unit) end pool = nil ]], -- Create key combinations e.g. -> (1, 2, 3, ..., 12, 13, 14, ..., 122, 123, 124) CreateBindings = [[ -- instantiate a keySet with a fixed format local keySet = newtable( set.PADDUP or set.PAD4, set.PADDLEFT or set.PAD3, set.PADDDOWN or set.PAD1, set.PADDRIGHT or set.PAD2 ); local current = 1 for _, k1 in ipairs(keySet) do bindings[current] = tonumber(k1) current = current + 1 for _, k2 in ipairs(keySet) do bindings[current] = tonumber(k2 .. k1) current = current + 1 for _, k3 in ipairs(keySet) do bindings[current] = tonumber(k3 .. k2 .. k1) current = current + 1 end end end table.sort(bindings) ]], -- Assign units to key combinations AssignUnits = [[ self:RunAttribute('CreateBindings') for i, unit in pairs(sorted) do local binding = bindings[i] lookup[binding] = unit end ]], -- Refresh everything on down press Refresh = [[ pool, down = ... if not down then self:RunAttribute('SetTarget') return end self:SetAttribute('macrotext', nil) self:RunAttribute('Wipe') self:RunAttribute('SetBindings', pool) self:RunAttribute('UpdateUnits', pool) self:RunAttribute('SortUnits') self:RunAttribute('AssignUnits', pool) self:RunAttribute('DisplayBindings') ]], -- Feed existing nameplate units into the unit table UpdatePlates = [[ for plate in pairs(plates) do units[plate] = true end ]], -- Find existing unit frames UpdateFrames = [[ local stack if not current then stack = newtable(self.GetChildren(self:GetParent())) elseif current:IsVisible() then local unit = current:GetAttribute('unit') if unit and not ignore[unit] and not headers[current] then units[unit] = true end stack = newtable(self.GetChildren(current)) end if stack then for i, frame in pairs(stack) do if frame:IsProtected() then current = frame self:RunAttribute('UpdateFrames') end end end ]], -- Update sources of units UpdateUnits = [[ local pool = ... if pool == 'frames' then self:RunAttribute('UpdateFrames') elseif pool == 'plates' or pool == 'tab' then self:RunAttribute('UpdatePlates') end ]], -- Sort the units by name, to retain some coherence when setting up bindings SortUnits = [[ local pool = self:GetAttribute('unitpool') if ( not pool ) then for unit in pairs(units) do table.insert(sorted, unit) end return table.sort(sorted) end local c = 0; for token in pool:gmatch('[%a%p]+') do local set = newtable() for unit in pairs(units) do if unit:match(token) then table.insert(set, unit) end end table.sort(set) for i, unit in ipairs(set) do sorted[c + i] = unit units[unit] = nil end c = #sorted end ]], -- Set the bindings that control the input SetBindings = [[ local pool = ... if pool == 'frames' then set = btns[frameSet] side = frameSet modifier = frameMod elseif pool == 'plates' then set = btns[plateSet] side = plateSet modifier = plateMod elseif pool == 'tab' then set = btns[tabSet] side = tabSet modifier = tabMod end if set then for binding, keyID in pairs(set) do self:SetBindingClick(true, binding, bindRef, keyID) self:SetBindingClick(true, modifier..binding, bindRef, keyID) end end ]], -- Display the bindings on frames/plates DisplayBindings = [[ local ghostMode = ... self:CallMethod('SetFramePool', pool, side) self:CallMethod('HideBindings', ghostMode) for binding, unit in pairs(lookup) do self:CallMethod('DisplayBinding', tostring(binding), unit, ghostMode) end ]], -- Wipe tables and settings Wipe = [[ lookup = wipe(lookup) units = wipe(units) sorted = wipe(sorted) bindings = wipe(bindings) defaultToTab = nil current = nil input = nil set = nil ]], OnNewSettings = [[ ignore.player = self:GetAttribute('ignorePlayer') ]], }) do EM:SetAttribute(name, script) end -- EM secure input wrappers for name, script in pairs({ PreClick = [[ -- Unit frames if button == 'LeftButton' then self:RunAttribute('Refresh', 'frames', down) -- Nameplates elseif button == 'RightButton' then self:RunAttribute('Refresh', 'plates', down) -- Combined nameplates/nearest elseif button == 'MiddleButton' then self:RunAttribute('Refresh', 'tab', down) if down then defaultToTab = true self:CallMethod('HighlightTarget', false) end end ]], }) do EM:WrapScript(EM, name, script) end -- Secure input wrappers for name, script in pairs({ OnClick = [[ if down then owner:RunAttribute('Input', button) end ]], }) do EM:WrapScript(Input, name, script) end --------------------------------------------------------------- -- Smart allocation --------------------------------------------------------------- -- Handles setup of bindings in a way where the set to use for -- input doesn't conflict with the hold binding itself. function EM:OnNewBindings(bindings) local keys = {}; for unitType, action in pairs(Actions) do for button, set in pairs(bindings) do for modifier, binding in pairs(set) do if (binding == action) then keys[unitType] = {button, modifier}; end end end end local forceSet = db('Settings/unitHotkeySet') if forceSet then forceSet = forceSet:lower() forceSet = forceSet:match('left') and 'L' or forceSet:match('right') and 'R' end for unitType, slug in pairs(keys) do local button, modifier = unpack(slug) if button and modifier then local set = forceSet or (Key.R[button] and 'L' or 'R'); self:Execute(format([[ %sSet = '%s' %sMod = '%s' ]], unitType, set, unitType, modifier)) end end end function EM:OnNewAttributes() self:SetAttribute('unitpool', (db('unitHotkeyPool') or ''):gsub(';', '\n')) self:SetAttribute('ghostMode', db('unitHotkeyGhostMode')) self:SetAttribute('ignorePlayer', db('unitHotkeyIgnorePlayer')) self:Execute([[self:RunAttribute('OnNewSettings')]]) end function EM:OnDataLoaded() self:OnNewAttributes() self:OnNewBindings(db.Gamepad:GetBindings()) end db:RegisterSafeCallback('OnNewBindings', EM.OnNewBindings, EM) db:RegisterSafeCallback('Settings/unitHotkeyPool', EM.OnNewAttributes, EM) db:RegisterSafeCallback('Settings/unitHotkeyGhostMode', EM.OnNewAttributes, EM) db:RegisterSafeCallback('Settings/unitHotkeyIgnorePlayer', EM.OnNewAttributes, EM) --------------------------------------------------------------- -- Frontend --------------------------------------------------------------- EM.FramePool = {} EM.ActiveFrames = 0 EM.UnitFrames = {} function EM:SetFramePool(unitType, side) self.unitType = unitType self.set = Key[side] if unitType == 'frames' then wipe(self.UnitFrames) self:RefreshUnitFrames() end end function EM:AddFrameForUnit(frame, unit) local frames = self.UnitFrames frames[unit] = frames[unit] or {} frames[unit][frame] = true end function EM:GetUnitFramesForUnit(unit) return pairs(self.UnitFrames[unit] or {}) end function EM:RefreshUnitFrames(current) local stack if not current then stack = {self:GetParent():GetChildren()} elseif current:IsVisible() then local unit = current:GetAttribute('unit') if unit then self:AddFrameForUnit(current, unit) end stack = {current:GetChildren()} end if stack then for i, frame in pairs(stack) do if not frame:IsForbidden() and frame:IsProtected() then self:RefreshUnitFrames(frame) end end end end function EM:Filter(input) local filter = '^' .. input for idx, frame in self:GetFrames() do if frame.isActive and frame.binding:match(filter) then local icon, level, previous = frame.Keys, 0 for id in input:gmatch('%d') do id = tonumber(id) if previous then previous:Hide() tremove(frame.ShownKeys, previous.shownID) end previous = icon[id] icon = icon[id] level = level + 1 end frame:Adjust(level > 1 and level) frame:IndicateFilterMatch() else frame:Clear() end end end function EM:DisplayBinding(binding, unit, ghostMode) local plate = not ghostMode and self.GetNamePlateForUnit(unit) if plate and plate.UnitFrame then self:AddFrameForUnit(plate.UnitFrame, unit) end for frame in self:GetUnitFramesForUnit(unit) do local hotkey = self:GetHotkey(binding) hotkey:DrawIconsForBinding(binding) hotkey:SetAlpha(ghostMode and 0.5 or 1) hotkey.unit = unit if self.unitType == 'frames' then hotkey:SetUnitFrame(frame) end end end function EM:HideBindings(unit, ghostMode) self.ActiveFrames = 0 for _, frame in self:GetFrames() do if unit and frame.unit == unit then frame:Animate(ghostMode) else frame:Clear() end end end function EM:GetHotkey(binding) local frame self.ActiveFrames = self.ActiveFrames + 1 if self.ActiveFrames > #self.FramePool then frame = CreateFrame('Frame', 'ConsolePortEasyMotionDisplay'..self.ActiveFrames, self) frame:SetFrameStrata("TOOLTIP") frame.size = db('unitHotkeySize') or 32 frame.offsetX = db('unitHotkeyOffsetX') or 0 frame.offsetY = db('unitHotkeyOffsetY') or -8 frame.anchor = db('unitHotkeyAnchor') or 'CENTER' frame:SetSize(1, 1) frame:Hide() frame.Keys = {} frame.ShownKeys = {} frame.GetNamePlateForUnit = C_NamePlate.GetNamePlateForUnit Mixin(frame, HotkeyMixin) self.FramePool[self.ActiveFrames] = frame else frame = self.FramePool[self.ActiveFrames] end frame.binding = binding frame.isActive = true return frame end function EM:GetFrames() return pairs(self.FramePool) end --------------------------------------------------------------- -- Hotkey mixin --------------------------------------------------------------- function HotkeyMixin:Clear() self.isActive = false self.binding = nil self.unit = nil self:SetParent(EM) self:SetFrameLevel(1) self:SetSize(1, 1) self:SetScale(1) self:ClearAllPoints() self:Hide() for _, icon in pairs(self.ShownKeys) do icon:Hide() end wipe(self.ShownKeys) end function HotkeyMixin:Adjust(depth) local offset = depth and #self.ShownKeys + depth or #self.ShownKeys self:SetWidth( offset * ( self.size * 0.75 ) ) end function HotkeyMixin:IndicateFilterMatch() if not self.Indicate then self.Indicate = self:CreateAnimationGroup() self.Indicate.Enlarge = self.Indicate:CreateAnimation('SCALE') --- self.Indicate.Enlarge:SetOrigin('CENTER', 0, 0) self.Indicate.Enlarge:SetScale(1.35, 1.35) self.Indicate.Enlarge:SetDuration(0.1) self.Indicate.Enlarge:SetSmoothing('OUT') --- end self.Indicate:Finish() self.Indicate:Play() end function HotkeyMixin:DrawIconsForBinding(binding) binding = binding or self.binding local icon, shown = self.Keys, 0 for id in binding:gmatch('%d') do id = tonumber(id) local size = self.size if icon[id] then icon = icon[id] else icon[id] = self:CreateTexture(nil, 'OVERLAY') icon = icon[id] icon:SetTexture(db.Icons[32][Index[id]]) icon:SetSize(size, size) end shown = shown + 1 self.ShownKeys[shown] = icon icon.shownID = shown icon:SetPoint('LEFT', ( shown - 1) * ( size * 0.75 ), 0) icon:Show() end self:Adjust() end function HotkeyMixin:Animate(ghostMode) if not self.Match then self.Match = self:CreateAnimationGroup() self.Match.Enlarge = self.Match:CreateAnimation('SCALE') self.Match.Shrink = self.Match:CreateAnimation('SCALE') self.Match.Alpha = self.Match:CreateAnimation('ALPHA') --- self.Match.Enlarge:SetOrigin('CENTER', 0, 0) self.Match.Enlarge:SetScale(2, 2) self.Match.Enlarge:SetDuration(0.1) self.Match.Enlarge:SetSmoothing('OUT') --- self.Match.Shrink:SetOrigin('CENTER', 0, 0) self.Match.Shrink:SetScale(0.25, 0.25) self.Match.Shrink:SetDuration(0.2) self.Match.Shrink:SetStartDelay(0.1) self.Match.Shrink:SetSmoothing('OUT') --- self.Match.Alpha:SetStartDelay(0.1) self.Match.Alpha:SetFromAlpha(1) self.Match.Alpha:SetToAlpha(0) self.Match.Alpha:SetDuration(0.2) --- Mixin(self.Match, GroupMixin) end self.Match:Finish() self.Match:SetScript('OnFinished', ghostMode and self.Match.RedrawOnFinish or self.Match.ClearOnFinish) self.Match:Play() end function HotkeyMixin:SetNamePlate(plate) if plate and plate.UnitFrame then self:SetParent(WorldFrame) self:SetPoint('CENTER', plate.UnitFrame, 0, 0) self:Show() self:SetScale(UIParent:GetScale()) self:SetFrameLevel(plate.UnitFrame:GetFrameLevel() + 1) end end function HotkeyMixin:SetUnitFrame(frame) if frame then self:SetParent(UIParent) self:SetPoint(self.anchor, frame, self.offsetX, self.offsetY) self:Show() self:SetScale(1) self:SetFrameLevel(frame:GetFrameLevel() + 1) end end function GroupMixin:ClearOnFinish() self:GetParent():Clear() end function GroupMixin:RedrawOnFinish() local parent = self:GetParent() parent:DrawIconsForBinding(parent.binding) parent:SetAlpha(0.5) end