view ObjectiveTracker/ObjectiveTracker.lua @ 37:e84d645c8ab8

- revised the tracker update function to build its complete data list up front and use the values as points of comparison for determining possible out of place blocks, which will be iterated over afterward to remove what wasn't re-used - also entailed revising the exact role of global event handlers and function hooks, limiting their directions of communication so one doesn't end up calling the other multiple or inifinity times - schema handling polish
author Nenue
date Mon, 18 Apr 2016 07:56:23 -0400
parents 69d03f8e293e
children 1f8f9cc3d956
line wrap: on
line source
--- ${PACKAGE_NAME}
-- @file-author@
-- @project-revision@ @project-hash@
-- @file-revision@ @file-hash@
-- Created: 3/26/2016 1:51 AM
local B, _G = select(2,...).frame, _G
local pairs, setmetatable, type, tostring, band, format = _G.pairs, _G.setmetatable, _G.type, _G.tostring, bit.band, string.format
local ipairs, tinsert, hooksecurefunc = _G.ipairs, _G.tinsert, _G.hooksecurefunc
local PlaySoundFile, IsQuestTask, SortQuestWatches, GetCurrentMapAreaID, GetZoneText, GetMinimapZoneText = PlaySoundFile, IsQuestTask, SortQuestWatches, GetCurrentMapAreaID, GetZoneText, GetMinimapZoneText
local QuestPOIUpdateIcons, GetCVar, IsPlayerInMicroDungeon, WorldMapFrame, GetCVarBool, SetMapToCurrentZone = QuestPOIUpdateIcons, GetCVar, IsPlayerInMicroDungeon, WorldMapFrame, GetCVarBool, SetMapToCurrentZone
local AddAutoQuestPopUp = AddAutoQuestPopUp
local T = B:RegisterModule("ObjectiveTracker", _G.VeneerObjectiveWrapper, 'BuffFrame')
local print = B.print('Objectives')
local ObjectiveTrackerFrame, VeneerObjectiveScroll, CreateFrame = _G.ObjectiveTrackerFrame, _G.VeneerObjectiveScroll, _G.CreateFrame
local Wrapper = _G.VeneerObjectiveWrapper
local Scroller = VeneerObjectiveWrapper.scrollArea
local Scroll = _G.VeneerObjectiveScroll
local unitLevel = UnitLevel('player')

--- Performance values
--[[
  self:RegisterEvent("QUEST_LOG_UPDATE");
	self:RegisterEvent("TRACKED_ACHIEVEMENT_LIST_CHANGED");
	self:RegisterEvent("QUEST_WATCH_LIST_CHANGED");
	self:RegisterEvent("QUEST_AUTOCOMPLETE");
	self:RegisterEvent("QUEST_ACCEPTED");
	self:RegisterEvent("SUPER_TRACKED_QUEST_CHANGED");
	self:RegisterEvent("SCENARIO_UPDATE");
	self:RegisterEvent("SCENARIO_CRITERIA_UPDATE");
	self:RegisterEvent("TRACKED_ACHIEVEMENT_UPDATE");
	self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
	self:RegisterEvent("ZONE_CHANGED");
	self:RegisterEvent("QUEST_POI_UPDATE");
	self:RegisterEvent("VARIABLES_LOADED");
	self:RegisterEvent("QUEST_TURNED_IN");
	self:RegisterEvent("PLAYER_MONEY");
 ]]


--- These are the bitfields used by Blizzard_ObjectiveTracker to determine which segments get parsed.
--- They are replicated here so that plugins can make use of any securehook args involving this info.
local OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST = OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST                       -- 0x0100
local OBJECTIVE_TRACKER_UPDATE_MODULE_AUTO_QUEST_POPUP = OBJECTIVE_TRACKER_UPDATE_MODULE_AUTO_QUEST_POPUP	-- 0x0200
local OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE = OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE		-- 0x0400
local OBJECTIVE_TRACKER_UPDATE_MODULE_SCENARIO = OBJECTIVE_TRACKER_UPDATE_MODULE_SCENARIO	    		        -- 0x0800
local OBJECTIVE_TRACKER_UPDATE_MODULE_ACHIEVEMENT	= OBJECTIVE_TRACKER_UPDATE_MODULE_ACHIEVEMENT           -- 0x1000


