view Overlay.lua @ 98:a44173c7a82c

- added (Hidden) to label when bar is being shown temporarily for config - added snap-indicator (lock) icon. Displays when not dragging, if anchored, as well. - changed bar color when anchored
author Flick <flickerstreak@gmail.com>
date Fri, 24 Oct 2008 18:14:07 +0000
parents 567a885cdfad
children f200bcb193d6
line wrap: on
line source
local ReAction         = ReAction
local L                = ReAction.L
local CreateFrame      = CreateFrame
local InCombatLockdown = InCombatLockdown
local floor            = math.floor
local min              = math.min
local format           = string.format
local GameTooltip      = GameTooltip
local Bar              = ReAction.Bar
local GetSize          = Bar.GetSize
local GetButtonSize    = Bar.GetButtonSize
local GetButtonGrid    = Bar.GetButtonGrid
local SetSize          = Bar.SetSize
local SetAnchor        = Bar.SetAnchor
local SetButtonSize    = Bar.SetButtonSize
local SetButtonGrid    = Bar.SetButtonGrid
local ApplyAnchor      = Bar.ApplyAnchor

local KB = LibStub("LibKeyBound-1.0")

ReAction:UpdateRevision("$Revision$")

--
-- Bar config overlay
--

local function GetNormalTextColor()
  return 1.0, 1.0, 1.0, 1.0
end

local function GetAnchoredTextColor()
  return 1.0, 1.0, 1.0, 1.0
end

local function GetNormalBgColor()
  return 0.7, 0.7, 1.0, 0.3
end

local function GetAnchoredBgColor()
  return 0.9, 0.2, 0.7, 0.3
end

local function StoreSize(bar)
  local f = bar:GetFrame()
  SetSize( bar, f:GetWidth(), f:GetHeight() )
end

local function StoreExtents(bar)
  local f = bar:GetFrame()
  local p, fr, rp, x, y = f:GetPoint(1)
  fr = fr or UIParent
  SetAnchor( bar, p, fr, rp, x, y )
  SetSize( bar, f:GetWidth(), f:GetHeight() )
end

local function RecomputeButtonSize(bar)
  local w, h = GetSize(bar)
  local bw, bh = GetButtonSize(bar)
  local r, c, s = GetButtonGrid(bar)

  local scaleW = (floor(w/c) - s) / bw
  local scaleH = (floor(h/r) - s) / bh
  local scale = min(scaleW, scaleH)

  SetButtonSize(bar, scale * bw, scale * bh, s)
end

local function RecomputeButtonSpacing(bar)
  local w, h = GetSize(bar)
  local bw, bh = GetButtonSize(bar)
  local r, c, s = GetButtonGrid(bar)

  SetButtonGrid(bar,r,c,min(floor(w/c) - bw, floor(h/r) - bh))
end

local function RecomputeGrid(bar)
  local w, h = GetSize(bar)
  local bw, bh = GetButtonSize(bar)
  local r, c, s = GetButtonGrid(bar)

  SetButtonGrid(bar, floor(h/(bh+s)), floor(w/(bw+s)), s)
end

local function ClampToButtons(bar)
  local bw, bh = GetButtonSize(bar)
  local r, c, s = GetButtonGrid(bar)
  SetSize(bar, (bw+s)*c + 1, (bh+s)*r + 1)
end

local function HideGameTooltip()
  GameTooltip:Hide()
end

