-- 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, })