view classes/Bar.lua @ 156:611e6ce08717

Use state-unitexists for unitwatch
author Flick <flickerstreak@gmail.com>
date Mon, 18 May 2009 23:08:34 +0000
parents 806a61b331a0
children e77f716af1b7
line wrap: on
line source
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local CreateFrame = CreateFrame
local floor = math.floor
local fmod = math.fmod
local format = string.format

ReAction:UpdateRevision("$Revision$")

local KB = LibStub("LibKeyBound-1.0")

---- Secure snippets ----
local _reaction_init = 
[[
  anchorKeys = newtable("point","relPoint","x","y")

  state = nil
  set_state = nil
  state_override = nil
  unit_exists = nil

  showAll = false
  hidden = false

  defaultAlpha = 1.0
  defaultScale = 1.0
  defaultAnchor = newtable()

  activeStates = newtable()
  settings = newtable()
  extensions = newtable()
]]

local _reaction_refresh = 
[[
  local oldState = state
  state = state_override or set_state or state

  local hide = nil
  if state then
    local settings = settings[state]
    if settings then
      -- show/hide
      hide = settings.hide
      -- re-anchor
      local old_anchor = activeStates.anchor
      activeStates.anchor = settings.anchorEnable and state
      if old_anchor ~= activeStates.anchor or not set_state then
        if activeStates.anchor then
          if settings.anchorPoint then
            self:ClearAllPoints()
            local f = self:GetAttribute("frameref-anchor-"..state)
            if f then
              self:SetPoint(settings.anchorPoint, f, settings.anchorRelPoint, settings.anchorX, settings.anchorY)
            end
          end
        elseif defaultAnchor.point then
          self:ClearAllPoints()
          self:SetPoint(defaultAnchor.point, defaultAnchor.frame, 
                        defaultAnchor.relPoint, defaultAnchor.x, defaultAnchor.y)
        end
      end
      -- re-scale
      local old_scale = activeStates.scale
      activeStates.scale = settings.enableScale and state
      if old_scale ~= activeStates.scale or not set_state then
        self:SetScale(activeStates.scale and settings.scale or defaultScale)
      end
      -- alpha
      local old_alpha = activeStates.alpha
      activeStates.alpha = settings.enableAlpha and state
      if old_alpha ~= activeStates.alpha or not set_state then
        self:SetAlpha(activeStates.alpha and settings.alpha or defaultAlpha)
      end
    end
  end

  -- hide if state or unit_exists says to
  hide = not showAll and (hide or unithide)
  if hide ~= hidden then
    hidden = hide
    if hide then
      self:Hide()
    else
      self:Show()
    end
  end

  for _, attr in pairs(extensions) do
    control:RunAttribute(attr)
  end
  
  control:ChildUpdate()

  if showAll then
    control:CallMethod("UpdateHiddenLabel", state and settings[state] and settings[state].hide)
  end

  if oldState ~= state then
    control:CallMethod("StateRefresh", state)
  end
]]

local _onstate_reaction = -- function( self, stateid, newstate )
[[
  set_state = newstate
]] .. _reaction_refresh

local _onstate_showgrid = -- function( self, stateid, newstate )
[[
  control:ChildUpdate(stateid,newstate)
  control:CallMethod("UpdateShowGrid")
]]

local _onstate_unitexists = -- function( self, stateid, newstate )
[[
  unithide = not newstate
]] .. _reaction_refresh

local _onclick =  -- function( self, button, down )
[[
  if state_override == button then
    state_override = nil -- toggle
  else
    state_override = button
  end
]] .. _reaction_refresh


---- Bar class ----
local Bar   = { }
local weak  = { __mode = "k" }
local frameList = { }

ReAction.Bar = Bar -- export to ReAction