local anchorInside  = { inside = true }
local anchorOutside = { outside = true }
local edges = { "BOTTOM", "TOP", "LEFT", "RIGHT" }
local oppositeEdges = {
  TOP = "BOTTOM",
  BOTTOM = "TOP",
  LEFT = "RIGHT",
  RIGHT = "LEFT"
}
local pointsOnEdge = {
  BOTTOM = { "BOTTOM", "BOTTOMLEFT",  "BOTTOMRIGHT",  },
  TOP    = { "TOP",    "TOPLEFT",     "TOPRIGHT",     },
  RIGHT  = { "RIGHT",  "BOTTOMRIGHT", "TOPRIGHT",     },
  LEFT   = { "LEFT",   "BOTTOMLEFT",  "TOPLEFT",      },
}
local edgeSelector = {
  BOTTOM = 1,  -- select x of x,y
  TOP    = 1,  -- select x of x,y
  LEFT   = 2,  -- select y of x,y
  RIGHT  = 2,  -- select y of x,y  
}
local snapPoints = {
  [anchorOutside] = {
    BOTTOMLEFT  = {"BOTTOMRIGHT","TOPLEFT","TOPRIGHT"},
    BOTTOM      = {"TOP"},
    BOTTOMRIGHT = {"BOTTOMLEFT","TOPRIGHT","TOPLEFT"},
    RIGHT       = {"LEFT"},
    TOPRIGHT    = {"TOPLEFT","BOTTOMRIGHT","BOTTOMLEFT"},
    TOP         = {"BOTTOM"},
    TOPLEFT     = {"TOPRIGHT","BOTTOMLEFT","BOTTOMRIGHT"},
    LEFT        = {"RIGHT"},
    CENTER      = {"CENTER"}
  },
  [anchorInside] = {
    BOTTOMLEFT  = {"BOTTOMLEFT"},
    BOTTOM      = {"BOTTOM"},
    BOTTOMRIGHT = {"BOTTOMRIGHT"},
    RIGHT       = {"RIGHT"},
    TOPRIGHT    = {"TOPRIGHT"},
    TOP         = {"TOP"},
    TOPLEFT     = {"TOPLEFT"},
    LEFT        = {"LEFT"},
    CENTER      = {"CENTER"}
  }
}
local insidePointOffsetFuncs = {
  BOTTOMLEFT  = function(x, y) return x, y end,
  BOTTOM      = function(x, y) return 0, y end,
  BOTTOMRIGHT = function(x, y) return -x, y end,
  RIGHT       = function(x, y) return -x, 0 end,
  TOPRIGHT    = function(x, y) return -x, -y end,
  TOP         = function(x, y) return 0, -y end,
  TOPLEFT     = function(x, y) return x, -y end,
  LEFT        = function(x, y) return x, 0 end,
  CENTER      = function(x, y) return x, y end,
}
local pointCoordFuncs = {
  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,
  CENTER      = function(f) return f:GetCenter() end,
}
local edgeBoundsFuncs = {
  BOTTOM = function(f) return f:GetLeft(), f:GetRight() end,
  LEFT   = function(f) return f:GetBottom(), f:GetTop() end
}
edgeBoundsFuncs.TOP   = edgeBoundsFuncs.BOTTOM
edgeBoundsFuncs.RIGHT = edgeBoundsFuncs.LEFT


-- Returns absolute coordinates x,y of the named point 'p' of frame 'f'
local function GetPointCoords( f, p )
  local x, y = pointCoordFuncs[p](f)
  if not(x and y) then
    local cx, cy = f:GetCenter()
    x = x or cx
    y = y or cy
  end
  return x, y
end


-- Returns true if frame 'f1' can be anchored to frame 'f2'
local function CheckAnchorable( f1, f2 )
  -- can't anchor a frame to itself or to nil
  if f1 == f2 or f2 == nil then
    return false
  end
  
  -- can always anchor to UIParent
  if f2 == UIParent then
    return true
  end
  
  -- also can't do circular anchoring of frames 
  -- walk the anchor chain, which generally shouldn't be that expensive
  -- (who nests draggables that deep anyway?)
  for i = 1, f2:GetNumPoints() do
    local _, f = f2:GetPoint(i)
    if not f then f = f2:GetParent() end
    return CheckAnchorable(f1,f)
  end
  
  return true
end

-- Returns true if frames f1 and f2 specified edges overlap
local function CheckEdgeOverlap( f1, f2, e )
  local l1, u1 = edgeBoundsFuncs[e](f1)
  local l2, u2 = edgeBoundsFuncs[e](f2)
  return l1 <= l2 and l2 <= u1 or l2 <= l1 and l1 <= u2
end

