Files
HailMaryGuildTools/Modules/Tracker/Frame.lua
Torsten Brendgen fc5a8aa361 initial commit
2026-04-10 21:30:31 +02:00

1232 lines
44 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- UI/TrackerFrame.lua
-- Hail Mary Guild Tools Tracker-Frame mit Pool-System
--
-- Features:
-- • Bar-Modus: UP/DOWN mit einstellbarem Spacing
-- • Icon-Modus: 4 Richtungen (UP/DOWN/LEFT/RIGHT), einstellbares Spacing
-- • Icon-Overlay: Sweep-Kreis ODER Text-Timer
-- • Text-Anchor: onIcon | above | below | left | right (kein Abschneiden / kein "...")
-- • Lock: blendet Titel und Hintergrund aus
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local LSM = LibStub("LibSharedMedia-3.0")
HMGT.TrackerFrame = {}
local TF = HMGT.TrackerFrame
local MAX_ROWS = 40
local DEFAULT_FONT = "Fonts\\FRIZQT__.TTF"
local LABEL_H = 14 -- Pixel pro Text-Zeile außerhalb des Icons
local CHARGE_TEXT_Y_OFFSET = -5
local HEADER_HEIGHT = 20
local VALID_POINTS = {
TOPLEFT = true, TOP = true, TOPRIGHT = true,
LEFT = true, CENTER = true, RIGHT = true,
BOTTOMLEFT = true, BOTTOM = true, BOTTOMRIGHT = true,
}
local function ResolveAnchorTarget(anchorTo, anchorCustom)
if HMGT.GetAnchorTargetFrame then
return HMGT:GetAnchorTargetFrame(anchorTo, anchorCustom)
end
return UIParent
end
local HEADER_ANCHOR_MAP_UP = {
TOPLEFT = { point = "BOTTOMLEFT", y = -HEADER_HEIGHT },
TOP = { point = "BOTTOM", y = -HEADER_HEIGHT },
TOPRIGHT = { point = "BOTTOMRIGHT", y = -HEADER_HEIGHT },
LEFT = { point = "BOTTOMLEFT", y = -(HEADER_HEIGHT * 0.5) },
CENTER = { point = "BOTTOM", y = -(HEADER_HEIGHT * 0.5) },
RIGHT = { point = "BOTTOMRIGHT", y = -(HEADER_HEIGHT * 0.5) },
BOTTOMLEFT = { point = "BOTTOMLEFT", y = 0 },
BOTTOM = { point = "BOTTOM", y = 0 },
BOTTOMRIGHT = { point = "BOTTOMRIGHT", y = 0 },
}
local function UsesBottomHeader(settings)
return type(settings) == "table"
and settings.showBar == true
and settings.growDirection == "UP"
end
local function ApplyTitleLayout(frame)
if not frame or not frame.titleText then
return
end
frame.titleText:ClearAllPoints()
if UsesBottomHeader(frame._settings) then
frame.titleText:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 4, 2)
else
frame.titleText:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -2)
end
end
-- ══════════════════════════════════════════════════════════════
-- FONT-HELFER
-- ══════════════════════════════════════════════════════════════
local function GetFontPath(fontName)
if not fontName or fontName == "" then return DEFAULT_FONT end
return LSM:Fetch("font", fontName) or DEFAULT_FONT
end
local function SafeSetFont(fs, path, size, outline)
-- SetFontObject (gesetzt bei Erstellung) "klebt" und überschreibt SetFont.
-- Erst nil setzen damit SetFont dauerhaft wirkt.
fs:SetFontObject(nil)
if not fs:SetFont(path or DEFAULT_FONT, size or 12, outline or "OUTLINE") then
fs:SetFont(DEFAULT_FONT, size or 12, outline or "OUTLINE")
end
end
local function Utf8Sub(text, charCount)
if type(text) ~= "string" then
return ""
end
if not charCount or charCount <= 0 then
return ""
end
if not utf8 or type(utf8.offset) ~= "function" then
return text:sub(1, charCount)
end
local cutoff = utf8.offset(text, charCount + 1)
if cutoff then
return text:sub(1, cutoff - 1)
end
return text
end
local function Utf8Len(text)
if type(text) ~= "string" then
return 0
end
if utf8 and type(utf8.len) == "function" then
local len = utf8.len(text)
if len then
return len
end
end
return #text
end
local measureFontString
local function GetIconTextBlockWidth(settings)
local iconSize = tonumber(settings and settings.iconSize) or 32
return math.max(60, math.floor(iconSize * 2.25))
end
local function GetMeasureFontString()
if not measureFontString then
measureFontString = UIParent:CreateFontString(nil, "OVERLAY")
measureFontString:SetWordWrap(false)
end
return measureFontString
end
local function TruncateTextToWidth(fontString, text, maxWidth)
if type(text) ~= "string" or text == "" or not maxWidth or maxWidth <= 0 then
return text or ""
end
local measure = GetMeasureFontString()
local fontPath, fontSize, fontFlags = fontString:GetFont()
if fontPath then
measure:SetFont(fontPath, fontSize or 12, fontFlags or "")
else
measure:SetFont(DEFAULT_FONT, 12, "OUTLINE")
end
measure:SetText(text)
if measure:GetStringWidth() <= maxWidth then
return text
end
local ellipsis = "..."
measure:SetText(ellipsis)
if measure:GetStringWidth() > maxWidth then
return ""
end
local low = 0
local high = Utf8Len(text)
while low < high do
local mid = math.ceil((low + high) / 2)
local candidate = Utf8Sub(text, mid) .. ellipsis
measure:SetText(candidate)
if measure:GetStringWidth() <= maxWidth then
low = mid
else
high = mid - 1
end
end
if low <= 0 then
return ellipsis
end
return Utf8Sub(text, low) .. ellipsis
end
local function GetVisibleContentBounds(frame)
if not frame then
return nil, nil
end
local top
local bottom
local function Consider(region)
if not region or not region.IsShown or not region:IsShown() then
return
end
local regionTop = region:GetTop()
local regionBottom = region:GetBottom()
if not regionTop or not regionBottom then
return
end
if not top or regionTop > top then
top = regionTop
end
if not bottom or regionBottom < bottom then
bottom = regionBottom
end
end
for i = 1, MAX_ROWS do
Consider(frame._barPool and frame._barPool[i] or nil)
Consider(frame._iconPool and frame._iconPool[i] or nil)
end
return top, bottom
end
local function CreateOutline(parent)
local outline = {
top = parent:CreateTexture(nil, "OVERLAY"),
bottom = parent:CreateTexture(nil, "OVERLAY"),
left = parent:CreateTexture(nil, "OVERLAY"),
right = parent:CreateTexture(nil, "OVERLAY"),
}
return outline
end
local function SetOutline(outline, target, r, g, b, a, enabled)
if not outline or not target then return end
if not enabled then
outline.top:Hide()
outline.bottom:Hide()
outline.left:Hide()
outline.right:Hide()
return
end
local rr = r or 1
local gg = g or 1
local bb = b or 1
local aa = a or 1
outline.top:SetColorTexture(rr, gg, bb, aa)
outline.bottom:SetColorTexture(rr, gg, bb, aa)
outline.left:SetColorTexture(rr, gg, bb, aa)
outline.right:SetColorTexture(rr, gg, bb, aa)
outline.top:ClearAllPoints()
outline.top:SetPoint("TOPLEFT", target, "TOPLEFT", -1, 1)
outline.top:SetPoint("TOPRIGHT", target, "TOPRIGHT", 1, 1)
outline.top:SetHeight(1)
outline.bottom:ClearAllPoints()
outline.bottom:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", -1, -1)
outline.bottom:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", 1, -1)
outline.bottom:SetHeight(1)
outline.left:ClearAllPoints()
outline.left:SetPoint("TOPLEFT", target, "TOPLEFT", -1, 0)
outline.left:SetPoint("BOTTOMLEFT", target, "BOTTOMLEFT", -1, 0)
outline.left:SetWidth(1)
outline.right:ClearAllPoints()
outline.right:SetPoint("TOPRIGHT", target, "TOPRIGHT", 1, 0)
outline.right:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", 1, 0)
outline.right:SetWidth(1)
outline.top:Show()
outline.bottom:Show()
outline.left:Show()
outline.right:Show()
end
local function GetEntryStateKind(data)
local spellEntry = type(data) == "table" and data.spellEntry or nil
if not spellEntry or not HMGT_SpellData or not HMGT_SpellData.GetStateKind then
return nil
end
return HMGT_SpellData.GetStateKind(spellEntry)
end
local function IsAvailabilityEntry(data)
return GetEntryStateKind(data) == "availability"
end
local function GetEntryCountText(data)
if type(data) ~= "table" then
return nil
end
local maxCharges = tonumber(data.maxCharges)
if not maxCharges or maxCharges <= 1 then
return nil
end
local currentCharges = tonumber(data.currentCharges) or 0
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
return string.format("%d/%d", currentCharges, maxCharges)
end
local function ShouldShowBarSpellTooltip(frame)
return type(frame) == "table"
and type(frame._settings) == "table"
and frame._settings.showBar == true
end
local function ShouldEnableBarMouse(frame)
if type(frame) ~= "table" or type(frame._settings) ~= "table" then
return false
end
return ShouldShowBarSpellTooltip(frame) or frame._settings.locked ~= true
end
local function ShowBlizzardSpellTooltip(anchor, data)
if not anchor or type(data) ~= "table" then
return
end
local spellId = tonumber(data.spellId)
if not spellId and type(data.spellEntry) == "table" then
spellId = tonumber(data.spellEntry.spellId)
end
if not spellId or spellId <= 0 then
return
end
GameTooltip:SetOwner(anchor, "ANCHOR_RIGHT")
local ok = false
if type(GameTooltip.SetSpellByID) == "function" then
ok = pcall(GameTooltip.SetSpellByID, GameTooltip, spellId)
end
if not ok then
pcall(GameTooltip.SetHyperlink, GameTooltip, ("spell:%d"):format(spellId))
end
HMGT:SafeShowTooltip(GameTooltip)
end
-- ══════════════════════════════════════════════════════════════
-- BAR-ROW: einmalige Erstellung (NIEMALS im Combat-Ticker!)
-- ══════════════════════════════════════════════════════════════
local function CreateBarRow(parent)
local row = CreateFrame("Frame", nil, parent)
row:SetHeight(20)
row:Hide()
row.icon = row:CreateTexture(nil, "ARTWORK")
row.icon:SetSize(20, 20)
row.icon:SetPoint("LEFT", row, "LEFT", 0, 0)
row.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
row.bar = CreateFrame("StatusBar", nil, row)
row.bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") -- wird in UpdateBarRow überschrieben
row.bar:SetMinMaxValues(0, 1)
row.bar:SetValue(1)
row.bar:SetPoint("LEFT", row.icon, "RIGHT", 2, 0)
row.bar:SetPoint("RIGHT", row, "RIGHT", 0, 0)
row.bar:SetHeight(20)
row.bar._ownerRow = row
row.bar._trackerFrame = parent
row.barTexture = row.bar:CreateTexture(nil, "ARTWORK")
row.barTexture:SetTexture("Interface\\TargetingFrame\\UI-StatusBar")
row.bar:SetStatusBarTexture(row.barTexture)
row.bar:EnableMouse(false)
row.barBg = row.bar:CreateTexture(nil, "BACKGROUND")
row.barBg:SetAllPoints()
row.barBg:SetColorTexture(0.1, 0.1, 0.1, 0.8)
row.spark = row.bar:CreateTexture(nil, "OVERLAY")
row.spark:SetSize(8, 20)
row.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
row.spark:SetBlendMode("ADD")
row.nameText = row.bar:CreateFontString(nil, "OVERLAY")
row.nameText:SetPoint("LEFT", row.bar, "LEFT", 4, 0)
row.nameText:SetJustifyH("LEFT")
row.nameText:SetFontObject(GameFontNormal)
row.timeText = row.bar:CreateFontString(nil, "OVERLAY")
row.timeText:SetPoint("RIGHT", row.bar, "RIGHT", -4, 0)
row.timeText:SetJustifyH("RIGHT")
row.timeText:SetFontObject(GameFontNormal)
row.border = CreateOutline(row)
row.bar:SetScript("OnEnter", function(bar)
local trackerFrame = bar._trackerFrame
local ownerRow = bar._ownerRow
if ShouldShowBarSpellTooltip(trackerFrame) and ownerRow and ownerRow._data then
ShowBlizzardSpellTooltip(bar, ownerRow._data)
return
end
if trackerFrame and trackerFrame._settings and trackerFrame._settings.locked ~= true then
GameTooltip:SetOwner(bar, "ANCHOR_RIGHT")
GameTooltip:SetText(L["TT_DRAG"])
HMGT:SafeShowTooltip(GameTooltip)
end
end)
row.bar:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
row.bar:SetScript("OnMouseDown", function(bar, button)
if button ~= "LeftButton" then
return
end
local trackerFrame = bar._trackerFrame
local onDragStart = trackerFrame and trackerFrame:GetScript("OnDragStart")
if onDragStart then
onDragStart(trackerFrame)
end
end)
row.bar:SetScript("OnMouseUp", function(bar, button)
if button ~= "LeftButton" then
return
end
local trackerFrame = bar._trackerFrame
local onDragStop = trackerFrame and trackerFrame:GetScript("OnDragStop")
if onDragStop then
onDragStop(trackerFrame)
end
end)
return row
end
-- ══════════════════════════════════════════════════════════════
-- ICON-CELL: einmalige Erstellung
-- ══════════════════════════════════════════════════════════════
local function CreateIconCell(parent)
local cell = CreateFrame("Frame", nil, parent)
cell:SetSize(32, 32)
cell:Hide()
-- Icon-Bereich (immer sz×sz)
cell.iconFrame = CreateFrame("Frame", nil, cell)
cell.iconFrame:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0)
cell.iconFrame:SetSize(32, 32)
cell.bg = cell.iconFrame:CreateTexture(nil, "BACKGROUND")
cell.bg:SetAllPoints(cell.iconFrame)
cell.bg:SetColorTexture(0, 0, 0, 0.6)
cell.icon = cell.iconFrame:CreateTexture(nil, "ARTWORK")
cell.icon:SetAllPoints(cell.iconFrame)
cell.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
cell.cooldown = CreateFrame("Cooldown", nil, cell.iconFrame, "CooldownFrameTemplate")
cell.cooldown:SetAllPoints(cell.iconFrame)
cell.cooldown:SetDrawSwipe(true)
cell.cooldown:SetSwipeColor(0, 0, 0, 0.8)
cell.cooldown:SetHideCountdownNumbers(true)
cell.border = CreateOutline(cell)
-- Text-Labels werden je nach textAnchor umgehängt.
-- Namen außerhalb des Icons werden auf Icon-Breite begrenzt und bei Bedarf gekürzt.
cell.nameText = cell:CreateFontString(nil, "OVERLAY")
cell.nameText:SetJustifyH("LEFT")
cell.nameText:SetWordWrap(false)
cell.nameText:SetFontObject(GameFontNormalSmall)
cell.timeText = cell:CreateFontString(nil, "OVERLAY")
cell.timeText:SetJustifyH("CENTER")
cell.timeText:SetWordWrap(false)
cell.timeText:SetFontObject(GameFontNormalSmall)
cell.chargeText = cell.iconFrame:CreateFontString(nil, "OVERLAY")
cell.chargeText:SetJustifyH("CENTER")
cell.chargeText:SetWordWrap(false)
cell.chargeText:SetFontObject(GameFontNormalSmall)
-- Zentrale Cooldown-Zeit (für Sweep-Modus)
cell.centerText = cell.iconFrame:CreateFontString(nil, "OVERLAY")
cell.centerText:SetJustifyH("CENTER")
cell.centerText:SetWordWrap(false)
cell.centerText:SetFontObject(GameFontNormalSmall)
-- Tooltip
cell:SetScript("OnEnter", function(c)
if not c._data then return end
ShowBlizzardSpellTooltip(c, c._data)
end)
cell:SetScript("OnLeave", function() GameTooltip:Hide() end)
return cell
end
-- ══════════════════════════════════════════════════════════════
-- HAUPTFRAME ERSTELLEN
-- ══════════════════════════════════════════════════════════════
function TF:CreateTrackerFrame(name, settings)
local frame = CreateFrame("Frame", "HMGT_Frame_" .. name, UIParent)
frame:SetSize(settings.width or 250, 30)
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", settings.posX or 200, settings.posY or -200)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame.bg = frame:CreateTexture(nil, "BACKGROUND")
frame.bg:SetAllPoints()
frame.bg:SetColorTexture(0, 0, 0, 0.5)
frame.titleText = frame:CreateFontString(nil, "OVERLAY")
frame.titleText:SetFontObject(GameFontNormalSmall)
frame.titleText:SetTextColor(0.8, 0.8, 0.8)
frame.titleText:SetText(name)
frame:EnableMouse(not settings.locked)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function(f)
if not f._settings.locked then
f._hmgtDragging = true
f:StartMoving()
end
end)
frame:SetScript("OnDragStop", function(f)
f._hmgtDragging = nil
f:StopMovingOrSizing()
local l = f:GetLeft()
local visualTop = f:GetTop()
if UsesBottomHeader(f._settings) then
local bottom = f:GetBottom()
if bottom then
visualTop = bottom + HEADER_HEIGHT
end
end
if l and visualTop then
f._settings.posX = l
f._settings.posY = visualTop - UIParent:GetHeight()
-- Draggen setzt immer den absoluten Anker auf UIParent.
f._settings.anchorTo = "UIParent"
f._settings.anchorPoint = "TOPLEFT"
f._settings.anchorRelPoint = "TOPLEFT"
f._settings.anchorX = f._settings.posX
f._settings.anchorY = f._settings.posY
TF:ApplyAnchor(f)
end
end)
frame:SetScript("OnEnter", function(f)
if not f._settings.locked then
GameTooltip:SetOwner(f, "ANCHOR_RIGHT")
GameTooltip:SetText(L["TT_DRAG"])
HMGT:SafeShowTooltip(GameTooltip)
end
end)
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
frame._settings = settings
frame._name = name
frame._barPool = {}
frame._iconPool = {}
for i = 1, MAX_ROWS do
frame._barPool[i] = CreateBarRow(frame)
frame._iconPool[i] = CreateIconCell(frame)
end
ApplyTitleLayout(frame)
-- Initialen Lock-Status anwenden
TF:SetLocked(frame, settings.locked)
TF:ApplyAnchor(frame)
return frame
end
-- ══════════════════════════════════════════════════════════════
-- BAR-ROW UPDATE
-- ══════════════════════════════════════════════════════════════
local function UpdateBarRow(frame, index, data)
local row = frame._barPool[index]
if not row then return end
local s = frame._settings
local h = s.barHeight or 20
local gap = s.barSpacing or 2
row:SetWidth(frame:GetWidth())
row:SetAlpha(data.outOfRange and (data.outOfRangeAlpha or 0.4) or 1)
row:SetHeight(h)
row:ClearAllPoints()
if s.growDirection == "UP" then
row:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, HEADER_HEIGHT + ((index - 1) * (h + gap)))
else
row:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -((index - 1) * (h + gap)) - HEADER_HEIGHT)
end
row.icon:SetSize(h, h)
row.icon:SetTexture(
data.spellEntry and HMGT_SpellData.GetSpellIcon(data.spellEntry.spellId)
or "Interface\\Icons\\INV_Misc_QuestionMark"
)
local remaining = data.remaining
local total = data.total
local progress = (total > 0) and (remaining / total) or 0
local r, g, b = 0.2, 0.8, 0.2
if s.colorByClass and data.class then
r, g, b = HMGT:GetClassColor(data.class)
else
if remaining > 10 then
r, g, b = 1.0, 0.2, 0.2
elseif remaining > 5 then
r, g, b = 1.0, 0.8, 0.2
else
r, g, b = 0.2, 0.8, 0.2
end
end
-- Bar-Textur aus Einstellungen
local barTex = LSM:Fetch("statusbar", s.barTexture or "Blizzard")
or "Interface\\TargetingFrame\\UI-StatusBar"
if row.barTexture then
row.barTexture:SetTexture(barTex)
row.bar:SetStatusBarTexture(row.barTexture)
else
row.bar:SetStatusBarTexture(barTex)
end
local bc = s.borderColor or {}
local br = bc.r or bc[1] or 1
local bg = bc.g or bc[2] or 1
local bb = bc.b or bc[3] or 1
local ba = bc.a or bc[4] or 1
SetOutline(row.border, row.bar, br, bg, bb, ba, s.borderEnabled == true)
row.bar:SetHeight(h)
row.bar:SetStatusBarColor(r, g, b, 0.85)
if row.barTexture then
row.barTexture:SetVertexColor(r, g, b, 0.85)
end
row.bar:SetMinMaxValues(0, 1)
row.bar:SetValue(progress)
row.spark:SetHeight(h)
local bw = row.bar:GetWidth() or 200
row.spark:ClearAllPoints()
row.spark:SetPoint("CENTER", row.bar, "LEFT", bw * progress, 0)
local fp = GetFontPath(s.font)
local fsz = s.fontSize or 12
local fo = s.fontOutline or "OUTLINE"
SafeSetFont(row.nameText, fp, fsz, fo)
SafeSetFont(row.timeText, fp, fsz, fo)
row._data = data
row.bar:EnableMouse(ShouldEnableBarMouse(frame))
local displayName
if data.spellEntry and s.showPlayerName then
displayName = (data.playerName or "") .. " " .. data.spellEntry.name
elseif data.spellEntry then
displayName = data.spellEntry.name
else
displayName = data.playerName or ""
end
row.nameText:SetText(displayName)
row.nameText:SetTextColor(1, 1, 1)
if s.showPlayerName then
row.nameText:SetText(data.playerName or "")
else
row.nameText:SetText("")
end
local chargesTxt = GetEntryCountText(data)
if remaining > 0 then
local t = HMGT:FormatTime(remaining)
row.timeText:SetText(chargesTxt and (chargesTxt .. " " .. t) or t)
row.timeText:SetTextColor(1, 1, 1)
else
row.timeText:SetText(chargesTxt or "|cff00ff00Bereit|r")
end
row:Show()
end
-- ══════════════════════════════════════════════════════════════
-- ICON-ZELLEN: Layout-Berechnung
-- ══════════════════════════════════════════════════════════════
--- Berechnet für eine Zelle (index) die xOff/yOff relativ zum Frame-Anchor,
--- die Zelldimensionen sowie ob TOPRIGHT statt TOPLEFT als Anchor benutzt wird.
local function CalcCellLayout(frame, index)
local s = frame._settings
local sz = s.iconSize or 32
local n = s.iconCols or 6
local gap = s.iconSpacing or 2
local dir = s.growDirection or "DOWN"
local anchor = s.textAnchor or "below"
local overlay = s.iconOverlay or "sweep"
local isSweep = (overlay == "sweep" or overlay == "swipe")
local showN = s.showPlayerName
local textBlockW = GetIconTextBlockWidth(s)
local extraW = 0
local extraH = 0
if anchor == "left" or anchor == "right" then
if isSweep then
extraW = showN and (textBlockW + 4) or 0
else
extraW = textBlockW + 4
end
elseif isSweep then
extraH = showN and LABEL_H or 0
elseif anchor ~= "onIcon" then
extraH = showN and (LABEL_H * 2) or LABEL_H
end
local cellW = sz + extraW
local cellH = sz + extraH
local step = gap -- Pixel zwischen Zellen
local col, row, xOff, yOff, useRight
if dir == "DOWN" or dir == "UP" then
col = (index - 1) % n
row = math.floor((index - 1) / n)
xOff = col * (cellW + step)
if dir == "DOWN" then
yOff = -(row * (cellH + step)) - 20
useRight = false
else
-- UP: Zellen wachsen von unten nach oben
yOff = row * (cellH + step)
useRight = false
end
else
-- LEFT / RIGHT: n Icons pro Spalte
row = (index - 1) % n
col = math.floor((index - 1) / n)
yOff = -(row * (cellH + step)) - 20
if dir == "RIGHT" then
xOff = col * (cellW + step)
useRight = false
else -- LEFT: vom rechten Rand aufbauen
xOff = -(col * (cellW + step))
useRight = true
end
end
return xOff, yOff, sz, cellW, cellH, useRight
end
--- Gibt die Gesamt-Breite und -Höhe des Icon-Rasters zurück
local function CalcIconGridSize(count, s)
local sz = s.iconSize or 32
local n = s.iconCols or 6
local gap = s.iconSpacing or 2
local dir = s.growDirection or "DOWN"
local anchor = s.textAnchor or "below"
local overlay = s.iconOverlay or "sweep"
local isSweep = (overlay == "sweep" or overlay == "swipe")
local showN = s.showPlayerName
local textBlockW = GetIconTextBlockWidth(s)
local extraW = 0
local extraH = 0
if anchor == "left" or anchor == "right" then
if isSweep then
extraW = showN and (textBlockW + 4) or 0
else
extraW = textBlockW + 4
end
elseif isSweep then
extraH = showN and LABEL_H or 0
elseif anchor ~= "onIcon" then
extraH = showN and (LABEL_H * 2) or LABEL_H
end
local cellH = sz + extraH
local cellW = sz + extraW
if dir == "DOWN" or dir == "UP" then
local cols = math.min(count, n)
local rows = math.ceil(count / n)
return cols * (cellW + gap) - gap,
20 + rows * (cellH + gap) - gap
else
local rows = math.min(count, n)
local cols = math.ceil(count / n)
return cols * (cellW + gap) - gap,
20 + rows * (cellH + gap) - gap
end
end
-- ══════════════════════════════════════════════════════════════
-- ICON-CELL UPDATE
-- ══════════════════════════════════════════════════════════════
local function UpdateIconCell(frame, index, data)
local cell = frame._iconPool[index]
if not cell then return end
local s = frame._settings
local sz = s.iconSize or 32
local overlay = s.iconOverlay or "sweep"
local isSweep = (overlay == "sweep" or overlay == "swipe")
local anchor = s.textAnchor or "below"
local showN = s.showPlayerName
local textBlockW = GetIconTextBlockWidth(s)
local xOff, yOff, _, cellW, cellH, useRight = CalcCellLayout(frame, index)
-- ── Zell- und iconFrame-Größe ────────────────────────────
cell:SetSize(cellW, cellH)
cell:SetAlpha(data.outOfRange and (data.outOfRangeAlpha or 0.4) or 1)
cell.iconFrame:SetSize(sz, sz)
cell.iconFrame:ClearAllPoints()
-- Icon-Bereich positionieren (abhängig von textAnchor)
if anchor == "left" then
cell.iconFrame:SetPoint("TOPRIGHT", cell, "TOPRIGHT", 0, 0)
elseif anchor == "right" then
cell.iconFrame:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0)
elseif anchor == "above" and overlay == "timer" then
-- Texte oben, Icon unten
cell.iconFrame:SetPoint("BOTTOMLEFT", cell, "BOTTOMLEFT", 0, 0)
else
-- Icon oben (Standard: below / onIcon)
cell.iconFrame:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0)
end
-- ── Zell-Position im Frame ───────────────────────────────
cell:ClearAllPoints()
if useRight then
cell:SetPoint("TOPRIGHT", frame, "TOPRIGHT", xOff, yOff)
else
cell:SetPoint("TOPLEFT", frame, "TOPLEFT", xOff, yOff)
end
-- ── Icon-Textur ──────────────────────────────────────────
cell.icon:SetTexture(
data.spellEntry and HMGT_SpellData.GetSpellIcon(data.spellEntry.spellId)
or "Interface\\Icons\\INV_Misc_QuestionMark"
)
-- ── Icon-Rahmen: Textur + Größe dynamisch ────────────────
-- UI-ActionButton-Border: 64px Textur, ~11px Padding pro Seite
-- → Border muss sz*(64/42) groß sein damit die 42px-Mitte = sz
-- Für benutzerdefinierte Texturen: fester Offset von sz*0.25
local bc = s.borderColor or {}
local br = bc.r or bc[1] or 1
local bg = bc.g or bc[2] or 1
local bb = bc.b or bc[3] or 1
local ba = bc.a or bc[4] or 1
SetOutline(cell.border, cell.iconFrame, br, bg, bb, ba, s.borderEnabled == true)
local remaining = data.remaining
local total = data.total
local countText = GetEntryCountText(data)
local hasCharges = countText ~= nil
local isAvailabilityEntry = IsAvailabilityEntry(data)
local showRaidTimeOnIcon = s.showRemainingOnIcon == true and remaining > 0
local showChargeOnIcon = hasCharges and ((s.showChargesOnIcon == true) or isAvailabilityEntry)
local fp = GetFontPath(s.font)
local fsz = math.max(8, (s.fontSize or 12) - 2)
local fo = s.fontOutline or "OUTLINE"
cell.chargeText:SetText("")
cell.chargeText:Hide()
-- ── Overlay-Modus ────────────────────────────────────────
if isSweep then
-- Sweep-Kreis
cell.cooldown:Show()
if remaining > 0 and total > 0 then
cell.cooldown:SetCooldown(GetTime() - (total - remaining), total)
else
cell.cooldown:Clear()
end
cell.icon:SetVertexColor(1, 1, 1)
-- Mittige Restzeit im Icon anzeigen
cell.centerText:ClearAllPoints()
cell.centerText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
SafeSetFont(cell.centerText, fp, fsz, fo)
if remaining > 0 then
cell.centerText:SetText(HMGT:FormatTime(remaining))
cell.centerText:SetTextColor(1, 1, 1)
cell.centerText:Show()
else
cell.centerText:SetText("")
cell.centerText:Hide()
end
-- Im Icon-Modus Spielernamen optional unter dem Icon anzeigen.
cell.nameText:ClearAllPoints()
SafeSetFont(cell.nameText, fp, fsz, fo)
if showN then
if anchor == "left" then
cell.nameText:SetPoint("RIGHT", cell.iconFrame, "LEFT", -4, 0)
cell.nameText:SetWidth(textBlockW)
cell.nameText:SetJustifyH("RIGHT")
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", textBlockW))
elseif anchor == "right" then
cell.nameText:SetPoint("LEFT", cell.iconFrame, "RIGHT", 4, 0)
cell.nameText:SetWidth(textBlockW)
cell.nameText:SetJustifyH("LEFT")
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", textBlockW))
else
cell.nameText:SetPoint("TOPLEFT", cell.iconFrame, "BOTTOMLEFT", 0, -2)
cell.nameText:SetWidth(sz)
cell.nameText:SetJustifyH("LEFT")
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", sz))
end
cell.nameText:SetTextColor(0.9, 0.9, 0.9)
cell.nameText:Show()
else
cell.nameText:SetText("")
cell.nameText:Hide()
end
-- Im Sweep-Modus bei Charge-Spells die Aufladungen anzeigen.
if showChargeOnIcon then
cell.chargeText:ClearAllPoints()
cell.chargeText:SetPoint("BOTTOM", cell.iconFrame, "BOTTOM", 0, CHARGE_TEXT_Y_OFFSET)
SafeSetFont(cell.chargeText, fp, math.max(7, fsz - 1), fo)
cell.chargeText:SetText(countText)
cell.chargeText:SetTextColor(0.85, 0.95, 1)
cell.chargeText:Show()
cell.timeText:SetText("")
cell.timeText:Hide()
elseif countText then
cell.timeText:ClearAllPoints()
cell.timeText:SetPoint("BOTTOMRIGHT", cell.iconFrame, "BOTTOMRIGHT", -2, 2)
SafeSetFont(cell.timeText, fp, math.max(7, fsz - 1), fo)
cell.timeText:SetText(countText)
cell.timeText:SetTextColor(0.85, 0.95, 1)
cell.timeText:Show()
else
cell.timeText:SetText("")
cell.timeText:Hide()
end
else
-- Timer-Modus: kein Sweep, Icon grau wenn auf CD
cell.cooldown:Clear()
cell.cooldown:Hide()
cell.centerText:SetText("")
cell.centerText:Hide()
cell.icon:SetVertexColor(remaining > 0 and 0.5 or 1, remaining > 0 and 0.5 or 1, remaining > 0 and 0.5 or 1)
-- ── Texte: onIcon ────────────────────────────────────
if anchor == "onIcon" then
-- Name oben links auf dem Icon (Overlay)
cell.nameText:ClearAllPoints()
cell.nameText:SetPoint("TOPLEFT", cell.iconFrame, "TOPLEFT", 2, -2)
SafeSetFont(cell.nameText, fp, fsz, fo)
if showN then
local short = (data.playerName or ""):match("^(%a%a%a)") or
(data.playerName or ""):sub(1, 3)
cell.nameText:SetText(short)
cell.nameText:SetTextColor(1, 1, 1)
cell.nameText:Show()
else
cell.nameText:SetText("")
cell.nameText:Hide()
end
-- Zeit unten mittig auf dem Icon
cell.timeText:ClearAllPoints()
if showChargeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
else
cell.timeText:SetPoint("BOTTOM", cell.iconFrame, "BOTTOM", 0, 2)
end
SafeSetFont(cell.timeText, fp, fsz, fo)
cell.timeText:Show()
-- ── Texte: below (Standard) ──────────────────────────
elseif anchor == "below" then
-- Icon ist am TOP der Zelle
if showN then
cell.nameText:ClearAllPoints()
cell.nameText:SetPoint("TOPLEFT", cell.iconFrame, "BOTTOMLEFT", 0, -2)
cell.nameText:SetWidth(sz)
SafeSetFont(cell.nameText, fp, fsz, fo)
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", sz))
cell.nameText:SetTextColor(0.9, 0.9, 0.9)
cell.nameText:Show()
cell.timeText:ClearAllPoints()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
else
cell.timeText:SetPoint("TOP", cell.iconFrame, "BOTTOM", 0, -(LABEL_H + 4))
end
else
cell.nameText:SetText("")
cell.nameText:Hide()
cell.timeText:ClearAllPoints()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
else
cell.timeText:SetPoint("TOP", cell.iconFrame, "BOTTOM", 0, -2)
end
end
SafeSetFont(cell.timeText, fp, fsz, fo)
cell.timeText:Show()
-- ── Texte: left / right ──────────────────────────────
elseif anchor == "left" or anchor == "right" then
local justify = anchor == "left" and "RIGHT" or "LEFT"
local point = anchor == "left" and "RIGHT" or "LEFT"
local relPoint = anchor == "left" and "LEFT" or "RIGHT"
local xOffset = anchor == "left" and -4 or 4
cell.nameText:ClearAllPoints()
cell.nameText:SetWidth(textBlockW)
cell.nameText:SetJustifyH(justify)
SafeSetFont(cell.nameText, fp, fsz, fo)
cell.timeText:ClearAllPoints()
cell.timeText:SetWidth(textBlockW)
cell.timeText:SetJustifyH(justify)
SafeSetFont(cell.timeText, fp, fsz, fo)
if showN then
cell.nameText:SetPoint(point, cell.iconFrame, relPoint, xOffset, -((LABEL_H * 0.5) + 1))
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", textBlockW))
cell.nameText:SetTextColor(0.9, 0.9, 0.9)
cell.nameText:Show()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
cell.timeText:SetWidth(sz)
cell.timeText:SetJustifyH("CENTER")
else
cell.timeText:SetPoint(point, cell.iconFrame, relPoint, xOffset, (LABEL_H * 0.5) + 1)
end
else
cell.nameText:SetText("")
cell.nameText:Hide()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
cell.timeText:SetWidth(sz)
cell.timeText:SetJustifyH("CENTER")
else
cell.timeText:SetPoint(point, cell.iconFrame, relPoint, xOffset, 0)
end
end
cell.timeText:Show()
-- ── Texte: above ─────────────────────────────────────
else -- above
-- Icon ist am BOTTOM der Zelle (iconFrame wurde oben bereits nach unten gesetzt)
if showN then
cell.nameText:ClearAllPoints()
cell.nameText:SetPoint("BOTTOMLEFT", cell.iconFrame, "TOPLEFT", 0, 4)
cell.nameText:SetWidth(sz)
SafeSetFont(cell.nameText, fp, fsz, fo)
cell.nameText:SetText(TruncateTextToWidth(cell.nameText, data.playerName or "", sz))
cell.nameText:SetTextColor(0.9, 0.9, 0.9)
cell.nameText:Show()
cell.timeText:ClearAllPoints()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
else
cell.timeText:SetPoint("BOTTOM", cell.iconFrame, "TOP", 0, LABEL_H + 6)
end
else
cell.nameText:SetText("")
cell.nameText:Hide()
cell.timeText:ClearAllPoints()
if showRaidTimeOnIcon then
cell.timeText:SetPoint("CENTER", cell.iconFrame, "CENTER", 0, 0)
else
cell.timeText:SetPoint("BOTTOM", cell.iconFrame, "TOP", 0, 4)
end
end
SafeSetFont(cell.timeText, fp, fsz, fo)
cell.timeText:Show()
end
if showChargeOnIcon then
cell.chargeText:ClearAllPoints()
cell.chargeText:SetPoint("BOTTOM", cell.iconFrame, "BOTTOM", 0, CHARGE_TEXT_Y_OFFSET)
SafeSetFont(cell.chargeText, fp, math.max(7, fsz - 1), fo)
cell.chargeText:SetText(countText)
cell.chargeText:SetTextColor(0.85, 0.95, 1)
cell.chargeText:Show()
end
-- Timer-Farbe
if remaining > 0 then
local timeStr = HMGT:FormatTime(remaining)
if countText and not showChargeOnIcon then
timeStr = string.format("%s %s", countText, timeStr)
end
cell.timeText:SetText(timeStr)
if remaining < 5 then cell.timeText:SetTextColor(1, 0.2, 0.2)
elseif remaining < 15 then cell.timeText:SetTextColor(1, 0.8, 0.2)
else cell.timeText:SetTextColor(0.9, 0.9, 0.9) end
else
if isAvailabilityEntry and showChargeOnIcon then
cell.timeText:SetText("")
cell.timeText:Hide()
elseif countText and not showChargeOnIcon then
cell.timeText:SetText(countText)
elseif s.showReadyText == false then
cell.timeText:SetText("")
cell.timeText:Hide()
else
cell.timeText:SetText("|cff00ff00Bereit|r")
end
end
end
cell._data = data
cell:Show()
end
-- ══════════════════════════════════════════════════════════════
-- HAUPTUPDATE
-- ══════════════════════════════════════════════════════════════
function TF:UpdateFrame(frame, entries, skipFilter)
local s = frame._settings
if (not skipFilter) and HMGT.FilterDisplayEntries then
entries = HMGT:FilterDisplayEntries(s, entries) or entries
end
for i = 1, MAX_ROWS do
frame._barPool[i]:Hide()
frame._iconPool[i]:Hide()
end
if #entries == 0 then
frame:SetHeight(HEADER_HEIGHT)
ApplyTitleLayout(frame)
return
end
local h = s.barHeight or 20
local gap = s.barSpacing or 2
if s.showBar then
local count = math.min(#entries, MAX_ROWS)
frame:SetHeight(HEADER_HEIGHT + count * (h + gap))
frame:SetWidth(s.width or 250)
for i = 1, count do
UpdateBarRow(frame, i, entries[i])
end
else
local count = math.min(#entries, MAX_ROWS)
local w, ht = CalcIconGridSize(count, s)
frame:SetWidth(math.max(w, s.iconSize or 32))
frame:SetHeight(math.max(ht, 20))
for i = 1, count do
UpdateIconCell(frame, i, entries[i])
end
end
SafeSetFont(
frame.titleText,
GetFontPath(s.font),
math.max(9, (s.fontSize or 12) - 1),
s.fontOutline or "OUTLINE"
)
ApplyTitleLayout(frame)
end
-- ══════════════════════════════════════════════════════════════
-- HILFSFUNKTIONEN
-- ══════════════════════════════════════════════════════════════
function TF:SetTitle(frame, title)
frame.titleText:SetText(title or "")
end
function TF:GetContentTopInset(frame)
if not frame or not frame.GetTop then
return 0
end
local frameTop = frame:GetTop()
local contentTop = GetVisibleContentBounds(frame)
if not frameTop or not contentTop then
return 0
end
return math.max(0, frameTop - contentTop)
end
--- Gesperrt: Maus deaktivieren + Titel/Hintergrund ausblenden
function TF:SetLocked(frame, locked)
frame:EnableMouse(not locked)
if locked then
frame.bg:Hide()
frame.titleText:Hide()
else
frame.bg:Show()
frame.bg:SetColorTexture(0, 0, 0, 0.5)
frame.titleText:Show()
end
end
function TF:Clear(frame)
for i = 1, MAX_ROWS do
frame._barPool[i]:Hide()
frame._iconPool[i]:Hide()
end
frame:SetHeight(HEADER_HEIGHT)
end
function TF:ApplyAnchor(frame)
if not frame or not frame._settings then return end
local s = frame._settings
local anchorPoint = VALID_POINTS[s.anchorPoint] and s.anchorPoint or "TOPLEFT"
local relPoint = VALID_POINTS[s.anchorRelPoint] and s.anchorRelPoint or "TOPLEFT"
local x = s.anchorX
local y = s.anchorY
local target = ResolveAnchorTarget(s.anchorTo, s.anchorCustom)
if x == nil then x = s.posX or 0 end
if y == nil then y = s.posY or 0 end
if UsesBottomHeader(s) then
local mapped = HEADER_ANCHOR_MAP_UP[anchorPoint]
if mapped then
anchorPoint = mapped.point
y = y + mapped.y
end
end
frame:ClearAllPoints()
frame:SetPoint(anchorPoint, target, relPoint, x, y)
end