function Bar:New( name, config )
  if type(config) ~= "table" then
    error("ReAction.Bar: config table required")
  end

  -- create new self
  self = setmetatable( 
    { 
      config  = config,
      name    = name,
      buttons = setmetatable( { }, weak ),
      width   = config.width or 480,
      height  = config.height or 40,
    }, 
    {__index = self} )
  
  -- The frame type is 'Button' in order to have an OnClick handler. However, the frame itself is
  -- not mouse-clickable by the user.
  local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
  name = name and "ReAction-"..name
  local f = name and frameList[name]
  if not f then
    f = CreateFrame("Button", name, parent, "SecureHandlerStateTemplate, SecureHandlerClickTemplate")
    if name then
      frameList[name] = f
    end
  end
  f:SetFrameStrata("MEDIUM")
  f:SetWidth(self.width)
  f:SetHeight(self.height)
  f:SetAlpha(config.alpha or 1.0)
  f:Show()
  f:EnableMouse(false)
  f:SetClampedToScreen(true)
  ReAction.gridProxy:AddFrame(f)

  -- secure handlers
  f:Execute(_reaction_init)
  f:SetAttribute("_onstate-reaction",   _onstate_reaction)
  f:SetAttribute("_onstate-showgrid",   _onstate_showgrid)
  f:SetAttribute("_onstate-unitexists", _onstate_unitexists)
  f:SetAttribute("_onclick",            _onclick)

  -- secure handler CallMethod()s
  f.UpdateShowGrid    = function() self:UpdateShowGrid() end
  f.StateRefresh      = function() self:RefreshControls() end
  f.UpdateHiddenLabel = function(f,hidden) self:SetLabelSubtext(hidden and L["Hidden"]) end

  -- Override the default frame accessor to provide strict read-only access
  function self:GetFrame()
    return f
  end

  self:ApplyAnchor()
  self:SetConfigMode(ReAction:GetConfigMode())
  self:SetKeybindMode(ReAction:GetKeybindMode())

  ReAction.RegisterCallback(self, "OnConfigModeChanged")
  KB.RegisterCallback(self, "LIBKEYBOUND_ENABLED")
  KB.RegisterCallback(self, "LIBKEYBOUND_DISABLED")
  KB.RegisterCallback(self, "LIBKEYBOUND_MODE_COLOR_CHANGED","LIBKEYBOUND_ENABLED")

  return self
end

function Bar:Destroy()
  local f = self:GetFrame()
  f:UnregisterAllEvents()
  self:ShowControls(false)
  ReAction.UnregisterAllCallbacks(self)
  KB.UnregisterAllCallbacks(self)
  ReAction.gridProxy:RemoveFrame(f)
  f:SetParent(UIParent)
  f:ClearAllPoints()
  f:Hide()
end

--
-- Events
--

function Bar:OnConfigModeChanged(event, mode)
  self:SetConfigMode(mode)
end

function Bar:LIBKEYBOUND_ENABLED(evt)
  self:SetKeybindMode(true)
end

function Bar:LIBKEYBOUND_DISABLED(evt)
  self:SetKeybindMode(false)
end

--
-- Accessors
--

function Bar:GetName()
  return self.name
end

-- only ReAction:RenameBar() should call this function. Calling from any other
-- context will desync the bar list in the ReAction class.
function Bar:SetName(name)
  self.name = name
  if self.overlay then
    self.overlay:SetLabel(self.name)
  end
end

function Bar:GetFrame()
  -- this method is included for documentation purposes. It is overridden
  -- for each object in the :New() method.
  error("Invalid Bar object: used without initialization")
end

function Bar:GetConfig()
  return self.config
end

function Bar:GetAnchor()
  local c = self.config
  return (c.point or "CENTER"), 
         (c.anchor or self:GetFrame():GetParent():GetName()), 
         (c.relpoint or c.point or "CENTER"), 
         (c.x or 0), 
         (c.y or 0)
end

function Bar:SetAnchor(point, frame, relativePoint, x, y)
  local c = self.config
  c.point = point or c.point
  c.anchor = frame or c.anchor
  c.relpoint = relativePoint or c.relpoint
  c.x = x or c.x
  c.y = y or c.y
  self:ApplyAnchor()
  ReAction:RefreshBar(self)
end

function Bar:GetSize()
  local f = self:GetFrame()
  return f:GetWidth(), f:GetHeight()
end

function Bar:SetSize(w,h)
  local f = self:GetFrame()
  self.config.width = w
  self.config.height = h
  f:SetWidth(w)
  f:SetHeight(h)
end

function Bar:GetButtonSize()
  local w = self.config.btnWidth or 32
  local h = self.config.btnHeight or 32
  -- TODO: get from modules?
  return w,h
end

function Bar:SetButtonSize(w,h)
  if w > 0 and h > 0 then
    self.config.btnWidth = w
    self.config.btnHeight = h
  end
  ReAction:RefreshBar(self)
end

function Bar:GetNumButtons()
  local r,c = self:GetButtonGrid()
  return r*c
end

function Bar:GetButtonGrid()
  local cfg = self.config
  local r = cfg.btnRows or 1
  local c = cfg.btnColumns or 1
  local s = cfg.spacing or 4
  return r,c,s
end