-- Returns true if point p1 on frame f1 overlaps edge e2 on frame f2
local function CheckPointEdgeOverlap( f1, p1, f2, e2 )
  local l, u = edgeBoundsFuncs[e2](f2)
  local x, y = GetPointCoords(f1,p1)
  x = select(edgeSelector[e2], x, y)
  return l <= x and x <= u
end

-- Returns the distance between corresponding edges. It is 
-- assumed that the passed in edges e1 and e2 are the same or opposites
local function GetEdgeDistance( f1, f2, e1, e2 )
  local x1, y1 = pointCoordFuncs[e1](f1)
  local x2, y2 = pointCoordFuncs[e2](f2)
  return math.abs((x1 or y1) - (x2 or y2))
end

local globalSnapTargets = { [UIParent] = anchorInside }

local function GetClosestFrameEdge(f1,f2,a)
  local dist, edge, opp
  if f2:IsVisible() and CheckAnchorable(f1,f2) then
    for _, e in pairs(edges) do
      local o = a.inside and e or oppositeEdges[e]
      if CheckEdgeOverlap(f1,f2,e) then
        local d = GetEdgeDistance(f1, f2, e, o)
        if not dist or (d < dist) then
          dist, edge, opp = d, e, o
        end
      end
    end
  end
  return dist, edge, opp
end

local function GetClosestVisibleEdge( f )
  local r, o, e1, e2
  local a = anchorOutside
  for _, b in ReAction:IterateBars() do
    local d, e, opp = GetClosestFrameEdge(f,b:GetFrame(),a)
    if d and (not r or d < r) then
      r, o, e1, e2 = d, b:GetFrame(), e, opp
    end
  end
  for f2, a2 in pairs(globalSnapTargets) do
    local d, e, opp = GetClosestFrameEdge(f,f2,a2)
    if d and (not r or d < r) then
      r, o, e1, e2, a = d, f2, e, opp, a2
    end
  end
  return o, e1, e2, a
end

local function GetClosestVisiblePoint(f1)
  local f2, e1, e2, a = GetClosestVisibleEdge(f1)
  if f2 then
    local rsq, p, rp, x, y
    -- iterate pointsOnEdge in order and use < to prefer edge centers to corners
    for _, p1 in ipairs(pointsOnEdge[e1]) do
      if CheckPointEdgeOverlap(f1,p1,f2,e2) then
        for _, p2 in pairs(snapPoints[a][p1]) do
          local x1, y1 = GetPointCoords(f1,p1)
          local x2, y2 = GetPointCoords(f2,p2)
          local dx = x1 - x2
          local dy = y1 - y2
          local rsq2 = dx*dx + dy*dy
          if not rsq or rsq2 < rsq then
            rsq, p, rp, x, y = rsq2, p1, p2, dx, dy
          end
        end
      end
    end
    return f2, p, rp, x, y
  end
end

local function GetClosestPointSnapped(f1, rx, ry, xOff, yOff)
  local o, p, rp, x, y = GetClosestVisiblePoint(f1)
  local s = false

  local insideOffsetFunc = p and insidePointOffsetFuncs[p]
  local coordFunc = p and pointCoordFuncs[p]
  if not insideOffsetFunc or not coordFunc then
    return
  end
  
  local sx, sy = insidePointOffsetFuncs[p](xOff or 0, yOff or 0)
  local xx, yy = pointCoordFuncs[p](f1)
  if xx and yy then
    if math.abs(x) <= rx then
      x = sx
      s = true
    end
    if math.abs(y) <= ry then
      y = sy
      s = true
    end
  elseif xx then
    if math.abs(x) <= rx then
      x = sx
      s = true
      if math.abs(y) <= ry then
        y = sy
      end
    end
  elseif yy then
    if math.abs(y) <= ry then
      y = sy
      s = true
      if math.abs(x) <= rx then
        x = sx
      end
    end
  end

  if x == -0 then x = 0 end
  if y == -0 then y = 0 end
  
  if s then
    return o, p, rp, math.floor(x), math.floor(y)
  end
end

