view classes/ReBar.lua @ 4:dfd829db3ad0

(none)
author Flick <flickerstreak@gmail.com>
date Tue, 20 Mar 2007 21:19:34 +0000
parents ReBar.lua@8e0ff8ae4c08
children f920db5fc6b1
line wrap: on
line source

-- private constants
local insideFrame  = 1
local outsideFrame = 2

local pointFindTable = {
  BOTTOMLEFT  = function(f) return f:GetLeft(),  f:GetBottom() end,
  BOTTOM      = function(f) return nil,          f:GetBottom() end,
  BOTTOMRIGHT = function(f) return f:GetRight(), f:GetBottom() end,
  RIGHT       = function(f) return f:GetRight(), nil end,
  TOPRIGHT    = function(f) return f:GetRight(), f:GetTop() end,
  TOP         = function(f) return nil,          f:GetTop() end,
  TOPLEFT     = function(f) return f:GetLeft(),  f:GetTop() end,
  LEFT        = function(f) return f:GetLeft(),  nil end,
}

local oppositePointTable = {
  BOTTOMLEFT  = "TOPRIGHT",
  BOTTOM      = "TOP",
  BOTTOMRIGHT = "TOPLEFT",
  RIGHT       = "LEFT",
  TOPRIGHT    = "BOTTOMLEFT",
  TOP         = "BOTTOM",
  TOPLEFT     = "BOTTOMRIGHT",
  LEFT        = "RIGHT"
}

local anchoredLabelColor = { r =0.6, g = 0.2, b = 1.0 }
local nonAnchoredLabelColor = { r = 1.0, g = 0.82, b = 0.0 }

-- private variables
local stickyTargets = {
  [UIParent] = insideFrame,
  [WorldFrame] = insideFrame
}

-- ReBar is an Ace 2 class prototype object.
ReBar = AceLibrary("AceOO-2.0").Class("AceEvent-2.0")

local dewdrop = AceLibrary("Dewdrop-2.0")

function ReBar.prototype:init( config, id )
  ReBar.super.prototype.init(self)

  local buttonClass = config and config.btnConfig and config.btnConfig.type and getglobal(config.btnConfig.type)
  self.config  = config
  self.barID   = id
  self.class   = { button = buttonClass }
  self.buttons = { }

  -- create the bar and control widgets
  self.barFrame     = CreateFrame("Frame", "ReBar_"..self.barID, UIParent, "ReBarTemplate")
  self.controlFrame = getglobal(self.barFrame:GetName().."Controls")
  self.controlFrame.reBar = self
  self.barFrame:SetClampedToScreen(true)

  -- set the text label on the control widget
  self.labelString = getglobal(self.controlFrame:GetName().."LabelString")
  self.labelString:SetText(id)

  -- initialize the bar layout
  self:ApplySize()
  self:ApplyAnchor()
  self:LayoutButtons()
  self:ApplyVisibility()

  -- add bar to stickyTargets list
  stickyTargets[self.barFrame] = outsideFrame
  
  -- initialize dewdrop menu
	dewdrop:Register(self.controlFrame, 'children', function()
	    dewdrop:FeedAceOptionsTable(ReActionGlobalMenuOptions)
	    dewdrop:FeedAceOptionsTable(GenerateReActionBarOptions(self))
	    dewdrop:FeedAceOptionsTable(GenerateReActionButtonOptions(self))
	  end,
	  'cursorX', true, 
	  'cursorY', true
	)
end


function ReBar.prototype:Destroy()
  if self.barFrame == dewdrop:GetOpenedParent() then
    dewdrop:Close()
    dewdrop:Unregister(self.barFrame)
  end

  self:HideControls()
  self.barFrame:Hide()
  self.barFrame:ClearAllPoints()
  self.barFrame:SetParent(nil)
  self.barFrame:SetPoint("BOTTOMRIGHT", UIParent, "TOPLEFT", 0, 0)
  
  -- need to keep around self.config for dewdrop menus in the process of deleting self 

  while #self.buttons > 0 do
    self.class.button:release(table.remove(self.buttons))
  end

  -- remove from sticky targets table
  stickyTargets[self.barFrame] = nil
  
  -- remove from global table
  -- for some reason after a destroy/recreate the globals still reference
  -- the old frames
  setglobal(self.barFrame:GetName(), nil)
  setglobal(self.barFrame:GetName().."Controls", nil)
  setglobal(self.controlFrame:GetName().."LabelString", nil)
