-- 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