function Bar:SetButtonGrid(r,c,s)
  if r > 0 and c > 0 and s > 0 then
    local cfg = self.config
    cfg.btnRows = r
    cfg.btnColumns = c
    cfg.spacing = s
  end
  ReAction:RefreshBar(self)
end

function Bar:GetAlpha()
  return self.config.alpha or 1.0
end

function Bar:SetAlpha(value)
  self.config.alpha = value
  self:GetFrame():SetAlpha(value or 1.0)
  self:UpdateDefaultStateAlpha()
  ReAction:RefreshBar(self)
end

function Bar:IterateButtons()
  -- iterator returns button, idx and does NOT iterate in index order
  return pairs(self.buttons)
end

--
-- Methods
--

function Bar:SetConfigMode(mode)
  self:SetSecureData("showAll",mode)
  self:ShowControls(mode)
  for b in self:IterateButtons() do
    b:ShowGridTemp(mode)
    b:UpdateActionIDLabel(mode)
  end
end

function Bar:SetKeybindMode(mode)
  self:SetSecureData("showAll",mode)
  for b in self:IterateButtons() do
    b:SetKeybindMode(mode)
  end
end

function Bar:ApplyAnchor()
  local f = self:GetFrame()
  local c = self.config
  local p = c.point

  f:SetWidth(c.width)
  f:SetHeight(c.height)
  f:ClearAllPoints()
  
  if p then
    local a = f:GetParent()
    if c.anchor then
      local bar = ReAction:GetBar(c.anchor)
      if bar then
        a = bar:GetFrame()
      else
        a = _G[c.anchor]
      end
    end
    local fr = a or f:GetParent()
    f:SetPoint(p, a or f:GetParent(), c.relpoint, c.x or 0, c.y or 0)
  else
    f:SetPoint("CENTER")
  end

  self:UpdateDefaultStateAnchor()
end

function Bar:ClipNButtons( n )
  local cfg = self.config
  local r = cfg.btnRows or 1
  local c = cfg.btnColumns or 1

  cfg.btnRows = ceil(n/c)
  cfg.btnColumns = min(n,c)
end

function Bar:AddButton(idx, button)
  local f = self:GetFrame()

  -- store in a weak reverse-index array
  self.buttons[button] = idx

  -- Store a properly wrapped reference to the child frame as an attribute 
  -- (accessible via "frameref-btn#")
  f:SetFrameRef(format("btn%d",idx), button:GetFrame())
end

function Bar:RemoveButton(button)
  local idx = self.buttons[button]
  if idx then
    self:GetFrame():SetAttribute(format("frameref-btn%d",idx),nil)
    self.buttons[button] = nil
  end
end

function Bar:PlaceButton(button, baseW, baseH)
  local idx = self.buttons[button]
  if idx then 
    local r, c, s = self:GetButtonGrid()
    local bh, bw = self:GetButtonSize()
    local row, col = floor((idx-1)/c), fmod((idx-1),c) -- zero-based
    local x, y = col*bw + (col+0.5)*s, -(row*bh + (row+0.5)*s)
    local scale = bw/baseW
    local b = button:GetFrame()

    b:ClearAllPoints()
    b:SetPoint("TOPLEFT",x/scale,y/scale)
    b:SetScale(scale)
  end
end

function Bar:SkinButton()
  -- does nothing by default
end

function Bar:UpdateShowGrid()
  for button in self:IterateButtons() do
    button:UpdateShowGrid()
  end
end

function Bar:ShowControls(show)
  if show then
    if not self.overlay then
      self.overlay = Bar.Overlay:New(self) -- see Overlay.lua
    end
    self.overlay:Show()
    self:RefreshSecureState()
  elseif self.overlay then
    self.overlay:Hide()
  end
end

function Bar:RefreshControls()
  if self.overlay and self.overlay:IsShown() then
    self.overlay:RefreshControls()
  end
end

function Bar:SetLabelSubtext(text)
  if self.overlay then 
    self.overlay:SetLabelSubtext(text) 
  end
end

--
-- Secure state functions
--

function Bar:GetSecureState()
  local env = GetManagedEnvironment(self:GetFrame())
  return env and env.state
end

function Bar:GetStateProperty(state, propname)
  -- override in modules/State.lua for now
end

function Bar:SetStateProperty(state, propname, value)
  -- override in modules/State.lua for now
end

function Bar:RefreshSecureState()
  self:GetFrame():Execute(_reaction_refresh)
end

