Files
Freewood-OctoWoW-Core/UI/Tabs.lua
T
2026-10-27 14:06:11 +00:00

542 lines
18 KiB
Lua

-- Freewood-OctoWoW-Core: die drei eigenen Tabs (Uebersicht, Austausch, Einstellungen)
-- Module registrieren ihre Tabs ueber Freewood.UI:RegisterTab(...), siehe MainWindow.lua.
local ADDON, FW = ...
local UI = FW.UI
local Export = FW.Export
------------------------------------------------------------------------
-- Uebersicht: geladene Module
------------------------------------------------------------------------
local function BuildOverview(content)
local L = FW.L
local f = CreateFrame("Frame", nil, content)
local title = UI.CreateLabel(f, L.OVERVIEW_HEADER, { font = "heading", size = 16, color = "goldLight" })
title:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
local _, build, _, toc = GetBuildInfo()
local gameVersion = (GetBuildInfo())
local info = UI.CreateLabel(f,
string.format(L.OVERVIEW_INFO, FW.version, tostring(gameVersion), tostring(build)),
{ size = 11, color = "textDim", justify = "RIGHT" })
info:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -2)
local divider = UI.CreateDivider(f)
divider:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -30)
divider:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -30)
local scroll = UI.CreateScrollFrame(f)
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -44)
scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
local empty = UI.CreateLabel(scroll.child, L.OVERVIEW_NONE, { size = 12, color = "textDim", justifyV = "TOP" })
empty:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 0, 0)
empty:SetPoint("TOPRIGHT", scroll.child, "TOPRIGHT", 0, 0)
local ROW_H, GAP = 58, 8
local rows = {}
local function GetRow(i)
local row = rows[i]
if row then
return row
end
row = UI.CreatePanel(scroll.child, "card", "bronze")
row:SetHeight(ROW_H)
row.name = UI.CreateLabel(row, "", { font = "heading", size = 14, color = "goldLight" })
row.name:SetPoint("TOPLEFT", row, "TOPLEFT", 14, -10)
row.version = UI.CreateLabel(row, "", { size = 11, color = "textDim" })
row.version:SetPoint("LEFT", row.name, "RIGHT", 10, 0)
row.desc = UI.CreateLabel(row, "", { size = 12, color = "text" })
row.desc:SetPoint("TOPLEFT", row.name, "BOTTOMLEFT", 0, -6)
row.status = UI.CreateLabel(row, "", { size = 12, justify = "RIGHT" })
row.status:SetPoint("RIGHT", row, "RIGHT", -14, 0)
rows[i] = row
return row
end
function f.Refresh()
local list = FW.moduleList
if #list == 0 then
empty:Show()
else
empty:Hide()
end
for i, mod in ipairs(list) do
local row = GetRow(i)
local y = (i - 1) * (ROW_H + GAP)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 0, -y)
row:SetPoint("TOPRIGHT", scroll.child, "TOPRIGHT", 0, -y)
row.name:SetText(mod.title or mod.name)
row.version:SetText(mod.version and ("v" .. mod.version) or "")
row.desc:SetText(mod.description or "")
if mod:IsEnabled() then
row.status:SetText("|cff" .. UI.Hex("success") .. L.MODULE_ACTIVE .. "|r")
else
row.status:SetText("|cff" .. UI.Hex("textDim") .. L.MODULE_INACTIVE .. "|r")
end
row:Show()
end
for i = #list + 1, #rows do
rows[i]:Hide()
end
scroll:SetContentHeight(math.max(#list * (ROW_H + GAP), 40))
end
f.Refresh()
return f
end
------------------------------------------------------------------------
-- Austausch: Export und Import
------------------------------------------------------------------------
local function BuildExchange(content)
local L = FW.L
local f = CreateFrame("Frame", nil, content)
f.selected = nil
local intro = UI.CreateLabel(f, L.EXCHANGE_INTRO, { size = 12, color = "textDim", justifyV = "TOP" })
intro:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
intro:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
local kinds = CreateFrame("Frame", nil, f)
kinds:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -46)
kinds:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -46)
kinds:SetHeight(28)
local none = UI.CreateLabel(kinds, L.EXCHANGE_NO_HANDLERS, { size = 12, color = "textDim" })
none:SetPoint("LEFT", kinds, "LEFT", 0, 0)
local area = UI.CreateTextArea(f)
area:SetPoint("TOPLEFT", kinds, "BOTTOMLEFT", 0, -12)
area:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 46)
local status = UI.CreateLabel(f, "", { size = 12 })
status:SetWidth(520)
local function SetStatus(message, colorKey)
status:SetText("|cff" .. UI.Hex(colorKey or "text") .. message .. "|r")
end
local function ErrorText(key, extra)
local text = L[key]
if key == "ERR_NO_HANDLER" then
text = string.format(text, tostring(extra))
elseif key == "ERR_INVALID" and type(extra) == "string" then
text = text .. " (" .. extra .. ")"
end
return text
end
local kindButtons = {}
local function RefreshKindButtons()
for _, b in ipairs(kindButtons) do
b:SetStyle(b.kind == f.selected and "primary" or "default")
end
end
local function RebuildKinds()
for _, b in ipairs(kindButtons) do
b:Hide()
end
local list = Export.handlerList
if #list == 0 then
none:Show()
f.selected = nil
return
end
none:Hide()
local found = false
for _, h in ipairs(list) do
if h.kind == f.selected then
found = true
end
end
if not found then
f.selected = list[1].kind
end
local x = 0
for i, handler in ipairs(list) do
local b = kindButtons[i]
if not b then
b = UI.CreateButton(kinds, "", 150, 28, function(self)
f.selected = self.kind
RefreshKindButtons()
end)
kindButtons[i] = b
end
b.kind = handler.kind
local label = Export.GetLabel(handler)
b:SetLabel(label)
b:SetWidth(math.max(110, b.label:GetStringWidth() + 32))
b:ClearAllPoints()
b:SetPoint("LEFT", kinds, "LEFT", x, 0)
b:Show()
x = x + b:GetWidth() + 8
end
RefreshKindButtons()
end
local exportButton = UI.CreateButton(f, L.EXCHANGE_EXPORT, 130, 30, function()
if not f.selected then
return -- der Hinweis "kein Modul" steht schon ueber dem Textfeld
end
local code, err, extra = Export.ExportKind(f.selected)
if code then
area:SetText(code)
area:SelectAll()
SetStatus(string.format(L.EXCHANGE_EXPORTED, #code), "success")
else
SetStatus(ErrorText(err, extra), "danger")
end
end, "primary")
exportButton:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
local importButton = UI.CreateButton(f, L.EXCHANGE_IMPORT, 130, 30, function()
local text = area:GetText() or ""
if not text:find("%S") then
SetStatus(L.EXCHANGE_EMPTY, "danger")
return
end
local ok, result, extra = Export.Import(text)
if ok then
local message = type(result) == "string" and result ~= "" and result or L.EXCHANGE_IMPORTED
SetStatus(message, "success")
else
SetStatus(ErrorText(result, extra), "danger")
end
end)
importButton:SetPoint("LEFT", exportButton, "RIGHT", 8, 0)
local clearButton = UI.CreateButton(f, L.EXCHANGE_CLEAR, 100, 30, function()
area:SetText("")
status:SetText("")
end, "ghost")
clearButton:SetPoint("LEFT", importButton, "RIGHT", 8, 0)
status:SetPoint("LEFT", clearButton, "RIGHT", 16, 0)
function f.Refresh()
RebuildKinds()
end
f.Refresh()
return f
end
------------------------------------------------------------------------
-- Einstellungen
------------------------------------------------------------------------
-- Module koennen eigene Abschnitte in den Einstellungen anmelden:
-- UI:RegisterSettingsSection(id, { order = 100, height = 260, build = function(parent) return frame end, refresh = function(frame) end })
-- Die Abschnitte stehen in zwei Spalten rechts neben den allgemeinen Einstellungen. Die Hoehe eines Abschnitts
-- ist der Wert frame.flowH, wenn er ihn meldet (UI.CreateSection tut das), sonst def.height. Die ganze Seite
-- scrollt, wenn sie hoeher ist als das Fenster; die Breiten richten sich nach der Fensterbreite.
local settingsSections = {}
local settingsFrame, settingsBody, settingsScroll, settingsLeft
local vline1, vline2
-- Abschnitt mit Ueberschrift, Trennlinie und einem Stapel darunter: local f, stack = UI.CreateSection(parent, "Titel")
function UI.CreateSection(parent, titleText)
local f = CreateFrame("Frame", nil, parent)
local title = UI.CreateLabel(f, titleText, { font = "heading", size = 14, color = "goldLight" })
title:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
local divider = UI.CreateDivider(f)
divider:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -26)
divider:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -26)
local stack = UI.CreateStack(f, { y = 30, gap = 6, pad = 0 })
stack.onHeight = function(h)
f.flowH = h + 14
f:SetHeight(f.flowH)
if f.onFlow then f.onFlow() end
end
f.stack = stack
f.title = title
return f, stack
end
local SECTION_GAP = 36 -- Abstand zwischen den beiden Spalten
local LEFT_GAP = 40 -- Abstand zwischen allgemeinen Einstellungen und Abschnitten
local function LeftWidth(W)
return FW.Util.Clamp(math.floor(W * 0.28), 300, 420)
end
local layingOut, pendingLayout, layoutDepth = false, false, 0
local function LayoutSections()
if not settingsBody then
return
end
if layingOut then
pendingLayout = true -- eine Hoehe hat sich waehrend des Anordnens geaendert: danach noch einmal
return
end
layingOut = true
local W = settingsBody:GetWidth() or 0
if W < 200 then
W = 1000
end
local leftW = LeftWidth(W)
local x1 = leftW + LEFT_GAP
local colW = math.max(200, math.floor((W - x1 - SECTION_GAP) / 2))
local x2 = x1 + colW + SECTION_GAP
settingsLeft:SetWidth(leftW)
local ypos = { 0, 0 }
for i, def in ipairs(settingsSections) do
def.col = (ypos[1] >= ypos[2]) and 1 or 2 -- jeweils in die kuerzere Spalte
local h = (def.frame and def.frame.flowH) or def.height or 260
def.y = ypos[def.col]
ypos[def.col] = ypos[def.col] - h - 10
local frame = def.frame
if frame then
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", settingsBody, "TOPLEFT", def.col == 2 and x2 or x1, def.y)
frame:SetWidth(colW)
frame:SetHeight(h)
end
end
local contentH = math.max(-ypos[1], -ypos[2], settingsLeft.flowH or 0) + 20
settingsBody.contentH = contentH
settingsBody:SetHeight(contentH)
settingsScroll:SetContentHeight(contentH)
vline1:ClearAllPoints()
vline1:SetPoint("TOPLEFT", settingsBody, "TOPLEFT", x1 - 20, 0)
vline1:SetPoint("BOTTOMLEFT", settingsBody, "TOPLEFT", x1 - 20, -contentH)
vline2:ClearAllPoints()
vline2:SetPoint("TOPLEFT", settingsBody, "TOPLEFT", x2 - SECTION_GAP / 2, 0)
vline2:SetPoint("BOTTOMLEFT", settingsBody, "TOPLEFT", x2 - SECTION_GAP / 2, -contentH)
layingOut = false
if pendingLayout and layoutDepth < 4 then
pendingLayout = false
layoutDepth = layoutDepth + 1
LayoutSections()
layoutDepth = layoutDepth - 1
end
pendingLayout = false
end
local function BuildSection(def)
if def.frame or not settingsBody then
return
end
local ok, frame = pcall(def.build, settingsBody)
if ok and frame then
def.frame = frame
frame.onFlow = LayoutSections
if def.refresh then
pcall(def.refresh, frame)
end
end
end
function UI:RegisterSettingsSection(id, def)
def.id = id
def.order = def.order or 100
settingsSections[#settingsSections + 1] = def
table.sort(settingsSections, function(a, b)
return a.order < b.order
end)
BuildSection(def)
LayoutSections()
end
local function BuildSettings(content)
local L = FW.L
local f = CreateFrame("Frame", nil, content)
local title = UI.CreateLabel(f, L.SETTINGS_HEADER, { font = "heading", size = 16, color = "goldLight" })
title:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
local divider = UI.CreateDivider(f)
divider:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -30)
divider:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -30)
-- Alles darunter scrollt, wenn das Fenster zu niedrig ist
local scroll = UI.CreateScrollFrame(f)
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -42)
scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
local body = scroll.child
settingsScroll, settingsBody, settingsFrame = scroll, body, f
-- allgemeine Einstellungen links (Stapel)
local lf = CreateFrame("Frame", nil, body)
lf:SetPoint("TOPLEFT", body, "TOPLEFT", 0, 0)
lf:SetWidth(400)
settingsLeft = lf
local stack = UI.CreateStack(lf, { y = 0, gap = 8 })
stack.onHeight = function(h)
lf.flowH = h
lf:SetHeight(h)
end
local minimapBox = UI.CreateCheckbox(lf, L.SETTINGS_MINIMAP,
function()
return not FW.settings.minimap.hide
end,
function(value)
FW.Minimap:SetHidden(not value)
end)
stack:Add(minimapBox, { fit = true })
local debugBox = UI.CreateCheckbox(lf, L.SETTINGS_DEBUG,
function()
return FW.settings.debug and true or false
end,
function(value)
FW.settings.debug = value and true or false
end)
stack:Add(debugBox, { fit = true })
-- Fenstergroesse: Zeile mit - / Prozent / +
local scaleLabel = UI.CreateLabel(lf, L.SETTINGS_SCALE, { size = 12 })
stack:Add(scaleLabel, { height = 16, gap = 18 })
local scaleRow = CreateFrame("Frame", nil, lf)
scaleRow:SetHeight(26)
stack:Add(scaleRow, { height = 26, gap = 4, stretch = true })
local scaleValue = UI.CreateLabel(scaleRow, "", { size = 12, color = "goldLight", justify = "CENTER" })
scaleValue:SetWidth(60)
local function RefreshScale()
scaleValue:SetText(string.format("%d%%", math.floor(UI:GetScale() * 100 + 0.5)))
end
UI:OnScaleChanged(function() RefreshScale() end)
local minus = UI.CreateButton(scaleRow, "-", 28, 26, function()
UI:SetScale(UI:GetScale() - 0.05)
end)
minus:SetPoint("LEFT", scaleRow, "LEFT", 0, 0)
scaleValue:SetPoint("LEFT", minus, "RIGHT", 4, 0)
local plus = UI.CreateButton(scaleRow, "+", 28, 26, function()
UI:SetScale(UI:GetScale() + 0.05)
end)
plus:SetPoint("LEFT", scaleValue, "RIGHT", 4, 0)
-- Sprache: Beschriftung oben, drei Knoepfe gleich breit darunter
local langLabel = UI.CreateLabel(lf, L.SETTINGS_LANGUAGE, { size = 12 })
stack:Add(langLabel, { height = 16, gap = 18 })
local langRow = CreateFrame("Frame", nil, lf)
langRow:SetHeight(26)
stack:Add(langRow, { height = 26, gap = 4, stretch = true })
local langButtons = {}
local function RefreshLang()
for _, b in ipairs(langButtons) do
b:SetStyle(FW.settings.locale == b.code and "primary" or "default")
end
end
local options = {
{ code = "auto", text = L.LANG_AUTO },
{ code = "deDE", text = L.LANG_DE },
{ code = "enUS", text = L.LANG_EN },
}
for i, option in ipairs(options) do
local b = UI.CreateButton(langRow, option.text, 110, 26, function(self)
FW.settings.locale = self.code
RefreshLang()
end)
b.code = option.code
langButtons[#langButtons + 1] = b
end
local function LayoutLang(w)
w = w or langRow:GetWidth() or 0
if w < 60 then return end
local bw = math.floor((w - 12) / 3)
for i, b in ipairs(langButtons) do
b:ClearAllPoints()
b:SetWidth(bw)
b:SetPoint("LEFT", langRow, "LEFT", (i - 1) * (bw + 6), 0)
end
end
langRow:SetScript("OnSizeChanged", function(self) LayoutLang(self:GetWidth()) end)
LayoutLang(stack:Width())
local note = UI.CreateLabel(lf, L.SETTINGS_LANGUAGE_NOTE, { size = 11, color = "textDim", justifyV = "TOP" })
stack:Add(note, { fit = true, gap = 8 })
local reset = UI.CreateButton(lf, L.SETTINGS_RESET_POS, 220, 28, function()
UI:ResetPosition()
end)
stack:Add(reset, { height = 28, gap = 16, stretch = true })
function f.Refresh()
minimapBox:Refresh()
debugBox:Refresh()
RefreshScale()
RefreshLang()
for _, def in ipairs(settingsSections) do
if def.frame and def.refresh then
pcall(def.refresh, def.frame)
end
end
stack:Layout()
LayoutSections()
end
-- senkrechte Trennstriche zwischen den Spalten
local function VLine()
local line = body:CreateTexture(nil, "ARTWORK")
line:SetWidth(1)
line:SetColorTexture(UI.Color("gold", 0.35))
return line
end
vline1, vline2 = VLine(), VLine()
body:SetScript("OnSizeChanged", function(self)
local w = self:GetWidth()
if type(w) == "number" and w > 0 then
stack:Layout()
LayoutSections()
end
end)
for _, def in ipairs(settingsSections) do
BuildSection(def)
end
stack:Layout()
LayoutSections()
f.Refresh()
return f
end
------------------------------------------------------------------------
-- Anmelden
------------------------------------------------------------------------
UI:RegisterTab("overview", {
label = function() return FW.L.TAB_OVERVIEW end,
order = 10,
build = BuildOverview,
onShow = function(f) f.Refresh() end,
})
UI:RegisterTab("exchange", {
label = function() return FW.L.TAB_EXCHANGE end,
order = 110,
build = BuildExchange,
onShow = function(f) f.Refresh() end,
})
UI:RegisterTab("settings", {
label = function() return FW.L.TAB_SETTINGS end,
order = 20,
build = BuildSettings,
onShow = function(f) f.Refresh() end,
})