local function CreateSnapIndicator()
  local si = CreateFrame("Frame",nil,UIParent)
  si:SetFrameStrata("HIGH")
  si:SetHeight(16)
  si:SetWidth(16)
  local tex = si:CreateTexture()
  tex:SetAllPoints()
  tex:SetTexture("Interface\\AddOns\\ReAction\\img\\lock")
  tex:SetBlendMode("ADD")
  tex:SetDrawLayer("OVERLAY")
  return si
end

local si1 = CreateSnapIndicator()
local si2 = CreateSnapIndicator()

local function DisplaySnapIndicator( f, rx, ry, xOff, yOff )
  local o, p, rp, x, y, snap = GetClosestPointSnapped(f, rx, ry, xOff, yOff)
  if o then
    si1:ClearAllPoints()
    si2:ClearAllPoints()
    si1:SetPoint("CENTER", f, p, 0, 0)
    local xx, yy = pointCoordFuncs[rp](o)
    x = math.abs(x) <=rx and xx and 0 or x
    y = math.abs(y) <=ry and yy and 0 or y
    si2:SetPoint("CENTER", o, rp, x, y)
    si1:Show()
    si2:Show()
  else
    if si1:IsVisible() then
      si1:Hide()
      si2:Hide()
    end
  end
  return o, p
end

local function HideSnapIndicator()
  if si1:IsVisible() then
    si1:Hide()
    si2:Hide()
  end
end

local function UpdateLabelString(bar)
  local label = bar.controlLabelString
  if label then
    local name = bar.labelName
    if name and bar.labelSubtext then
      name = format("%s (%s)", name, bar.labelSubtext)
    end
    label:SetText(name or "")
  end
end