local OBJECTIVE_TRACKER_UPDATE_STATIC = OBJECTIVE_TRACKER_UPDATE_STATIC					                         	-- 0x0000
local OBJECTIVE_TRACKER_UPDATE_ALL = OBJECTIVE_TRACKER_UPDATE_ALL			                    	          		-- 0xFFFF
local OBJECTIVE_TRACKER_UPDATE_ID = OBJECTIVE_TRACKER_UPDATE_ID                                           -- 0

local OBJECTIVE_TRACKER_UPDATE_QUEST = OBJECTIVE_TRACKER_UPDATE_QUEST				        	                  	-- 0x0001
local OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED = OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED		               		    -- 0x0002
local OBJECTIVE_TRACKER_UPDATE_TASK_ADDED = OBJECTIVE_TRACKER_UPDATE_TASK_ADDED		    	              		-- 0x0004
local OBJECTIVE_TRACKER_UPDATE_SCENARIO = OBJECTIVE_TRACKER_UPDATE_SCENARIO			      	                 	-- 0x0008
local OBJECTIVE_TRACKER_UPDATE_SCENARIO_NEW_STAGE = OBJECTIVE_TRACKER_UPDATE_SCENARIO_NEW_STAGE		       	-- 0x0010
local OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT = OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT			                   	-- 0x0020
local OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT_ADDED = OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT_ADDED		  	      -- 0x0040
local OBJECTIVE_TRACKER_UPDATE_SCENARIO_BONUS_DELAYED = OBJECTIVE_TRACKER_UPDATE_SCENARIO_BONUS_DELAYED		-- 0x0080

local OBJECTIVE_TRACKER_UPDATE_REASON = OBJECTIVE_TRACKER_UPDATE_ALL -- default
--- Used to determine which trackers are listening for money events

T.strings = {}
T.strings.CLICK_TO_ACCCEPT = 'Click to Accept'
T.strings.CLICK_TO_COMPLETE = 'Click to complete'
T.colors ={
  enable = true,
}

T.watchMoneyReasons = 0

--- Baseline defaults table; values defined in the files that they pertain to
T.defaults = {}


--- Tracker display order
T.orderedNames = {'Bonus', 'AutoQuest', 'Quest', 'Cheevs'}

--- ipairs() argument tables
T.orderedHandlers = setmetatable({}, {__mode = "k"})
T.orderedTrackers = setmetatable({}, {__mode = "k"})
T.indexedTrackers = setmetatable({}, {__mode = "k"})

--- pairs() argument tables
T.namedTrackers = setmetatable({}, {__mode = "k"})

local WRAPPER_ANCHOR_POINT = 'TOPRIGHT'
local WRAPPER_OFFSET_X = 0
local WRAPPER_OFFSET_Y = -200
local WRAPPER_MAX_HEIGHT = 670
local WRAPPER_WIDTH = 280
local WRAPPER_HEADER_HEIGHT = 24

T.defaults.Wrapper = {
  AnchorPoint = WRAPPER_ANCHOR_POINT,
  OffsetX = WRAPPER_OFFSET_X,
  OffsetY = WRAPPER_OFFSET_Y,
  Height = WRAPPER_MAX_HEIGHT,
  Width = WRAPPER_WIDTH,
  HeaderHeight = WRAPPER_HEADER_HEIGHT,
  TextSpacing = 3,
  TextIndent = 4,
  TitleSpacing = 3,
  TitleIndent = 4,
}



