Files
Bagshui/Components/Bagshui.Menus.lua
T
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

445 lines
19 KiB
Lua

-- Bagshui Core: Menus
-- Menu-related functionality that makes sense at the Bagshui level instead of on the Menus class.
Bagshui:LoadComponent(function()
-- If there's an open menu that we own, close it.
---@param startLevel number? Close all menus at this level and higher. Will close all menus if not specified.
---@param force boolean? Close menus even if `Bagshui:IsMenuOpen()` returns false.
---@param keepLowerLevelsOpen boolean? When true, call `Bagshui:BlockAutoMenuClose()` for all levels lower than `startLevel`.
function Bagshui:CloseMenus(startLevel, force, keepLowerLevelsOpen)
if not self:IsMenuOpen() and not force then
return
end
_G.CloseDropDownMenus(startLevel)
if keepLowerLevelsOpen and type(startLevel) == "number" and startLevel > 1 then
self:BlockAutoMenuClose(1, startLevel - 1)
end
end
-- Return true if one of our menus is open (can be restricted to a specific menu type if needed).
-- This is done by checking the `UIDROPDOWNMENU_OPEN_MENU` global variable to see if it contains our
-- menu frame's name. We also need to be sure that dropdown level 1 is visible, because
-- `UIDROPDOWNMENU_OPEN_MENU` doesn't get cleared when menus are closed.
---@param menuType string? Only return true if this specific menu type is open.
---@return boolean
function Bagshui:IsMenuOpen(menuType)
return
-- Bagshui menu was the last one opened.
(self.menuFrame and _G.UIDROPDOWNMENU_OPEN_MENU == self.menuFrame.bagshuiData.name)
-- Menu is actually open.
and _G.DropDownList1:IsVisible()
-- menuType filter.
and ((menuType and self.menuFrame.bagshuiData.lastMenuTypeLoaded == menuType) or not menuType)
-- Nothing was open.
or false
end
--- Prevent the automatic 2-second menu close countdown from kicking in.
--- Primarily used to keep the Settings menu open when the mouse cursor isn't over it.
---@param startLevel number? Lowest level to keep open, inclusive (1-3).
---@param endLevel number? Highest level to keep open, inclusive (1-3).
---@param menuType string? If provided, only stop the countdown when this menu is open.
function Bagshui:BlockAutoMenuClose(startLevel, endLevel, menuType)
if menuType and not self:IsMenuOpen(menuType) then
return
end
for level = startLevel or 1, endLevel or 3, 1 do
_G.UIDropDownMenu_StopCounting(_G["DropDownList" .. level])
end
-- Need a second pass to ensure nothing has started the countdown again.
if menuType then
self:QueueClassCallback(self, self.BlockAutoMenuClose, 1.5, nil, startLevel, endLevel, menuType)
end
end
local TOGGLE_DROPDOWN_MENU_FIRST_PASS = "BAGSHUI_TOGGLE_DROPDOWN_MENU_FIRST_PASS"
local TOGGLE_DROPDOWN_MENU_LEVEL_1_RECHECK = "BAGSHUI_TOGGLE_DROPDOWN_MENU_LEVEL_1_RECHECK"
local TOGGLE_DROPDOWN_MENU_SECOND_PASS = "BAGSHUI_TOGGLE_DROPDOWN_MENU_THIRD_PASS"
--- Fun hack time!
--- 1.12's ToggleDropDownMenu doesn't accurately correct for menus going off the edge of the
--- screen due to some apparent weirdness with frame GetRight/Bottom() vs GetScreenWidth/Height(),
--- so I guess we'll do it. The basic technique comes from EngInventory -- thanks to whoever figured this out!
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
---@param level number Menu level parameter for the original `ToggleDropDownMenu()`.
---@param value any Menu value parameter for the original `ToggleDropDownMenu()`.
---@param dropDownFrame table Parameter for the original `ToggleDropDownMenu()`.
---@param anchorName string? Parameter for the original `ToggleDropDownMenu()`.
---@param xOffset number? Parameter for the original `ToggleDropDownMenu()`.
---@param yOffset number? Parameter for the original `ToggleDropDownMenu()`.
---@param bagshuiAdditionalPassReason string? When another pass needs to be made to re-check the menu's position, this extra parameter is added.
function Bagshui:ToggleDropDownMenu(
wowApiFunctionName,
level,
value,
dropDownFrame,
anchorName,
xOffset,
yOffset,
_,
_,
_,
bagshuiAdditionalPassReason
)
-- Record whether we're in one of the additional passes.
local phase = 0
if bagshuiAdditionalPassReason == TOGGLE_DROPDOWN_MENU_FIRST_PASS then
phase = 1
elseif bagshuiAdditionalPassReason == TOGGLE_DROPDOWN_MENU_LEVEL_1_RECHECK then
phase = 1.5
elseif bagshuiAdditionalPassReason == TOGGLE_DROPDOWN_MENU_SECOND_PASS then
phase = 2
end
-- Let Blizzard code open the menu on first pass.
-- Additional passes must avoid calling this or the menu will be closed.
-- Calling on an additional pass can also trigger errors because the meaning of `this` will have
-- changed, and ToggleDropDownMenu() won't be able to populate its tempFrame variable.
if phase == 0 then
self.hooks:OriginalHook(wowApiFunctionName, level, value, dropDownFrame, anchorName, xOffset, yOffset)
-- At one point there were crashes happening when right-clicking inventory windows to open the menu.
-- It SEEMS like they were fixed by avoiding touching cursor-anchored menus during the level 1 checks.
-- If there are still crashing issues reported, this may need to be enabled in lieu of the
-- immediate call above. (It's not ideal, since there will be a flash of the menu in the wrong position.)
-- Bagshui:QueueEvent(function()
-- self:ToggleDropDownMenu(wowApiFunctionName, level, value, dropDownFrame, anchorName, xOffset, yOffset, nil, nil, nil, TOGGLE_DROPDOWN_MENU_FIRST_PASS)
-- end)
-- return
end
-- Don't mess with anyone else's menus.
if not self:IsMenuOpen() then
return
end
-- Track whether we need to make a second pass.
local secondPassNeeded = false
-- Find the open menu frame.
local frame = _G["DropDownList" .. level]
local parentFrame = _G["DropDownList" .. (level - 1)]
if not frame or (level > 1 and not parentFrame) then
return
end
-- Special stuff for top-level menus -- but not Settings, because
-- Inventory:FixSettingsMenuPosition() takes care of that.
if level == 1 then
-- Blizzard's ToggleDropDownMenu() code always forces a TOPLEFT anchor when the menu doesn't go off the screen.
-- We really don't want that, so we're going to re-anchor to the desired point if it was messed with.
if
anchorName ~= "cursor" -- Game can crash if we mess with cursor-anchored menus.
and phase < 1.5
and not self.menuFrame.bagshuiData.noFirstLevelRepositionNeeded -- The code opening the menu will take care of any positioning issues.
and (frame:GetNumPoints() or 0) > 0
then
local point = frame:GetPoint(1)
local correctedAnchorPoint, correctedAnchorToPoint
if
self.menuFrame.bagshuiData.anchorPoint ~= nil
and point ~= nil
and point ~= self.menuFrame.bagshuiData.anchorPoint
then
if point == "TOPLEFT" then
-- Blizz's ToggleDropDownMenu() moved the menu to a TOPLEFT anchor and we didn't want that. Move it back.
correctedAnchorPoint = self.menuFrame.bagshuiData.anchorPoint
correctedAnchorToPoint = self.menuFrame.bagshuiData.anchorToPoint
elseif string.find(self.menuFrame.bagshuiData.anchorPoint, "RIGHT$") and string.find(point, "LEFT$") then
-- Blizz's ToggleDropDownMenu() moved the menu to a LEFT anchor due to it going offscreen,
-- but we wanted a RIGHT anchor, so we'll move from bottom to top or vice-versa but keep the RIGHT anchoring.
correctedAnchorPoint = BsUtil.FlipAnchorPointComponent(self.menuFrame.bagshuiData.anchorPoint, 1)
correctedAnchorToPoint = BsUtil.FlipAnchorPointComponent(self.menuFrame.bagshuiData.anchorToPoint, 1)
end
end
-- Need to apply the positioning fix.
if correctedAnchorPoint and correctedAnchorToPoint then
-- self:PrintDebug("!!! need to reposition from " .. point .. " to " .. correctedAnchorPoint)
frame:ClearAllPoints()
frame:SetPoint(
correctedAnchorPoint,
(self.menuFrame.bagshuiData.anchorToFrame or anchorName),
correctedAnchorToPoint,
xOffset,
yOffset
)
-- We still need to do all the normal checks since this may move the frame off the screen.
Bagshui:QueueEvent(function()
self:ToggleDropDownMenu(
wowApiFunctionName,
level,
value,
dropDownFrame,
anchorName,
xOffset,
yOffset,
nil,
nil,
nil,
TOGGLE_DROPDOWN_MENU_LEVEL_1_RECHECK
)
end)
return
end
end
-- Check level 1 to see if its right edge is past the horizontal halfway point, and if so, expand menus to the left.
self._toggleDropDownMenu_ExpandMenusLeft = (
frame:GetLeft()
and ((frame:GetRight() * frame:GetScale()) > (_G.UIParent:GetRight() * _G.UIParent:GetScale()) / 2)
)
end
-- Figure out what adjustments are required.
local adjustX = BsUtil.GetFrameOffscreenAmount(frame, "x")
local adjustY = BsUtil.GetFrameOffscreenAmount(frame, "y")
local fullMoveLeft = 0 -- Assume a flip to left-expansion is not necessary.
-- Shift a submenu to the left of the parent if it's being X-adjusted by a negative amount or we're in left-expansion mode.
if
phase < 2
and level > 1 -- Submenu
and (adjustX < 0 or self._toggleDropDownMenu_ExpandMenusLeft)
then
-- This check is needed to ensure we don't double-move a menu that Blizzard code has already moved to the left of the parent.
if frame:GetLeft() * frame:GetScale() > parentFrame:GetLeft() * parentFrame:GetScale() then
-- Move to the left of the parent menu.
fullMoveLeft = -((parentFrame:GetWidth() * parentFrame:GetScale()) + (frame:GetWidth() * frame:GetScale()))
+ BsSkin.menuShiftLeft
secondPassNeeded = true
elseif adjustX == 0 then
-- Submenu is already to the left, but let's fix Blizzard's weird mismatched extra space for left-expanding menus.
adjustX = BsSkin.menuShiftLeftFix
end
end
-- Tweak submenu position if the skin requires it.
if level > 1 and adjustX == 0 and BsSkin.menuShiftRight ~= 0 then
adjustX = BsSkin.menuShiftRight
end
-- Special checks for level 1.
if level == 1 then
local mouseX, mouseY = _G.GetCursorPosition()
-- Move menus to the other side of the cursor instead of just shifting them slightly.
if anchorName == "cursor" and adjustX < 0 and mouseX < (frame:GetRight() * frame:GetScale()) then
adjustX = -(frame:GetWidth() * frame:GetScale())
end
-- Open menus upwards when they're too close to the bottom right of the screen and could be covered by GameTooltip.
if
phase < 2
and (frame:GetBottom() * frame:GetScale()) - (100 * _G.UIParent:GetScale()) < (_G.UIParent:GetBottom() * _G.UIParent:GetScale())
and (frame:GetRight() * frame:GetScale()) > (_G.UIParent:GetRight() * _G.UIParent:GetScale()) - (300 * _G.UIParent:GetScale())
and mouseY > (frame:GetBottom() * frame:GetScale())
then
-- It feels like this should be something like (frame:GetHeight() * frame:GetScale()) or even
-- ((frame:GetHeight() * frame:GetScale()) / _G.UIParent:GetScale()), but that gives
-- inconsistent results at different scales and this simpler solution seems to work so WHO CARES.
adjustY = frame:GetHeight()
-- When anchoring to another frame, move the menu above that frame.
if anchorName ~= "cursor" and type(anchorName) == "table" and anchorName.GetParent then
adjustY = adjustY + ((anchorName:GetHeight() or 0) * anchorName:GetScale()) + ((yOffset or 0) * -1)
end
-- Since we're flipping the menu upwards, a second pass is needed in case it goes off the top.
secondPassNeeded = true
end
end
-- Make adjustments by shifting the menu exactly the right amount.
if adjustY ~= 0 or adjustX ~= 0 or fullMoveLeft ~= 0 then
-- It's preferable to move menu level 2+ frames relative to their parent so they stay anchored
-- correctly if the UI shifts around. However, WoW will crash on a blind call to GetPoint(1)
-- if GetNumPoints() is 0, so we need just adjust against UIParent in that case.
-- (Trying to do this with level 1 seems to be unreliable, so we'll force the use of UIParent for that.)
if level > 1 and (frame:GetNumPoints() or 0) > 0 then
-- Grab the menu's current anchor point
local point, anchorToFrame, anchorToPoint, xOffset, yOffset = frame:GetPoint(1)
-- Move to the left of the parent anchor frame
if fullMoveLeft ~= 0 then
adjustX = -BsSkin.menuShiftLeft
point = BsUtil.FlipAnchorPointComponent(point, 2) -- <TOP/BOTTOM>LEFT -> <TOP/BOTTOM>RIGHT
anchorToPoint = BsUtil.FlipAnchorPointComponent(anchorToPoint, 2) -- <TOP/BOTTOM>LEFT -> <TOP/BOTTOM>RIGHT
end
-- Set new, adjusted point.
frame:ClearAllPoints()
frame:SetPoint(point, anchorToFrame, anchorToPoint, (xOffset or 0) + adjustX, (yOffset or 0) + adjustY)
else
-- No current parent frame (or level 1) -- just anchor to UIParent and adjust.
adjustX = frame:GetLeft() + (fullMoveLeft ~= 0 and fullMoveLeft or adjustX)
adjustY = frame:GetTop() + adjustY
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", _G.UIParent, "BOTTOMLEFT", adjustX, adjustY)
end
end
-- Make a second check in case we accidentally shifted the menu offscreen when flipping it to the left.
if phase < 2 and secondPassNeeded then
Bagshui:QueueEvent(function()
self:ToggleDropDownMenu(
wowApiFunctionName,
level,
value,
dropDownFrame,
anchorName,
xOffset,
yOffset,
nil,
nil,
nil,
TOGGLE_DROPDOWN_MENU_SECOND_PASS
)
end)
end
-- Reset parameter.
self.menuFrame.bagshuiData.noFirstLevelRepositionNeeded = nil
end
-- Another hack, albeit much more about vanity and not actually fixing anything.
-- Here, we're filling some missing functionality in `UIDropDownMenu_AddButton()`,
-- namely icon colors, position, and size. This function will set or reset values as appropriate.
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
---@param menuItem table Menu entry.
---@param level number? Menu level.
function Bagshui:UIDropDownMenu_AddButton(wowApiFunctionName, menuItem, level)
self.hooks:OriginalHook(wowApiFunctionName, menuItem, level)
-- Grab info about current menu item.
-- This is the same as what's done in UIDropDownMenu_AddButton(),
-- but without incrementing numButtons to the next value.
local listFrame = _G["DropDownList" .. (level or 1)]
if not listFrame then
return
end
local listFrameName = listFrame:GetName()
if not listFrameName then
return
end
local index = listFrame.numButtons
if not index then
return
end
local button = _G[listFrameName .. "Button" .. index]
if not button then
return
end
local xOffset
-- Set/reset opacity.
button:SetAlpha(menuItem._bagshuiAlpha or 1)
-- Arrow adjustments.
local expandArrow = _G[listFrameName .. "Button" .. index .. "ExpandArrow"]
if expandArrow then
if not self._originalExpandArrowXOffset then
_, _, _, self._originalExpandArrowXOffset = expandArrow:GetPoint(1)
end
-- Fix arrow position on notCheckable items so the arrow aligns with that of checkable items.
xOffset = (menuItem._bagshui and menuItem.hasArrow and menuItem.notCheckable) and -10
or self._originalExpandArrowXOffset
expandArrow:SetPoint("RIGHT", xOffset, 0)
-- Hide if requested.
if menuItem._bagshuiHideArrow then
expandArrow:Hide()
end
-- Disable when menu item is disabled. WHY IS THIS NOT THE DEFAULT.
if menuItem.disabled then
expandArrow:Disable()
expandArrow:SetAlpha(0.5)
else
expandArrow:Enable()
expandArrow:SetAlpha(1)
end
end
-- Icon adjustments.
local icon = _G[button:GetName() .. "Icon"]
if menuItem.icon and icon then
-- Store original values if we don't yet know them.
if not self._originalMenuIconSize then
self._originalMenuIconSize = icon:GetWidth()
end
if not self._originalMenuIconXOffset then
_, _, _, self._originalMenuIconXOffset = icon:GetPoint(1)
end
-- Icon colors.
local r = menuItem._bagshuiTextureR or (menuItem._bagshui and BS_COLOR.YELLOW[1]) or 1
local g = menuItem._bagshuiTextureG or (menuItem._bagshui and BS_COLOR.YELLOW[2]) or 1
local b = menuItem._bagshuiTextureB or (menuItem._bagshui and BS_COLOR.YELLOW[3]) or 1
local a = (menuItem._bagshui and menuItem.disabled and not menuItem.isTitle) and 0.5
or menuItem._bagshuiTextureA
or 1
icon:SetVertexColor(r, g, b, a)
-- Icon size.
local iconSize = menuItem._bagshuiTextureSize or (menuItem._bagshui and 12) or self._originalMenuIconSize
icon:SetWidth(iconSize)
icon:SetHeight(iconSize)
-- Icon position.
xOffset = menuItem._bagshuiTextureXOffset
or (menuItem._bagshui and ((menuItem.hasArrow and not menuItem._bagshuiHideArrow) and (menuItem.notCheckable and -28 or -24) or menuItem._bagshuiFakeTitle and -14 or menuItem.notCheckable and -16) or -12)
or self._originalMenuIconXOffset
icon:SetPoint("RIGHT", xOffset, 0)
end
-- Color Swatch + Arrow hack - hide the arrow.
if menuItem.hasColorSwatch and menuItem.hasArrow and _G[button:GetName() .. "ExpandArrow"] then
_G[button:GetName() .. "ExpandArrow"]:Hide()
end
-- Other Color Swatch stuff.
local colorSwatch = _G[button:GetName() .. "ColorSwatch"]
if menuItem.hasColorSwatch and colorSwatch then
-- Inexplicably, Blizzard doesn't disable the swatch when the menu item is disabled.
if menuItem.disabled then
colorSwatch:Disable()
else
colorSwatch:Enable()
end
-- Color Swatch custom function hack.
if menuItem._bagshuiColorSwatchFunc then
-- Store the Blizzard OnClick function only once so we don't overwrite it with one of ours.
if not colorSwatch._bagshuiOldOnClick then
colorSwatch._bagshuiOldOnClick = colorSwatch:GetScript("OnClick")
end
colorSwatch:SetScript("OnClick", menuItem._bagshuiColorSwatchFunc)
elseif colorSwatch._bagshuiOldOnClick then
-- Restore the Blizzard OnClick.
colorSwatch:SetScript("OnClick", colorSwatch._bagshuiOldOnClick)
colorSwatch._bagshuiOldOnClick = nil
end
-- Color Swatch position hack.
if menuItem._bagshuiColorSwatchRightAnchor then
if not colorSwatch._bagshuiOldRightAnchor and colorSwatch:GetNumPoints() > 0 then
_, _, _, colorSwatch._bagshuiOldRightAnchor, _ = colorSwatch:GetPoint(1)
end
colorSwatch:SetPoint("RIGHT", menuItem._bagshuiColorSwatchRightAnchor, 0)
elseif colorSwatch._bagshuiOldRightAnchor then
colorSwatch:SetPoint("RIGHT", colorSwatch._bagshuiOldRightAnchor, 0)
menuItem._bagshuiOldRightAnchor = nil
end
end
end
end)