-- usage: SetSecureData(globalname, [tblkey1, tblkey2, ...], value)
function Bar:SetSecureData( ... )
  local n = select('#',...)
  if n < 2 then
    error("ReAction.Bar:SetSecureData() requires at least 2 arguments")
  end
  local f = self:GetFrame()
  f:SetAttribute("data-depth",n-1)
  f:SetAttribute("data-value",select(n,...))
  for i = 1, n-1 do
    local key = select(i,...)
    if key == nil then
      error("ReAction.Bar:SetSecureData() - nil table key in argument list (#"..i..")")
    end
    f:SetAttribute("data-key-"..i, key)
  end
  f:Execute(
    [[
      local n = self:GetAttribute("data-depth")
      if n > 0 then
        local value = self:GetAttribute("data-value")
        local t = _G
        for i = 1, n do
          local key = self:GetAttribute("data-key-"..i)
          if not key then return end
          if not t[key] then
            t[key] = newtable()
          end
          if i == n then
            t[key] = value
          else
            t = t[key]
          end
        end
      end
    ]])
  self:RefreshSecureState()
end

function Bar:SetSecureStateData( state, key, value )
  self:SetSecureData("settings",state,key,value)
end

-- sets a snippet to be run as an extension to _onstate-reaction
function Bar:SetSecureStateExtension( id, snippet )
  if id == nil then
    error("ReAction.Bar:SetSecureStateExtension() requires an id")
  end
  local f = self:GetFrame()
  f:SetAttribute("input-secure-ext-id",id)
  f:SetAttribute("secure-ext-"..id,snippet)
  f:Execute(
    [[
      local id = self:GetAttribute("input-secure-ext-id")
      if id then
        extensions[id] = self:GetAttribute("secure-ext-"..id) or nil
      end
    ]])
  self:RefreshSecureState()
end

function Bar:SetFrameRef( name, refFrame )
  if refFrame then
    local _, explicit = refFrame:IsProtected()
    if not explicit then
      refFrame = nil
    end
  end
  if refFrame then
    self:GetFrame():SetFrameRef(name,refFrame)
  else
    self:GetFrame():SetAttribute("frameref-"..name,nil)
  end
end

function Bar:SetStateDriver( rule )
  if rule then
    RegisterStateDriver(self:GetFrame(),"reaction",rule)
  elseif self.statedriver then
    UnregisterStateDriver(self:GetFrame(),"reaction")
  end
  self.statedriver = rule
  self:RefreshSecureState()
end

-- pass unit=nil to set up the unit elsewhere, if you want something more complex
function Bar:RegisterUnitWatch( unit, enable )
  local f = self:GetFrame()
  if unit then
    f:SetAttribute("unit",unit)
  end
  if enable then
    RegisterUnitWatch(self:GetFrame(),true)
  elseif self.unitwatch then
    UnregisterUnitWatch(self:GetFrame())
  end
  self.unitwatch = enable
  self:RefreshSecureState()
end

-- set a keybind to push a value into "state-reaction" attribute
function Bar:SetStateKeybind( key, state )
  local f = self:GetFrame()
  local binds = self.statebinds
  if not binds then
    binds = { }
    self.statebinds = binds
  end

  -- clear the old binding, if any
  if binds[state] then
    SetOverrideBinding(f, false, binds[state], nil)
  end

  if key then
    SetOverrideBinding(f, false, key, state, nil) -- state name is virtual mouse button
  end
  binds[state] = key
end

function Bar:GetStateKeybind( state )
  if self.statebinds and state then
    return self.statebinds[state]
  end
end

function Bar:UpdateDefaultStateAnchor()
  local point, frame, relPoint, x, y = self:GetAnchor()
  local f = self:GetFrame()
  f:SetAttribute("defaultAnchor-point",point)
  f:SetAttribute("defaultAnchor-relPoint",relPoint)
  f:SetAttribute("defaultAnchor-x",x)
  f:SetAttribute("defaultAnchor-y",y)
  self:SetFrameRef("defaultAnchor",_G[frame or "UIParent"])
  f:Execute([[
    for _, k in pairs(anchorKeys) do
      defaultAnchor[k] = self:GetAttribute("defaultAnchor-"..k)
    end
    defaultAnchor.frame = self:GetAttribute("frameref-defaultAnchor")
  ]])
end

function Bar:UpdateDefaultStateAlpha()
  local f = self:GetFrame()
  f:SetAttribute("defaultAlpha",self:GetAlpha())
  f:Execute([[
    defaultAlpha = self:GetAttribute("defaultAlpha")
  ]])
end