local function CreateControls(bar)
  local f = bar:GetFrame()

  f:SetMovable(true)
  f:SetResizable(true)

  local overlay = CreateFrame("Button", nil, f)
  overlay:EnableMouse(true)
  overlay:SetFrameLevel(3) -- set it above the buttons
  overlay:SetPoint("TOPLEFT", -4, 4)
  overlay:SetPoint("BOTTOMRIGHT", 4, -4)
  overlay:SetBackdrop({
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 16,
    insets = { left = 0, right = 0, top = 0, bottom = 0 },
  })

  -- textures
  local bgTex = overlay:CreateTexture(nil,"BACKGROUND")
  bgTex:SetTexture(0.7,0.7,1.0,0.2)
  bgTex:SetPoint("TOPLEFT",4,-4)
  bgTex:SetPoint("BOTTOMRIGHT",-4,4)
  local hTex = overlay:CreateTexture(nil,"HIGHLIGHT")
  hTex:SetTexture(0.7,0.7,1.0,0.2)
  hTex:SetPoint("TOPLEFT",4,-4)
  hTex:SetPoint("BOTTOMRIGHT",-4,4)
  hTex:SetBlendMode("ADD")
  local aTex = overlay:CreateTexture(nil,"ARTWORK")
  aTex:SetTexture("Interface\\AddOns\\ReAction\\img\\lock")
  aTex:SetWidth(16)
  aTex:SetHeight(16)
  aTex:Hide()

  -- label
  local label = overlay:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
  label:SetAllPoints()
  label:SetJustifyH("CENTER")
  label:SetShadowColor(0,0,0,1)
  label:SetShadowOffset(3,-3)
  label:SetTextColor(GetNormalTextColor())
  label:SetText(bar:GetName())
  label:Show()
  bar.controlLabelString = label  -- so that bar:SetLabel() can update it

  local function UpdateAnchorDecoration()
    local point, anchor, relPoint, x, y = f:GetPoint(1)
    if point then
      local ofsx, ofsy = insidePointOffsetFuncs[point](x,y)
      if (anchor and anchor ~= UIParent) or (ofsx == 0 and ofsy == 0) then
        bgTex:SetTexture( GetAnchoredBgColor() )
        hTex:SetTexture( GetAnchoredBgColor() )
        label:SetTextColor( GetAnchoredTextColor() )
        aTex:ClearAllPoints()
        aTex:SetPoint(point)
        aTex:Show()
        return
      end
    end
    bgTex:SetTexture( GetNormalBgColor() )
    hTex:SetTexture( GetNormalBgColor() )
    label:SetTextColor( GetNormalTextColor() )
    aTex:Hide()
  end

  local function StopResize()
    f:StopMovingOrSizing()
    f.isMoving = false
    f:SetScript("OnUpdate",nil)
    StoreSize(bar)
    ClampToButtons(bar)
    --ApplyAnchor(bar)
    ReAction:RefreshOptions()
  end

  -- edge drag handles
  for _, point in pairs({"LEFT","TOP","RIGHT","BOTTOM"}) do
    local edge = CreateFrame("Frame",nil,overlay)
    edge:EnableMouse(true)
    edge:SetWidth(8)
    edge:SetHeight(8)
    if point == "TOP" or point == "BOTTOM" then
      edge:SetPoint(point.."LEFT")
      edge:SetPoint(point.."RIGHT")
    else
      edge:SetPoint("TOP"..point)
      edge:SetPoint("BOTTOM"..point)
    end
    local tex = edge:CreateTexture(nil,"HIGHLIGHT")
    tex:SetTexture(1.0,0.82,0,0.7)
    tex:SetBlendMode("ADD")
    tex:SetAllPoints()
    edge:RegisterForDrag("LeftButton")
    edge:SetScript("OnMouseDown",
      function()
        local bw, bh = GetButtonSize(bar)
        local r, c, s = GetButtonGrid(bar)
        f:SetMinResize( bw+s+1, bh+s+1 )
        f:StartSizing(point)
        f:SetScript("OnUpdate", 
          function()
            RecomputeGrid(bar)
          end
        )
      end
    )
    edge:SetScript("OnMouseUp", StopResize)
    edge:SetScript("OnEnter",
      function()
        GameTooltip:SetOwner(f, "ANCHOR_"..point)
        GameTooltip:AddLine(L["Drag to add/remove buttons"])
        GameTooltip:Show()
      end
    )
    edge:SetScript("OnLeave", HideGameTooltip)
    edge:Show()
  end

  -- corner drag handles, nested in an anonymous frame so that they are on top
  local foo = CreateFrame("Frame",nil,overlay)
  foo:SetAllPoints(true)
  for _, point in pairs({"BOTTOMLEFT","TOPLEFT","BOTTOMRIGHT","TOPRIGHT"}) do
    local corner = CreateFrame("Frame",nil,foo)
    corner:EnableMouse(true)
    corner:SetWidth(12)
    corner:SetHeight(12)
    corner:SetPoint(point)
    local tex = corner:CreateTexture(nil,"HIGHLIGHT")
    tex:SetTexture(1.0,0.82,0,0.7)
    tex:SetBlendMode("ADD")
    tex:SetAllPoints()
    corner:RegisterForDrag("LeftButton","RightButton")
    local function UpdateTooltip()
      local size, size2 = bar:GetButtonSize()
      local rows, cols, spacing = bar:GetButtonGrid()
      size = (size == size2) and tostring(size) or format("%dx%d",size,size2)
      GameTooltipTextRight4:SetText(size)
      GameTooltipTextRight5:SetText(tostring(spacing))
    end
    corner:SetScript("OnMouseDown",
      function(_,btn)
        local bw, bh = GetButtonSize(bar)
        local r, c, s = GetButtonGrid(bar)
        if btn == "LeftButton" then -- button resize
          f:SetMinResize( (s+12)*c+1, (s+12)*r+1 )
          f:SetScript("OnUpdate", 
            function()
              RecomputeButtonSize(bar)
              UpdateTooltip()
            end
          )
        elseif btn == "RightButton" then -- spacing resize
          f:SetMinResize( bw*c, bh*r )
          f:SetScript("OnUpdate", 
            function()
              RecomputeButtonSpacing(bar)
              UpdateTooltip()
            end
          )
        end
        f:StartSizing(point)
      end
    )
    corner:SetScript("OnMouseUp",StopResize)
    corner:SetScript("OnEnter",
      function()
        GameTooltip:SetOwner(f, "ANCHOR_"..point)
        GameTooltip:AddLine(L["Drag to resize buttons"])
        GameTooltip:AddLine(L["Right-click-drag"])
        GameTooltip:AddLine(L["to change spacing"])
        local size, size2 = bar:GetButtonSize()
        local rows, cols, spacing = bar:GetButtonGrid()
        size = (size == size2) and tostring(size) or format("%dx%d",size,size2)
        GameTooltip:AddDoubleLine(L["Size:"], size)
        GameTooltip:AddDoubleLine(L["Spacing:"], tostring(spacing))
        GameTooltip:Show()
      end
    )
    corner:SetScript("OnLeave", 
      function()
        GameTooltip:Hide()
        f:SetScript("OnUpdate",nil)
      end
    )

  end

  overlay:RegisterForDrag("LeftButton")
  overlay:RegisterForClicks("RightButtonUp")
  
  overlay:SetScript("OnDragStart",
    function()
      f:StartMoving()
      f.isMoving = true
      local w,h = bar:GetButtonSize()
      f:ClearAllPoints()
      UpdateAnchorDecoration()
      f:SetScript("OnUpdate", function()
          if IsShiftKeyDown() then
            local f, p = DisplaySnapIndicator(f,w,h)
          else
            HideSnapIndicator()
          end
        end)
    end
  )

  local function UpdateDragTooltip()
    GameTooltip:SetOwner(f, "ANCHOR_TOPRIGHT")
    GameTooltip:AddLine(bar.name)
    GameTooltip:AddLine(L["Drag to move"])
    GameTooltip:AddLine(("|cff00ff00%s|r %s"):format(L["Shift-drag"],L["to anchor to nearby frames"]))
    GameTooltip:AddLine(("|cff00cccc%s|r %s"):format(L["Right-click"],L["for options"]))
    local _, a = bar:GetAnchor()
    if a and a ~= "UIParent" then
      GameTooltip:AddLine(L["Currently anchored to <%s>"]:format(a))
    end
    GameTooltip:Show()
  end

  overlay:SetScript("OnDragStop",
    function()
      f:StopMovingOrSizing()
      f.isMoving = false
      f:SetScript("OnUpdate",nil)

      if IsShiftKeyDown() then
        local w, h = bar:GetButtonSize()
        local a, p, rp, x, y = GetClosestPointSnapped(f,w,h)
        if a then
          f:ClearAllPoints()
          f:SetPoint(p,a,rp,x,y)
        end
        HideSnapIndicator()
      end

      StoreExtents(bar)
      ReAction:RefreshOptions()
      UpdateDragTooltip()
      UpdateAnchorDecoration()
    end
  )

  overlay:SetScript("OnEnter",
    function()
      UpdateDragTooltip()
    end
  )

  overlay:SetScript("OnLeave", HideGameTooltip)

  overlay:SetScript("OnClick",
    function()
      ReAction:ShowEditor(bar)
    end
  )

  function overlay:LIBKEYBOUND_ENABLED(evt)
    self:SetFrameLevel(1)
  end

  function overlay:LIBKEYBOUND_DISABLED(evt)
    self:SetFrameLevel(3)
  end

  KB.RegisterCallback(overlay,"LIBKEYBOUND_ENABLED")
  KB.RegisterCallback(overlay,"LIBKEYBOUND_DISABLED")

  if ReAction:GetKeybindMode() then
    overlay:SetFrameLevel(1)
  end

  bar:SetLabel(bar:GetName())
  UpdateLabelString(bar)
  UpdateAnchorDecoration()

  return overlay
end


-- export methods to the Bar prototype

function Bar:ShowControls(show)
  local f = self.controlFrame
  if show then
    if not f then
      f = CreateControls(self)
      self.controlFrame = f
    end
    f:Show()
  elseif f then
    f:Hide()
  end
end

function Bar:SetLabel(name)
  self.labelName = name
  UpdateLabelString(self)
end

function Bar:SetLabelSubtext(text)
  self.labelSubtext = text
  UpdateLabelString(self)
end