--- Tracker module definitions begin here; innards dealing with data retreival and output are defined further in
T.DefaultHandler = {
  previousHeight = 0,

  name = "temp",
  displayName = "temp",
  updateReasonModule = 0xFF00,
  updateReasonEvent  = 0x00FF,

  numWatched = 0,   --- number of entries being handled
  numBlocks = 0,    --- number of blocks created
  actualBlocks = 0, --- number of blocks in use
  Info = {},        --- stored watch list information, keyed by whatever unique ID is involved for that tracker
  Watched = {},     --- stores whether the given unique ID is tracked

  freeBlocks = {},  --- blocks hidden due to list shrinkage
  usedBlocks = {},  --- block in use
  BlockInfo = {},   --- by block creation offset, used by framescript
  WatchList = {},   --- ordered manifest of watched items

  --- Indexes
  InfoBlock = {},   --- by unique ID
  LogBlock = {},    --- by API log offset, used by GetBlock if possible
  WatchBlock = {},  --- block by internal offset, used in GetBlock scope
  WatchInfo = {},   --- info by internal offset, used in Update scope
}

T.AutoQuest = {
  name = "AutoQuest",
  displayName = "Notice",
  updateReasonModule = OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST,
  updateReasonEvents = OBJECTIVE_TRACKER_UPDATE_QUEST + OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED,
  LogInfo = {},
  QuestBlock = {},
}
T.Quest = {
  name = "Quest",
  displayName = "Quests",
  updateReasonModule = OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST,
  updateReasonEvents = OBJECTIVE_TRACKER_UPDATE_QUEST + OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED,
  itemButtons = {},
  freeButtons = {},
  LogInfo = {},
  QuestBlock = {},
}
T.Cheevs = {
  name = "Cheevs",
  displayName = "Achievements",
  schema = 'achievement',
  updateReasonModule = OBJECTIVE_TRACKER_UPDATE_MODULE_ACHIEVEMENT,
  updateReasonEvents = OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT +
      OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT_ADDED,
}
T.Bonus = {
  name = "Bonus",
  displayName = "Bonus Objectives",
  updateReasonModule = OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE,
  updateReasonEvents = OBJECTIVE_TRACKER_UPDATE_QUEST + OBJECTIVE_TRACKER_UPDATE_TASK_ADDED,
  QuestBlock = {},
}

T.Scenario = {
  name = 'Scenario',
  displayName = 'Scenario Objectives',
  updateReasonModule = OBJECTIVE_TRACKER_UPDATE_MODULE_SCENARIO,
  updateReasonEvents = OBJECTIVE_TRACKER_UPDATE_SCENARIO_NEW_STAGE + OBJECTIVE_TRACKER_UPDATE_SCENARIO_BONUS_DELAYED
}

local Tracker_string = function (self)
  return self.name
end
local Tracker_call = function (self, reason)
  self:Update(reason)
end
local Handler_Initialize = function (self, name, index)
  local c = T.Conf.Wrapper
  print('Initializing |cFF00FFFF'..name..'|r module...')

  local handler = setmetatable(T[name] or {}, {
    __tostring = Tracker_string,
    __call = Tracker_call
  })
  if type(T.orderedHandlers[index]) == 'table' then
    return T.orderedHandlers[index]
  end

  print('|cFFFFFF00Acquiring locals')
  local preset = {}
  for k, _ in pairs(handler) do
    preset[k] = true
  end


  print('|cFFFF8800Inheriting')
  for k, v in pairs(self) do
    if not handler[k] then
      if type(v) == 'table' then
        -- assume all tables to be local data; don't inherit or ref
        handler[k] = {}
      else
        handler[k] = self[k]
      end
      print('copying', k)
    end
  end
  print('|cFFFF4400'..tostring(name)..'|r:')
  for k, v in pairs(handler) do
    print(format("%32s %8s %s", (preset[k] and '|cFFFFFFFF' or '|cFFFFFF00') .. k .. '|r', type(v), tostring(v)))
  end

  T[name] = handler

  local trackerName = 'Veneer'..name..'Tracker'
  local handler = T[name]
  local frame = CreateFrame('Frame', trackerName, _G.VeneerObjectiveScroll, 'VeneerTrackerTemplate')
  frame.title:SetText(handler.displayName)
  frame:SetWidth(c.Width)
  frame.previousOffset = 0

  handler.frame = frame
  handler.numBlocks = 0
  handler.actualBlocks = 0
  handler.trackerName = trackerName
  handler.lines = {}
  T.orderedTrackers[index] = frame
  T.namedTrackers[name] = frame
  T.indexedTrackers[handler] = frame
  print('|cFFFF0088' .. trackerName .. '|r created for |cFF00FFFF' .. handler.displayName .. '|r module')

  T.orderedHandlers[index] = handler
  return true
