From 1f142340809ef04e5c10bcc6dec802d0923edb8b Mon Sep 17 00:00:00 2001 From: Vollmer Date: Wed, 4 Sep 2024 21:17:35 +0200 Subject: [PATCH] [Feature] Global colors (#50) * add Default.Colors * implement generic for creating ImportExport frames * update annotations * create DB getter/setter for colors * init CUF_DB.colors * update defaults * add colorTab * Fire UpdateWidget callback when changing colors * use global colors for Cast Bar * a bit of cleanup * add util table functions for importing * use new util fns * a little cleanup * update colorTab section structure to be more generic * make GetTextures generic * add support for texture dropdowns * Fire UpdateWidget event when updating texture * use global texture for Cast Bar * remove old empower color options * remove old color options for cast bar * update db structure for cast bar * update sizing logic * add reaction colors * reshuffle * add scroll frame * fire UpdateAppearance --- Annotations.lua | 4 +- Cell_UnitFrames.toc | 2 + Data/DBUtil.lua | 5 + Data/Database.lua | 14 ++ Data/Defaults.lua | 82 +++++++--- Locales/enUS.lua | 24 ++- Menu/Builder.lua | 93 ++--------- Menu/ColorTab.lua | 322 +++++++++++++++++++++++++++++++++++++++ Menu/ImportExport.lua | 178 ++++++++++++++++++++++ Util/Utils.lua | 65 +++++++- WidgetAnnotations.lua | 16 -- Widgets/Bars/CastBar.lua | 33 ++-- 12 files changed, 696 insertions(+), 142 deletions(-) create mode 100644 Menu/ColorTab.lua create mode 100644 Menu/ImportExport.lua diff --git a/Annotations.lua b/Annotations.lua index 9423f4c..616e2af 100644 --- a/Annotations.lua +++ b/Annotations.lua @@ -49,7 +49,9 @@ ---@field ClearItems function ---@class CellColorPicker: Frame, BackdropTemplate ----@field SetColor function +---@field SetColor fun(self: CellColorPicker, r: number|table, g: number?, b: number?, a: number?) +---@field label FontString +---@field onChange fun(r: number, g: number, b: number, a: number) ---@class CellSlider: Slider ---@field afterValueChangedFn function diff --git a/Cell_UnitFrames.toc b/Cell_UnitFrames.toc index 908d56f..650df00 100644 --- a/Cell_UnitFrames.toc +++ b/Cell_UnitFrames.toc @@ -35,8 +35,10 @@ Core/SlashCommands.lua Menu/Builder.lua Menu/AuraFilterList.lua Menu/Menu.lua +Menu/ImportExport.lua Menu/GeneralTab.lua Menu/UnitFramesTab.lua +Menu/ColorTab.lua Widgets/Common.lua diff --git a/Data/DBUtil.lua b/Data/DBUtil.lua index 9e3906a..63330c2 100644 --- a/Data/DBUtil.lua +++ b/Data/DBUtil.lua @@ -6,6 +6,7 @@ local DB = CUF.DB local Util = CUF.Util local L = CUF.L +local Defaults = CUF.Defaults -- Props that should only be initialized once -- eg we only want to initialize filters, not keep adding to them @@ -26,6 +27,10 @@ function DB.InitDB() ---@field manual CUF.database.backup CUF_DB.backups = CUF_DB.backups or {} + ---@type Defaults.Colors + CUF_DB.colors = CUF_DB.colors or Util:CopyDeep(Defaults.Colors) + Util:AddMissingProps(CUF_DB.colors, Defaults.Colors) + DB.CreateAutomaticBackup() end diff --git a/Data/Database.lua b/Data/Database.lua index 9a5ba32..9e1da1c 100644 --- a/Data/Database.lua +++ b/Data/Database.lua @@ -101,6 +101,12 @@ function DB.GetMasterLayout(rawValue) return CUF_DB.masterLayout end +--- Returns the colors table from DB +---@return Defaults.Colors +function DB.GetColors() + return CUF_DB.colors +end + ----------------------------------------- -- MARK: General Setters ----------------------------------------- @@ -109,3 +115,11 @@ end function DB.SetMasterLayout(layout) CUF_DB.masterLayout = layout end + +--- Sets the color of a specific color type +---@param which Defaults.Colors.Types +---@param colorName string +---@param val RGBAOpt|string +function DB.SetColor(which, colorName, val) + DB.GetColors()[which][colorName] = val +end diff --git a/Data/Defaults.lua b/Data/Defaults.lua index ed07856..ff6ed72 100644 --- a/Data/Defaults.lua +++ b/Data/Defaults.lua @@ -1,6 +1,10 @@ ---@class CUF local CUF = select(2, ...) +------------------------------------------------- +-- MARK: Defaults +------------------------------------------------- + ---@class CUF.defaults local Defaults = CUF.Defaults Defaults.Options = {} @@ -101,6 +105,58 @@ Defaults.Options.fontWidth = { auxValue = 3, } +---@alias Defaults.Colors.Types +---| "castBar" +---| "reaction" + +---@class Defaults.Colors +Defaults.Colors = { + castBar = { + texture = "Interface\\Buttons\\WHITE8X8", + interruptible = { 0.2, 0.57, 0.5, 1 }, + nonInterruptible = { 0.43, 0.43, 0.43, 1 }, + background = { 0, 0, 0, 0.8 }, + stageZero = { 0.2, 0.57, 0.5, 1 }, + stageOne = { 0.3, 0.47, 0.45, 1 }, + stageTwo = { 0.4, 0.4, 0.4, 1 }, + stageThree = { 0.54, 0.3, 0.3, 1 }, + stageFour = { 0.65, 0.2, 0.3, 1 }, + fullyCharged = { 0.77, 0.1, 0.2, 1 }, + }, + reaction = { + friendly = { 0.29, 0.69, 0.3, 1 }, + hostile = { 0.78, 0.25, 0.25, 1 }, + neutral = { 0.85, 0.77, 0.36, 1 }, + pet = { 0.29, 0.69, 0.3, 1 }, + }, +} + +Defaults.ColorsMenuOrder = { + castBar = { + { "texture", "texture" }, + { "background", "rgb" }, + { "interruptible", "rgb" }, + { "nonInterruptible", "rgb" }, + { "Empowers", "seperator" }, + { "stageZero", "rgb" }, + { "stageOne", "rgb" }, + { "stageTwo", "rgb" }, + { "stageThree", "rgb" }, + { "stageFour", "rgb" }, + { "fullyCharged", "rgb" } + }, + reaction = { + { "friendly", "rgb" }, + { "hostile", "rgb" }, + { "neutral", "rgb" }, + { "pet", "rgb" }, + }, +} + +------------------------------------------------- +-- MARK: Widgets (Text) +------------------------------------------------- + ---@class WidgetTables Defaults.Widgets = { ---@type NameTextWidgetTable @@ -166,7 +222,7 @@ Defaults.Widgets = { offsetX = 0, relativePoint = "CENTER", }, - }, + }, -- MARK: Widgets (Auras) ---@type AuraWidgetTable buffs = { enabled = false, @@ -252,7 +308,7 @@ Defaults.Widgets = { offsetX = 0, relativePoint = "TOPRIGHT", }, - }, + }, -- MARK: Widgets (Icons) ---@type RaidIconWidgetTable raidIcon = { enabled = false, @@ -342,7 +398,7 @@ Defaults.Widgets = { offsetX = -15, relativePoint = "CENTER", }, - }, + }, -- MARK: Widgets (Bars) ---@type ShieldBarWidgetTable shieldBar = { enabled = false, @@ -358,6 +414,7 @@ Defaults.Widgets = { ---@type CastBarWidgetTable castBar = { enabled = false, + useClassColor = true, frameLevel = 10, position = { point = "TOPLEFT", @@ -369,13 +426,6 @@ Defaults.Widgets = { width = 200, height = 30, }, - color = { - texture = "Interface\\AddOns\\Cell\\Media\\statusbar.tga", - useClassColor = true, - interruptible = { 0.2, 0.57, 0.5, 1 }, - nonInterruptible = { 0.43, 0.43, 0.43, 1 }, - background = { 0, 0, 0, 0.8 }, - }, reverse = false, timer = { enabled = true, @@ -410,14 +460,6 @@ Defaults.Widgets = { empower = { useFullyCharged = true, showEmpowerName = false, - pipColors = { - stageZero = { 0.2, 0.57, 0.5, 1 }, - stageOne = { 0.3, 0.47, 0.45, 1 }, - stageTwo = { 0.4, 0.4, 0.4, 1 }, - stageThree = { 0.54, 0.3, 0.3, 1 }, - stageFour = { 0.65, 0.2, 0.3, 1 }, - fullyCharged = { 0.77, 0.1, 0.2, 1 }, - } }, border = { showBorder = true, @@ -433,6 +475,10 @@ Defaults.Widgets = { } } +------------------------------------------------- +-- MARK: Units +------------------------------------------------- + ---@class Size ---@field [1] number ---@field [2] number diff --git a/Locales/enUS.lua b/Locales/enUS.lua index e4b0bf5..a39b5a6 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -8,6 +8,7 @@ CUF.L = L -- Tabs L.unitFramesTab = "Unit Frames" L.generalTab = "General" +L.colorTab = "Colors" L.MasterLayout = "Master Layout" L.CUFLayoutMasterNone = "|cffffb5c5None|r" @@ -89,7 +90,6 @@ L["duration"] = "Duration" L["duration-and-max"] = "Duration & Max" L.ShowSpell = "Show Spell" L.Empower = "Empower" -L.Stage = "Stage" L.FullyCharged = "Fully Charged" L.UseFullyCharged = "Use Fully Charged" L.ShowEmpowerName = "Show Empower Name" @@ -146,6 +146,7 @@ L.Backup_automatic = "Automatic Backup" L.HideDefaultCastBar = "Hide Default Cast Bar" L.HideDefaultCastBarTooltip = [[Hides the default cast bar. Reload to show it again after disabling this option.]] +L.texture = "Texture" -- Custom Formats L.ValidTags = "Valid Tags" @@ -177,3 +178,24 @@ L["def"] = "Displays the deficit." L["def:short"] = "Displays the deficit as a shortvalue." L["def:per"] = "Displays the deficit as a percentage." L["def:per-short"] = "Displays the deficit as a percentage without decimals." + +-- Colors +L.stageZero = "Stage 0" +L.stageOne = "Stage 1" +L.stageTwo = "Stage 2" +L.stageThree = "Stage 3" +L.stageFour = "Stage 4" +L.fullyCharged = "Fully Charged" +L.background = "Background" +L.interruptible = "Interruptible" +L.nonInterruptible = "Non-Interruptible" +L.holyPower = "Holy Power" +L.arcaneCharges = "Arcane Charges" +L.soulShards = "Soul Shards" + +L.reaction = "Reaction" +L.friendly = "Friendly" +L.hostile = "Hostile" +L.neutral = "Neutral" + +L.ImportExportColors = "Import & Export Color Settings" diff --git a/Menu/Builder.lua b/Menu/Builder.lua index 2b87932..ce2c500 100644 --- a/Menu/Builder.lua +++ b/Menu/Builder.lua @@ -43,7 +43,6 @@ Builder.MenuOptions = { FullAnchor = 19, ColorPicker = 20, CastBarGeneral = 21, - CastBarColor = 22, CastBarTimer = 23, CastBarSpell = 24, CastBarSpark = 25, @@ -1047,19 +1046,15 @@ end local LSM = LibStub("LibSharedMedia-3.0", true) local textures -local textureToName = {} +Builder.textureToName = {} ----@class TextureDropdownItem ----@field [1] string ----@field [2] Texture - ----@return TextureDropdownItem[] -local function GetTextures() +---@return table +function Builder:GetTextures() if textures then return textures end textures = F:Copy(LSM:HashTable("statusbar")) for name, texture in pairs(textures) do - textureToName[texture] = name + Builder.textureToName[texture] = name end return textures @@ -1081,7 +1076,7 @@ function Builder:CreateTextureDropdown(parent, widgetName, path) textureDropdown.Get_DB = HandleWidgetOption local textureDropdownItems = {} - for name, tex in pairs(GetTextures()) do + for name, tex in pairs(self:GetTextures()) do tinsert(textureDropdownItems, { ["text"] = name, ["texture"] = tex, @@ -1094,7 +1089,7 @@ function Builder:CreateTextureDropdown(parent, widgetName, path) local function LoadPageDB() local tex = textureDropdown.Get_DB(widgetName, path) - textureDropdown:SetSelected(textureToName[tex], tex) + textureDropdown:SetSelected(Builder.textureToName[tex], tex) end Handler:RegisterOption(LoadPageDB, widgetName, "TextureDropdown_" .. path) @@ -1356,42 +1351,8 @@ function Builder:CreateCastBarGeneralOptions(parent, widgetName) f.reverseCB = self:CreateCheckBox(f, widgetName, L.Reverse, const.OPTION_KIND.REVERSE) self:AnchorBelow(f.reverseCB, f.anchorOptions.relativeDropdown) - return f -end - ----@param parent Frame ----@param widgetName WIDGET_KIND ----@return CastBarColorOptions -function Builder:CreateCastBarColorOptions(parent, widgetName) - ---@class CastBarColorOptions: OptionsFrame - local f = CUF:CreateFrame(nil, parent, 1, 1, true, true) - f.id = "CastBarColorOptions" - f.optionHeight = 110 - - -- Title - f.title = self:CreateOptionTitle(f, "Colors") - local path = const.OPTION_KIND.COLOR .. "." - - -- First Row - f.texture = self:CreateTextureDropdown(f, widgetName, path .. const.OPTION_KIND.TEXTURE) - self:AnchorBelow(f.texture, f.title) - - f.classColorCB = self:CreateCheckBox(f, widgetName, L.UseClassColor, - path .. const.OPTION_KIND.USE_CLASS_COLOR) - self:AnchorRight(f.classColorCB, f.texture) - - -- Second Row - f.interruptible = self:CreateColorPickerOptions(f, widgetName, L.Interruptible, - path .. const.OPTION_KIND.INTERRUPTIBLE) - self:AnchorBelow(f.interruptible, f.texture) - - f.nonInterruptible = self:CreateColorPickerOptions(f, widgetName, L.NonInterruptible, - path .. const.OPTION_KIND.NON_INTERRUPTIBLE) - self:AnchorRightOfColorPicker(f.nonInterruptible, f.interruptible) - - f.background = self:CreateColorPickerOptions(f, widgetName, L.Background, - path .. const.OPTION_KIND.BACKGROUND) - self:AnchorRightOfColorPicker(f.background, f.nonInterruptible) + f.classColorCB = self:CreateCheckBox(f, widgetName, L.UseClassColor, const.OPTION_KIND.USE_CLASS_COLOR) + self:AnchorRightOfCB(f.classColorCB, f.reverseCB) return f end @@ -1469,7 +1430,7 @@ function Builder:CreateCastBarEmpowerOptions(parent, widgetName) ---@class CastBarEmpowerOptions: OptionsFrame local f = CUF:CreateFrame(nil, parent, 1, 1, true, true) f.id = "CastBarEmpowerOptions" - f.optionHeight = 170 + f.optionHeight = 60 -- Title f.title = self:CreateOptionTitle(f, "Empower") @@ -1486,41 +1447,6 @@ function Builder:CreateCastBarEmpowerOptions(parent, widgetName) f.showEmpowerName:SetPoint("TOPLEFT", f.useFullyCharged, "TOPLEFT", (self.spacingX * 1.5) + f.useFullyCharged.label:GetWidth(), 0) - -- Second Row - local colorPath = const.OPTION_KIND.EMPOWER .. "." .. const.OPTION_KIND.PIP_COLORS .. "." - - f.stageZero = self:CreateColorPickerOptions(f, widgetName, - L.Stage .. " " .. 0 .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.STAGE_ZERO) - self:AnchorBelow(f.stageZero, f.useFullyCharged) - - f.stageOne = self:CreateColorPickerOptions(f, widgetName, - L.Stage .. " " .. 1 .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.STAGE_ONE) - self:AnchorRightOfColorPicker(f.stageOne, f.stageZero) - - f.stageTwo = self:CreateColorPickerOptions(f, widgetName, - L.Stage .. " " .. 2 .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.STAGE_TWO) - self:AnchorRightOfColorPicker(f.stageTwo, f.stageOne) - - -- Third Row - f.stageThree = self:CreateColorPickerOptions(f, widgetName, - L.Stage .. " " .. 3 .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.STAGE_THREE) - self:AnchorBelowCB(f.stageThree, f.stageZero) - - f.stageFour = self:CreateColorPickerOptions(f, widgetName, - L.Stage .. " " .. 4 .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.STAGE_FOUR) - self:AnchorRightOfColorPicker(f.stageFour, f.stageThree) - - -- Fourth Row - f.fullyCharged = self:CreateColorPickerOptions(f, widgetName, - L.FullyCharged .. " " .. L["Color"], - colorPath .. const.OPTION_KIND.FULLY_CHARGED) - self:AnchorBelowCB(f.fullyCharged, f.stageThree) - return f end @@ -1615,7 +1541,6 @@ Builder.MenuFuncs = { [Builder.MenuOptions.FrameLevel] = Builder.CreateFrameLevelOptions, [Builder.MenuOptions.ColorPicker] = Builder.CreateColorPickerOptions, [Builder.MenuOptions.CastBarGeneral] = Builder.CreateCastBarGeneralOptions, - [Builder.MenuOptions.CastBarColor] = Builder.CreateCastBarColorOptions, [Builder.MenuOptions.CastBarTimer] = Builder.CreateCastBarTimerFontOptions, [Builder.MenuOptions.CastBarSpell] = Builder.CreateCastBarSpellFontOptions, [Builder.MenuOptions.CastBarSpark] = Builder.CreateCastBarSparkOptions, diff --git a/Menu/ColorTab.lua b/Menu/ColorTab.lua new file mode 100644 index 0000000..8305315 --- /dev/null +++ b/Menu/ColorTab.lua @@ -0,0 +1,322 @@ +---@class CUF +local CUF = select(2, ...) + +local Cell = CUF.Cell + +local L = CUF.L +local DB = CUF.DB +local Menu = CUF.Menu +local Util = CUF.Util +local const = CUF.constants +local Builder = CUF.Builder + +---@class ColorTab: Menu.Tab +local ColorTab = {} +ColorTab.id = "colorTab" +ColorTab.paneHeight = 17 +ColorTab.sectionsHeight = 0 + +------------------------------------------------- +-- MARK: Import / Export +------------------------------------------------- + +function ColorTab:CreateImportExport() + local section = CUF:CreateFrame(nil, self.window, self.window:GetWidth(), 45, true, true) + section:SetPoint("TOPLEFT") + self.importExportSection = section + + self.importExportFrame = CUF.ImportExport:CreateImportExportFrame("Colors", + function(imported) + Util:SafeImport(imported, CUF_DB.colors) + + self:UpdateColors() + CUF:Fire("UpdateUnitButtons") + CUF:Fire("UpdateWidget", DB.GetMasterLayout()) + end, + DB.GetColors, + function(imported) + -- We allow missing keys here since we might be importing old versions + -- And it's not a big deal if any new ones are missing + return Util:IsValidCopy(imported, CUF.Defaults.Colors, true) + end) + + local pane = Cell:CreateTitledPane(section, L.ImportExportColors, section:GetWidth(), self.paneHeight) + pane:SetPoint("TOPLEFT") + + local buttonWidth = (section:GetWidth() / 2) - 5 + local importButton = CUF:CreateButton(section, L["Import"], { buttonWidth, 20 }, function() + self.importExportFrame:ShowImport() + end) + importButton:SetPoint("TOPLEFT", pane, "BOTTOMLEFT", 0, -5) + + local exportButton = CUF:CreateButton(section, L["Export"], { buttonWidth, 20 }, function() + self.importExportFrame:ShowExport() + end) + exportButton:SetPoint("TOPRIGHT", pane, "BOTTOMRIGHT", 0, -5) +end + +------------------------------------------------- +-- MARK: Elements +------------------------------------------------- + +--- Create a color picker +---@param which Defaults.Colors.Types +---@param colorName string +---@param colorTable table +---@param parent Frame +---@return CUF.ColorSection.ColorPicker, number +local function CreateColorPicker(which, colorName, colorTable, parent) + ---@class CUF.ColorSection.ColorPicker: CellColorPicker + local cp = Cell:CreateColorPicker(parent, L[colorName], true) + cp.id = colorName + cp:SetColor(colorTable[colorName]) + cp.onChange = function(r, g, b, a) + DB.SetColor(which, colorName, { r, g, b, a }) + if which == "castBar" then + CUF:Fire("UpdateWidget", DB.GetMasterLayout(), nil, which, const.OPTION_KIND.COLOR) + else + CUF:Fire("UpdateAppearance", "color") + end + end + + local cpWidth = math.max(cp:GetWidth() + cp.label:GetWidth() + 5, (ColorTab.window:GetWidth() / 3) - 15) + + return cp, cpWidth +end + +--- Create a sperator title +---@param title string +---@param parent Frame +---@return Frame +local function CreateSperatorTitle(title, parent) + local sectionTitle = CUF:CreateFrame(nil, parent, 1, 1, true, true) --[[@as OptionTitle]] + sectionTitle:SetPoint("TOPLEFT") + sectionTitle.title = sectionTitle:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS_TITLE") + sectionTitle.title:SetText(L[title]) + sectionTitle.title:SetScale(1) + sectionTitle.title:SetPoint("TOPLEFT") + + sectionTitle:SetHeight(sectionTitle.title:GetStringHeight()) + return sectionTitle +end + +---- Create a texture dropdown +---@param which Defaults.Colors.Types +---@param colorName string +---@param colorTable table +---@param parent Frame +---@return Frame +local function CreateTextureDropdown(which, colorName, colorTable, parent) + ---@class CUF.ColorSection.Dropdown: CellDropdown + local textureDropdown = Cell:CreateDropdown(parent, 160, "texture") + textureDropdown:SetLabel(L[colorName]) + textureDropdown.id = colorName + + local textureDropdownItems = {} + for name, tex in pairs(Builder:GetTextures()) do + tinsert(textureDropdownItems, { + ["text"] = name, + ["texture"] = tex, + ["onClick"] = function() + DB.SetColor(which, colorName, tex) + CUF:Fire("UpdateWidget", DB.GetMasterLayout(), nil, which, const.OPTION_KIND.COLOR) + end, + }) + end + textureDropdown:SetItems(textureDropdownItems) + textureDropdown:SetSelected(Builder.textureToName[colorTable[colorName]], colorTable[colorName]) + + return textureDropdown +end + +------------------------------------------------- +-- MARK: Sections +------------------------------------------------- + +--- Update all color pickers with the current color table +--- +--- This is called when a new color table is imported +function ColorTab:UpdateColors() + for _, section in pairs(self.colorSections) do + local colorTable = DB.GetColors()[section.id] + for _, cp in pairs(section.cps) do + cp:SetColor(colorTable[cp.id]) + end + for _, dropdown in pairs(section.dropdowns) do + dropdown:SetSelected(Builder.textureToName[colorTable[dropdown.id]], colorTable[dropdown.id]) + end + end +end + +--- Create sections with color pickers for each color type +function ColorTab:CreateSections() + local cpGap = (self.window:GetWidth() / 3) * 0.80 + local sectionGap = 10 + + ---@class ColorTab.colorSection: Frame + ---@field scrollFrame CellScrollFrame + local colorSection = CUF:CreateFrame("CUF_Menu_ColorSection", self.window, + self.window:GetWidth(), + self.window:GetHeight() - self.importExportSection:GetHeight() - (sectionGap * 2), true, true) + colorSection:SetPoint("TOPLEFT", self.importExportSection, "BOTTOMLEFT", 0, -sectionGap) + + Cell:CreateScrollFrame(colorSection) + colorSection.scrollFrame:SetScrollStep(50) + + self.colorSections = {} ---@type CUF.ColorSection[] + + local prevSection + local colorTables = DB.GetColors() + local colorOrder = CUF.Defaults.ColorsMenuOrder + + for which, order in pairs(colorOrder) do + ---@class CUF.ColorSection: Frame + local section = CUF:CreateFrame(colorSection:GetName() .. "_" .. Util:ToTitleCase(which), + colorSection.scrollFrame.content, self.window:GetWidth() - 10, 1, false, true) + section.id = which + section.cps = {} ---@type CUF.ColorSection.ColorPicker[] + section.dropdowns = {} ---@type CUF.ColorSection.Dropdown[] + + local sectionTitle = CUF:CreateFrame(nil, section, 1, 1, true, true) --[[@as OptionTitle]] + sectionTitle:SetPoint("TOPLEFT", sectionGap, -sectionGap) + sectionTitle.title = sectionTitle:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS_TITLE") + sectionTitle.title:SetText(L[which]) + sectionTitle.title:SetScale(1.2) + sectionTitle.title:SetPoint("TOPLEFT") + sectionTitle:SetHeight(sectionTitle.title:GetStringHeight()) + + local gridLayout = { + maxColumns = 3, + currentRow = 1, + currentColumn = 1, + currentColumnWidth = sectionGap * 2, + firstInRow = nil + } + local baseHeight = 35 + + ---@type table + local colorTable = colorTables[which] + for _, info in ipairs(order) do + local colorName, colorType = info[1], info[2] + local element, elementWidth + + if colorType == "seperator" then + element = CreateSperatorTitle(colorName, section) + gridLayout.currentColumn = 1 + gridLayout.currentColumnWidth = section:GetWidth() + gridLayout.currentRow = gridLayout.currentRow + 1 + + element:SetPoint("TOPLEFT", gridLayout.firstInRow, "BOTTOMLEFT", 0, -sectionGap) + gridLayout.firstInRow = element + baseHeight = baseHeight + element:GetHeight() + sectionGap + elseif colorType == "texture" then + element = CreateTextureDropdown(which, colorName, colorTable, section) + gridLayout.currentColumn = 1 + gridLayout.currentColumnWidth = section:GetWidth() + + if gridLayout.currentRow > 1 then + gridLayout.currentRow = gridLayout.currentRow + 1 + end + + -- Start of a new row + if not gridLayout.firstInRow then + element:SetPoint("TOPLEFT", sectionTitle, "BOTTOMLEFT", 0, -sectionGap * 2.5) + else + element:SetPoint("TOPLEFT", gridLayout.firstInRow, "BOTTOMLEFT", 0, -sectionGap * 2.5) + end + gridLayout.firstInRow = element + + baseHeight = baseHeight + element:GetHeight() + sectionGap * 2.5 + + tinsert(section.dropdowns, element) + elseif colorType == "rgb" then + element, elementWidth = CreateColorPicker(which, colorName, colorTable, section) + + -- Move to the next column, or wrap to the next row if necessary + if gridLayout.currentColumn > gridLayout.maxColumns + or (gridLayout.currentColumnWidth + elementWidth) > (section:GetWidth()) then + gridLayout.currentColumn = 1 + gridLayout.currentRow = gridLayout.currentRow + 1 + gridLayout.currentColumnWidth = sectionGap * 2 + end + + gridLayout.currentColumnWidth = gridLayout.currentColumnWidth + elementWidth + tinsert(section.cps, element) + + -- Position the element in the grid + if gridLayout.currentColumn == 1 then + -- Start of a new row + if not gridLayout.firstInRow then + element:SetPoint("TOPLEFT", sectionTitle, "BOTTOMLEFT", 0, -sectionGap) + else + element:SetPoint("TOPLEFT", gridLayout.firstInRow, "BOTTOMLEFT", 0, -sectionGap) + end + gridLayout.firstInRow = element + baseHeight = baseHeight + element:GetHeight() + sectionGap + else + -- Position to the right of the previous element + element:SetPoint("TOPLEFT", section.cps[#section.cps - 1], "TOPRIGHT", cpGap, 0) + end + end + + gridLayout.currentColumn = gridLayout.currentColumn + 1 + end + + section:SetHeight(baseHeight) + self.sectionsHeight = self.sectionsHeight + baseHeight + sectionGap + + if not prevSection then + section:SetPoint("TOPLEFT", colorSection.scrollFrame.content) + else + section:SetPoint("TOPLEFT", prevSection, "BOTTOMLEFT", 0, -sectionGap) + end + + prevSection = section + tinsert(self.colorSections, section) + end + + self.colorSection = colorSection +end + +------------------------------------------------- +-- MARK: Show/Hide +------------------------------------------------- + +function ColorTab:ShowTab() + if not self.window then + self:Create() + self.init = true + end + + self.window:Show() + + self.colorSection.scrollFrame:SetContentHeight(self.sectionsHeight) + self.colorSection.scrollFrame:ResetScroll() +end + +function ColorTab:HideTab() + if not self.window or not self.window:IsShown() then return end + self.window:Hide() +end + +function ColorTab:IsShown() + return ColorTab.window and ColorTab.window:IsShown() +end + +------------------------------------------------- +-- MARK: Create +------------------------------------------------- + +function ColorTab:Create() + local sectionWidth = Menu.tabAnchor:GetWidth() + + self.window = CUF:CreateFrame("CUF_Menu_Color", Menu.window, + sectionWidth, + 335, true) + self.window:SetPoint("TOPLEFT", Menu.tabAnchor, "TOPLEFT") + + self:CreateImportExport() + self:CreateSections() +end + +Menu:AddTab(ColorTab) diff --git a/Menu/ImportExport.lua b/Menu/ImportExport.lua new file mode 100644 index 0000000..6bd273d --- /dev/null +++ b/Menu/ImportExport.lua @@ -0,0 +1,178 @@ +---@class CUF +local CUF = select(2, ...) + +local L = CUF.L +local F = Cell.funcs +local P = Cell.pixelPerfectFuncs +local Menu = CUF.Menu + +local Serializer = LibStub:GetLibrary("LibSerialize") +local LibDeflate = LibStub:GetLibrary("LibDeflate") +local deflateConfig = { level = 9 } + +---@class CUF.ImportExport +local ImportExport = {} + +CUF.ImportExport = ImportExport + +------------------------------------------------- +-- MARK: Show +------------------------------------------------- + +---@param self CUF.ImportExport.Frame +local function ShowImportFrame(self) + self:Show() + self.isImport = true + self.importBtn:Show() + self.importBtn:SetEnabled(false) + + self.exported = "" + self.title:SetText(L["Import"]) + self.textArea:SetText("") + self.textArea.eb:SetFocus(true) +end + +---@param self CUF.ImportExport.Frame +local function ShowExportFrame(self) + self:Show() + self.isImport = false + self.importBtn:Hide() + + self.title:SetText(L["Export"] .. ": " .. L[self.which]) + + local prefix = "!CUF:" .. CUF.version .. ":" .. string.upper(self.which) .. "!" + + self.exported = Serializer:Serialize(self.exportFn()) -- serialize + self.exported = LibDeflate:CompressDeflate(self.exported, deflateConfig) -- compress + self.exported = LibDeflate:EncodeForPrint(self.exported) -- encode + self.exported = prefix .. self.exported + + self.textArea:SetText(self.exported) + self.textArea.eb:SetFocus(true) +end + +------------------------------------------------- +-- MARK: Create +------------------------------------------------- + +---@param which string +---@param importFn fun(imported: any) +---@param exportFn fun(): table +---@param verifyFn fun(imported: any): boolean +---@param minVersion number? +function ImportExport:CreateImportExportFrame(which, importFn, exportFn, verifyFn, minVersion) + ---@class CUF.ImportExport.Frame: Frame, BackdropTemplate + local importExportFrame = CreateFrame("Frame", "CUF_ImportExport", Menu.window, "BackdropTemplate") + importExportFrame.which = which + importExportFrame.exportFn = exportFn + + importExportFrame:Hide() + Cell:StylizeFrame(importExportFrame, nil, Cell:GetAccentColorTable()) + importExportFrame:EnableMouse(true) + importExportFrame:SetFrameLevel(Menu.window:GetFrameLevel() + 50) + importExportFrame:SetSize(Menu.window:GetWidth() - 5, 170) + importExportFrame:SetPoint("CENTER", 1, 0) + + -- close + local closeBtn = Cell:CreateButton(importExportFrame, "×", "red", { 18, 18 }, false, false, "CELL_FONT_SPECIAL", + "CELL_FONT_SPECIAL") + closeBtn:SetPoint("TOPRIGHT", P:Scale(-5), P:Scale(-1)) + closeBtn:SetScript("OnClick", function() importExportFrame:Hide() end) + + -- import + local importBtn = Cell:CreateButton(importExportFrame, L["Import"], "green", { 57, 18 }) + importBtn:Hide() + importBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", P:Scale(1), 0) + importBtn:SetScript("OnClick", function() + -- lower frame level + importExportFrame:SetFrameLevel(Menu.window:GetFrameLevel() + 20) + + local popup = Cell:CreateConfirmPopup(Menu.window, 200, L["Overwrite "] .. L[which] .. "?", + function() + importFn(importExportFrame.imported) + importExportFrame:Hide() + end, nil, true) + popup:SetPoint("TOPLEFT", importExportFrame, 117, -50) + importExportFrame.textArea.eb:ClearFocus() + end) + importExportFrame.importBtn = importBtn + + -- title + local title = importExportFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_CLASS") + title:SetPoint("TOPLEFT", 5, -5) + importExportFrame.title = title + + -- textArea + local textArea = Cell:CreateScrollEditBox(importExportFrame, function(eb, userChanged) + if userChanged then + if importExportFrame.isImport then + importExportFrame.imported = {} + local text = eb:GetText() + -- check + local version, type, data = string.match(text, + "^!CUF:(%d+):(.+)!(.+)$") + version = tonumber(version) + + if type and type == string.upper(which) and version and data then + if not minVersion or version >= minVersion then + local success + data = LibDeflate:DecodeForPrint(data) -- decode + success, data = pcall(LibDeflate.DecompressDeflate, LibDeflate, data) -- decompress + success, data = Serializer:Deserialize(data) -- deserialize + + if success and data and verifyFn(data) then + title:SetText(L["Import"] .. ": " .. L[which]) + importExportFrame.imported = data + importBtn:SetEnabled(true) + else + title:SetText(L["Import"] .. ": |cffff2222" .. L["Error"]) + importBtn:SetEnabled(false) + end + else -- incompatible version + title:SetText(L["Import"] .. ": |cffff2222" .. L["Incompatible Version"]) + importBtn:SetEnabled(false) + end + else + title:SetText(L["Import"] .. ": |cffff2222" .. L["Error"]) + importBtn:SetEnabled(false) + end + else + eb:SetText(importExportFrame.exported) + eb:SetCursorPosition(0) + eb:HighlightText() + end + end + end) + Cell:StylizeFrame(textArea.scrollFrame, { 0, 0, 0, 0 }, Cell:GetAccentColorTable()) + textArea:SetPoint("TOPLEFT", P:Scale(5), P:Scale(-20)) + textArea:SetPoint("BOTTOMRIGHT", P:Scale(-5), P:Scale(5)) + + -- highlight text + textArea.eb:SetScript("OnEditFocusGained", function() textArea.eb:HighlightText() end) + textArea.eb:SetScript("OnMouseUp", function() + if not importExportFrame.isImport then + textArea.eb:HighlightText() + end + end) + importExportFrame.textArea = textArea + + importExportFrame:SetScript("OnHide", function() + importExportFrame:Hide() + importExportFrame.isImport = false + importExportFrame.exported = "" + importExportFrame.imported = {} + -- hide mask + Menu.window.mask:Hide() + end) + + importExportFrame:SetScript("OnShow", function() + -- raise frame level + importExportFrame:SetFrameLevel(Menu.window:GetFrameLevel() + 50) + Menu.window.mask:Show() + end) + + importExportFrame.ShowImport = ShowImportFrame + importExportFrame.ShowExport = ShowExportFrame + + return importExportFrame +end diff --git a/Util/Utils.lua b/Util/Utils.lua index fa58574..031262d 100644 --- a/Util/Utils.lua +++ b/Util/Utils.lua @@ -3,6 +3,7 @@ local CUF = select(2, ...) local Cell = CUF.Cell local F = Cell.funcs +local DB = CUF.DB ---@class CUF.Util local Util = CUF.Util @@ -91,6 +92,52 @@ function Util:CopyDeep(table, seen) return setmetatable(res, getmetatable(table)) end +--- Check if a table is a valid copy of another table, used for import checks +--- +--- This function will check if a table is a valid copy of another table. +--- It will check if the table has the same structure as the template table. +--- @param table table The table to check +--- @param template table The template table to check against +--- @param allowMissing boolean? Whether to allow missing keys +--- @return boolean +function Util:IsValidCopy(table, template, allowMissing) + if type(table) ~= "table" or type(template) ~= "table" then + return false + end + + for k, v in pairs(template) do + if (not table[k] and not allowMissing) + or (not self:IsPropSameType(table[k], v)) then + return false + end + + if type(v) == "table" then + if not Util:IsValidCopy(table[k], v, allowMissing) then + return false + end + end + end + + -- TODO: Maybe check if table has keys not present in template? + -- right now that is dealt with in SafeImport so for now w/e + + return true +end + +--- Safely perform an import +--- +--- This does not overwrite the current table. +--- It instead iterates over the imported table and copies the valid props to the current table. +---@param imported table +---@param current table +function Util:SafeImport(imported, current) + for k, v in pairs(imported) do + if current[k] and self:IsPropSameType(current[k], v) then + current[k] = self:CopyDeep(v) + end + end +end + ------------------------------------------------- -- MARK: IterateAllUnitButtons ------------------------------------------------- @@ -205,25 +252,25 @@ function Util:GetUnitClassColor(unit, class, guid) -- Friendly if selectionType == 3 then - return unpack(CUF.constants.COLORS.FRIENDLY) + return unpack(DB.GetColors().reaction.friendly) end -- Hostile if selectionType == 0 then - return unpack(CUF.constants.COLORS.HOSTILE) + return unpack(DB.GetColors().reaction.hostile) end -- Pet if selectionType == 4 then if UnitIsEnemy(unit, "player") then - return unpack(CUF.constants.COLORS.HOSTILE) + return unpack(DB.GetColors().reaction.hostile) end - return unpack(CUF.constants.COLORS.FRIENDLY) + return unpack(DB.GetColors().reaction.pet) end -- Neutral - return unpack(CUF.constants.COLORS.NEUTRAL) + return unpack(DB.GetColors().reaction.neutral) end --- Converts a dictionary table to an array eg. @@ -263,6 +310,14 @@ function Util:GetAllLayoutNamesAsString(formatted) return table.concat(layoutNames, ", ") end +--- Check if two values have the same type +---@param a any +---@param b any +---@return boolean +function Util:IsPropSameType(a, b) + return type(a) == type(b) +end + ------------------------------------------------- -- MARK: Frames ------------------------------------------------- diff --git a/WidgetAnnotations.lua b/WidgetAnnotations.lua index 6878c21..9f91e8b 100644 --- a/WidgetAnnotations.lua +++ b/WidgetAnnotations.lua @@ -193,7 +193,6 @@ ---@field frameLevel number ---@field position PositionOpt ---@field size SizeOpt ----@field color CastBarColorsOpt ---@field reverse boolean ---@field timer BigFontOpt ---@field timerFormat CastBarTimerFormat @@ -204,13 +203,7 @@ ---@field empower EmpowerOpt ---@field border BorderOpt ---@field icon CastBarIconOpt - ----@class CastBarColorsOpt ----@field texture string ---@field useClassColor boolean ----@field interruptible RGBAOpt ----@field nonInterruptible RGBAOpt ----@field background RGBAOpt ---@class CastBarSparkOpt ---@field enabled boolean @@ -223,18 +216,9 @@ ---@field zoom number ---@class EmpowerOpt ----@field pipColors EmpowerColorOpt ---@field useFullyCharged boolean ---@field showEmpowerName boolean ----@class EmpowerColorOpt ----@field stageZero RGBAOpt ----@field stageOne RGBAOpt ----@field stageTwo RGBAOpt ----@field stageThree RGBAOpt ----@field stageFour RGBAOpt ----@field fullyCharged RGBAOpt - ------------------------------------------------- -- MARK: Generic Options ------------------------------------------------- diff --git a/Widgets/Bars/CastBar.lua b/Widgets/Bars/CastBar.lua index 5bb90de..525c28d 100644 --- a/Widgets/Bars/CastBar.lua +++ b/Widgets/Bars/CastBar.lua @@ -20,7 +20,6 @@ local Handler = CUF.Handler menu:AddWidget(const.WIDGET_KIND.CAST_BAR, Builder.MenuOptions.CastBarGeneral, - Builder.MenuOptions.CastBarColor, Builder.MenuOptions.CastBarEmpower, Builder.MenuOptions.CastBarTimer, Builder.MenuOptions.CastBarSpell, @@ -38,21 +37,10 @@ function W.UpdateCastBarWidget(button, unit, setting, subSetting, ...) local styleTable = DB.GetCurrentWidgetTable(const.WIDGET_KIND.CAST_BAR, unit) if not setting or setting == const.OPTION_KIND.COLOR then - if not subSetting or subSetting == const.OPTION_KIND.TEXTURE then - castBar.statusBar:SetStatusBarTexture(styleTable.color.texture) - end - if not subSetting or subSetting == const.OPTION_KIND.USE_CLASS_COLOR then - castBar.useClassColor = styleTable.color.useClassColor - end - if not subSetting or subSetting == const.OPTION_KIND.INTERRUPTIBLE then - castBar.interruptibleColor = styleTable.color.interruptible - end - if not subSetting or subSetting == const.OPTION_KIND.NON_INTERRUPTIBLE then - castBar.nonInterruptibleColor = styleTable.color.nonInterruptible - end - if not subSetting or subSetting == const.OPTION_KIND.BACKGROUND then - castBar.background:SetVertexColor(unpack(styleTable.color.background)) - end + castBar:SetCastBarColorStyle() + end + if not setting or setting == const.OPTION_KIND.USE_CLASS_COLOR then + castBar.useClassColor = styleTable.useClassColor end if not setting or setting == const.OPTION_KIND.TIMER then @@ -748,8 +736,12 @@ end local function SetEmpowerStyle(self, styleTable) self.useFullyCharged = styleTable.useFullyCharged self.showEmpowerSpellName = styleTable.showEmpowerName +end + +---@param self CastBarWidget +local function SetCastBarColorStyle(self) + local colors = DB.GetColors().castBar - local colors = styleTable.pipColors self.PipColorMap = { [0] = colors.stageZero, [1] = colors.stageOne, @@ -758,6 +750,12 @@ local function SetEmpowerStyle(self, styleTable) [4] = colors.stageFour, [5] = colors.fullyCharged, } + + self.interruptibleColor = colors.interruptible + self.nonInterruptibleColor = colors.nonInterruptible + self.background:SetVertexColor(unpack(colors.background)) + + self.statusBar:SetStatusBarTexture(colors.texture) end ---@param self CastBarWidget @@ -941,6 +939,7 @@ function W:CreateCastBar(button) castBar.Disable = Disable castBar.Update = Update + castBar.SetCastBarColorStyle = SetCastBarColorStyle castBar.SetCastBarColor = SetCastBarColor castBar.SetEmpowerStyle = SetEmpowerStyle castBar.SetBorderStyle = SetBorderStyle