-- Bagshui Inventory Prototype: Settings Menu Bagshui:AddComponent(function() local Inventory = Bagshui.prototypes.Inventory -- Maximum number of previously selected colors to store. local COLOR_HISTORY_LIMIT = 10 -- Do-nothing function to be used when a setting doesn't have an update function. local function emptyFunction() end --- Show the Inventory settings menu. ---@param menu table Menu table. function Inventory:OpenSettingsMenu(menu) -- Docked frames should show the parent frame's menu. if self.dockTo then Bagshui.components[self.dockTo].menus:OpenMenu("Settings") return end -- This is where the menu should anchor, in theory. See comments on -- Inventory:FixSettingsMenuPosition() for more. self.lastSettingsAnchor = self.settings.windowAnchorYPoint .. self.settings.windowAnchorXPoint self.lastSettingsXOffset = BsSkin.menuShiftRight * (self.settings.windowAnchorXPoint == "LEFT" and 1 or -1) -- Special property Bagshui.menuFrame.bagshuiData.noFirstLevelRepositionNeeded = true -- Open the menu. self.menus:ShowMenu( "Settings", self.uiFrame, self.lastSettingsXOffset, 0, self.lastSettingsAnchor, BsUtil.FlipAnchorPointComponent(self.lastSettingsAnchor, 2) ) -- Anchor the menu to the side of the main window. self:FixSettingsMenuPosition() -- Prevent the settings menu from disappearing after 2 seconds. self.menus:BlockAutoMenuClose(1, 3, "Settings") end --- The goal is to keep the settings menu anchored to the corner of the main window. --- This is initially done naively by simply anchoring it to the opposite side of the window anchor. --- Most of the time, it's probably ok, but if the window has been moved to the opposite side without changing --- the anchor, the menu could be offscreen. This is addressed by a second pass where the menu is checked --- to see if an X-adjustment would be necessary to bring it back onscreen. If so, move it to the other side. --- There's also a bug in Blizzard's ToggleDropDownMenu() where custom menu positions aren't truly respected, --- so we end up working around that here as well. It could probably be generalized into the --- `Bagshui:ToggleDropDownMenu()` hook by checking the menu frame's anchor as compared to what was desired, --- but since this is the only time we're trying for something other than `TOPLEFT` as the menu anchor it's --- fine for now. function Inventory:FixSettingsMenuPosition(secondPass) if not self.menus then return end if self.menus:IsMenuOpen("Settings") then local menu = _G.DropDownList1 -- Grab the dropdown menu reference. if not secondPass then menu:ClearAllPoints() menu:SetPoint( self.lastSettingsAnchor, self.uiFrame, BsUtil.FlipAnchorPointComponent(self.lastSettingsAnchor, 2), self.lastSettingsXOffset, 0 ) -- Trigger second pass position check on next frame update. Bagshui:QueueClassCallback(self, self.FixSettingsMenuPosition, nil, nil, true) else -- Check to see if the Settings menu has been positioned offscreen horizontally -- or is overlapping the Inventory window, and if so, move it to the other side. if (BsUtil.GetFrameOffscreenAmount(menu, "x") ~= 0) or ( (menu:GetLeft() or 0) >= (self.uiFrame:GetLeft() or 0) and (menu:GetRight() or 0) <= (self.uiFrame:GetRight() or 0) ) then self.lastSettingsAnchor = BsUtil.FlipAnchorPointComponent(self.lastSettingsAnchor, 2) self.lastSettingsXOffset = self.lastSettingsXOffset * -1 menu:ClearAllPoints() menu:SetPoint( self.lastSettingsAnchor, self.uiFrame, BsUtil.FlipAnchorPointComponent(self.lastSettingsAnchor, 2), self.lastSettingsXOffset, 0 ) end end end end --- Construct the Settings menu table based on the Settings configuration. Can be called recursively for nested settings. ---@param configTable table Settings configuration table from Config\Settings.lua. ---@param menuBaseTable table Menu table to place the items in as generated by `Menus:AddMenu()`. ---@param menuLevel number Level in which to place items (1-3). ---@param menuValue any Used to set the `value` property of the menu item. function Inventory:BuildSettingsMenuFromConfig(configTable, menuBaseTable, menuLevel, menuValue) assert((menuLevel > 1 and menuValue ~= nil) or menuLevel <= 1, "menuValue must be provided when menuLevel > 1") -- The menu level default is 1. local level = menuLevel or 1 -- Level 1 of menu tables is flat, but submenu tables all have a value key. local menuTable = level > 1 and menuBaseTable.levels[level][menuValue] or menuBaseTable.levels[level] -- Loop through the given settings configuration table. for _, settingInfo in ipairs(configTable) do -- Omit hidden settings. if not settingInfo.hidden and ( type(settingInfo.hideFunc) ~= "function" or not settingInfo.hideFunc() ) then if type(settingInfo.settings) == "table" then -- Recurse for nested submenu, indicated by the presence of settingInfo.settings. -- However, if there's no submenuName or we've reached the cap of menu levels, nothing can be done with it. if settingInfo.submenuName and level < _G.UIDROPDOWNMENU_MAXLEVELS then local submenuValue = settingInfo.submenuValue or settingInfo.submenuName -- Create a parent item to open the submenu. table.insert( menuTable, { text = settingInfo.submenuName, tooltipTitle = settingInfo.tooltipTitle or settingInfo.submenuName, tooltipText = settingInfo.tooltipText, value = submenuValue, hasArrow = true, } ) -- Initialize the submenu table. menuBaseTable.levels[level + 1][submenuValue] = {} -- Recurse to build it. self:BuildSettingsMenuFromConfig(settingInfo.settings, menuBaseTable, level + 1, submenuValue) end elseif settingInfo.mainTitle then -- The mainTitle property creates an automatically-named menu item -- with the Close button graphic as its icon, providing an obvious -- way to dismiss the menu. local menuItem = { text = BS_FONT_COLOR.BAGSHUI .. string.format(L.Menu_Settings, self.inventoryTypeLocalized) .. FONT_COLOR_CODE_CLOSE, _bagshuiFakeTitle = true, func = function() self:CloseMenusAndClearFocuses() end, } BsSkin.closeButtonSkinFunc(menuItem) table.insert(menuTable, menuItem) elseif settingInfo.menuTitle then -- This is just a title, not a setting. table.insert( menuTable, { text = settingInfo.menuTitle, isTitle = true, notCheckable = true, keepShownOnClick = true, } ) else -- Now we've found a setting. local settingName = settingInfo.name or "" local settingFriendlyName = settingInfo.title or L[settingInfo.name] or settingInfo.name -- Base menu item. local menuItem = { _bagshuiSettingName = settingInfo.name, _bagshuiSettingFriendlyName = settingFriendlyName, -- Store the base friendly name so we can manipulate the text property later. _bagshuiFakeTitle = settingInfo.fakeTitle, -- Fake titles are styled like titles but are can be clicked and can have submenus. _bagshuiAlpha = (settingInfo.faded and 0.6 or 1), text = settingFriendlyName, notCheckable = settingInfo.notCheckable, notClickable = settingInfo.notClickable, disabled = settingInfo.disabled, tooltipTitle = L_nil[settingName .. "_TooltipTitle"] or settingInfo.tooltipTitle or settingFriendlyName, tooltipText = (L_nil[settingName .. "_TooltipText"] or settingInfo.tooltipText), keepShownOnClick = true, } -- Add scope indicators. if settingInfo.scope == BS_SETTING_SCOPE.ACCOUNT then menuItem.tooltipText = (menuItem.tooltipText and (menuItem.tooltipText .. BS_NEWLINE) or "") .. GRAY_FONT_COLOR_CODE .. L.SettingScope_Account .. FONT_COLOR_CODE_CLOSE menuItem.icon = BsUtil.GetFullTexturePath("Icons\\Characters") menuItem._bagshuiTextureA = 0.45 elseif settingInfo.scope == BS_SETTING_SCOPE.CHARACTER then menuItem.tooltipText = (menuItem.tooltipText and (menuItem.tooltipText .. BS_NEWLINE) or "") .. GRAY_FONT_COLOR_CODE .. L.SettingScope_Character .. FONT_COLOR_CODE_CLOSE menuItem.icon = BsUtil.GetFullTexturePath("Icons\\Character") menuItem._bagshuiTextureA = 0.45 end -- Choices, numbers, and colors need submenus. if settingInfo.type == BS_SETTING_TYPE.CHOICES or settingInfo.type == BS_SETTING_TYPE.NUMBER or settingInfo.type == BS_SETTING_TYPE.COLOR then menuItem.hasArrow = true local subLevel = level + 1 menuBaseTable.levels[subLevel][settingInfo.name] = {} local subMenu = menuBaseTable.levels[subLevel][settingInfo.name] if settingInfo.type == BS_SETTING_TYPE.CHOICES or (settingInfo.type == BS_SETTING_TYPE.NUMBER and settingInfo.choices) then -- Choices / Numbers with a static list of choices. if settingInfo.choicesAutoSplitMenuType then -- This setting has an auto-split menu instead of a static list of choices. -- Upvalues here for auto-split menu functions. local settings = self.settings local settingName = settingInfo.name local func = settingInfo.choicesAutoSplitMenuFunc local tooltipTextFunc = settingInfo.choicesAutoSplitMenuTooltipTextFunc -- Triggering value and parameters for auto-split menu. menuItem.value = { autoSplitMenuType = settingInfo.choicesAutoSplitMenuType, menuLevel = 1, func = function(menuLevel, settingValue) if type(func) == "function" then func(settings, settingName, settingValue) end self.settings[settingName] = settingValue self.menus:Refresh(menuLevel) end, checkFunc = function(menuOption) return self.settings[settingName] == menuOption end, tooltipTitleFunc = settingInfo.choicesAutoSplitMenuTooltipTitleFunc or function(_, defaultTitle) -- Only provide a tooltip title if none was provided by the menu. return defaultTitle or menuItem.tooltipTitle end, tooltipTextFunc = function(objectId, tooltipText) if tooltipTextFunc then tooltipText = tooltipTextFunc(objectId, tooltipText, self) end return tooltipText end, omitFunc = settingInfo.choicesAutoSplitMenuOmitFunc, keepShownOnClick = true, } menuItem.hasArrow = true elseif settingInfo.choices then -- Static list of choices. for _, choice in ipairs(settingInfo.choices) do table.insert( subMenu, { _bagshuiSettingName = settingInfo.name, text = choice.text or choice.value, value = choice.value, tooltipTitle = menuItem.tooltipTitle, tooltipText = (menuItem.tooltipText or "") .. (choice.tooltip and " " .. choice.tooltip or ""), } ) end end elseif settingInfo.type == BS_SETTING_TYPE.NUMBER then -- Numbers. -- Range of values for the submenu. if type(settingInfo.min) == "number" and type(settingInfo.max) == "number" then for i = settingInfo.min, settingInfo.max, (settingInfo.step or 1) do table.insert( subMenu, { _bagshuiSettingName = settingInfo.name, text = type(settingInfo.valueDisplayFunc) == "function" and settingInfo.valueDisplayFunc(i) or i, value = i, tooltipTitle = menuItem.tooltipTitle, tooltipText = menuItem.tooltipText, } ) end end elseif settingInfo.type == BS_SETTING_TYPE.COLOR then -- Colors. -- Create submenu for color history. self.menus:InitializeEmptyMenu( subMenu, { _bagshuiSettingName = settingInfo.name, _bagshuiSettingUpdateFunction = menuItem._bagshuiSettingUpdateFunction, } ) end end -- Apply text modifications. if type(settingInfo.nameFunc) == "function" then menuItem.text = settingInfo.nameFunc(menuItem.text) menuItem.tooltipTitle = settingInfo.nameFunc(menuItem.tooltipTitle) menuItem.tooltipText = settingInfo.nameFunc(menuItem.tooltipText) end -- Add the reset hint last because submenu items shouldn't have it, only the parent item. if settingInfo.type ~= BS_SETTING_TYPE.TRIGGER then menuItem.tooltipText = (menuItem.tooltipText and (menuItem.tooltipText .. BS_NEWLINE) or "") .. "~1~" -- Insertion point for tooltip modifications. .. GRAY_FONT_COLOR_CODE .. L.Setting_Reset_TooltipText .. FONT_COLOR_CODE_CLOSE end -- Store a backup copy of menu texts so they can be modified at display-time. -- This probably could have been done in a more sane way but a quick hack is good enough. menuItem._bagshuiOriginalText = menuItem.text menuItem._bagshuiOriginalTooltipTitle = menuItem.tooltipTitle menuItem._bagshuiOriginalTooltipText = menuItem.tooltipText table.insert( menuTable, menuItem ) end end end end --- Prepare a Settings menu item for display. Called from `Menus:LoadMenu()` when a menu item --- has a `_bagshuiSettingName` property to build out the menu item properties. ---@param menuItem table Properties for the menu item, which will be modified in-place. ---@param menu table[] Parent table of `menuItem`. ---@param menuLevel number Menu level, 1-3. ---@param itemNum number Index of `menuItem` within `menu`. ---@param menuType string Identifier for a menu that has been registered via `AddMenu()`. function Inventory:GenerateSettingsMenuItem(menuItem, menu, menuLevel, itemNum, menuType) local settingName = menuItem._bagshuiSettingName local settingsStorage = menuItem._bagshuiSettingsStorage or self.settings local settingInfo = menuItem._bagshuiSettingInfo or self.settings:GetSettingInfo(settingName) local currentSettingValue = settingsStorage[settingName] local defaultValue = settingInfo.defaultValue local settingUpdateFunction = menuItem._bagshuiSettingUpdateFunction or emptyFunction if not self._generateSettingsMenuItem_ResetOnModifierKeys then --- Wrapper for `self:ResetSettingOnModifierKeys()`. ---@param arg1 table Argument table containing `settingName`, `settingStorage`, and `menuLevel`. self._generateSettingsMenuItem_ResetOnModifierKeys = function(arg1) self:ResetSettingOnModifierKeys(arg1) end end -- Update texts at display-time. if menuItem._bagshuiOriginalText then menuItem.text = menuItem._bagshuiOriginalText if type(settingInfo.textDisplayFunc) == "function" then menuItem.text = settingInfo.textDisplayFunc(menuItem.text, settingInfo.name, self.settings) end end if menuItem._bagshuiOriginalTooltipText then menuItem.tooltipText = menuItem._bagshuiOriginalTooltipText if type(settingInfo.tooltipTextDisplayFunc) == "function" then menuItem.tooltipText = settingInfo.tooltipTextDisplayFunc(menuItem.tooltipText, settingInfo.name, self.settings) end menuItem.tooltipText = string.gsub(menuItem.tooltipText, "~1~", "") end -- Add arg1 for use with ResetSettingOnModifierKeys(). if type(menuItem.arg1) ~= "table" then menuItem.arg1 = {} end menuItem.arg1.settingName = settingName menuItem.arg1.settingsStorage = settingsStorage -- Need to store menu level because the global menu level variable will be wrong -- for items with submenus. menuItem.arg1.menuLevel = menuLevel -- Figure out what the display text for the setting should be, when a `text` -- property is available for the selected value within a list of choices. local currentSettingFriendlyValue = currentSettingValue if settingInfo.choices and ( (settingInfo.type == BS_SETTING_TYPE.CHOICES and not settingInfo.autoSplitMenuType) or settingInfo.type == BS_SETTING_TYPE.NUMBER ) then for _, choice in ipairs(settingInfo.choices) do if choice.value == currentSettingValue then currentSettingFriendlyValue = choice.text or choice.value end end end -- Apply value display translation if available. if type(settingInfo.valueDisplayFunc) == "function" then currentSettingFriendlyValue = settingInfo.valueDisplayFunc(currentSettingFriendlyValue) end -- Process each type of setting into the appropriate menu item. if settingInfo.choicesAutoSplitMenuType then -- Stupid hack on our own code to pass menuLevel through to `arg1 of the -- auto-split menu `func`'set up in `BuildSettingsMenuFromConfig()`. menuItem.value.objectId = menuLevel elseif settingInfo.type == BS_SETTING_TYPE.BOOL then -- Boolean -- checked/unchecked, toggle setting on click. menuItem.checked = settingsStorage[settingName] menuItem.keepShownOnClick = true menuItem.func = function() if not self:ResetSettingOnModifierKeys(menuItem.arg1, nil, true) then settingsStorage[settingName] = not settingsStorage[settingName] self.menus:Refresh(menuLevel) end end elseif settingInfo.type == BS_SETTING_TYPE.CHOICES or settingInfo.type == BS_SETTING_TYPE.NUMBER then -- Choices / Numbers -- submenu with choices, update setting to selected choice on click. -- Determine whether this is INSIDE a submenu or whether an arrow to open the submenu is needed. if menuItem.value and not menuItem.hasArrow then -- Inside submenu -- checked/unchecked based on current value, change setting on click. -- Need to use tostring() here so we don't get caught by float nonsense where 1 somehow doesn't equal 1. menuItem.checked = tostring(menuItem.value) == tostring(currentSettingValue) menuItem.keepShownOnClick = false menuItem.func = function() Bagshui:CloseMenus(_G.UIDROPDOWNMENU_MENU_LEVEL, nil, true) settingsStorage[settingName] = _G.this.value settingUpdateFunction() self.menus:Refresh(menuLevel, -1) end else -- Parent menu item to open the submenu. menuItem.value = settingName menuItem.hasArrow = true -- Append current value to menu text. menuItem.text = menuItem._bagshuiSettingFriendlyName .. GRAY_FONT_COLOR_CODE .. " [" .. currentSettingFriendlyValue .. "]" .. FONT_COLOR_CODE_CLOSE menuItem.func = self._generateSettingsMenuItem_ResetOnModifierKeys end elseif settingInfo.type == BS_SETTING_TYPE.COLOR then -- Colors -- display swatch and prepare color history. menuItem.opacity = nil -- Determine whether this is INSIDE a submenu or whether an arrow to open the submenu is needed. if menuItem.value and not menuItem.hasArrow then -- Inside submenu -- populate color history, change setting on click. if Bagshui.colorHistory[itemNum] and itemNum < COLOR_HISTORY_LIMIT then local color = Bagshui.colorHistory[itemNum] menuItem.text = " " menuItem.tooltipTitle = L.Menu_Settings_ColorHistory_TooltipTitle menuItem._bagshuiHide = false menuItem._bagshuiColorSwatchRightAnchor = -20 menuItem.hasColorSwatch = true menuItem.notCheckable = true menuItem.r = color[1] menuItem.g = color[2] menuItem.b = color[3] menuItem.opacity = color[4] menuItem.func = function() Bagshui:CloseMenus(_G.UIDROPDOWNMENU_MENU_LEVEL, nil, true) settingsStorage[settingName] = color settingUpdateFunction() self.menus:Refresh(menuLevel, -1) self:AddToColorHistory(color) end menuItem._bagshuiColorSwatchFunc = menuItem.func else menuItem._bagshuiHide = true end else -- Parent menu item to open the submenu -- add color swatch and related functions -- to properly update colors as the color picker is changed, while handling things -- correctly so that the value is only saved if Okay is clicked. -- Opacity management variables that will be captured into the cancelFunc closure below. -- This is necessary because since we don't receive opacity as a parameter to cancelFunc. local hasOpacity = false local currentOpacity = (currentSettingValue and currentSettingValue[4]) or (defaultValue and defaultValue[4]) or 1 -- Menu item updates menuItem.hasArrow = true menuItem.value = settingName menuItem.hasColorSwatch = true menuItem.r = (currentSettingValue and currentSettingValue[1]) or (defaultValue and defaultValue[1]) or 1 menuItem.g = (currentSettingValue and currentSettingValue[2]) or (defaultValue and defaultValue[2]) or 1 menuItem.b = (currentSettingValue and currentSettingValue[3]) or (defaultValue and defaultValue[3]) or 1 if settingInfo.hasOpacity then hasOpacity = true menuItem.hasOpacity = true -- Inverting opacity slider because it's insane that 100% means transparent and 0% means opaque. menuItem.opacity = 1.0 - currentOpacity end local oldColor = { menuItem.r, menuItem.g, menuItem.b, menuItem.opacity } -- Shared function to reopen the menu that triggered the color picker. -- Getting submenus to open would take more digging than is worth it. local function reopenMenu() if self.menus._openMenu_last_MenuType == menuType and _G.UIDROPDOWNMENU_OPEN_MENU == Bagshui.menuFrame.bagshuiData.name then self.menus:OpenMenu( menuType, self.menus._openMenu_last_Arg1, self.menus._openMenu_last_Arg2, _G.UIParent, self.menus._openMenu_last_UIParentXOffset, self.menus._openMenu_last_UIParentYOffset ) end end -- Change setting on color / opacity change. local swatchFuncCallCount = 0 menuItem.swatchFunc = function() local color = { _G.ColorPickerFrame:GetColorRGB() } if hasOpacity then -- Un-invert opacity value since we're inverting it for the UI (see menuItem.opacity note above). table.insert(color, 1.0 - _G.OpacitySliderFrame:GetValue()) end settingsStorage[settingName] = color settingUpdateFunction() -- Only perform actions when Okay is clicked. swatchFunc() gets called -- immediately when the ColorPickerFrame is opened, so we skip it the -- first time. After that, we can tell whether Okay was clicked by -- checking whether ColorPickerFrame is still visible. if swatchFuncCallCount > 0 and not _G.ColorPickerFrame:IsVisible() then self:AddToColorHistory(oldColor) self:AddToColorHistory(color) reopenMenu() end swatchFuncCallCount = swatchFuncCallCount + 1 end menuItem.opacityFunc = menuItem.swatchFunc -- Color picker Cancel button click -- revert the setting. menuItem.cancelFunc = function(previousValues) local color -- Allow reverting a color setting to nil for Groups, which need -- nil to indicate the default color should be used. if currentSettingValue ~= nil then color = { previousValues.r, previousValues.g, previousValues.b, } -- Use upvalues that were created above since previousValues doesn't contain opacity. if hasOpacity then table.insert(color, currentOpacity) end end settingsStorage[settingName] = color settingUpdateFunction() reopenMenu() end menuItem.func = self._generateSettingsMenuItem_ResetOnModifierKeys end elseif settingInfo.type == BS_SETTING_TYPE.TRIGGER then -- Triggers -- just update the setting to true and the Settings class takes care of the rest. menuItem.checked = false menuItem.keepShownOnClick = false menuItem.func = function() settingsStorage[settingName] = true end end -- Decide whether the menu item should be disabled. if type(settingInfo.disableFunc) == "function" then menuItem.disabled = settingInfo.disableFunc(settingName, settingsStorage) end end --- Add the given color to the global Bagshui color picker history. --- @param color table Color to add: `{ r, g, b, [a] }`. function Inventory:AddToColorHistory(color) -- Due to float shenanigans, colors values may not be seen as equal unless we round them. for i = 1, 4 do color[i] = BsUtil.Round(color[i], 4) end -- Add color to first position if it's not already there. if not Bagshui.colorHistory[1] or ( Bagshui.colorHistory[1][1] ~= color[1] or Bagshui.colorHistory[1][2] ~= color[2] or Bagshui.colorHistory[1][3] ~= color[3] or Bagshui.colorHistory[1][4] ~= color[4] ) then table.insert(Bagshui.colorHistory, 1, color) end -- Remove any duplicates. for i = table.getn(Bagshui.colorHistory), 2, -1 do if Bagshui.colorHistory[i][1] == color[1] and Bagshui.colorHistory[i][2] == color[2] and Bagshui.colorHistory[i][3] == color[3] and Bagshui.colorHistory[i][4] == color[4] then table.remove(Bagshui.colorHistory, i) end end -- Trim history if needed. while table.getn(Bagshui.colorHistory) > COLOR_HISTORY_LIMIT do table.remove(Bagshui.colorHistory) end end --- When certain modifier keys are held down, reset the setting provided in the --- arg1 table to default. ---@param arg1 table Menu item information for setting: --- ``` --- { --- settingName [string] = name of setting, --- settingsStorage [table] = Settings class instance, --- menuLevel [number] = Level of menu item, --- } --- ``` ---@param arg2 any? Ignored. ---@param onlyRefreshOnReset boolean? Don't refresh the menu unless the setting was reset. ---@return boolean settingWasReset function Inventory:ResetSettingOnModifierKeys(arg1, arg2, onlyRefreshOnReset) if _G.IsAltKeyDown() and _G.IsControlKeyDown() and _G.IsShiftKeyDown() then arg1.settingsStorage:SetDefaults(true, nil, nil, arg1.settingName) self.menus:Refresh(arg1.menuLevel) return true elseif not onlyRefreshOnReset then self.menus:Refresh(arg1.menuLevel) end return false end end)