end

local Event = {}
Event.QUEST_LOG_UPDATE =  function()
  return OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST + OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE
end
local GetNumQuestWatches, AddQuestWatch, SetSuperTrackedQuestID = GetNumQuestWatches, AddQuestWatch, SetSuperTrackedQuestID
Event.QUEST_ACCEPTED = function(questLogIndex, questID, added)
  if ( IsQuestTask(questID) ) then
    return OBJECTIVE_TRACKER_UPDATE_TASK_ADDED, questID
  else

    AddQuestWatch(questID)
    --return OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED, questID, added
  end
end

Event.QUEST_REMOVED = function(questLogIndex, questID)
  return OBJECTIVE_TRACKER_UPDATE_QUEST, questID, false
end

Event.QUEST_WATCH_LIST_CHANGED = function(questID, added)
  if ( added == true ) then
    if ( not IsQuestTask(questID) ) then
      return OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED, questID, added
    end
  elseif questID then
    return OBJECTIVE_TRACKER_UPDATE_QUEST_ADDED, questID, added
  end
end

Event.QUEST_POI_UPDATE = function()
  QuestPOIUpdateIcons();
  if ( GetCVar("trackQuestSorting") == "proximity" ) then
    SortQuestWatches();
  end
  return OBJECTIVE_TRACKER_UPDATE_ALL
end
Event.SUPER_TRACKED_QUEST_CHANGED = function(questID)
  return OBJECTIVE_TRACKER_UPDATE_QUEST, questID
end
Event.ZONE_CHANGED = function()
  local inMicroDungeon = IsPlayerInMicroDungeon();
  if ( inMicroDungeon ~= T.inMicroDungeon ) then
    if ( not WorldMapFrame:IsShown() and GetCVarBool("questPOI") ) then
      SetMapToCurrentZone();			-- update the zone to get the right POI numbers for the tracker
    end
    --SortQuestWatches();
    T.inMicroDungeon = inMicroDungeon;
  end
  return OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE + OBJECTIVE_TRACKER_UPDATE_MODULE_SCENARIO
end
Event.QUEST_AUTOCOMPLETE = function(questId)
  AddAutoQuestPopUp(questId, "COMPLETE");
  return OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST + OBJECTIVE_TRACKER_UPDATE_MODULE_AUTO_QUEST_POPUP
end
Event.SCENARIO_CRITERIA_UPDATE =  function()
  return OBJECTIVE_TRACKER_UPDATE_SCENARIO
end
Event.SCENARIO_UPDATE = function(newStage)
  if ( newStage ) then
    return OBJECTIVE_TRACKER_UPDATE_SCENARIO_NEW_STAGE
  else
    return OBJECTIVE_TRACKER_UPDATE_SCENARIO
  end
end
Event.TRACKED_ACHIEVEMENT_UPDATE = function()
  return OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT
end
Event.TRACKED_ACHIEVEMENT_LIST_CHANGED = function(achievementID, added)
  return OBJECTIVE_TRACKER_UPDATE_ACHIEVEMENT_ADDED, achievementID, added
end
Event.ZONE_CHANGED_NEW_AREA = function ()
  if ( not WorldMapFrame:IsShown() and GetCVarBool("questPOI") ) then
    SetMapToCurrentZone();			-- update the zone to get the right POI numbers for the tracker
  end
  SortQuestWatches();
  T.currentZoneArea = GetCurrentMapAreaID()
  print('Updating zone ID to', T.currentZoneArea, '=', GetZoneText(), GetMinimapZoneText())


  return OBJECTIVE_TRACKER_UPDATE_TASK_ADDED
end