end


-- show/hide the control frame
function ReBar.prototype:ShowControls()
  self.controlFrame:Show()
  for _, b in ipairs(self.buttons) do
    b:BarUnlocked()
  end
end

function ReBar.prototype:HideControls()
  local b = self.barFrame
  if b.isMoving or b.resizing then
    b:StopMovingOrSizing()
    b:SetScript("OnUpdate",nil)
  end
  -- close any dewdrop menu owned by us
  if self.barFrame == dewdrop:GetOpenedParent() then
    dewdrop:Close()
  end
  for _, b in ipairs(self.buttons) do
    b:BarLocked()
  end
  self.controlFrame:Hide()
end




-- accessors
function ReBar.prototype:GetVisibility()
  return self.config.visible
end

function ReBar.prototype:ToggleVisibility()
  self.config.visible = not self.config.visible
  self:ApplyVisibility()
end

function ReBar.prototype:GetOpacity()
  return self.config.opacity or 100
end

function ReBar.prototype:SetOpacity( o )
  self.config.opacity = tonumber(o)
  self:ApplyVisibility()
  return self.config.opacity
end


-- layout methods
function ReBar.prototype:ApplySize()
  local buttonSz = self.config.size or 36
  local spacing  = self.config.spacing or 4
  local rows     = self.config.rows or 1
  local columns  = self.config.columns or 12
  local w = buttonSz * columns + spacing * (columns + 1)
  local h = buttonSz * rows + spacing * (rows + 1)
  local f = self.barFrame

  -- +1: avoid resizing oddities caused by fractional UI scale setting
  f:SetMinResize(buttonSz + spacing*2 + 1, buttonSz + spacing*2 + 1)
  f:SetWidth(w + 1)
  f:SetHeight(h + 1)
end

function ReBar.prototype:ApplyAnchor()
  local a = self.config.anchor
  local f = self.barFrame
  if a then
    f:ClearAllPoints()
    f:SetPoint(a.point,getglobal(a.to),a.relPoint,a.x,a.y)
    local color = anchoredLabelColor
    if a.to == "UIParent" or a.to == "WorldFrame" then
      color = nonAnchoredLabelColor
    end
    self.labelString:SetTextColor(color.r, color.g, color.b)
  end
end

function ReBar.prototype:ApplyVisibility()
  local v = self.config.visibility
  if type(v) == "table" then
    if v.class then
      local _, c = UnitClass("player")
      v = v.class[c]
    end
  elseif type(v) == "string" then
    local value = getglobal(v)
    v = value
  end
  
  if self.config.opacity then
    self.barFrame:SetAlpha(self.config.opacity / 100)
  end

  if v then
    self.barFrame:Show()
  else
    self.barFrame:Hide()
  end
end

function ReBar.prototype:LayoutButtons()
  local r = self.config.rows
  local c = self.config.columns
  local n = r * c
  local sp = self.config.spacing
  local sz = self.config.size
  local gSize = sp + sz
  
  for i = 1, n do
    if self.buttons[i] == nil then
      table.insert(self.buttons, self.class.button:acquire(self.barFrame, self.config.btnConfig, i))
    end
    local b = self.buttons[i]
    if b == nil then
      break -- handling for button types that support limited numbers
    end
    b:PlaceButton("TOPLEFT", sp + gSize * math.fmod(i-1,c), - (sp + gSize * math.floor((i-1)/c)), sz)
  end

  -- b == nil, above, should always be the case if and only if i == n. ReBar never monkeys
  -- with buttons in the middle of the sequence: it always adds or removes on the array end
  while #self.buttons > n do
    self.class.button:release(table.remove(self.buttons))
  end
  
end


function ReBar.prototype:StoreAnchor(f, p, rp, x, y)
  local name = f:GetName()
  -- no point if we can't store the name or the offsets are incomplete
  if name and x and y then
    self.config.anchor = { 
      to = name,
      point = p,
      relPoint = rp or p,
      x = x,
      y = y
    }
  end
end  



-- mouse event handlers (clicking/dragging/resizing the bar)
function ReBar.prototype:BeginDrag()
  local f = self.barFrame
  f:StartMoving()
  f.isMoving = true
  f:SetScript("OnUpdate", function() self:StickyIndicatorUpdate() end)
