From 25b4ed3abf56ee84981ef7e5da6cb6dcc46af36a Mon Sep 17 00:00:00 2001 From: Freewood <948+freewood@noreply.octowow.st> Date: Tue, 27 Oct 2026 14:06:11 +0000 Subject: [PATCH] Dateien nach "UI" hochladen --- UI/Kit.lua | 1039 +++++++++++++++++++++++++++++++++++++++++++++ UI/MainFrame.lua | 127 ++++++ UI/MainWindow.lua | 353 +++++++++++++++ UI/Minimap.lua | 100 +++++ UI/Tabs.lua | 541 +++++++++++++++++++++++ 5 files changed, 2160 insertions(+) create mode 100644 UI/Kit.lua create mode 100644 UI/MainFrame.lua create mode 100644 UI/MainWindow.lua create mode 100644 UI/Minimap.lua create mode 100644 UI/Tabs.lua diff --git a/UI/Kit.lua b/UI/Kit.lua new file mode 100644 index 0000000..6789de3 --- /dev/null +++ b/UI/Kit.lua @@ -0,0 +1,1039 @@ +-- Freewood-OctoWoW-Core: UI-Bausteine im Stil "Azeroth Codex" +-- Alles aus Farbflaechen, 1-Pixel-Raendern und Spielschriften. Keine eigenen Bilddateien. +-- Farben stammen aus den DESIGN.md-Dateien der Projektideen. + +local ADDON, FW = ... + +local UI = {} +FW.UI = UI + +------------------------------------------------------------------------ +-- Farben +------------------------------------------------------------------------ + +UI.hex = { + bgDeep = "0c0e14", + bg = "0f131f", + panel = "141824", + card = "1a2030", + hover = "242b40", + gold = "c89b3c", + goldLight = "f0bf5c", + goldAntique = "dfb75c", + bronze = "9d7826", + text = "e6e2d6", + textDim = "9a978c", + danger = "e0645a", + success = "6cc070", +} + +UI.rarity = { + [0] = "9d9d9d", -- Schlecht + [1] = "ffffff", -- Gewoehnlich + [2] = "1eff00", -- Ungewoehnlich + [3] = "0070dd", -- Selten + [4] = "a335ee", -- Episch + [5] = "ff8000", -- Legendaer + [6] = "e6cc80", -- Artefakt + [7] = "00ccff", -- Erbstueck +} + +local colorCache = {} + +local function HexToRGB(hex) + return tonumber(hex:sub(1, 2), 16) / 255, + tonumber(hex:sub(3, 4), 16) / 255, + tonumber(hex:sub(5, 6), 16) / 255 +end + +-- key: Name aus UI.hex oder ein Hex-Wert wie "ff8000". Rueckgabe: r, g, b, a +function UI.Color(key, alpha) + local c = colorCache[key] + if not c then + c = { HexToRGB(UI.hex[key] or key) } + colorCache[key] = c + end + return c[1], c[2], c[3], alpha or 1 +end + +function UI.Hex(key) + return UI.hex[key] or key +end + +function UI.RarityHex(quality) + return UI.rarity[quality or 1] or UI.rarity[1] +end + +------------------------------------------------------------------------ +-- Schriften +------------------------------------------------------------------------ + +function UI.GetFontPath(kind) + if kind == "heading" then + return "Fonts\\FRIZQT__.TTF" + end + return STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF" +end + +function UI.SetFont(fontString, kind, size, flags) + local ok = fontString:SetFont(UI.GetFontPath(kind), size or 12, flags or "") + if not ok then + fontString:SetFont(STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF", size or 12, flags or "") + end +end + +-- opts: font ("body"/"heading"), size, color, justify, justifyV, flags, layer +function UI.CreateLabel(parent, text, opts) + opts = opts or {} + local fs = parent:CreateFontString(nil, opts.layer or "OVERLAY") + UI.SetFont(fs, opts.font or "body", opts.size or 12, opts.flags) + fs:SetTextColor(UI.Color(opts.color or "text")) + fs:SetJustifyH(opts.justify or "LEFT") + fs:SetJustifyV(opts.justifyV or "MIDDLE") + fs:SetText(text or "") + return fs +end + +------------------------------------------------------------------------ +-- Flaechen und Raender +------------------------------------------------------------------------ + +function UI.AddBorder(frame, colorKey, alpha, thickness) + thickness = thickness or 1 + local b = {} + + b.top = frame:CreateTexture(nil, "BORDER") + b.top:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + b.top:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0) + b.top:SetHeight(thickness) + + b.bottom = frame:CreateTexture(nil, "BORDER") + b.bottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0) + b.bottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0) + b.bottom:SetHeight(thickness) + + b.left = frame:CreateTexture(nil, "BORDER") + b.left:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -thickness) + b.left:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, thickness) + b.left:SetWidth(thickness) + + b.right = frame:CreateTexture(nil, "BORDER") + b.right:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -thickness) + b.right:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, thickness) + b.right:SetWidth(thickness) + + frame.fwBorder = b + UI.SetBorderColor(frame, colorKey, alpha) + return b +end + +function UI.SetBorderColor(frame, colorKey, alpha) + local b = frame.fwBorder + if not b then + return + end + local r, g, bl, a = UI.Color(colorKey, alpha) + b.top:SetColorTexture(r, g, bl, a) + b.bottom:SetColorTexture(r, g, bl, a) + b.left:SetColorTexture(r, g, bl, a) + b.right:SetColorTexture(r, g, bl, a) +end + +function UI.CreatePanel(parent, bgKey, borderKey) + local p = CreateFrame("Frame", nil, parent) + p.bg = p:CreateTexture(nil, "BACKGROUND") + p.bg:SetAllPoints() + p.bg:SetColorTexture(UI.Color(bgKey or "panel")) + if borderKey then + UI.AddBorder(p, borderKey) + end + return p +end + +function UI.CreateDivider(parent, colorKey, alpha) + local t = parent:CreateTexture(nil, "ARTWORK") + t:SetHeight(1) + t:SetColorTexture(UI.Color(colorKey or "bronze", alpha or 0.6)) + return t +end + +------------------------------------------------------------------------ +-- Tooltips +------------------------------------------------------------------------ + +-- title: Text oder Funktion; lines: Text, Liste von Texten oder Funktion +function UI.AttachTooltip(frame, title, lines, anchor) + frame:HookScript("OnEnter", function(self) + GameTooltip:SetOwner(self, anchor or "ANCHOR_RIGHT") + local t = type(title) == "function" and title() or title + GameTooltip:SetText(t or "", 1, 1, 1) + local l = type(lines) == "function" and lines() or lines + if type(l) == "string" then + l = { l } + end + for _, line in ipairs(l or {}) do + GameTooltip:AddLine(line, 0.8, 0.8, 0.8, true) + end + GameTooltip:Show() + end) + frame:HookScript("OnLeave", function() + GameTooltip:Hide() + end) +end + +------------------------------------------------------------------------ +-- Knoepfe +------------------------------------------------------------------------ + +local BUTTON_STYLES = { + default = { bg = "card", bgHover = "hover", border = "bronze", borderHover = "gold", text = "text" }, + primary = { bg = "gold", bgHover = "goldLight", border = "goldLight", borderHover = "goldAntique", text = "bgDeep" }, + ghost = { bg = "bg", bgHover = "hover", border = "bg", borderHover = "bronze", text = "text" }, + -- Zerstoerende Aktionen: normal unauffaellig, beim Darueberfahren rot + danger = { bg = "card", bgHover = "hover", border = "bronze", borderHover = "danger", text = "text" }, + dangerActive = { bg = "card", bgHover = "hover", border = "danger", borderHover = "danger", text = "danger" }, +} + +local function RefreshButton(b) + local style = BUTTON_STYLES[b.fwStyleName] or BUTTON_STYLES.default + local enabled = b:IsEnabled() and true or false + local hovered = b.fwHover and enabled + b.bg:SetColorTexture(UI.Color(hovered and style.bgHover or style.bg)) + UI.SetBorderColor(b, hovered and style.borderHover or style.border) + b.label:SetTextColor(UI.Color(style.text)) + b:SetAlpha(enabled and 1 or 0.45) +end + +-- styleName: "default", "primary" oder "ghost" +function UI.CreateButton(parent, text, width, height, onClick, styleName) + local b = CreateFrame("Button", nil, parent) + b:SetSize(width or 120, height or 28) + b.fwStyleName = styleName or "default" + + b.bg = b:CreateTexture(nil, "BACKGROUND") + b.bg:SetAllPoints() + UI.AddBorder(b, (BUTTON_STYLES[b.fwStyleName] or BUTTON_STYLES.default).border) + + b.label = UI.CreateLabel(b, text, { size = 12, justify = "CENTER" }) + b.label:SetPoint("CENTER", 0, 0) + + b:SetScript("OnEnter", function(self) + self = self or this + if not self then return end + self.fwHover = true + RefreshButton(self) + end) + b:SetScript("OnLeave", function(self) + self = self or this + if not self then return end + self.fwHover = false + RefreshButton(self) + end) + b:SetScript("OnClick", function(self, ...) + if onClick then + onClick(self, ...) + end + end) + + function b:SetLabel(newText) + self.label:SetText(newText) + end + function b:SetStyle(name) + self.fwStyleName = name + RefreshButton(self) + end + function b:SetButtonEnabled(on) + if on then + self:Enable() + else + self:Disable() + end + RefreshButton(self) + end + + RefreshButton(b) + return b +end + +-- Tab-Knopf mit Goldlinie fuer den aktiven Zustand +function UI.CreateTabButton(parent, text) + local b = CreateFrame("Button", nil, parent) + b:SetHeight(34) + + b.hl = b:CreateTexture(nil, "BACKGROUND") + b.hl:SetAllPoints() + b.hl:SetColorTexture(UI.Color("hover", 0.5)) + b.hl:Hide() + + b.under = b:CreateTexture(nil, "ARTWORK") + b.under:SetHeight(2) + b.under:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0) + b.under:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0) + b.under:SetColorTexture(UI.Color("gold")) + b.under:Hide() + + b.label = UI.CreateLabel(b, text, { font = "heading", size = 13, color = "textDim", justify = "CENTER" }) + b.label:SetPoint("CENTER", 0, 1) + b:SetWidth(math.max(90, b.label:GetStringWidth() + 36)) + + function b:SetActive(active) + self.fwActive = active + if active then + self.label:SetTextColor(UI.Color("goldLight")) + self.under:Show() + self.hl:Hide() + else + self.label:SetTextColor(UI.Color("textDim")) + self.under:Hide() + end + end + function b:SetLabel(newText) + self.label:SetText(newText) + self:SetWidth(math.max(90, self.label:GetStringWidth() + 36)) + end + + b:SetScript("OnEnter", function(self) + if not self.fwActive then + self.hl:Show() + self.label:SetTextColor(UI.Color("text")) + end + end) + b:SetScript("OnLeave", function(self) + self.hl:Hide() + if not self.fwActive then + self.label:SetTextColor(UI.Color("textDim")) + end + end) + return b +end + +------------------------------------------------------------------------ +-- Kontrollkaestchen +------------------------------------------------------------------------ + +-- Hoehe eines umbrochenen Textes (Zeilen mal Zeilenhoehe, falls die Schrift keine Hoehe meldet) +function UI.TextHeight(fs, minHeight) + local h = fs.GetStringHeight and fs:GetStringHeight() + if type(h) ~= "number" or h <= 0 then + h = 14 + end + return math.max(h, minHeight or 0) +end + +-- Text auf eine Breite setzen und umbrechen; gibt die Hoehe zurueck +function UI.FitLabel(fs, width, minHeight) + if width and width > 0 then + fs:SetWidth(width) + end + fs:SetWordWrap(true) + fs:SetJustifyV("TOP") + fs:SetHeight(0) -- feste Hoehe aufheben, sonst misst die Schrift nur die abgeschnittene Hoehe + local h = UI.TextHeight(fs, minHeight) + fs:SetHeight(h) + return h +end + +-- Kontrollkaestchen. c:Fit(breite) setzt die Breite, bricht den Text um und passt die Hoehe an +-- (Rueckgabe: Hoehe). Haengt das Kaestchen links und rechts an einem Rahmen, passt es sich +-- beim Aendern der Fenstergroesse selbst an. +local CHECK_TEXT_X = 28 +local CHECK_MIN_H = 22 + +function UI.CreateCheckbox(parent, labelText, getFn, setFn) + local c = CreateFrame("Button", nil, parent) + c:SetHeight(24) + + local box = UI.CreatePanel(c, "card", "bronze") + box:SetSize(18, 18) + box:SetPoint("LEFT", c, "LEFT", 0, 0) + + local check = box:CreateTexture(nil, "ARTWORK") + check:SetPoint("TOPLEFT", box, "TOPLEFT", 4, -4) + check:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -4, 4) + check:SetColorTexture(UI.Color("goldLight")) + + c.label = UI.CreateLabel(c, labelText, { size = 12 }) + c.label:SetPoint("LEFT", box, "RIGHT", 10, 0) + c:SetWidth(math.max(120, c.label:GetStringWidth() + 40)) + c.fitWidth = nil + c.isFit = true + + function c:Fit(width) + width = width or self:GetWidth() + if not width or width <= 0 then + return self:GetHeight() + end + if not self.fitWidth then + -- erstes Fit: Kaestchen und Text oben ausrichten (mehrzeiliger Text) + box:ClearAllPoints() + box:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -2) + self.label:ClearAllPoints() + self.label:SetPoint("TOPLEFT", self, "TOPLEFT", CHECK_TEXT_X, -4) + end + self.fitWidth = width + self:SetWidth(width) + self.label:SetWidth(math.max(40, width - CHECK_TEXT_X)) + self.label:SetWordWrap(true) + self.label:SetJustifyV("TOP") + self.label:SetHeight(0) + local h = math.max(CHECK_MIN_H, UI.TextHeight(self.label, 14) + 8) + self.label:SetHeight(h - 4) + self:SetHeight(h) + return h + end + + function c:SetLabel(text) + self.label:SetText(text or "") + if self.fitWidth then + self:Fit(self.fitWidth) + end + end + + c:SetScript("OnSizeChanged", function(self) + -- Breite immer vom Rahmen selbst lesen (die Argumente des Ereignisses sind im Client nicht verlaesslich) + local w = self:GetWidth() + if self.fitWidth and type(w) == "number" and w > 0 and math.abs(w - self.fitWidth) > 0.5 then + self:Fit(w) + end + end) + + function c:Refresh() + if getFn() then + check:Show() + else + check:Hide() + end + end + + c:SetScript("OnClick", function(self) + setFn(not getFn()) + self:Refresh() + end) + c:SetScript("OnEnter", function() + UI.SetBorderColor(box, "gold") + end) + c:SetScript("OnLeave", function() + UI.SetBorderColor(box, "bronze") + end) + + c:Refresh() + return c +end + +------------------------------------------------------------------------ +-- Vertikaler Stapel: ordnet Bausteine untereinander an und passt sich der Breite an +-- local stack = UI.CreateStack(frame, { gap = 6, x = 0, y = 0 }) +-- stack:Add(widget, { gap = 10, indent = 0, stretch = true, fit = true, height = 30 }) +-- fit Kontrollkaestchen/Text: wird auf die Breite umgebrochen, Hoehe folgt dem Text +-- stretch Baustein haengt auch am rechten Rand (Trennlinien, Textblock) +-- height feste Hoehe (sonst die aktuelle Hoehe des Bausteins) +-- stack:Layout() ordnet neu; stack.height ist die Gesamthoehe. Aenderungen der Breite des +-- Rahmens ordnen automatisch neu an (stack.onHeight wird danach gerufen). +------------------------------------------------------------------------ + +function UI.CreateStack(parent, opts) + opts = opts or {} + local stack = { items = {}, gap = opts.gap or 6, x = opts.x or 0, y = opts.y or 0, pad = opts.pad or 0, height = 0 } + + function stack:Add(widget, o) + o = o or {} + self.items[#self.items + 1] = { w = widget, gap = o.gap, indent = o.indent or 0, stretch = o.stretch, + fit = o.fit, height = o.height, minHeight = o.minHeight } + return widget + end + + function stack:Width() + local w = parent.GetWidth and parent:GetWidth() or 0 + return math.max(0, (w or 0) - self.x - self.pad) + end + + function stack:Layout() + local width = self:Width() + local y = self.y + for _, it in ipairs(self.items) do + local w = it.w + if not w.stackHidden then + local avail = width - it.indent + local h + if it.fit then + if w.isFit then + h = w:Fit(avail) + else + h = UI.FitLabel(w, avail, it.minHeight) + end + elseif it.height then + h = it.height + w:SetHeight(h) + else + h = w:GetHeight() or 0 + end + y = y + (it.gap or self.gap) + w:ClearAllPoints() + w:SetPoint("TOPLEFT", parent, "TOPLEFT", self.x + it.indent, -y) + if it.stretch then + w:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -self.pad, -y) + end + y = y + h + end + end + self.height = y + self.pad + if self.onHeight then + self.onHeight(self.height) + end + return self.height + end + + function stack:Hook() + local old = parent.GetScript and parent:GetScript("OnSizeChanged") + parent:SetScript("OnSizeChanged", function(f, w, h) + if old then old(f, w, h) end + w = f:GetWidth() + if type(w) == "number" and w > 0 and math.abs(w - (stack.lastW or 0)) > 0.5 then + stack.lastW = w + stack:Layout() + end + end) + end + stack:Hook() + return stack +end + +------------------------------------------------------------------------ +-- Schieberegler (waagerecht). Der Wert wird beim Loslassen uebernommen (setFn), waehrend des +-- Ziehens zeigt nur die Anzeige den Wert; so ruckelt nichts, wenn der Regler selbst die Groesse aendert. +-- local s = UI.CreateSlider(parent, 150, { min = 50, max = 150, step = 5, get = fn, set = fn, format = fn }) +------------------------------------------------------------------------ + +function UI.CreateSlider(parent, width, opts) + local s = CreateFrame("Button", nil, parent) + s:SetSize(width or 150, 20) + local lo, hi, step = opts.min or 0, opts.max or 100, opts.step or 1 + + local track = s:CreateTexture(nil, "BACKGROUND") + track:SetColorTexture(UI.Color("card")) + track:SetHeight(4) + track:SetPoint("LEFT", s, "LEFT", 0, 0) + track:SetPoint("RIGHT", s, "RIGHT", 0, 0) + local fill = s:CreateTexture(nil, "ARTWORK") + fill:SetColorTexture(UI.Color("bronze")) + fill:SetHeight(4) + fill:SetPoint("LEFT", s, "LEFT", 0, 0) + local thumb = s:CreateTexture(nil, "OVERLAY") + thumb:SetSize(8, 16) + thumb:SetColorTexture(UI.Color("goldLight")) + + local value = lo + local function Snap(v) + v = math.floor((v - lo) / step + 0.5) * step + lo + return math.max(lo, math.min(hi, v)) + end + local function Show(v) + value = Snap(v) + local frac = (hi > lo) and ((value - lo) / (hi - lo)) or 0 + local travel = math.max(1, (s:GetWidth() or width or 150) - 8) + thumb:ClearAllPoints() + thumb:SetPoint("LEFT", s, "LEFT", frac * travel, 0) + fill:SetWidth(math.max(1, frac * travel + 4)) + if s.onText then s.onText(value) end + end + + function s:SetValue(v) Show(v) end + function s:GetValue() return value end + function s:Refresh() Show(opts.get and opts.get() or lo) end + + local function FromCursor() + local x = GetCursorPosition() + local scale = s:GetEffectiveScale() or 1 + local left = s:GetLeft() + if not x or not left then return value end + local travel = math.max(1, (s:GetWidth() or 150) - 8) + local frac = ((x / scale) - left - 4) / travel + return lo + math.max(0, math.min(1, frac)) * (hi - lo) + end + + s:SetScript("OnMouseDown", function(self) + Show(FromCursor()) + self:SetScript("OnUpdate", function() Show(FromCursor()) end) + end) + s:SetScript("OnMouseUp", function(self) + self:SetScript("OnUpdate", nil) + Show(FromCursor()) + if opts.set then opts.set(value) end + end) + s:SetScript("OnSizeChanged", function() Show(value) end) + s:SetScript("OnEnter", function() thumb:SetColorTexture(UI.Color("gold")) end) + s:SetScript("OnLeave", function() thumb:SetColorTexture(UI.Color("goldLight")) end) + return s +end + +------------------------------------------------------------------------ +-- Scrollbereich mit schmaler eigener Leiste +-- Ergebnis: holder (Frame). holder.child ist der Inhalt, holder:SetContentHeight(h) +-- passt die Hoehe an. Groesse ueber SetSize oder Anker festlegen. +------------------------------------------------------------------------ + +function UI.CreateScrollFrame(parent, width, height) + local holder = CreateFrame("Frame", nil, parent) + if width and height then + holder:SetSize(width, height) + end + + local sf = CreateFrame("ScrollFrame", nil, holder) + sf:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) + sf:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -12, 0) + + local child = CreateFrame("Frame", nil, sf) + child:SetSize(1, 1) + sf:SetScrollChild(child) + + local track = holder:CreateTexture(nil, "BACKGROUND") + track:SetColorTexture(UI.Color("card", 0.7)) + track:SetWidth(6) + track:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, 0) + track:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, 0) + + local thumb = CreateFrame("Button", nil, holder) + thumb:SetWidth(6) + local thumbTex = thumb:CreateTexture(nil, "OVERLAY") + thumbTex:SetAllPoints() + thumbTex:SetColorTexture(UI.Color("bronze")) + thumb:Hide() + thumb:SetScript("OnEnter", function() + thumbTex:SetColorTexture(UI.Color("gold")) + end) + thumb:SetScript("OnLeave", function() + thumbTex:SetColorTexture(UI.Color("bronze")) + end) + + local contentHeight = 1 + + local function MaxScroll() + return math.max(0, contentHeight - sf:GetHeight()) + end + + local function Update() + -- Breite des Inhalts nachziehen, falls OnSizeChanged sie nicht (rechtzeitig) gesetzt hat + local sw = sf:GetWidth() + if sw and sw > 1 and math.abs((child:GetWidth() or 0) - sw) > 0.5 then + child:SetWidth(sw) + end + local viewH = sf:GetHeight() + local range = MaxScroll() + if range <= 0 or viewH <= 0 then + thumb:Hide() + track:Hide() + sf:SetVerticalScroll(0) + return + end + track:Show() + local current = math.min(math.max(sf:GetVerticalScroll(), 0), range) + if current ~= sf:GetVerticalScroll() then + sf:SetVerticalScroll(current) + end + local thumbH = math.max(24, viewH * viewH / contentHeight) + thumb:SetHeight(thumbH) + thumb:ClearAllPoints() + thumb:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, -(current / range) * (viewH - thumbH)) + thumb:Show() + end + + sf:EnableMouseWheel(true) + sf:SetScript("OnMouseWheel", function(self, delta) + local target = self:GetVerticalScroll() - delta * 48 + self:SetVerticalScroll(math.min(math.max(target, 0), MaxScroll())) + Update() + end) + sf:SetScript("OnSizeChanged", function(self, w) + w = tonumber(w) or self:GetWidth() + if w and w > 0 then + child:SetWidth(w) + end + Update() + end) + + thumb:SetScript("OnMouseDown", function(self) + local _, startY = GetCursorPosition() + local startScroll = sf:GetVerticalScroll() + local scale = self:GetEffectiveScale() + self:SetScript("OnUpdate", function() + local _, y = GetCursorPosition() + local free = sf:GetHeight() - self:GetHeight() + if free <= 0 then + return + end + local target = startScroll + ((startY - y) / scale) * (MaxScroll() / free) + sf:SetVerticalScroll(math.min(math.max(target, 0), MaxScroll())) + Update() + end) + end) + thumb:SetScript("OnMouseUp", function(self) + self:SetScript("OnUpdate", nil) + end) + + holder.scroll = sf + holder.child = child + holder.UpdateScroll = Update + + function holder:SetContentHeight(h) + contentHeight = math.max(1, h) + child:SetHeight(contentHeight) + Update() + end + function holder:ScrollToTop() + sf:SetVerticalScroll(0) + Update() + end + + return holder +end + +------------------------------------------------------------------------ +-- Mehrzeiliges Textfeld (zum Kopieren und Einfuegen von Codes) +-- Ergebnis: area (Frame) mit area.edit (EditBox) +------------------------------------------------------------------------ + +function UI.CreateTextArea(parent, width, height) + local area = UI.CreateScrollFrame(parent, width, height) + + local back = area:CreateTexture(nil, "BACKGROUND", nil, -1) + back:SetPoint("TOPLEFT", area, "TOPLEFT", 0, 0) + back:SetPoint("BOTTOMRIGHT", area, "BOTTOMRIGHT", -12, 0) + back:SetColorTexture(UI.Color("bgDeep", 0.9)) + + local frame = CreateFrame("Frame", nil, area) + frame:SetPoint("TOPLEFT", area, "TOPLEFT", 0, 0) + frame:SetPoint("BOTTOMRIGHT", area, "BOTTOMRIGHT", -12, 0) + UI.AddBorder(frame, "bronze") + + local edit = CreateFrame("EditBox", nil, area.child) + edit:SetMultiLine(true) + edit:SetAutoFocus(false) + edit:SetMaxLetters(420000) + edit:SetTextInsets(8, 8, 6, 6) + edit:SetPoint("TOPLEFT", area.child, "TOPLEFT", 0, 0) + edit:SetPoint("TOPRIGHT", area.child, "TOPRIGHT", 0, 0) + if not edit:SetFont(UI.GetFontPath("body"), 12, "") then + edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "") + end + edit:SetTextColor(UI.Color("text")) + + local function Resize() + local h = math.max(area.scroll:GetHeight(), edit:GetHeight() or 0) + area:SetContentHeight(h) + end + + edit:SetScript("OnTextChanged", function() + C_Timer.After(0, Resize) + end) + edit:SetScript("OnEscapePressed", function(self) + self:ClearFocus() + end) + + -- Klick in den leeren Bereich setzt den Cursor ins Feld + area.scroll:EnableMouse(true) + area.scroll:SetScript("OnMouseDown", function() + edit:SetFocus() + end) + + area.edit = edit + function area:SetText(text) + edit:SetText(text or "") + C_Timer.After(0, Resize) + end + function area:GetText() + return edit:GetText() + end + function area:SelectAll() + edit:SetFocus() + edit:HighlightText() + end + + return area +end + +------------------------------------------------------------------------ +-- Item-Platz (Symbol mit Seltenheitsrahmen) +------------------------------------------------------------------------ + +function UI.CreateItemSlot(parent, size) + size = size or 44 + local slot = UI.CreatePanel(parent, "bgDeep", "bronze") + slot:SetSize(size, size) + + slot.icon = slot:CreateTexture(nil, "ARTWORK") + slot.icon:SetPoint("TOPLEFT", slot, "TOPLEFT", 2, -2) + slot.icon:SetPoint("BOTTOMRIGHT", slot, "BOTTOMRIGHT", -2, 2) + slot.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + + function slot:SetItem(itemID) + self.itemID = itemID + if not itemID then + self.icon:SetTexture(nil) + UI.SetBorderColor(self, "bronze") + return + end + FW.Util.RequestItem(itemID, function(info) + if self.itemID ~= itemID or not info then + return + end + self.icon:SetTexture(info.icon) + UI.SetBorderColor(self, UI.RarityHex(info.quality)) + end) + end + + return slot +end + +------------------------------------------------------------------------ +-- Kennzahl-Karte (kleine Beschriftung oben, Wert darunter) +-- card:SetValue(text, colorKey) +------------------------------------------------------------------------ + +function UI.CreateStatCard(parent, labelText, width, height) + local card = UI.CreatePanel(parent, "card", "bronze") + card:SetSize(width or 150, height or 54) + + card.label = UI.CreateLabel(card, labelText, { size = 10, color = "textDim", flags = "" }) + card.label:SetPoint("TOPLEFT", card, "TOPLEFT", 10, -8) + + card.value = UI.CreateLabel(card, "-", { font = "heading", size = 15, color = "text" }) + card.value:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 10, 8) + + function card:SetValue(text, colorKey) + self.value:SetText(text or "-") + self.value:SetTextColor(UI.Color(colorKey or "text")) + end + function card:SetLabel(text) + self.label:SetText(text) + end + + return card +end + +------------------------------------------------------------------------ +-- Marke (eckig, kein Pillenformat), z. B. fuer Raenge und Filter +-- chip:SetText(text, colorKey) +------------------------------------------------------------------------ + +function UI.CreateChip(parent, text, colorKey) + local chip = UI.CreatePanel(parent, "bgDeep", colorKey or "bronze") + chip:SetHeight(20) + + chip.label = UI.CreateLabel(chip, text, { size = 11, color = colorKey or "goldLight", justify = "CENTER" }) + chip.label:SetPoint("CENTER", chip, "CENTER", 0, 0) + chip:SetWidth(math.max(28, chip.label:GetStringWidth() + 16)) + + function chip:SetText(newText, newColorKey) + self.label:SetText(newText) + if newColorKey then + self.label:SetTextColor(UI.Color(newColorKey)) + UI.SetBorderColor(self, newColorKey) + end + self:SetWidth(math.max(28, self.label:GetStringWidth() + 16)) + end + + return chip +end + +------------------------------------------------------------------------ +-- Dropdown-Auswahl +-- opts.prefix Text vor dem Wert, zum Beispiel "Typ" ergibt "Typ: Waffe" +-- opts.allText Text fuer "keine Auswahl" (Wert nil); fehlt er, gibt es diesen Eintrag nicht +-- opts.getOptions Funktion, die eine Liste { { value = ..., text = ... }, ... } liefert (beim Oeffnen gelesen) +-- opts.textOf Funktion(value) -> Text, spart das Lesen der ganzen Liste fuer die Beschriftung (optional) +-- opts.onChange Funktion(value) nach einer Auswahl durch den Benutzer +-- opts.maxRows sichtbare Zeilen, bevor gescrollt wird (Standard 12) +-- Rueckgabe: Button mit :SetValue(v), :GetValue(), :Refresh(), :Open(), :Close() +------------------------------------------------------------------------ + +local DD_ROW = 20 +local ddPopup, ddCatcher, ddOwner + +local function CloseDropdown() + if ddPopup then ddPopup:Hide() end + if ddCatcher then ddCatcher:Hide() end + local owner = ddOwner + ddOwner = nil + if owner and owner.Refresh then owner:Refresh() end +end +UI.CloseDropdown = CloseDropdown + +local function EnsureDropdownPopup() + if ddPopup then + return + end + -- unsichtbare Flaeche hinter der Liste: ein Klick daneben schliesst sie + ddCatcher = CreateFrame("Button", nil, UIParent) + ddCatcher:SetAllPoints(UIParent) + ddCatcher:SetFrameStrata("FULLSCREEN_DIALOG") + ddCatcher:RegisterForClicks("AnyUp") + ddCatcher:SetScript("OnClick", CloseDropdown) + ddCatcher:Hide() + + ddPopup = UI.CreatePanel(UIParent, "bgDeep", "gold") + ddPopup:SetFrameStrata("FULLSCREEN_DIALOG") + ddPopup:SetFrameLevel((ddCatcher:GetFrameLevel() or 1) + 5) + ddPopup:EnableMouse(true) + ddPopup.list = UI.CreateScrollFrame(ddPopup) + ddPopup.list:SetPoint("TOPLEFT", ddPopup, "TOPLEFT", 2, -2) + ddPopup.list:SetPoint("BOTTOMRIGHT", ddPopup, "BOTTOMRIGHT", -2, 2) + ddPopup.rows = {} + ddPopup:Hide() +end + +local function DropdownRow(index) + local row = ddPopup.rows[index] + if row then + return row + end + local child = ddPopup.list.child + row = CreateFrame("Button", nil, child) + row:SetHeight(DD_ROW) + row:SetPoint("TOPLEFT", child, "TOPLEFT", 0, -(index - 1) * DD_ROW) + row:SetPoint("TOPRIGHT", child, "TOPRIGHT", 0, -(index - 1) * DD_ROW) + row.hl = row:CreateTexture(nil, "BACKGROUND") + row.hl:SetAllPoints() + row.hl:SetColorTexture(UI.Color("hover", 0.9)) + row.hl:Hide() + row.label = UI.CreateLabel(row, "", { size = 12 }) + row.label:SetPoint("LEFT", row, "LEFT", 8, 0) + row.label:SetPoint("RIGHT", row, "RIGHT", -6, 0) + row.label:SetWordWrap(false) + row:SetScript("OnEnter", function(self) self.hl:Show() end) + row:SetScript("OnLeave", function(self) self.hl:Hide() end) + row:SetScript("OnClick", function(self) + local owner = ddOwner + local value = self.value + CloseDropdown() + if owner then + owner:SetValue(value) + if owner.fwOnChange then owner.fwOnChange(value) end + end + end) + ddPopup.rows[index] = row + return row +end + +function UI.CreateDropdown(parent, width, height, opts) + opts = opts or {} + local dd = UI.CreateButton(parent, "", width or 150, height or 20, nil, "default") + dd.fwOnChange = opts.onChange + dd.fwValue = nil + + dd.label:ClearAllPoints() + dd.label:SetPoint("LEFT", dd, "LEFT", 8, 0) + dd.label:SetPoint("RIGHT", dd, "RIGHT", -20, 0) + dd.label:SetJustifyH("LEFT") + dd.label:SetWordWrap(false) + dd.arrow = dd:CreateTexture(nil, "OVERLAY") + dd.arrow:SetTexture("Interface\\Minimap\\ROTATING-MINIMAPARROW") + dd.arrow:SetTexCoord(0, 1, 1, 0) -- Pfeil nach unten + dd.arrow:SetVertexColor(UI.Color("goldLight")) + dd.arrow:SetSize(12, 12) + dd.arrow:SetPoint("RIGHT", dd, "RIGHT", -5, -1) + + local function Options() + local list = {} + if opts.allText then + list[1] = { value = nil, text = opts.allText } + end + local given = opts.getOptions and opts.getOptions() or {} + for _, o in ipairs(given) do + list[#list + 1] = o + end + return list + end + + local function TextOf(value) + if value == nil then + return opts.allText or "" + end + if opts.textOf then + return opts.textOf(value) + end + for _, o in ipairs(Options()) do + if o.value == value then + return o.text + end + end + return tostring(value) + end + + -- Beschriftung und Hervorhebung nach dem Wert richten + function dd:Refresh() + local text = TextOf(self.fwValue) + if opts.prefix then + text = opts.prefix .. ": " .. text + end + self:SetLabel(text) + if self.fwValue ~= nil or ddOwner == self then + self:SetStyle("primary") + else + self:SetStyle("default") + end + end + + function dd:SetValue(value) + self.fwValue = value + self:Refresh() + end + + function dd:GetValue() + return self.fwValue + end + + function dd:Close() + if ddOwner == self then CloseDropdown() end + end + + function dd:Open() + EnsureDropdownPopup() + local list = Options() + if #list == 0 then + return + end + ddOwner = self + local rows = math.min(#list, opts.maxRows or 12) + local w = math.max(self:GetWidth() or 0, 150) + ddPopup:SetSize(w, rows * DD_ROW + 4) + ddPopup:ClearAllPoints() + local bottom = self:GetBottom() + if bottom and bottom - (rows * DD_ROW + 8) < 0 then + ddPopup:SetPoint("BOTTOMLEFT", self, "TOPLEFT", 0, 2) -- unten kein Platz: darueber oeffnen + else + ddPopup:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2) + end + for i, o in ipairs(list) do + local row = DropdownRow(i) + row.value = o.value + row.label:SetText(o.text) + if o.value == self.fwValue then + row.label:SetTextColor(UI.Color("goldLight")) + else + row.label:SetTextColor(UI.Color("text")) + end + row:Show() + end + for i = #list + 1, #ddPopup.rows do + ddPopup.rows[i]:Hide() + end + ddPopup.list:SetContentHeight(#list * DD_ROW) + ddPopup.list:ScrollToTop() + ddCatcher:Show() + ddPopup:Show() + self:Refresh() + end + + dd:SetScript("OnClick", function(self) + if ddOwner == self then + CloseDropdown() + else + if ddOwner then CloseDropdown() end + self:Open() + end + end) + dd:SetScript("OnHide", function(self) self:Close() end) + + dd:Refresh() + return dd +end diff --git a/UI/MainFrame.lua b/UI/MainFrame.lua new file mode 100644 index 0000000..3b3d8ff --- /dev/null +++ b/UI/MainFrame.lua @@ -0,0 +1,127 @@ +-- Freewood-OctoWoW-Core: Aufbau des Hauptfensters (OctoWoW) +-- +-- Ersetzt MainWindow.xml. OctoWoW (1.12.1) bricht beim Laden ab, sobald ein nur x ODER y hat +-- ("No x or y attributes in element: Size"); dann entstehen keine Unterframes und das ganze Fenster fehlt. +-- Der Aufbau in Lua ist unabhaengig vom XML-Parser des Clients. Die Struktur ist dieselbe wie vorher: +-- FreewoodMainFrame.Header (Bg, Accent, Logo, Title, Subtitle) +-- FreewoodMainFrame.TabBar (Bg, Line) +-- FreewoodMainFrame.Footer (Bg, Left) +-- FreewoodMainFrame.Content +-- Farben: Hintergrund #0f131f, Kopfzeile #0c0e14, Leiste #141824, Gold #c89b3c, Bronze #9d7826. + +local ADDON, FW = ... + +if _G.FreewoodMainFrame then + return +end + +local function Solid(tex, r, g, b, a) + tex:SetTexture(r, g, b, a) +end + +local function NewTexture(parent, layer, r, g, b, a) + local t = parent:CreateTexture(nil, layer) + Solid(t, r, g, b, a) + return t +end + +local frame = CreateFrame("Frame", "FreewoodMainFrame", UIParent) +frame:SetFrameStrata("HIGH") +frame:SetToplevel(true) +frame:SetMovable(true) +frame:SetClampedToScreen(true) +frame:EnableMouse(true) +frame:SetWidth(960) +frame:SetHeight(760) +frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) +frame:Hide() + +-- Hintergrund und Rahmen +frame.Bg = NewTexture(frame, "BACKGROUND", 0.059, 0.075, 0.122, 0.97) +frame.Bg:SetAllPoints(frame) + +local gold = { 0.616, 0.471, 0.149, 1 } + +frame.BorderTop = NewTexture(frame, "BORDER", gold[1], gold[2], gold[3], gold[4]) +frame.BorderTop:SetHeight(1) +frame.BorderTop:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) +frame.BorderTop:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0) + +frame.BorderBottom = NewTexture(frame, "BORDER", gold[1], gold[2], gold[3], gold[4]) +frame.BorderBottom:SetHeight(1) +frame.BorderBottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0) +frame.BorderBottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0) + +frame.BorderLeft = NewTexture(frame, "BORDER", gold[1], gold[2], gold[3], gold[4]) +frame.BorderLeft:SetWidth(1) +frame.BorderLeft:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) +frame.BorderLeft:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0) + +frame.BorderRight = NewTexture(frame, "BORDER", gold[1], gold[2], gold[3], gold[4]) +frame.BorderRight:SetWidth(1) +frame.BorderRight:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0) +frame.BorderRight:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0) + +-- Kopfzeile mit Titel +local header = CreateFrame("Frame", nil, frame) +header:SetHeight(48) +header:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1) +header:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1) +frame.Header = header + +header.Bg = NewTexture(header, "BACKGROUND", 0.047, 0.055, 0.078, 1) +header.Bg:SetAllPoints(header) + +header.Accent = NewTexture(header, "ARTWORK", 0.784, 0.608, 0.235, 0.55) +header.Accent:SetHeight(1) +header.Accent:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", 0, 0) +header.Accent:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", 0, 0) + +header.Logo = header:CreateTexture(nil, "ARTWORK") +header.Logo:SetTexture("Interface\\AddOns\\Freewood-OctoWoW-Core\\Media\\FreeWood_Logo") +header.Logo:SetWidth(40) +header.Logo:SetHeight(40) +header.Logo:SetPoint("LEFT", header, "LEFT", 14, 0) + +header.Title = header:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") +header.Title:SetJustifyH("LEFT") +header.Title:SetPoint("LEFT", header.Logo, "RIGHT", 10, 0) + +header.Subtitle = header:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") +header.Subtitle:SetJustifyH("LEFT") +header.Subtitle:SetPoint("LEFT", header.Title, "RIGHT", 12, -2) + +-- Leiste fuer die Tabs +local tabBar = CreateFrame("Frame", nil, frame) +tabBar:SetHeight(36) +tabBar:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, 0) +tabBar:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, 0) +frame.TabBar = tabBar + +tabBar.Bg = NewTexture(tabBar, "BACKGROUND", 0.078, 0.094, 0.141, 1) +tabBar.Bg:SetAllPoints(tabBar) + +tabBar.Line = NewTexture(tabBar, "ARTWORK", 0.616, 0.471, 0.149, 0.5) +tabBar.Line:SetHeight(1) +tabBar.Line:SetPoint("BOTTOMLEFT", tabBar, "BOTTOMLEFT", 0, 0) +tabBar.Line:SetPoint("BOTTOMRIGHT", tabBar, "BOTTOMRIGHT", 0, 0) + +-- Fusszeile +local footer = CreateFrame("Frame", nil, frame) +footer:SetHeight(26) +footer:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 1, 1) +footer:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) +frame.Footer = footer + +footer.Bg = NewTexture(footer, "BACKGROUND", 0.047, 0.055, 0.078, 1) +footer.Bg:SetAllPoints(footer) + +footer.Left = footer:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") +footer.Left:SetJustifyH("LEFT") +footer.Left:SetPoint("LEFT", footer, "LEFT", 16, 0) + +-- Inhaltsbereich (hier setzen die Tabs ihre Frames ein) +local content = CreateFrame("Frame", nil, frame) +content:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 20, -18) +content:SetPoint("BOTTOMRIGHT", footer, "TOPRIGHT", -20, 12) +frame.Content = content diff --git a/UI/MainWindow.lua b/UI/MainWindow.lua new file mode 100644 index 0000000..e8c2cd3 --- /dev/null +++ b/UI/MainWindow.lua @@ -0,0 +1,353 @@ +-- Freewood-OctoWoW-Core: Verhalten des Hauptfensters +-- Aufbau und Farben stehen in MainWindow.xml. Hier: Ziehen, Position merken, +-- Tabs registrieren und umschalten, Oeffnen/Schliessen. + +local ADDON, FW = ... + +local UI = FW.UI +local frame = _G.FreewoodMainFrame +if not frame then + return +end + +UI.window = frame +table.insert(UISpecialFrames, "FreewoodMainFrame") -- Escape schliesst das Fenster + +local tabs = {} -- Reihenfolge der Tabs +local tabsById = {} +local activeId +local built = false + +------------------------------------------------------------------------ +-- Kopf, Ziehen, Position +------------------------------------------------------------------------ + +local header = frame.Header + +UI.SetFont(header.Title, "heading", 20) +header.Title:SetTextColor(UI.Color("goldLight")) +header.Title:SetText("Freewood") + +UI.SetFont(header.Subtitle, "body", 11) +header.Subtitle:SetTextColor(UI.Color("textDim")) + +UI.SetFont(frame.Footer.Left, "body", 11) +frame.Footer.Left:SetTextColor(UI.Color("textDim")) + +local closeButton = UI.CreateButton(header, "X", 30, 30, function() + frame:Hide() +end, "ghost") +closeButton:SetPoint("RIGHT", header, "RIGHT", -10, 0) + +-- Regler fuer die Fenstergroesse (50 bis 150 Prozent) in der Kopfzeile +local scaleText = UI.CreateLabel(header, "100%", { size = 11, color = "textDim", justify = "RIGHT" }) +scaleText:SetWidth(40) +scaleText:SetPoint("RIGHT", closeButton, "LEFT", -12, 0) +local scaleSlider = UI.CreateSlider(header, 150, { + min = 50, max = 150, step = 5, + get = function() return math.floor(UI:GetScale() * 100 + 0.5) end, + set = function(v) UI:SetScale(v / 100) end, +}) +scaleSlider:SetPoint("RIGHT", scaleText, "LEFT", -8, 0) +scaleSlider.onText = function(v) scaleText:SetText(string.format("%d%%", v)) end +UI.AttachTooltip(scaleSlider, function() return FW.L.WINDOW_SCALE end, function() return FW.L.WINDOW_SCALE_TIP end) +UI.scaleSlider = scaleSlider + +local scaleListeners = {} +-- Andere Bedienelemente (Einstellungen: - / +) melden sich an, um bei einer Aenderung nachzuziehen +function UI:OnScaleChanged(fn) + scaleListeners[#scaleListeners + 1] = fn +end + +header:EnableMouse(true) +header:RegisterForDrag("LeftButton") +header:SetScript("OnDragStart", function() + frame:StartMoving() +end) +header:SetScript("OnDragStop", function() + frame:StopMovingOrSizing() + local point, _, relativePoint, x, y = frame:GetPoint(1) + if point and FW.settings then + FW.settings.windowPos = { point = point, relativePoint = relativePoint, x = x, y = y } + end +end) + +function UI:ApplyPosition() + local pos = FW.settings and FW.settings.windowPos + frame:ClearAllPoints() + if type(pos) == "table" and pos.point then + frame:SetPoint(pos.point, UIParent, pos.relativePoint or pos.point, pos.x or 0, pos.y or 0) + else + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end +end + +------------------------------------------------------------------------ +-- Groesse: Standard, Grenzen, Ziehgriff unten rechts +------------------------------------------------------------------------ + +local DEFAULT_W, DEFAULT_H = 1240, 840 +local MIN_W, MIN_H = 1240, 840 -- so gross, dass die Einstellungen komplett passen +local MAX_W, MAX_H = 1500, 1100 + +-- Grenzen passend zum Bildschirm (Fenster soll nicht groesser als der Bildschirm werden) +local function SizeLimits() + local scale = frame:GetScale() or 1 + local sw = ((UIParent and UIParent:GetWidth()) or 1920) / scale + local sh = ((UIParent and UIParent:GetHeight()) or 1080) / scale + local maxW = math.max(MIN_W, math.min(MAX_W, sw - 20)) + local maxH = math.max(MIN_H, math.min(MAX_H, sh - 20)) + return maxW, maxH +end + +local function ClampSize(w, h) + local maxW, maxH = SizeLimits() + w = math.min(math.max(tonumber(w) or DEFAULT_W, MIN_W), maxW) + h = math.min(math.max(tonumber(h) or DEFAULT_H, MIN_H), maxH) + return w, h +end + +function UI:ApplySize() + local saved = FW.settings and FW.settings.windowSize + local w, h = DEFAULT_W, DEFAULT_H + if type(saved) == "table" then + w, h = saved.w or w, saved.h or h + end + frame:SetSize(ClampSize(w, h)) +end + +local function SetupResize() + if frame.SetResizable then + frame:SetResizable(true) + end + local maxW, maxH = SizeLimits() + if frame.SetResizeBounds then + frame:SetResizeBounds(MIN_W, MIN_H, maxW, maxH) + else + if frame.SetMinResize then frame:SetMinResize(MIN_W, MIN_H) end + if frame.SetMaxResize then frame:SetMaxResize(maxW, maxH) end + end + + local grip = CreateFrame("Button", nil, frame) + grip:SetSize(18, 18) + grip:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -3, 3) + grip:SetFrameLevel((frame:GetFrameLevel() or 1) + 30) + grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up") + grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down") + grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight") + grip:SetScript("OnMouseDown", function() + local mw, mh = SizeLimits() + if frame.SetResizeBounds then + frame:SetResizeBounds(MIN_W, MIN_H, mw, mh) + end + frame:StartSizing("BOTTOMRIGHT") + end) + grip:SetScript("OnMouseUp", function() + frame:StopMovingOrSizing() + local w, h = ClampSize(frame:GetWidth(), frame:GetHeight()) + frame:SetSize(w, h) + if FW.settings then + FW.settings.windowSize = { w = math.floor(w + 0.5), h = math.floor(h + 0.5) } + end + local point, _, relativePoint, x, y = frame:GetPoint(1) + if point and FW.settings then + FW.settings.windowPos = { point = point, relativePoint = relativePoint, x = x, y = y } + end + end) + UI.AttachTooltip(grip, function() return FW.L.WINDOW_RESIZE end, function() return FW.L.WINDOW_RESIZE_TIP end) + frame.resizeGrip = grip +end + +function UI:ResetPosition() + if FW.settings then + FW.settings.windowPos = false + FW.settings.windowSize = false + end + self:ApplySize() + self:ApplyPosition() +end + +function UI:SetScale(scale) + scale = FW.Util.Clamp(math.floor(scale * 20 + 0.5) / 20, 0.5, 1.5) + if FW.settings then + FW.settings.scale = scale + end + frame:SetScale(scale) + self:ApplySize() + scaleSlider:Refresh() + for _, fn in ipairs(scaleListeners) do + pcall(fn, scale) + end +end + +function UI:GetScale() + return (FW.settings and FW.settings.scale) or 1 +end + +FW:On("FW_DB_READY", function() + frame:SetScale(UI:GetScale()) + scaleSlider:Refresh() + SetupResize() + UI:ApplySize() + UI:ApplyPosition() + header.Subtitle:SetText("v" .. FW.version) +end) + +------------------------------------------------------------------------ +-- Tabs +-- def.label Text oder Funktion, die den Text liefert +-- def.order Reihenfolge (kleiner = weiter links) +-- def.build function(content) -> Frame (wird beim ersten Anzeigen aufgerufen) +-- def.onShow function(tabFrame) (bei jedem Anzeigen) +------------------------------------------------------------------------ + +local function ResolveLabel(def) + local label = def.label + if type(label) == "function" then + label = label() + end + return label or "?" +end + +local TAB_ROW_H = 34 + +-- Tabs in Zeilen umbrechen, wenn sie nicht in eine Zeile passen; die Leiste waechst mit, +-- der Inhaltsbereich haengt an ihrem unteren Rand und rutscht mit. +local function LayoutTabs() + table.sort(tabs, function(a, b) + if a.order ~= b.order then + return a.order < b.order + end + return a.seq < b.seq + end) + local barW = frame.TabBar:GetWidth() + if not barW or barW < 100 then + barW = (frame:GetWidth() or 1000) - 2 + end + local limit = barW - 12 + local x, row = 12, 0 + local rowOf = {} + for _, tab in ipairs(tabs) do + if tab.button then + local w = tab.button:GetWidth() + if x + w > limit and x > 12 then + row = row + 1 + x = 12 + end + rowOf[tab] = { row = row, x = x } + x = x + w + 4 + end + end + local rows = row + 1 + frame.TabBar:SetHeight(rows * TAB_ROW_H + 2) + for _, tab in ipairs(tabs) do + local at = rowOf[tab] + if at then + tab.button:ClearAllPoints() + tab.button:SetPoint("BOTTOMLEFT", frame.TabBar, "BOTTOMLEFT", at.x, 1 + (rows - 1 - at.row) * TAB_ROW_H) + end + end +end + +local function CreateTabButton(tab) + local button = UI.CreateTabButton(frame.TabBar, ResolveLabel(tab.def)) + button:SetScript("OnClick", function() + UI:SelectTab(tab.id) + end) + tab.button = button +end + +function UI:RegisterTab(id, def) + if tabsById[id] then + return tabsById[id] + end + local tab = { id = id, def = def, order = def.order or 100, seq = #tabs + 1 } + tabsById[id] = tab + tabs[#tabs + 1] = tab + if built then + CreateTabButton(tab) + LayoutTabs() + end + return tab +end + +function UI:SelectTab(id) + local tab = tabsById[id] + if not tab then + return + end + + for _, other in ipairs(tabs) do + if other.frame then + other.frame:Hide() + end + if other.button then + other.button:SetActive(other == tab) + end + end + + if not tab.frame and tab.def.build then + tab.frame = tab.def.build(frame.Content) + if tab.frame then + tab.frame:SetAllPoints(frame.Content) + end + end + if tab.frame then + tab.frame:Show() + if tab.def.onShow then + FW.SafeCall(tab.def.onShow, tab.frame) + end + end + activeId = id +end + +local function BuildOnce() + if built then + return + end + built = true + for _, tab in ipairs(tabs) do + CreateTabButton(tab) + end + LayoutTabs() + frame:HookScript("OnSizeChanged", function() + LayoutTabs() + end) +end + +frame:SetScript("OnShow", function() + BuildOnce() + local _, _, _, toc = GetBuildInfo() + local left = FW.L.TITLE .. " | " .. string.format(FW.L.SLASH_VERSION, FW.version, FW.API_VERSION, tostring(toc or "?")) + frame.Footer.Left:SetText(left) + UI:SelectTab(activeId or (tabs[1] and tabs[1].id)) +end) + +------------------------------------------------------------------------ +-- Oeffnen und Schliessen +------------------------------------------------------------------------ + +function UI:Show(tabId) + if tabId then + activeId = tabId + end + if frame:IsShown() then + if tabId then + self:SelectTab(tabId) + end + else + frame:Show() -- OnShow waehlt den Tab + end +end + +function UI:Hide() + frame:Hide() +end + +function UI:Toggle(tabId) + if frame:IsShown() then + frame:Hide() + else + self:Show(tabId) + end +end diff --git a/UI/Minimap.lua b/UI/Minimap.lua new file mode 100644 index 0000000..196161a --- /dev/null +++ b/UI/Minimap.lua @@ -0,0 +1,100 @@ +-- Freewood-OctoWoW-Core: Minimap-Knopf +-- Links: Fenster, Rechts: Einstellungen, Ziehen: um die Minimap verschieben. +-- Annahme: runde Minimap. + +local ADDON, FW = ... + +local UI = FW.UI +local Minimap_ = _G.Minimap + +local M = {} +FW.Minimap = M + +local button = CreateFrame("Button", "FreewoodMinimapButton", Minimap_) +button:SetSize(32, 32) +button:SetFrameStrata("MEDIUM") +button:SetFrameLevel(8) +button:RegisterForClicks("LeftButtonUp", "RightButtonUp") +button:RegisterForDrag("LeftButton") + +local overlay = button:CreateTexture(nil, "OVERLAY") +overlay:SetSize(53, 53) +overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder") +overlay:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0) + +local background = button:CreateTexture(nil, "BACKGROUND") +background:SetSize(20, 20) +background:SetTexture("Interface\\Minimap\\UI-Minimap-Background") +background:SetPoint("TOPLEFT", button, "TOPLEFT", 7, -5) + +local icon = button:CreateTexture(nil, "ARTWORK") +icon:SetSize(17, 17) +icon:SetTexture("Interface\\AddOns\\Freewood-OctoWoW-Core\\Media\\FreeWood_Logo.tga") +icon:SetPoint("TOPLEFT", button, "TOPLEFT", 7, -6) + +button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight") + +local atan2 = math.atan2 or math.atan + +local function UpdatePosition() + local angle = math.rad((FW.settings and FW.settings.minimap.angle) or 215) + local radius = (Minimap_:GetWidth() / 2) + 5 + button:ClearAllPoints() + button:SetPoint("CENTER", Minimap_, "CENTER", math.cos(angle) * radius, math.sin(angle) * radius) +end + +button:SetScript("OnDragStart", function(self) + self:SetScript("OnUpdate", function() + local mx, my = Minimap_:GetCenter() + local px, py = GetCursorPosition() + local scale = Minimap_:GetEffectiveScale() + px, py = px / scale, py / scale + FW.settings.minimap.angle = math.deg(atan2(py - my, px - mx)) % 360 + UpdatePosition() + end) +end) + +button:SetScript("OnDragStop", function(self) + self:SetScript("OnUpdate", nil) +end) + +button:SetScript("OnClick", function(_, mouseButton) + if mouseButton == "RightButton" then + UI:Show("settings") + else + UI:Toggle() + end +end) + +button:SetScript("OnEnter", function(self) + local L = FW.L + GameTooltip:SetOwner(self, "ANCHOR_LEFT") + GameTooltip:SetText("Freewood", 0.94, 0.75, 0.36) + GameTooltip:AddLine(L.MINIMAP_TIP_LEFT, 0.8, 0.8, 0.8) + GameTooltip:AddLine(L.MINIMAP_TIP_RIGHT, 0.8, 0.8, 0.8) + GameTooltip:AddLine(L.MINIMAP_TIP_DRAG, 0.6, 0.6, 0.6) + GameTooltip:Show() +end) +button:SetScript("OnLeave", function() + GameTooltip:Hide() +end) + +function M:SetHidden(hidden) + FW.settings.minimap.hide = hidden and true or false + if hidden then + button:Hide() + else + button:Show() + end +end + +M.button = button + +FW:On("FW_DB_READY", function() + UpdatePosition() + if FW.settings.minimap.hide then + button:Hide() + else + button:Show() + end +end) diff --git a/UI/Tabs.lua b/UI/Tabs.lua new file mode 100644 index 0000000..7d733ba --- /dev/null +++ b/UI/Tabs.lua @@ -0,0 +1,541 @@ +-- 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, +})