Event.PLAYER_MONEY = function()
  if T.watchMoneyReasons > 0 then
    return T.watchMoneyReasons
  end
end
Event.CRITERIA_COMPLETE = function()
  return OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE
end
Event.QUEST_TURN_IN = function(questID, xp, money)
  if ( IsQuestTask(questID) ) then
    T.Bonus:OnTurnIn(questID, xp, money)
    print('updating bonus modules (code', OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE, ',', questID, xp, money)
    return OBJECTIVE_TRACKER_UPDATE_MODULE_BONUS_OBJECTIVE, questID, xp, money
  else
    return OBJECTIVE_TRACKER_UPDATE_MODULE_QUEST, questID, xp, money
  end
end
T.Event = Event

--- Done once per ui load
local BlizzHooks = {
  ['AcceptQuest'] = 'AcceptQuest',
  ['AddQuestWatch'] = 'AddQuestWatch',
  ['RemoveQuestWatch'] = 'RemoveQuestWatch',
  ['AbandonQuest'] = 'AbandonQuest',
  ['AcknowledgeAutoAcceptQuest'] = 'AcknowledgeAutoAcceptQuest',
  ['AddAutoQuestPopUp'] = 'AddAutoQuestPopUp',
  ['RemoveAutoQuestPopUp'] = 'RemoveAutoQuestPopUp',
  ['AddTrackedAchievement'] = 'AddTrackedAchievement',
  ['RemoveTrackedAchievement'] = 'RemoveTrackedAchievement',
  ['SetSuperTrackedQuestID'] = 'SetSuperTrackedQuestID'
}
local VeneerData

T.SetWatchMoney = function(watchMoney, reason)
  if watchMoney then
    if band(T.watchMoneyReasons, reason) == 0 then
      T.watchMoneyReasons = T.watchMoneyReasons + reason;
    end
  else
    if band(T.watchMoneyReasons, reason) > 0 then
      T.watchMoneyReasons = T.watchMoneyReasons - reason;
    end
  end
end
T.animateReasons = 0
T.SetAnimate = function(reason)
  print('comparing', T.animateReasons, reason)
  if animate then
    if band(T.animateReasons, reason) == 0 then
      T.animateReasons = T.animateReasons + reason
    end
  else
    if band(T.animateReasons, reason) > 0 then
      T.animateReasons = T.animateReasons - reason
    end
  end
end

local iprint = B.print('Info')
T.SetRewards = function(t, questID)
  local previousSelection = GetQuestLogSelection()

  SelectQuestLogEntry(GetQuestLogIndexByID(questID))
  local numQuestChoices = GetNumQuestLogChoices();
  local skillName, skillIcon, skillPoints = GetQuestLogRewardSkillPoints();
  local xp = GetQuestLogRewardXP();
  local playerTitle = GetQuestLogRewardTitle();
  ProcessQuestLogRewardFactions();
  if previousSelection then
    SelectQuestLogEntry(previousSelection)
  end

  local rewards = {}
  local texture, name, isTradeskillSpell, isSpellLearned, hideSpellLearnText, isBoostSpell, garrFollowerID = GetQuestLogRewardSpell(questID)
  if name  then
    tinsert(rewards,{
      type = 'spell',
      name = name,
      texture = texture,
    })
  end

  t.numCurrencies = GetNumQuestLogRewardCurrencies(questID)
  for i = 1, t.numCurrencies do
    local name, texture, count = GetQuestLogRewardCurrencyInfo(i, questID)
    tinsert(rewards,{
      type = 'currency',
      index = i,
      name = name,
      texture = texture,
      count = count
    });
  end
  -- items
  t.numItems = GetNumQuestLogRewards(questID)
  for i = 1, t.numItems do
    local name, texture, count, quality, isUsable = GetQuestLogRewardInfo(i, questID)
    tinsert(rewards, {
      type = 'item',
      index = i ,
      name = name,
      texture = texture,
      count = count,
      quality = quality,
      isUsable = isUsable
    });
  end
  -- money

  local money = GetQuestLogRewardMoney(questID)
  if ( money > 0 ) then
    tinsert(rewards, {
      type = 'money',
      name = GetMoneyString(money),
      texture = "Interface\\Icons\\inv_misc_coin_01",
      count = 0,
    });
  end

  if #rewards >= 1 then
    t.rewardInfo = rewards
  end