end

function ReBar.prototype:FinishDrag()
  local f, p, rp, x, y 
  local bf = self.barFrame

  bf:StopMovingOrSizing()
  bf.isMoving = false

  bf:SetScript("OnUpdate",nil)
  if IsShiftKeyDown() then
    f, p, rp, x, y = self:GetStickyAnchor()
    ReBarStickyIndicator1:Hide()
    ReBarStickyIndicator2:Hide()
  end
  
  if f == nil then
    f = UIParent
    local _
    _, p,rp,x,y = self:GetClosestPointTo(f)
  end

  if f then
    self:StoreAnchor(f,p,rp,x,y)
    self:ApplyAnchor()
  end
end

function ReBar.prototype:BeginBarResize( sizingPoint )
  local f = self.barFrame
  f:StartSizing(sizingPoint)
  f.resizing = true
  f:SetScript("OnUpdate",function() self:ReflowButtons() end)
end

function ReBar.prototype:BeginButtonResize( sizingPoint, mouseBtn )
  local f = self.barFrame
  f:StartSizing(sizingPoint)
  f.resizing = true
  local r = self.config.rows
  local c = self.config.columns
  local s = self.config.spacing
  local sz = self.config.size
  if mouseBtn == "LeftButton" then
    f:SetMinResize(c*(12 + 2*s) +1, r*(12 + 2*s) +1)
    f:SetScript("OnUpdate",function() self:DragSizeButtons() end)
  elseif mouseBtn == "RightButton" then
    f:SetMinResize(c*sz+1, r*sz+1)
    f:SetScript("OnUpdate",function() self:DragSizeSpacing() end)
  end
end

function ReBar.prototype:FinishResize()
  local f = self.barFrame
  f:StopMovingOrSizing()
  f.resizing = false
  f:SetScript("OnUpdate",nil)
  self:ApplySize()
end




-- sticky anchoring functions
function ReBar.prototype:StickyIndicatorUpdate()
  local si1 = ReBarStickyIndicator1
  local si2 = ReBarStickyIndicator2
  if IsShiftKeyDown() then
    local f, p, rp, x, y = self:GetStickyAnchor()
    if f then
      si1:ClearAllPoints()
      si2:ClearAllPoints()
      si1:SetPoint("CENTER",self.barFrame,p,0,0)
      si2:SetPoint("CENTER",f,rp,x,y)
      si1:Show()
      si2:Show()
      return nil
    end
  end
  si1:Hide()
  si2:Hide()
  si1:ClearAllPoints()
  si2:ClearAllPoints()
end

function ReBar.prototype:CheckAnchorable(f)
  -- can't anchor to self or to a hidden frame
  if f == self.barFrame or not(f:IsShown()) then return false end

  -- also can't anchor to frames that are anchored to self
  for i = 1, f:GetNumPoints() do
    local _, f2 = f:GetPoint(i)
    if f2 == self.barFrame then return false end
  end

  return true
end


function ReBar.prototype:GetStickyAnchor()
  local snapRange = (self.config.size + self.config.spacing)
  local r2, f, p, rp, x, y = self:GetClosestAnchor()

  if f and p then
    local xx, yy = pointFindTable[p](f) 
    if r2 and r2 < (snapRange*snapRange) then
      if xx or math.abs(x) < snapRange then x = 0 end
      if yy or math.abs(y) < snapRange then y = 0 end
    elseif not(yy) and math.abs(x) < snapRange then
      x = 0
    elseif not(xx) and math.abs(y) < snapRange then
      y = 0
    else
      f = nil -- nothing in range
    end
  end
  return f, p, rp, x, y
end

function ReBar.prototype:GetClosestAnchor()
  -- choose the closest anchor point on the list of target frames
  local range2, frame, point, relPoint, offsetX, offsetY

  for f, tgtRegion in pairs(stickyTargets) do
    if self:CheckAnchorable(f) then
      local r2 ,p, rp, x, y = self:GetClosestPointTo(f,tgtRegion)
      if r2 then
        if not(range2 and range2 < r2) then
          range2, frame, point, relPoint, offsetX, offsetY = r2, f, p, rp, x, y
        end
      end
    end
  end

  return range2, frame, point, relPoint, offsetX, offsetY
end

function ReBar.prototype:GetClosestPointTo(f,inside)
  local range2, point, relPoint, offsetX, offsetY
  local pft = pointFindTable
  local cx, cy = self.barFrame:GetCenter()
  local fcx, fcy = f:GetCenter()
  local fh = f:GetHeight()
  local fw = f:GetWidth()

  -- compute whether edge bisector intersects target edge
  local dcx = math.abs(cx-fcx) < fw/2 and (cx-fcx)
  local dcy = math.abs(cy-fcy) < fh/2 and (cy-fcy)
  
  for p, func in pairs(pft) do
    local rp, x, y
    if inside == outsideFrame then
      rp = oppositePointTable[p]
      x, y = self:GetOffsetToPoint(f, func, pft[rp])
    else
      rp = p
      x, y = self:GetOffsetToPoint(f, func, func)
    end

    -- if anchoring to an edge, only anchor if the center point overlaps the other edge
    if (x or dcx) and (y or dcy) then
      local r2 = (x or 0)^2 + (y or 0)^2
      if range2 == nil or r2 < range2 then
        range2, point, relPoint, offsetX, offsetY = r2, p, rp, x or dcx, y or dcy
      end
    end
  end
  return range2, point, relPoint, offsetX, offsetY
end

function ReBar.prototype:GetOffsetToPoint(f,func,ffunc)
  local x, y = func(self.barFrame)  -- coordinates of the point on this frame
  local fx, fy = ffunc(f)  -- coordinates of the point on the target frame
  -- guarantees: if x then fx, if y then fy
  return x and (x-fx), y and (y-fy)
end






-- utility function to get the height, width, and button size attributes
function ReBar.prototype:GetLayout()
  local c = self.config
  local f = self.barFrame
  return f:GetWidth(), f:GetHeight(), c.size, c.rows, c.columns, c.spacing
end

-- add and remove buttons dynamically as the bar is resized
function ReBar.prototype:ReflowButtons()
  local w, h, sz, r, c, sp = self:GetLayout()

  self.config.rows = math.floor( (h - sp) / (sz + sp) )
  self.config.columns = math.floor( (w - sp) / (sz + sp) )

  if self.config.rows ~= r or self.config.columns ~= c then
    self:LayoutButtons()
  end
end


-- change the size of buttons as the bar is resized
function ReBar.prototype:DragSizeButtons()
  local w, h, sz, r, c, sp = self:GetLayout()

  local newSzW = math.floor((w - (c+1)*sp)/c)
  local newSzH = math.floor((h - (r+1)*sp)/r)
  
  self.config.size = math.max(12, math.min(newSzW, newSzH))

  if self.config.size ~= sz then
    self:LayoutButtons()
    self:UpdateResizeTooltip()
  end
end


-- change the spacing of buttons as the bar is resized
function ReBar.prototype:DragSizeSpacing()
  local w, h, sz, r, c, sp = self:GetLayout()

  local newSpW = math.floor((w - c*sz)/(c+1))
  local newSpH = math.floor((h - r*sz)/(r+1))

  self.config.spacing = math.max(0, math.min(newSpW, newSpH))

  if self.config.spacing ~= sp then
    self:LayoutButtons()
    self:UpdateResizeTooltip()
  end
end


-- update the drag tooltip to indicate current sizes
function ReBar.prototype:UpdateResizeTooltip()
  GameTooltipTextRight4:SetText(self.config.size)
  GameTooltipTextRight5:SetText(self.config.spacing)
  GameTooltip:Show()
end

function ReBar.prototype:ShowTooltip()
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_TOPRIGHT")
  GameTooltip:AddLine("Bar "..self.barID)
  GameTooltip:AddLine("Drag to move")
  GameTooltip:AddLine("Shift-drag for sticky mode")
  GameTooltip:AddLine("Right-click for options")
  GameTooltip:Show()
end

function ReBar.prototype:ShowButtonResizeTooltip(point)
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_"..point)
  GameTooltip:AddLine("Drag to resize buttons")
  GameTooltip:AddLine("Right-click-drag")
  GameTooltip:AddLine("to change spacing")
  GameTooltip:AddDoubleLine("Size: ", "0")
  GameTooltip:AddDoubleLine("Spacing: ", "0")
  self:UpdateResizeTooltip()
end

function ReBar.prototype:ShowBarResizeTooltip(point)
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_"..point)
  GameTooltip:AddLine("Drag to add/remove buttons")
  GameTooltip:Show()
end