end

local Play = function(file) if Devian and Devian.InWorkspace() then PlaySoundFile(file) end end

local tprint = B.print('Tracker')
T.OnHookedFunc = function(name, ...)
  tprint('|cFFFF8800securehook:|r', name, '|cFF00FFFFargs:|r', ...)
  local updateReason, arg1, arg2, arg3 = T[name](...)
  if updateReason then
    print('|cFF00FFFFupdate reason:|r', updateReason, arg1, arg2, arg3)
    T:Update(updateReason, arg1, arg2, arg3)
  end
end

function T:OnEvent (event, ...)
  local isHandled
  tprint('OnEvent(|cFF00FF00'.. event ..'|r):', ...)
  local reason, arg1, arg2, arg3
  if Event[event] then
    if type(Event[event]) == 'function' then
      Play([[Interface\Addons\SharedMedia_MyMedia\sound\Info.ogg]])
      reason, arg1, arg2, arg3 = Event[event](...)
    else
      Play([[Interface\Addons\SharedMedia_MyMedia\sound\Heart.ogg]])
      reason = Event[event]
    end
  else
    print('no event handler set for', event)
    Play([[Interface\Addons\SharedMedia_MyMedia\sound\IM.ogg]])
  end
  if reason then
    print('update reason:', reason, 'args:', arg1, arg2, arg3)
    T:Update(reason, arg1, arg2, arg3)
  else
    print('no reason value returned')
    Play([[Interface\Addons\SharedMedia_MyMedia\sound\Quack.ogg]])
  end

end


function T:OnInitialize()
  local c = T.Conf.Wrapper
  VeneerData = _G.VeneerData
  VeneerData.CallLog = VeneerData.CallLog or {}
  if not T.isHooked then
    T.isHooked = true
    for blizzFunc, veneerFunc in pairs(BlizzHooks) do
      if T[veneerFunc] then
        hooksecurefunc(blizzFunc, function(...) return T.OnHookedFunc(blizzFunc, ...) end)
      else
        hooksecurefunc(blizzFunc, function(...)
          print('|cFFFF0088securehook('..tostring(blizzFunc)..')|r args:', ...)
          tinsert(VeneerData.CallLog, {blizzFunc, ...})
        end)
      end
    end
  end

  T.Conf.TasksLog = T.Conf.TasksLog or {}

  ObjectiveTrackerFrame:UnregisterAllEvents()
  ObjectiveTrackerFrame:Hide()
  _G.MinimapCluster:Hide()

  for id, name in ipairs(T.orderedNames) do
    if not T.orderedHandlers[id] then
      Handler_Initialize(T.DefaultHandler, name, id)
    end
  end
  self:SetSize(c.Width, 40)
  T.InitializeWidgets()
end

--- Done any time the the minimize button is toggled up
function T:OnEnable()

  print(B.Conf.VeneerObjectiveWrapper.enabled)


  for event, action in pairs(Event) do
    print('|cFFFF0088listen to', event, 'for action|r', tostring(action))
    Wrapper:RegisterEvent(event)
  end

  local c = T.Conf.Wrapper
  Wrapper.previousHeight = 0
  Scroller:SetScrollChild(Scroll)
  Scroller:SetWidth(c.Width)
  Scroll:SetWidth(c.Width)
  Scroll:ClearAllPoints()
  Scroll:SetPoint('TOP', Scroller, 'TOP')
  self:SetScript('OnEvent', T.OnEvent)

  Scroller:Show()

  local from, target, to, x, y = Wrapper:GetPoint(1)
  print(from, target:GetName(), to, x,y)


  -- run once to prime the data structure
  T.UpdateActionButtons()
end

function T:OnDisable()
  self:UnregisterAllEvents()
  Scroller:Hide()
end