Moving the real window made the client re-lay out every item button, texture and cooldown model on each mouse movement, freezing the game for seconds with the Bags and Bank open (profiling showed almost no Lua time during the freezes). A single empty outline frame now follows the mouse, and the window is moved once on drop. Clicking the outline finishes a move whose drop was missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
1754 lines
66 KiB
Lua
1754 lines
66 KiB
Lua
-- Bagshui Inventory Prototype: UI
|
|
--
|
|
-- Creation of the main window frame and general Inventory-specific UI stuff
|
|
|
|
Bagshui:AddComponent(function()
|
|
local Inventory = Bagshui.prototypes.Inventory
|
|
|
|
--- Set up everything related to the UI that needs to be done at startup.
|
|
--- This could probably be broken into multiple functions because it's a bit of a monstrosity at this point.
|
|
function Inventory:InitUi()
|
|
-- Instance of the `InventoryUi` class specific to this Inventory class instance.
|
|
-- `InventoryUi` is built up in the Inventory.Ui.*.lua files.
|
|
self.ui = Bagshui.prototypes.InventoryUi:New(self)
|
|
local ui = self.ui
|
|
|
|
-- Fill `self.toolbarAndMainMenuItems` so it can be iterated to build the toolbar and menus.
|
|
self:PopulateToolbarAndMainMenuItems()
|
|
|
|
-- Tables to store UI object references.
|
|
for _, name in ipairs({ "buttons", "frames", "tooltips", "text", "ordering" }) do
|
|
ui[name] = {}
|
|
end
|
|
local buttons = ui.buttons
|
|
buttons.bagSlots = {}
|
|
buttons.itemSlots = {}
|
|
buttons.toolbar = {}
|
|
local frames = ui.frames
|
|
frames.groups = {}
|
|
frames.groupMoveTargets = {}
|
|
local tooltips = ui.tooltips
|
|
|
|
-- Prepare the window.
|
|
|
|
self.uiFrame = ui:CreateWindowFrame("Frame")
|
|
|
|
-- Skins can give the inventory window a different backdrop than other windows.
|
|
if BsSkin.inventoryWindowBackdrop then
|
|
local backdrop = BsSkin.inventoryWindowBackdrop
|
|
self.uiFrame:SetBackdrop({
|
|
bgFile = backdrop.bgFile,
|
|
edgeFile = backdrop.edgeFile,
|
|
tile = true,
|
|
tileSize = backdrop.tileSize,
|
|
edgeSize = backdrop.edgeSize,
|
|
insets = { left = backdrop.insets, top = backdrop.insets, right = backdrop.insets, bottom = backdrop.insets },
|
|
})
|
|
end
|
|
local uiFrame = self.uiFrame
|
|
|
|
-- Keep the window on the screen so it can't be dragged out of reach.
|
|
if uiFrame.SetClampedToScreen then
|
|
uiFrame:SetClampedToScreen(true)
|
|
end
|
|
|
|
-- Start with the window in the correct position and hidden.
|
|
self:FixWindowPosition()
|
|
uiFrame:Hide()
|
|
|
|
-- Add scripts.
|
|
|
|
uiFrame.bagshuiData.lastDirtyCheck = _G.GetTime()
|
|
uiFrame:SetScript("OnUpdate", function()
|
|
-- Mark this window as dirty if any "child" windows are open.
|
|
if _G.GetTime() - _G.this.bagshuiData.lastDirtyCheck > 0.075 then
|
|
_G.this.bagshuiData.dirty = Bagshui:ChildWindowsVisible()
|
|
_G.this.bagshuiData.lastDirtyCheck = _G.GetTime()
|
|
end
|
|
end)
|
|
|
|
local oldOnShow = uiFrame:GetScript("OnShow")
|
|
uiFrame:SetScript("OnShow", function()
|
|
self:UiFrame_OnShow()
|
|
if oldOnShow then
|
|
oldOnShow()
|
|
end
|
|
end)
|
|
local oldOnHide = uiFrame:GetScript("OnHide")
|
|
uiFrame:SetScript("OnHide", function()
|
|
self:UiFrame_OnHide()
|
|
if oldOnHide then
|
|
oldOnHide()
|
|
end
|
|
end)
|
|
|
|
local function resetClick()
|
|
self.clickedOnce = nil
|
|
end
|
|
|
|
uiFrame:SetScript("OnMouseDown", function()
|
|
-- Clear a pending item sale.
|
|
if self.itemPendingSale then
|
|
self:ClearItemPendingSale()
|
|
end
|
|
|
|
if _G.arg1 == "LeftButton" then
|
|
-- Close non-Settings menus on left mouse down (Settings is closed OnMouseUp).
|
|
if not self.menus:IsMenuOpen("Settings") then
|
|
Bagshui:CloseMenus()
|
|
end
|
|
|
|
-- When the Hearthstone was picked up by dragging it from the
|
|
-- Hearthstone button, take it off the cursor.
|
|
if self.pickedUpHearthstoneFromButton and Bagshui:GetCursorItem() == self.hearthstoneItemRef then
|
|
_G.ClearCursor()
|
|
end
|
|
self.pickedUpHearthstoneFromButton = nil
|
|
|
|
-- Double-click actions.
|
|
if
|
|
self.settings.windowDoubleClickActions
|
|
and self.lastMouseDown
|
|
and _G.GetTime() - self.lastMouseDown < 0.5
|
|
and self.clickedOnce
|
|
then
|
|
if _G.IsAltKeyDown() then
|
|
-- Toggle window position lock.
|
|
self.settings.windowLocked = not self.settings.windowLocked
|
|
else
|
|
-- Toggle toolbars.
|
|
self.ignoreNextSettingChange = true
|
|
if
|
|
self.settings.showHeader == self.settings.showFooter
|
|
and self.headerWasHiddenOnDoubleClick == self.footerWasHiddenOnDoubleClick
|
|
then
|
|
self.headerWasHiddenOnDoubleClick = false
|
|
self.footerWasHiddenOnDoubleClick = false
|
|
self.settings.showHeader = not self.settings.showHeader
|
|
self.settings.showFooter = not self.settings.showFooter
|
|
else
|
|
if self.settings.showHeader == false then
|
|
self.headerWasHiddenOnDoubleClick = true
|
|
self.settings.showHeader = true
|
|
elseif self.headerWasHiddenOnDoubleClick then
|
|
self.headerWasHiddenOnDoubleClick = false
|
|
self.settings.showHeader = false
|
|
elseif self.settings.showFooter == false then
|
|
self.footerWasHiddenOnDoubleClick = true
|
|
self.settings.showFooter = true
|
|
elseif self.footerWasHiddenOnDoubleClick then
|
|
self.footerWasHiddenOnDoubleClick = false
|
|
self.settings.showFooter = false
|
|
end
|
|
end
|
|
self.ignoreNextSettingChange = true
|
|
-- self.settings.showHeader = not self.settings.showHeader
|
|
-- self.settings.showFooter = self.settings.showHeader
|
|
self:Update()
|
|
end
|
|
end
|
|
self.lastMouseDown = _G.GetTime()
|
|
self.clickedOnce = true
|
|
Bagshui:QueueEvent(resetClick, 0.25)
|
|
elseif _G.arg1 == "RightButton" then
|
|
-- Show right-click menu.
|
|
Bagshui:HideTooltips()
|
|
self.ui.tooltips.mini:Hide()
|
|
self.menus:OpenMenu("Main", nil, nil, "cursor")
|
|
end
|
|
end)
|
|
|
|
uiFrame:SetScript("OnMouseUp", function()
|
|
-- Only close Settings menu on mouse up so window can be dragged without closing it.
|
|
-- (All other menus are closed OnMouseDown).
|
|
if self.menus:IsMenuOpen("Settings") then
|
|
Bagshui:CloseMenus()
|
|
end
|
|
ui.frames.searchBox:ClearFocus()
|
|
self:ClearEditModeCursor()
|
|
self:EditModeWindowUpdate()
|
|
end)
|
|
|
|
-- Util.CreateWindowFrame() sets default handlers for these to make window dragging
|
|
-- worked, but Inventory windows need their own handlers due to docking.
|
|
uiFrame:SetScript("OnDragStart", function()
|
|
self:UiFrame_OnDragStart()
|
|
end)
|
|
uiFrame:SetScript("OnDragStop", function()
|
|
self:UiFrame_OnDragStop()
|
|
end)
|
|
|
|
uiFrame:SetScript("OnEnter", function()
|
|
-- If the cursor was holding an item in Edit Mode and left the window,
|
|
-- OnLeave would have hidden it, so bring it back.
|
|
self:ShowEditModeCursor()
|
|
-- Ensure the Bag Bar state is correct.
|
|
self:UpdateBagBar()
|
|
-- Prevent menus from closing.
|
|
_G.UIDropDownMenu_StopCounting(_G.DropDownList1)
|
|
end)
|
|
|
|
uiFrame:SetScript("OnLeave", function()
|
|
-- Edit Mode actions are Inventory class instance-specific, so it makes sense to
|
|
-- turn the cursor back to normal when it leaves the window.
|
|
if not _G.MouseIsOver(_G.this) then
|
|
self:HideEditModeCursor()
|
|
end
|
|
end)
|
|
|
|
-- Header and footer frames.
|
|
|
|
ui.frames.header = ui:CreateHeaderFooter(
|
|
"FrameHeader",
|
|
uiFrame,
|
|
"TOP",
|
|
BsSkin.inventoryHeaderFooterHeight,
|
|
BsSkin.inventoryWindowPadding,
|
|
-BsSkin.inventoryWindowPadding - BsSkin.inventoryHeaderFooterYAdjustment
|
|
)
|
|
local header = ui.frames.header
|
|
|
|
ui.frames.footer = ui:CreateHeaderFooter(
|
|
"FrameFooter",
|
|
uiFrame,
|
|
"BOTTOM",
|
|
BsSkin.inventoryHeaderFooterHeight,
|
|
BsSkin.inventoryWindowPadding,
|
|
BsSkin.inventoryWindowPadding + BsSkin.inventoryHeaderFooterYAdjustment
|
|
)
|
|
local footer = ui.frames.footer
|
|
|
|
footer:SetScript("OnLeave", function()
|
|
self:PrintDebug("footer OnLeave")
|
|
end)
|
|
|
|
-- Main content frame for groups and items.
|
|
ui.frames.main = _G.CreateFrame("Frame", ui:CreateElementName("FrameMain"), uiFrame)
|
|
local mainFrame = ui.frames.main
|
|
mainFrame:SetPoint("TOP", header, "BOTTOM", 0, 0)
|
|
mainFrame:SetPoint("BOTTOM", footer, "TOP", 0, 0)
|
|
mainFrame:SetPoint("LEFT", uiFrame, BsSkin.inventoryWindowPadding, 0)
|
|
mainFrame:SetPoint("RIGHT", uiFrame, -BsSkin.inventoryWindowPadding, 0)
|
|
mainFrame:EnableMouse()
|
|
mainFrame:RegisterForDrag("LeftButton")
|
|
-- Need to hack up the mouse methods so they're passed to the parent frame.
|
|
ui:PassMouseEventsThrough(mainFrame, uiFrame)
|
|
|
|
-- "No inventory data" message for when the cache has never been populated
|
|
-- (like when we haven't yet gotten Bank data).
|
|
ui.text.noData = mainFrame:CreateFontString(nil, nil, "GameFontHighlightSmall")
|
|
ui.text.noData:SetText(L.Inventory_NoData)
|
|
ui.text.noData:SetAllPoints(mainFrame)
|
|
ui.text.noData:SetAlpha(0.4)
|
|
ui.text.noData:Hide()
|
|
|
|
-- Toolbar.
|
|
|
|
-- Shared functions for spell cast buttons.
|
|
|
|
--- Return `true` if the current button does *not* have an associated spell
|
|
--- in its `bagshuiData.spellName` property.
|
|
---@return boolean
|
|
local function inventory_SpellButton_NoSpell()
|
|
return (
|
|
not _G.this.bagshuiData
|
|
or not _G.this.bagshuiData.spellName
|
|
or not BsCharacter.spellNamesToIds[_G.this.bagshuiData.spellName]
|
|
)
|
|
end
|
|
|
|
--- Cast the configured spell on a button.
|
|
local function inventory_SpellButton_OnClick()
|
|
if inventory_SpellButton_NoSpell() then
|
|
return
|
|
end
|
|
_G.CastSpell(BsCharacter.spellNamesToIds[_G.this.bagshuiData.spellName], _G.BOOKTYPE_SPELL)
|
|
end
|
|
|
|
--- Show the spell tooltip associated with a button.
|
|
local function inventory_SpellButton_OnEnter()
|
|
if inventory_SpellButton_NoSpell() then
|
|
return
|
|
end
|
|
_G.GameTooltip:SetOwner(_G.this, "ANCHOR_" .. BsUtil.FlipAnchorPoint(self.settings.windowAnchorXPoint))
|
|
_G.GameTooltip:SetSpell(BsCharacter.spellNamesToIds[_G.this.bagshuiData.spellName], _G.BOOKTYPE_SPELL)
|
|
_G.GameTooltip:Show()
|
|
return false
|
|
end
|
|
|
|
--- Hide the tooltip.
|
|
local function inventory_SpellButton_OnLeave()
|
|
if _G.GameTooltip:IsOwned(_G.this) then
|
|
_G.GameTooltip:Hide()
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Permanent variables for OnUpdate to keep the garbage collector happy.
|
|
local spellButton_OnUpdate_cooldownStart, spellButton_OnUpdate_cooldownDuration, spellButton_OnUpdate_cooldownEnable
|
|
|
|
--- Manage the spell cooldown.
|
|
local function inventory_SpellButton_OnUpdate()
|
|
if inventory_SpellButton_NoSpell() then
|
|
return
|
|
end
|
|
spellButton_OnUpdate_cooldownStart, spellButton_OnUpdate_cooldownDuration, spellButton_OnUpdate_cooldownEnable =
|
|
_G.GetSpellCooldown(BsCharacter.spellNamesToIds[_G.this.bagshuiData.spellName], _G.BOOKTYPE_SPELL)
|
|
if spellButton_OnUpdate_cooldownEnable and (spellButton_OnUpdate_cooldownDuration or 0) > 0 then
|
|
self.ui:SetIconButtonCooldown(
|
|
_G.this,
|
|
spellButton_OnUpdate_cooldownStart,
|
|
spellButton_OnUpdate_cooldownDuration,
|
|
spellButton_OnUpdate_cooldownEnable
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Top left icon.
|
|
buttons.toolbar.menu = ui:CreateIconButton({
|
|
name = "Menu",
|
|
parentFrame = header,
|
|
anchorPoint = "LEFT",
|
|
anchorToFrame = header,
|
|
anchorToPoint = "LEFT",
|
|
width = 16,
|
|
height = 16,
|
|
xOffset = 5,
|
|
onClick = function()
|
|
Bagshui:HideTooltips()
|
|
self.ui.tooltips.mini:Hide()
|
|
self.menus:OpenMenu("Main", nil, nil, _G.this, -5, -5)
|
|
end,
|
|
onEnter = function()
|
|
_G.this.bagshuiData.overrideTooltip = (
|
|
(not self.settings.showFooter and not self.alwaysShowUsageSummary)
|
|
or (self.settings.bagUsageDisplay ~= BS_INVENTORY_BAG_USAGE_DISPLAY.ALWAYS and not self.settings.showBagBar)
|
|
)
|
|
|
|
if _G.this.bagshuiData.overrideTooltip then
|
|
self:ShowUsageSummary(_G.this, false)
|
|
return false
|
|
end
|
|
end,
|
|
onLeave = function()
|
|
if _G.this.bagshuiData.overrideTooltip then
|
|
self:HideUsageSummary(_G.this, false)
|
|
return false
|
|
end
|
|
end,
|
|
texture = self.inventoryType,
|
|
tooltipTitle = L.Toolbar_Menu_TooltipTitle,
|
|
tooltipAnchorPoint = "",
|
|
})
|
|
|
|
-- Offline indicator.
|
|
buttons.toolbar.offline = ui:CreateIconButton({
|
|
name = "State",
|
|
parentFrame = header,
|
|
anchorPoint = "LEFT",
|
|
anchorToFrame = buttons.toolbar.menu,
|
|
anchorToPoint = "RIGHT",
|
|
xOffset = BsSkin.toolbarSpacing,
|
|
onClick = function()
|
|
-- Return to current character if viewing an alt.
|
|
if self.activeCharacterId ~= Bagshui.currentCharacterId then
|
|
self:SetCharacter(Bagshui.currentCharacterId)
|
|
return
|
|
end
|
|
end,
|
|
texture = "Offline",
|
|
tooltipTitle = string.format(_G.FRIENDS_LIST_OFFLINE_TEMPLATE, self.inventoryTypeLocalized),
|
|
noTooltipDelay = true,
|
|
noTooltipTextDelay = true,
|
|
vertexColor = BS_COLOR.RED,
|
|
})
|
|
buttons.toolbar.offline.bagshuiData.noRecolor = true
|
|
buttons.toolbar.offline:Hide()
|
|
|
|
-- Error indicator.
|
|
buttons.toolbar.error = ui:CreateIconButton({
|
|
name = "Error",
|
|
parentFrame = header,
|
|
anchorPoint = "LEFT",
|
|
anchorToFrame = buttons.toolbar.menu, -- Will be updated by UpdateToolbar().
|
|
anchorToPoint = "RIGHT",
|
|
xOffset = BsSkin.toolbarSpacing,
|
|
onClick = function()
|
|
BsLogWindow:Open()
|
|
end,
|
|
texture = "Exclamation",
|
|
tooltipTitle = L.Error,
|
|
tooltipDelay = 0,
|
|
vertexColor = BS_COLOR.UI_ORANGE,
|
|
})
|
|
buttons.toolbar.error.bagshuiData.noRecolor = true
|
|
buttons.toolbar.error:Hide()
|
|
|
|
-- Edit Mode indicator.
|
|
buttons.toolbar.editMode = ui:CreateIconButton({
|
|
name = "EditMode",
|
|
parentFrame = header,
|
|
anchorPoint = "LEFT",
|
|
anchorToFrame = buttons.toolbar.menu, -- Will be updated by UpdateToolbar().
|
|
anchorToPoint = "RIGHT",
|
|
xOffset = BsSkin.toolbarSpacing,
|
|
onClick = function()
|
|
-- Exit Edit Mode.
|
|
if self.editMode then
|
|
_G.PlaySound("igMainMenuOptionCheckBoxOff")
|
|
self:ToggleEditMode()
|
|
return
|
|
end
|
|
end,
|
|
texture = "Edit",
|
|
tooltipTitle = L.Toolbar_ExitEditMode,
|
|
})
|
|
buttons.toolbar.editMode:Hide()
|
|
|
|
-- Close button.
|
|
buttons.toolbar.close = ui:CreateCloseButton(
|
|
"ButtonClose",
|
|
header,
|
|
"RIGHT",
|
|
-6 + (BsSkin.closeButtonInventoryWindowXOffsetAdjustment or 0),
|
|
0 + (BsSkin.closeButtonInventoryWindowYOffsetAdjustment or 0),
|
|
function()
|
|
self:Close()
|
|
end
|
|
)
|
|
buttons.toolbar.close.bagshuiData.noRecolor = true
|
|
|
|
local nextAnchor = buttons.toolbar.close
|
|
local nextOffset = -BsSkin.toolbarCloseButtonOffset
|
|
|
|
-- Catalog is a special case.
|
|
buttons.toolbar.catalog = ui:CreateIconButton({
|
|
name = "Catalog",
|
|
parentFrame = header,
|
|
anchorToFrame = nextAnchor,
|
|
onClick = function()
|
|
BsCatalog:Toggle()
|
|
end,
|
|
texture = "Catalog",
|
|
tooltipTitle = L.Toolbar_Catalog_TooltipTitle,
|
|
tooltipText = L.Toolbar_Catalog_TooltipText,
|
|
})
|
|
nextAnchor = buttons.toolbar.catalog
|
|
nextOffset = -BsSkin.toolbarSpacing
|
|
|
|
-- Right toolbar order, consumed by `Inventory:UpdateToolbarAnchoring()`
|
|
-- to manage anchoring based on what is visible.
|
|
self.ui.ordering.topRightToolbar = {
|
|
buttons.toolbar.close,
|
|
-BsSkin.toolbarCloseButtonOffset,
|
|
buttons.toolbar.catalog,
|
|
}
|
|
|
|
-- Inventory type buttons -- iterating backwards (true parameter) because buttons are added right to left.
|
|
for inventoryType, inventoryTypeLocalized in self:OtherInventoryTypesInToolbarIconOrder(true) do
|
|
local inventoryClass = Bagshui.components[inventoryType]
|
|
buttons.toolbar[inventoryType] = ui:CreateIconButton({
|
|
name = inventoryType,
|
|
parentFrame = header,
|
|
anchorToFrame = nextAnchor,
|
|
onClick = function()
|
|
inventoryClass:Toggle()
|
|
end,
|
|
texture = inventoryType,
|
|
tooltipTitle = inventoryTypeLocalized,
|
|
})
|
|
nextAnchor = buttons.toolbar[inventoryType]
|
|
nextOffset = -BsSkin.toolbarSpacing
|
|
|
|
table.insert(self.ui.ordering.topRightToolbar, buttons.toolbar[inventoryType])
|
|
end
|
|
table.insert(self.ui.ordering.topRightToolbar, -BsSkin.toolbarGroupSpacing)
|
|
|
|
-- Add remainder of toolbar buttons right to left.
|
|
nextOffset = -BsSkin.toolbarGroupSpacing
|
|
for i = table.getn(self.toolbarAndMainMenuItems), 1, -1 do
|
|
local item = self.toolbarAndMainMenuItems[i]
|
|
item.parentFrame = header
|
|
item.anchorToFrame = nextAnchor
|
|
item.tooltipSecondDelay = BS_TOOLTIP_DELAY_SECONDS.TOOLBAR_DEFAULT
|
|
item.xOffset = item.xOffset or nextOffset
|
|
|
|
local button = ui:CreateIconButton(item)
|
|
|
|
-- The toolbar button is stored in buttons.toolbar with the first letter
|
|
-- of its name lowercased (button.toolbar.showHide instead of buttons.toolbar.ShowHide).
|
|
-- (item.id is set in InventoryUi:PopulateToolbarAndMainMenuItems()).
|
|
buttons.toolbar[item.id] = button
|
|
|
|
nextAnchor = button
|
|
|
|
-- Store special spacing directives so they can be read by Inventory:UpdateToolbarAnchoring().
|
|
if item.xOffset and item.xOffset ~= nextOffset then
|
|
table.insert(self.ui.ordering.topRightToolbar, item.xOffset)
|
|
end
|
|
table.insert(self.ui.ordering.topRightToolbar, button)
|
|
end
|
|
|
|
-- Search box (hidden until Search button is clicked).
|
|
frames.searchBox = ui:CreateSearchBox(
|
|
"SearchBox",
|
|
header,
|
|
85, -- Width
|
|
18, -- Height
|
|
|
|
-- OnTextChanged - trigger search.
|
|
function()
|
|
self.searchText = _G.this.bagshuiData.searchText -- Set by autogenerated function in CreateSearchBox().
|
|
self:UpdateItemSlotColors()
|
|
if self.dockedInventory then
|
|
self.dockedInventory:UpdateItemSlotColors()
|
|
end
|
|
if not self.searchTextSetFromEvent then
|
|
Bagshui:RaiseEvent("BAGSHUI_INVENTORY_SEARCH", nil, self.inventoryType, self.searchText)
|
|
end
|
|
self.searchTextSetFromEvent = false
|
|
end,
|
|
|
|
-- OnEnterPressed -- open Catalog if a modifier is down.
|
|
function()
|
|
if self.searchText and (_G.IsAltKeyDown() or _G.IsShiftKeyDown() or _G.IsControlKeyDown()) then
|
|
self:SearchCatalog()
|
|
end
|
|
end,
|
|
|
|
-- OnIconClick -- open Catalog on right-click.
|
|
function()
|
|
if self.searchText and (_G.arg1 == "RightButton" or (_G.arg1 == "LeftButton" and _G.IsAltKeyDown())) then
|
|
self:SearchCatalog()
|
|
end
|
|
end
|
|
)
|
|
frames.searchBox:SetPoint("TOPRIGHT", buttons.toolbar.resort, -BsSkin.toolbarGroupSpacing - 12, 2)
|
|
frames.searchBox:Hide()
|
|
|
|
table.insert(self.ui.ordering.topRightToolbar, frames.searchBox)
|
|
|
|
-- Hide search box when empty.
|
|
local oldOnEditFocusLost = frames.searchBox:GetScript("OnEditFocusLost")
|
|
frames.searchBox:SetScript("OnEditFocusLost", function()
|
|
oldOnEditFocusLost()
|
|
if not self.searchText then
|
|
_G.PlaySound("igMainMenuOptionCheckBoxOff")
|
|
frames.searchBox:Hide()
|
|
buttons.toolbar.search:Show()
|
|
if self.temporarilyShowWindowHeader then
|
|
self.temporarilyShowWindowHeader = false
|
|
self:ForceUpdateWindow()
|
|
else
|
|
self:UpdateToolbar()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Add tooltip text for search icon inside search box.
|
|
frames.searchBox.bagshuiData.searchIcon.bagshuiData.tooltipText =
|
|
string.format(L.Toolbar_Search_TooltipText, self.inventoryTypeLocalized)
|
|
|
|
-- Status text (can't be done sooner because it anchors to the search button).
|
|
|
|
ui.frames.status, ui.text.status = self.ui:CreateLabel(header)
|
|
ui.text.status:SetText(" ")
|
|
ui.frames.status:SetHeight(ui.text.status:GetHeight() + 2)
|
|
ui.frames.status:SetPoint("LEFT", buttons.toolbar.main, "RIGHT", BsSkin.toolbarSpacing, 0)
|
|
ui.frames.status:SetPoint("RIGHT", buttons.toolbar.search, "LEFT", -BsSkin.toolbarSpacing, 0)
|
|
ui.frames.status:Show()
|
|
|
|
-- The last thing anchored to the leftmost right toolbar icon is the status text.
|
|
table.insert(self.ui.ordering.topRightToolbar, ui.frames.status)
|
|
|
|
-- Left toolbar order, consumed by `Inventory:UpdateToolbarAnchoring()`
|
|
-- to manage anchoring based on what is visible.
|
|
self.ui.ordering.topLeftToolbar = {
|
|
buttons.toolbar.menu,
|
|
buttons.toolbar.offline,
|
|
buttons.toolbar.error,
|
|
buttons.toolbar.editMode,
|
|
ui.frames.status,
|
|
}
|
|
|
|
-- Pre-create item slot buttons. More will be created on the fly as needed.
|
|
-- This needs to happen before bag slot buttons because the size of item slot buttons dictates things about bag slot buttons
|
|
for buttonNum = 1, self.initialItemSlotButtons do
|
|
ui:CreateInventoryItemSlotButton(buttonNum)
|
|
end
|
|
|
|
-- Create groups we know need to exist. Like tem slot buttons, more will be created if required.
|
|
for _, row in pairs(self.layout) do
|
|
for _, group in pairs(row) do
|
|
ui:CreateGroup()
|
|
end
|
|
end
|
|
|
|
-- Ensure at least one group exists so its label text can be measured for sizing the mini
|
|
-- tooltip and for use during UpdateWindow().
|
|
if table.getn(frames.groups) == 0 then
|
|
ui:CreateGroup()
|
|
end
|
|
|
|
-- Bag bar.
|
|
|
|
frames.bagBar = _G.CreateFrame("Frame", ui:CreateElementName("BagBar"), footer)
|
|
frames.bagBar:SetPoint("LEFT", footer)
|
|
frames.bagBar:SetPoint("TOP", footer)
|
|
frames.bagBar:SetPoint("BOTTOM", footer)
|
|
frames.bagBar.bagshuiData = {
|
|
baseWidth = (buttons.itemSlots[1].bagshuiData.originalSizeAdjusted + BsSkin.bagBarSpacing)
|
|
* table.getn(self.containerIds),
|
|
}
|
|
frames.bagBar:SetWidth(frames.bagBar.bagshuiData.baseWidth)
|
|
frames.bagBar:EnableMouse(true)
|
|
frames.bagBar:RegisterForDrag("LeftButton")
|
|
ui:PassMouseEventsThrough(frames.bagBar, uiFrame)
|
|
|
|
-- Toggle available space display on mouseover.
|
|
frames.bagBar:SetScript("OnEnter", function()
|
|
frames.bagBar.bagshuiData.mouseIsOver = true
|
|
self:UpdateBagBar()
|
|
end)
|
|
frames.bagBar:SetScript("OnLeave", function()
|
|
frames.bagBar.bagshuiData.mouseIsOver = false
|
|
self:UpdateBagBar()
|
|
end)
|
|
|
|
-- Actual bag slot setup.
|
|
ui:CreateBagSlotButtons()
|
|
|
|
-- Shared functions for utilization displays.
|
|
|
|
local function usageSummary_OnEnter()
|
|
self:ShowUsageSummary(_G.this, true)
|
|
end
|
|
local function usageSummary_OnLeave()
|
|
self:HideUsageSummary(_G.this, true)
|
|
end
|
|
|
|
--- Create a space utilization display.
|
|
--- Frame and FontStrings will *not* have positions set.
|
|
---@param parent table? Parent frame.
|
|
---@param justifyH string? FontString justification.
|
|
---@return table spaceSummary
|
|
local function CreateSpaceSummary(parent, justifyH)
|
|
local spaceSummary = _G.CreateFrame("Frame", nil, parent)
|
|
spaceSummary:SetHitRectInsets(
|
|
-(BsSkin.bagBarSpacing / 2),
|
|
-(BsSkin.bagBarSpacing / 2),
|
|
-(BsSkin.bagBarSpacing / 2),
|
|
-(BsSkin.bagBarSpacing / 2)
|
|
)
|
|
spaceSummary.bagshuiData = {
|
|
text = spaceSummary:CreateFontString(nil, nil, "NumberFontNormalSmall"),
|
|
subtext = spaceSummary:CreateFontString(nil, nil, "NumberFontNormalSmall"),
|
|
}
|
|
spaceSummary.bagshuiData.text:SetTextColor(1, 1, 1, 0.75)
|
|
spaceSummary.bagshuiData.text:SetShadowColor(0, 0, 0, 0.75)
|
|
spaceSummary.bagshuiData.text:SetShadowOffset(1, -1)
|
|
spaceSummary.bagshuiData.text:SetJustifyH(justifyH or "CENTER")
|
|
spaceSummary.bagshuiData.text:SetJustifyV("MIDDLE")
|
|
spaceSummary.bagshuiData.subtext:SetTextColor(1, 1, 1, 0.65)
|
|
spaceSummary.bagshuiData.subtext:SetShadowColor(0, 0, 0, 0.75)
|
|
spaceSummary.bagshuiData.subtext:SetShadowOffset(1, -1)
|
|
spaceSummary.bagshuiData.subtext:SetJustifyH(justifyH or "CENTER")
|
|
spaceSummary.bagshuiData.subtext:SetJustifyV("MIDDLE")
|
|
spaceSummary.bagshuiData.subtext:SetFont(spaceSummary.bagshuiData.subtext:GetFont(), 9)
|
|
|
|
-- Show individual bag type information on mouseover.
|
|
spaceSummary:EnableMouse(true)
|
|
spaceSummary:RegisterForDrag("LeftButton")
|
|
ui:PassMouseEventsThrough(spaceSummary, uiFrame, true)
|
|
spaceSummary:SetScript("OnEnter", usageSummary_OnEnter)
|
|
spaceSummary:SetScript("OnLeave", usageSummary_OnLeave)
|
|
|
|
return spaceSummary
|
|
end
|
|
|
|
-- This is the main utilization display that goes to the right of the bag bar.
|
|
frames.spaceSummary = CreateSpaceSummary(frames.bagBar, "CENTER")
|
|
frames.spaceSummary:SetPoint(
|
|
"LEFT",
|
|
buttons.bagSlots[table.getn(buttons.bagSlots)],
|
|
"RIGHT",
|
|
BsSkin.bagBarSpacing - 2,
|
|
0
|
|
)
|
|
frames.spaceSummary:SetPoint("TOP", frames.bagBar)
|
|
frames.spaceSummary:SetPoint("BOTTOM", frames.bagBar)
|
|
frames.spaceSummary:SetWidth(50)
|
|
frames.spaceSummary.bagshuiData.text:SetPoint("BOTTOM", frames.spaceSummary, "CENTER", 0, -2)
|
|
frames.spaceSummary.bagshuiData.subtext:SetPoint("TOP", frames.spaceSummary, "CENTER", 0, -3)
|
|
|
|
-- Create two "mini" utilization displays to use when the bag bar is hidden.
|
|
-- One is for the bottom left and the other for the top left when the bottom toolbar is hidden.
|
|
-- Reusing just one would be nice but this is more expedient.
|
|
for _, position in ipairs({ "Bottom", "Top" }) do
|
|
local name = "miniSpaceSummary" .. position
|
|
frames[name] = CreateSpaceSummary(footer, "LEFT")
|
|
frames[name]:SetPoint("LEFT", footer, "LEFT", BsSkin.toolbarSpacing, 0)
|
|
frames[name]:SetWidth(100)
|
|
frames[name]:SetHeight(18)
|
|
frames[name].bagshuiData.text:SetPoint("LEFT", frames[name], "LEFT", 0, 0)
|
|
frames[name].bagshuiData.subtext:SetPoint("LEFT", frames[name].bagshuiData.text, "RIGHT", 1, 0.5)
|
|
end
|
|
-- Make the top one conditionally part of the top left toolbar.
|
|
frames.miniSpaceSummaryTop:SetParent(header)
|
|
frames.miniSpaceSummaryTop:ClearAllPoints()
|
|
frames.miniSpaceSummaryTop.bagshuiData.text:SetPoint("LEFT", frames.miniSpaceSummaryTop, "LEFT", 0, -1)
|
|
table.insert(self.ui.ordering.topLeftToolbar, 2, frames.miniSpaceSummaryTop)
|
|
|
|
-- Bottom right toolbar anchor.
|
|
frames.bottomRightToolbarAnchor = _G.CreateFrame("Frame", nil, footer)
|
|
frames.bottomRightToolbarAnchor:SetHeight(25)
|
|
frames.bottomRightToolbarAnchor:SetWidth(1)
|
|
frames.bottomRightToolbarAnchor:SetPoint("RIGHT", footer, "RIGHT", 5, 0)
|
|
|
|
-- Money frame.
|
|
|
|
local moneyFrameName = ui:CreateElementName("Money")
|
|
frames.money = _G.CreateFrame("Frame", ui:CreateElementName("Money"), footer, "SmallMoneyFrameTemplate")
|
|
frames.money.bagshuiData = {
|
|
name = moneyFrameName,
|
|
texts = {
|
|
gold = _G[moneyFrameName .. "GoldButtonText"],
|
|
silver = _G[moneyFrameName .. "SilverButtonText"],
|
|
copper = _G[moneyFrameName .. "CopperButtonText"],
|
|
},
|
|
autoLayoutXOffset = 12,
|
|
}
|
|
frames.money:SetWidth(22)
|
|
frames.money:SetHeight(25)
|
|
frames.money:SetPoint("RIGHT", 8, 0)
|
|
|
|
-- Need custom OnEnter/OnLeave to show tooltip with all characters' money.
|
|
|
|
frames.money:EnableMouse(true)
|
|
|
|
frames.money:SetScript("OnEnter", function()
|
|
if self.editMode then
|
|
return
|
|
end
|
|
_G.GameTooltip:SetOwner(frames.money, "ANCHOR_TOPRIGHT", -8, 0)
|
|
BsCatalog:AddTooltipInfo(BS_CATALOG_LOCATIONS.MONEY, _G.GameTooltip)
|
|
_G.GameTooltip:Show()
|
|
end)
|
|
|
|
frames.money:SetScript("OnLeave", function()
|
|
if _G.GameTooltip:IsOwned(frames.money) then
|
|
_G.GameTooltip:Hide()
|
|
end
|
|
end)
|
|
|
|
local function moneyFrameOnEnter()
|
|
frames.money:GetScript("OnEnter")()
|
|
end
|
|
local function moneyFrameOnLeave()
|
|
frames.money:GetScript("OnLeave")()
|
|
end
|
|
|
|
for _, child in ipairs({ frames.money:GetChildren() }) do
|
|
if child.HasScript and child:HasScript("OnEnter") then
|
|
child:SetScript("OnEnter", moneyFrameOnEnter)
|
|
child:SetScript("OnLeave", moneyFrameOnLeave)
|
|
end
|
|
end
|
|
|
|
-- Hearthstone button.
|
|
|
|
buttons.toolbar.hearthstone = ui:CreateIconButton({
|
|
name = "Hearthstone",
|
|
parentFrame = footer,
|
|
anchorPoint = "RIGHT",
|
|
anchorToFrame = frames.money,
|
|
anchorToPoint = "LEFT",
|
|
disable = false,
|
|
onClick = function()
|
|
if Bagshui:GetCursorItem() == self.hearthstoneItemRef then
|
|
_G.ClearCursor()
|
|
return
|
|
end
|
|
if self.hearthstoneItemRef then
|
|
_G.UseContainerItem(self.hearthstoneItemRef.bagNum, self.hearthstoneItemRef.slotNum, true)
|
|
else
|
|
Bagshui:ShowAndLogErrorMessage(L.Error_HearthstoneNotFound)
|
|
end
|
|
end,
|
|
texture = "Hearthstone",
|
|
onEnter = function()
|
|
if self.hearthstoneItemRef then
|
|
_G.this.bagshuiData.bagNum = self.hearthstoneItemRef.bagNum
|
|
_G.this.bagshuiData.slotNum = self.hearthstoneItemRef.slotNum
|
|
self:ItemButton_OnEnter()
|
|
end
|
|
end,
|
|
onLeave = function()
|
|
self:ItemButton_OnLeave()
|
|
end,
|
|
onUpdate = function()
|
|
self:ItemButton_OnUpdate(_G.arg1)
|
|
end,
|
|
})
|
|
-- Make the Hearthstone toolbar button compatible with our `ContainerFrameItemButton_` hackery.
|
|
ui:AddItemSlotButtonGetIdProxy(buttons.toolbar.hearthstone)
|
|
|
|
-- Allow picking up the Hearthstone from the button.
|
|
buttons.toolbar.hearthstone:RegisterForDrag("LeftButton")
|
|
buttons.toolbar.hearthstone:SetScript("OnDragStart", function()
|
|
local hearthstone = self.hearthstoneItemRef
|
|
if hearthstone then
|
|
Bagshui:PickupItem(hearthstone, self, nil, true)
|
|
end
|
|
self.pickedUpHearthstoneFromButton = true
|
|
end)
|
|
|
|
-- Even though we always create the hearthstone button, it may not be enabled
|
|
-- for this class instance.
|
|
if not self.hearthButton then
|
|
buttons.toolbar.hearthstone:Hide()
|
|
end
|
|
|
|
-- Clam (open container) button.
|
|
buttons.toolbar.clam = ui:CreateIconButton({
|
|
name = "Clam",
|
|
parentFrame = footer,
|
|
anchorPoint = "RIGHT",
|
|
anchorToFrame = buttons.toolbar.hearthstone,
|
|
anchorToPoint = "LEFT",
|
|
disable = false,
|
|
texture = "Clam",
|
|
tooltipTitle = L.OpenContainer,
|
|
onClick = function()
|
|
if self.nextOpenableItemBagNum and self.nextOpenableItemSlotNum then
|
|
_G.UseContainerItem(self.nextOpenableItemBagNum, self.nextOpenableItemSlotNum)
|
|
end
|
|
end,
|
|
onEnter = function()
|
|
-- Actual work will be handled in OnUpdate.
|
|
_G.this.bagshuiData.mouseIsOver = true
|
|
end,
|
|
onLeave = function()
|
|
_G.this.bagshuiData.mouseIsOver = false
|
|
self.highlightItemsInContainerId = nil
|
|
self.highlightItemsContainerSlot = nil
|
|
self:UpdateItemSlotColors()
|
|
if self.nextOpenableItemSlotButton then
|
|
self:ItemButton_OnLeave(self.nextOpenableItemSlotButton)
|
|
elseif self.lastHighlightedOpenableButton then
|
|
self:ItemButton_OnLeave(self.lastHighlightedOpenableButton)
|
|
end
|
|
self.lastHighlightedOpenableButton = nil
|
|
end,
|
|
onUpdate = function()
|
|
if not _G.this.bagshuiData.mouseIsOver then
|
|
-- We need to essentially fake an OnLeave event when the
|
|
-- last container is opened because the normal OnLeave
|
|
-- won't fire, which leads to the light highlighted item
|
|
-- slot button thinking it's still moused over.
|
|
if _G.this.bagshuiData.wasUpdated then
|
|
_G.this.bagshuiData.wasUpdated = false
|
|
end
|
|
return
|
|
end
|
|
self:HighlightNextOpenable()
|
|
_G.this.bagshuiData.wasUpdated = true
|
|
end,
|
|
})
|
|
|
|
-- Disenchant button.
|
|
buttons.toolbar.disenchant = ui:CreateIconButton({
|
|
name = "Disenchant",
|
|
parentFrame = footer,
|
|
anchorPoint = "RIGHT",
|
|
anchorToFrame = buttons.toolbar.clam,
|
|
anchorToPoint = "LEFT",
|
|
disable = false,
|
|
texture = "Disenchant",
|
|
onClick = inventory_SpellButton_OnClick,
|
|
onEnter = inventory_SpellButton_OnEnter,
|
|
onLeave = inventory_SpellButton_OnLeave,
|
|
onUpdate = inventory_SpellButton_OnUpdate,
|
|
})
|
|
buttons.toolbar.disenchant.bagshuiData.spellName = L.Spell_Disenchant
|
|
|
|
-- Pick Lock button.
|
|
buttons.toolbar.pickLock = ui:CreateIconButton({
|
|
name = "PickLock",
|
|
parentFrame = footer,
|
|
anchorPoint = "RIGHT",
|
|
anchorToFrame = buttons.toolbar.disenchant,
|
|
anchorToPoint = "LEFT",
|
|
disable = false,
|
|
texture = "PickLock",
|
|
onClick = inventory_SpellButton_OnClick,
|
|
onEnter = inventory_SpellButton_OnEnter,
|
|
onLeave = inventory_SpellButton_OnLeave,
|
|
onUpdate = inventory_SpellButton_OnUpdate,
|
|
})
|
|
buttons.toolbar.pickLock.bagshuiData.spellName = L.Spell_PickLock
|
|
|
|
-- Bottom right toolbar order, consumed by `Inventory:UpdateToolbarAnchoring()`
|
|
-- to manage anchoring based on what is visible.
|
|
self.ui.ordering.bottomRightToolbar = {
|
|
frames.bottomRightToolbarAnchor,
|
|
frames.money,
|
|
-BsSkin.toolbarGroupSpacing,
|
|
buttons.toolbar.hearthstone,
|
|
-BsSkin.toolbarGroupSpacing,
|
|
buttons.toolbar.clam,
|
|
buttons.toolbar.pickLock,
|
|
buttons.toolbar.disenchant,
|
|
}
|
|
|
|
-- Tooltips.
|
|
|
|
-- Mini is used to display full group label names on mouseover and for toolbar tooltips.
|
|
-- It's scaled based on the first group's label text, so that needs to be created first.
|
|
-- There is a mini tooltip per Inventory class instance because it is sized based on the
|
|
-- group label text.
|
|
tooltips.mini = Bagshui:CreateTooltip("MiniTooltip")
|
|
local miniTooltipName = tooltips.mini.bagshuiData.name -- We'll need to know the name in a bit to grab the TextLeft1 font string.
|
|
|
|
-- Scale the Mini tooltip down to match the size of the group label text (as close as possible).
|
|
-- This is being done in a loop because it's the only way I could figure to get an accurate result.
|
|
local miniTooltipTargetFontSize = ui.frames.groups[1].bagshuiData.text:GetHeight()
|
|
local miniTooltipScaleStep = 0.03
|
|
local miniTooltipScale = 1 + miniTooltipScaleStep -- Start higher than 1 because the next step is to subtract
|
|
local miniTooltipActualFontSize
|
|
-- Tooltip needs to have and owner and content to measure.
|
|
tooltips.mini:SetOwner(uiFrame, "ANCHOR_PRESERVE")
|
|
tooltips.mini:SetText(" ")
|
|
repeat
|
|
-- Set the scale, measure the result, see if it's small enough yet.
|
|
miniTooltipScale = miniTooltipScale - miniTooltipScaleStep
|
|
tooltips.mini:SetScale(miniTooltipScale)
|
|
tooltips.mini:Show()
|
|
miniTooltipActualFontSize = _G[miniTooltipName .. "TextLeft1"]:GetHeight() * miniTooltipScale
|
|
until miniTooltipActualFontSize <= miniTooltipTargetFontSize or miniTooltipScale <= 0.5
|
|
tooltips.mini:SetScale(miniTooltipScale - miniTooltipScaleStep)
|
|
-- Prepare for later use.
|
|
tooltips.mini:Hide()
|
|
end
|
|
|
|
--- Creates the list of items stored in `self.toolbarAndMainMenuItems`, which is
|
|
--- used to build both the main window toolbar and the main menu Actions list.
|
|
function Inventory:PopulateToolbarAndMainMenuItems()
|
|
-- Notes:
|
|
-- - Textures are assumed to be in the Icons folder.
|
|
-- - Items are in top-to-bottom menu order and left-to-right toolbar order.
|
|
-- - onClick is the toolbar button click function.
|
|
-- - Unless func is specified, the menu item will click the toolbar button.
|
|
|
|
self.toolbarAndMainMenuItems = {
|
|
|
|
{
|
|
-- Changing this name property will break several things, including
|
|
-- the `onClick` function below and `Inventory:Search()`.
|
|
name = "Search",
|
|
texture = "Search",
|
|
tooltipTitle = L.Toolbar_Search_TooltipTitle,
|
|
tooltipText = string.format(L.Toolbar_Search_TooltipText, self.inventoryTypeLocalized),
|
|
xOffset = -BsSkin.toolbarGroupSpacing,
|
|
onClick = function()
|
|
_G.PlaySound("igMainMenuOptionCheckBoxOn")
|
|
self.ui.buttons.toolbar.search:Hide() -- This relies on the `name` property of this button being "Search".
|
|
if not self.settings.showHeader then
|
|
self.temporarilyShowWindowHeader = true
|
|
self:ForceUpdateWindow()
|
|
end
|
|
self.ui.frames.searchBox:Show()
|
|
self:UpdateToolbar()
|
|
self.ui.frames.searchBox:SetFocus()
|
|
end,
|
|
},
|
|
|
|
{
|
|
name = "Resort",
|
|
texture = "Resort",
|
|
tooltipTitle = L.Toolbar_Resort_TooltipTitle,
|
|
tooltipText = L.Toolbar_Resort_TooltipText,
|
|
disable = true,
|
|
onClick = function()
|
|
_G.PlaySound("igQuestLogOpen")
|
|
self:Resort()
|
|
end,
|
|
},
|
|
|
|
{
|
|
name = "Restack",
|
|
texture = "Restack",
|
|
tooltipTitle = L.Toolbar_Restack_TooltipTitle,
|
|
tooltipText = L.Toolbar_Restack_TooltipText,
|
|
xOffset = -BsSkin.toolbarGroupSpacing,
|
|
disable = true,
|
|
onClick = function()
|
|
_G.PlaySound("UChatScrollButton")
|
|
self:Restack()
|
|
end,
|
|
},
|
|
|
|
{
|
|
name = "ShowHide",
|
|
texture = "Show",
|
|
tooltipTitle = L.Toolbar_Show_TooltipTitle,
|
|
tooltipText = L.Toolbar_Show_TooltipText,
|
|
onClick = function()
|
|
self:ToggleProperty("showHidden")
|
|
end,
|
|
_bagshuiCheckedFunc = function()
|
|
return self.showHidden
|
|
end,
|
|
},
|
|
|
|
{
|
|
name = "HighlightChanges",
|
|
texture = "HighlightChanges",
|
|
tooltipTitle = L.Toolbar_HighlightChanges_TooltipTitle,
|
|
tooltipText = L.Toolbar_HighlightChanges_TooltipText,
|
|
xOffset = -BsSkin.toolbarGroupSpacing,
|
|
onClick = function()
|
|
if _G.IsAltKeyDown() then
|
|
self:ResetStockState()
|
|
else
|
|
self:ToggleProperty("highlightChanges")
|
|
end
|
|
end,
|
|
_bagshuiCheckedFunc = function()
|
|
return self.highlightChanges
|
|
end,
|
|
},
|
|
|
|
{
|
|
name = "Character",
|
|
texture = "CharacterRobed",
|
|
tooltipTitle = L.Toolbar_Character_TooltipTitle,
|
|
tooltipText = string.format(L.Toolbar_Character_TooltipText, self.inventoryTypeLocalized),
|
|
xOffset = -BsSkin.toolbarGroupSpacing,
|
|
menuHasArrow = true,
|
|
menuHideArrow = true,
|
|
menuIgnoreOnClick = true,
|
|
-- There's a chicken-and-egg issue between building the toolbar item
|
|
-- list and constructing the menus because the menu won't exist yet.
|
|
-- when this code is executed. Placing the property retrieval
|
|
-- in a function is the workaround.
|
|
getMenuValueProp = function()
|
|
return self.menus.menuList.Character.levels[1]
|
|
end,
|
|
onClickBeforeCloseMenusAndClearFocuses = function()
|
|
-- Whether the character menu was open before the default button OnClick closed it.
|
|
_G.this.bagshuiData.characterMenuWasOpen = self.menus:IsMenuOpen("Character")
|
|
end,
|
|
onClick = function()
|
|
if _G.this.bagshuiData.characterMenuWasOpen then
|
|
Bagshui:CloseMenus()
|
|
else
|
|
-- Open the menu with a sensible alignment based on window anchoring.
|
|
self.menus:OpenMenu(
|
|
"Character", -- menuType
|
|
_G.this, -- arg1
|
|
nil, -- arg2
|
|
_G.this, -- anchorFrame
|
|
(5 * (self.settings.windowAnchorXPoint == "LEFT" and -1 or 1)), -- xOffset
|
|
-5, -- yOffset
|
|
"TOP" .. self.settings.windowAnchorXPoint, -- anchorPoint
|
|
"BOTTOM" .. self.settings.windowAnchorXPoint -- anchorToPoint
|
|
)
|
|
end
|
|
end,
|
|
},
|
|
}
|
|
|
|
-- Pre-set full texture paths and firstLetterLowercased name.
|
|
for _, item in ipairs(self.toolbarAndMainMenuItems) do
|
|
item.id = BsUtil.LowercaseFirstLetter(item.name)
|
|
if item.texture then
|
|
item.texture = BsUtil.GetFullTexturePath("Icons\\" .. item.texture)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Iterator function to provide the list of inventory types that AREN'T the current class, in the
|
|
--- order defined by INVENTORY_TYPE_UI_ORDER (reversed if requested).
|
|
---@param reverse boolean?
|
|
---@param includeSelf boolean? The current instance's inventory type will be omitted unless this is true.
|
|
---@return function # Iterator function that returns the inventory type along with the localized value.
|
|
function Inventory:OtherInventoryTypesInToolbarIconOrder(reverse, includeSelf)
|
|
local inventoryTypes = BS_INVENTORY_TYPE_UI_ORDER
|
|
local startIndex = reverse and table.getn(inventoryTypes) + 1 or 0
|
|
local endIndex = reverse and 0 or table.getn(inventoryTypes) + 1
|
|
local step = reverse and -1 or 1
|
|
local i = startIndex
|
|
---@return string inventoryType
|
|
---@return string inventoryTypeLocalized
|
|
return function()
|
|
repeat
|
|
i = i + step
|
|
until inventoryTypes[i] ~= self.inventoryType or includeSelf or i == endIndex
|
|
-- Need to use L_nil here so that nil is returned at the end of the loop
|
|
return inventoryTypes[i], L_nil[inventoryTypes[i]]
|
|
end
|
|
end
|
|
|
|
--- Reset everything in preparation for displaying the window, including the items below,
|
|
--- then trigger categorization and sorting.
|
|
--- - Edit Mode off.
|
|
--- - Empty slot stacks collapsed.
|
|
--- - Top toolbar hidden if it was temporarily visible.
|
|
--- - Pending trade-with-another player item cleared.
|
|
function Inventory:UiFrame_OnShow()
|
|
if self.dockTo then
|
|
-- Used by SetDockedToFrameVisibility() to decide whether the frame to which this
|
|
-- one is docked should also be closed when this frame is closed.
|
|
self.dockingFrameVisibleOnLastOpen = Bagshui.components[self.dockTo]:Visible()
|
|
end
|
|
|
|
_G.PlaySound(self.openSound)
|
|
|
|
self.online = true
|
|
self.editMode = false
|
|
self.expandEmptySlotStacks = false
|
|
self.temporarilyShowWindowHeader = false
|
|
self.highlightChanges = false
|
|
self.queuedTradeItem = nil -- Used by Inventory:TradeFrame_OnShow().
|
|
self.itemPendingSale = nil -- Used by Inventory:ItemButton_OnClick() to confirm sale of protected items.
|
|
|
|
-- Reset any item highlighting.
|
|
self.highlightItemsInContainerId = nil
|
|
self.highlightItemsContainerSlot = nil
|
|
|
|
-- Because Update() is called from the UI frame's OnShow function, self:Visible() becomes true
|
|
-- before CategorizeAndSort() executes. Normally, CategorizeAndSort() bails if self:Visible() is true,
|
|
-- so forceResort is set to true to ensure inventory is properly sorted whenever the window is opened.
|
|
-- Of course, we don't want this to happen when automatic resorting is off.
|
|
if not self.settings.disableAutomaticResort then
|
|
self.forceResort = true
|
|
end
|
|
self.resortNeeded = true
|
|
self.inventoryUpdateAllowed = true
|
|
self.cacheUpdateNeeded = true
|
|
self:NotifySkinOfPositionChange()
|
|
-- Do NOT use QueueUpdate() here.
|
|
self:Update()
|
|
end
|
|
|
|
--- Perform tasks that need to occur when the window is closed, including:
|
|
--- - Take the frame out of move mode.
|
|
--- - Close all menus.
|
|
--- - Reset the search box.
|
|
--- - Clear the Edit Mode cursor.
|
|
--- - Reset the
|
|
--- - If there's a frame docked to this one, close it.
|
|
function Inventory:UiFrame_OnHide()
|
|
self.uiFrame:StopMovingOrSizing()
|
|
self:ReleaseDragOutline()
|
|
-- If the mouse button was released while the game hitched, OnDragStop may never have fired.
|
|
-- A stuck flag would make FixWindowPosition() (and window rescue) skip this window forever.
|
|
self.dragInProgress = false
|
|
self:RunUpdateDeferredByDrag()
|
|
self:ClearSearch()
|
|
self:CloseMenusAndClearFocuses()
|
|
self:ClearEditModeCursor()
|
|
self:SetCharacter(Bagshui.currentCharacterId)
|
|
|
|
_G.PlaySound(self.closeSound)
|
|
|
|
-- If there's a frame docked to this frame, close it too.
|
|
if self.dockedInventory then
|
|
self.dockedInventory:Close()
|
|
end
|
|
end
|
|
|
|
-- Outline that follows the mouse while an inventory window is dragged.
|
|
-- Moving the real window makes the game re-position every item button, texture and cooldown
|
|
-- inside it on each mouse movement, which froze the whole game for seconds with the Bags and
|
|
-- Bank open. The outline is a single empty frame, and the real window moves once, on drop.
|
|
local dragOutline
|
|
|
|
local function GetDragOutline()
|
|
if not dragOutline then
|
|
dragOutline = _G.CreateFrame("Frame", "BagshuiDragOutline", _G.UIParent)
|
|
dragOutline:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
dragOutline:SetMovable(true)
|
|
dragOutline:EnableMouse(true)
|
|
if dragOutline.SetClampedToScreen then
|
|
dragOutline:SetClampedToScreen(true)
|
|
end
|
|
dragOutline:SetBackdrop({
|
|
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = true,
|
|
tileSize = 16,
|
|
edgeSize = 16,
|
|
insets = { left = 4, right = 4, top = 4, bottom = 4 },
|
|
})
|
|
dragOutline:SetBackdropColor(0, 0, 0, 0.3)
|
|
dragOutline:SetBackdropBorderColor(1, 1, 1, 0.9)
|
|
-- If the drop was ever missed, clicking the outline finishes the move.
|
|
dragOutline:SetScript("OnMouseUp", function()
|
|
if dragOutline.owner then
|
|
dragOutline.owner:UiFrame_OnDragStop()
|
|
end
|
|
end)
|
|
dragOutline:Hide()
|
|
end
|
|
return dragOutline
|
|
end
|
|
|
|
--- Stop and hide the drag outline if this window owns it.
|
|
---@return number? left, number? top Where the outline was dropped (window coordinates).
|
|
function Inventory:ReleaseDragOutline()
|
|
if not dragOutline or dragOutline.owner ~= self then
|
|
return
|
|
end
|
|
dragOutline:StopMovingOrSizing()
|
|
local left, top = dragOutline:GetLeft(), dragOutline:GetTop()
|
|
dragOutline:Hide()
|
|
dragOutline.owner = nil
|
|
return left, top
|
|
end
|
|
|
|
--- Handle dragging (start).
|
|
function Inventory:UiFrame_OnDragStart()
|
|
if self.dockTo then
|
|
Bagshui.components[self.dockTo]:UiFrame_OnDragStart()
|
|
elseif not self.settings.windowLocked then
|
|
-- Has to be self instead of "this" because it can be called from docked windows.
|
|
self.dragInProgress = true
|
|
self.dragStartedAt = _G.GetTime()
|
|
|
|
local frame = self.uiFrame
|
|
local left, top = frame:GetLeft(), frame:GetTop()
|
|
if left and top then
|
|
-- Same parent and scale as the window, so both use the same coordinates.
|
|
local outline = GetDragOutline()
|
|
outline.owner = self
|
|
outline:SetScale(frame:GetScale())
|
|
outline:SetWidth(frame:GetWidth())
|
|
outline:SetHeight(frame:GetHeight())
|
|
outline:ClearAllPoints()
|
|
outline:SetPoint("TOPLEFT", _G.UIParent, "BOTTOMLEFT", left, top)
|
|
outline:Show()
|
|
outline:StartMoving()
|
|
else
|
|
frame:StartMoving()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Handle dragging (stop).
|
|
function Inventory:UiFrame_OnDragStop()
|
|
if self.dockTo then
|
|
Bagshui.components[self.dockTo]:UiFrame_OnDragStop()
|
|
elseif not self.settings.windowLocked then
|
|
-- Has to be self instead of "this" because it can be called from docked windows.
|
|
if not self.dragInProgress then
|
|
return
|
|
end
|
|
self.dragInProgress = false
|
|
local left, top = self:ReleaseDragOutline()
|
|
if left and top then
|
|
-- Move the real window once, to where the outline was dropped.
|
|
self.uiFrame:ClearAllPoints()
|
|
self.uiFrame:SetPoint("TOPLEFT", _G.UIParent, "BOTTOMLEFT", left, top)
|
|
else
|
|
self.uiFrame:StopMovingOrSizing()
|
|
end
|
|
self:FixSettingsMenuPosition()
|
|
self:SaveWindowPosition()
|
|
-- If the window was dropped (nearly) off the screen, bring it back right away
|
|
-- instead of saving an unreachable position.
|
|
self:RescueWindow()
|
|
self:NotifySkinOfPositionChange()
|
|
-- Catch up on anything that changed while the window was moving.
|
|
self:RunUpdateDeferredByDrag()
|
|
if self.dockedInventory then
|
|
self.dockedInventory:RunUpdateDeferredByDrag()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Is this window (or the window it's docked to) being dragged right now?
|
|
--- Gives up after 10 seconds in case OnDragStop never fired (game hitch while releasing the mouse),
|
|
--- so updates can't stay paused.
|
|
---@return boolean
|
|
function Inventory:IsDragInProgress()
|
|
local owner = self.dockTo and Bagshui.components[self.dockTo] or self
|
|
return (
|
|
owner.dragInProgress
|
|
and owner.dragStartedAt
|
|
and (_G.GetTime() - owner.dragStartedAt) < 10
|
|
) and true or false
|
|
end
|
|
|
|
--- Run an `Update()` that was skipped because the window was being dragged.
|
|
function Inventory:RunUpdateDeferredByDrag()
|
|
if self.updateDeferredByDrag then
|
|
local cascade = self.updateDeferredByDragCascade
|
|
self.updateDeferredByDrag = false
|
|
self.updateDeferredByDragCascade = nil
|
|
self:QueueUpdate(nil, cascade)
|
|
end
|
|
end
|
|
|
|
--- Display the inventory utilization summary tooltip.
|
|
---@param owner table Frame to which the tooltip should bet attached.
|
|
---@param attachedToBagBar boolean? When `true`, call the bag bar's OnEnter.
|
|
---@param extraText string? Text to append to the tooltip.
|
|
---@param delay boolean|number? Time to wait before showing the tooltip.
|
|
---@param tooltip table? Tooltip to use instead of GameTooltip.
|
|
function Inventory:ShowUsageSummary(owner, attachedToBagBar, tooltip, extraText, delay)
|
|
if self.editMode then
|
|
return
|
|
end
|
|
|
|
owner = owner or _G.this
|
|
tooltip = tooltip or _G.GameTooltip
|
|
|
|
_G.this.bagshuiData.mouseIsOver = true
|
|
|
|
-- Trigger bag bar OnEnter to avoid flickering.
|
|
if attachedToBagBar then
|
|
if self.ui.frames.bagBar:IsVisible() then
|
|
self.ui.frames.bagBar:GetScript("OnEnter")()
|
|
end
|
|
end
|
|
|
|
-- Add overall info.
|
|
tooltip:SetOwner(
|
|
owner,
|
|
"ANCHOR_" .. BsUtil.FlipAnchorPoint(self.settings.windowAnchorXPoint),
|
|
-BsSkin.tooltipExtraOffset,
|
|
BsSkin.tooltipExtraOffset
|
|
)
|
|
tooltip:AddDoubleLine(
|
|
-- Available: 10 Used: 8/18
|
|
string.format(L.Symbol_Colon, L.Available)
|
|
.. " "
|
|
.. tostring(self.availableSlots),
|
|
string.format(L.Symbol_Colon, L.Used)
|
|
.. " "
|
|
.. string.format("%s/%s", tostring(self.usedSlots), tostring(self.totalSlots))
|
|
)
|
|
|
|
-- Only show per-bag-type info if there are profession bags.
|
|
local hasProfessionBags = false
|
|
for containerType, spaceInfo in pairs(self.containerSpace) do
|
|
if (spaceInfo.total or 0) > 0 and containerType ~= L.Bag then
|
|
hasProfessionBags = true
|
|
end
|
|
end
|
|
|
|
-- Add per-bag-type info, excluding any bag types where total is 0 (necessary
|
|
-- because we don't bother cleaning up the containerSpace table when all bags
|
|
-- of a given type are unequipped).
|
|
if hasProfessionBags then
|
|
for _, containerType in ipairs(BS_INVENTORY_CONTAINER_TYPE_ORDER) do
|
|
local spaceInfo = self.containerSpace[containerType]
|
|
if spaceInfo and (spaceInfo.total or 0) > 0 then
|
|
-- Bag: 2 6/8
|
|
tooltip:AddDoubleLine(
|
|
string.format(L.Symbol_Colon, containerType) .. " " .. tostring(spaceInfo.available),
|
|
string.format("%s/%s", tostring(spaceInfo.used), tostring(spaceInfo.total)),
|
|
HIGHLIGHT_FONT_COLOR.r,
|
|
HIGHLIGHT_FONT_COLOR.g,
|
|
HIGHLIGHT_FONT_COLOR.b,
|
|
HIGHLIGHT_FONT_COLOR.r,
|
|
HIGHLIGHT_FONT_COLOR.g,
|
|
HIGHLIGHT_FONT_COLOR.b
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
if delay then
|
|
Bagshui:ShowTooltipAfterDelay(
|
|
tooltip,
|
|
owner,
|
|
nil,
|
|
type(delay) == "number" and delay or BS_TOOLTIP_DELAY_SECONDS.DEFAULT,
|
|
nil,
|
|
function()
|
|
if extraText then
|
|
Bagshui:QueueEvent(function()
|
|
if tooltip:IsOwned(owner) and tooltip:IsVisible() then
|
|
tooltip:AddLine(extraText)
|
|
tooltip:Show()
|
|
end
|
|
end, BS_TOOLTIP_DELAY_SECONDS.TOOLBAR_DEFAULT)
|
|
end
|
|
end
|
|
)
|
|
else
|
|
if extraText then
|
|
tooltip:AddLine(extraText)
|
|
tooltip:Show()
|
|
end
|
|
tooltip:Show()
|
|
end
|
|
end
|
|
|
|
--- Companion to `ShowUsageSummary()`.
|
|
---@param owner table Tooltip will only be hidden if still owned by this frame.
|
|
---@param attachedToBagBar boolean? When `true`, call the bag bar's OnLeave.
|
|
---@param tooltip table? Tooltip to use instead of GameTooltip.
|
|
function Inventory:HideUsageSummary(owner, attachedToBagBar, tooltip)
|
|
owner = owner or _G.this
|
|
tooltip = tooltip or _G.GameTooltip
|
|
|
|
owner.bagshuiData.mouseIsOver = false
|
|
|
|
-- Hide tooltip.
|
|
if tooltip:IsOwned(owner) then
|
|
tooltip:Hide()
|
|
end
|
|
|
|
-- Decide whether to keep displaying slot available/used counts.
|
|
if attachedToBagBar and self.ui.frames.bagBar:IsVisible() then
|
|
self.ui.frames.bagBar:GetScript("OnLeave")()
|
|
end
|
|
end
|
|
|
|
--- Spotlights the item that will be targeted by clicking the Open Container button.
|
|
function Inventory:HighlightNextOpenable()
|
|
-- Avoid firing OnEnter constantly.
|
|
if self.lastHighlightedOpenableButton ~= self.nextOpenableItemSlotButton then
|
|
self.highlightItemsInContainerId = self.nextOpenableItemBagNum
|
|
self.highlightItemsContainerSlot = self.nextOpenableItemSlotNum
|
|
if self.nextOpenableItemSlotButton then
|
|
self:ItemButton_OnEnter(self.nextOpenableItemSlotButton)
|
|
end
|
|
self:UpdateItemSlotColors()
|
|
self.lastHighlightedOpenableButton = self.nextOpenableItemSlotButton
|
|
end
|
|
end
|
|
|
|
--- Visible serves two functions:
|
|
--- 1. Get the current visibility of `self.uiFrame`.
|
|
--- 2. Optionally perform a UI visibility action prior to checking visibility.
|
|
---@param visibilityAction BS_INVENTORY_UI_VISIBILITY_ACTION? Action to perform, if any.
|
|
---@return number|boolean|nil isVisible Result of `self.uiFrame:IsVisible()`.
|
|
function Inventory:Visible(visibilityAction)
|
|
if not self.uiFrame then
|
|
return false
|
|
end
|
|
if visibilityAction and type(self[visibilityAction]) == "function" then
|
|
self[visibilityAction](self)
|
|
end
|
|
return self.uiFrame:IsVisible()
|
|
end
|
|
|
|
--- Display the window.
|
|
function Inventory:Open()
|
|
if not self.uiFrame:IsVisible() then
|
|
-- Set to `EVENT_PREFIX_` when the event ends in `_OPENED`.
|
|
-- This will allow for matching against the corresponding
|
|
-- `EVENT_PREFIX_CLOSED`.
|
|
self.lastOpenEventTrigger = _G.event
|
|
and ((string.find(_G.event, "_OPENED$")) and (string.gsub(_G.event, "OPENED$", "")) or (string.find(
|
|
_G.event,
|
|
"_SHOW$"
|
|
)) and (string.gsub(_G.event, "SHOW$", "")))
|
|
or nil
|
|
end
|
|
self.uiFrame:Show()
|
|
self.uiFrame:Raise()
|
|
self:SetDockedToFrameVisibility(BS_INVENTORY_UI_VISIBILITY_ACTION.OPEN)
|
|
end
|
|
|
|
--- Hide the window.
|
|
function Inventory:Close()
|
|
if self.uiFrame:IsVisible() then
|
|
-- Don't close if the close request came from an event trigger but
|
|
-- the window not opened by the corresponding event.
|
|
if
|
|
type(_G.event) == "string"
|
|
and (string.find(_G.event, "_CLOSED$"))
|
|
and (self.lastOpenEventTrigger == nil or (not string.find(_G.event, "^" .. self.lastOpenEventTrigger)))
|
|
then
|
|
return
|
|
end
|
|
self.uiFrame:Hide()
|
|
end
|
|
end
|
|
|
|
--- Open if closed, close if opened.
|
|
function Inventory:Toggle(keepDockedToFrameOpen)
|
|
if self.uiFrame:IsVisible() then
|
|
self:Close()
|
|
if not keepDockedToFrameOpen then
|
|
self:SetDockedToFrameVisibility(BS_INVENTORY_UI_VISIBILITY_ACTION.CLOSE)
|
|
end
|
|
else
|
|
self:Open()
|
|
end
|
|
end
|
|
|
|
--- OpenCloseToggle is called from hooked functions and checks whether the hook is
|
|
--- enabled before performing the action. It calls `self:Visible()` when the hook
|
|
--- is enabled and the original hooked function when the hook is disabled.
|
|
---@param uiVisibilityAction BS_INVENTORY_UI_VISIBILITY_ACTION
|
|
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
|
|
---@param bagNumParam number? `arg1` from the hooked API function, if applicable.
|
|
---@param forceCallOriginalHook boolean? When true, always call the hooked API function.
|
|
function Inventory:OpenCloseToggle(uiVisibilityAction, wowApiFunctionName, bagNumParam, forceCallOriginalHook)
|
|
local callOriginalHook = true
|
|
if self:GetHookEnabled(wowApiFunctionName, bagNumParam) then
|
|
callOriginalHook = false
|
|
-- Only perform the action if it's unconditional or if it's for a container belonging to this instance.
|
|
if not bagNumParam or self.myContainerIds[bagNumParam] then
|
|
self:Visible(uiVisibilityAction)
|
|
end
|
|
end
|
|
|
|
if callOriginalHook or forceCallOriginalHook then
|
|
self.hooks:OriginalHook(wowApiFunctionName, bagNumParam)
|
|
end
|
|
end
|
|
|
|
--- Make sure the visibility state of the frame this one is docked to doesn't get out of sync.
|
|
---@param visibilityAction BS_INVENTORY_UI_VISIBILITY_ACTION
|
|
function Inventory:SetDockedToFrameVisibility(visibilityAction)
|
|
if not self.dockTo then
|
|
return
|
|
end
|
|
-- Only close the docking frame if it wasn't visible when this frame was opened.
|
|
if
|
|
visibilityAction ~= BS_INVENTORY_UI_VISIBILITY_ACTION.CLOSE
|
|
or (visibilityAction == BS_INVENTORY_UI_VISIBILITY_ACTION.CLOSE and not self.dockingFrameVisibleOnLastOpen)
|
|
then
|
|
Bagshui.components[self.dockTo]:Visible(visibilityAction)
|
|
end
|
|
end
|
|
|
|
--- Keep the window in the right place.
|
|
---@param noRescueAttempts boolean? Don't call `Inventory:RescueWindow()`.
|
|
function Inventory:FixWindowPosition(noRescueAttempts)
|
|
if self.dragInProgress then
|
|
return
|
|
end
|
|
|
|
-- As soon as a frame is dragged, its anchor changes to TOPLEFT, which leads to undesirable behavior
|
|
-- when the frame is resized. We have to reset the anchor and position to keep things happy.
|
|
self.uiFrame:ClearAllPoints()
|
|
self.uiFrame:SetPoint(
|
|
self.settings.windowAnchorYPoint .. self.settings.windowAnchorXPoint,
|
|
_G.UIParent,
|
|
self.settings.windowAnchorYPoint .. self.settings.windowAnchorXPoint,
|
|
self.settings.windowAnchorXOffset / self.uiFrame:GetScale(),
|
|
self.settings.windowAnchorYOffset / self.uiFrame:GetScale()
|
|
)
|
|
|
|
-- Ensure the Settings menu is in the right spot.
|
|
self:FixSettingsMenuPosition()
|
|
|
|
-- Make sure the window is onscreen.
|
|
if not noRescueAttempts then
|
|
self:RescueWindow()
|
|
end
|
|
end
|
|
|
|
--- Get the window back on the screen if it goes wandering.
|
|
---@param requested boolean? User-requested reset. Don't check, just do it.
|
|
function Inventory:RescueWindow(requested)
|
|
-- Can't check if the frame isn't positioned.
|
|
if not self.uiFrame:GetTop() then
|
|
return
|
|
end
|
|
|
|
local rescued = false
|
|
|
|
-- Make sure boundary frames are scaled correctly.
|
|
Bagshui:ManageBoundaryFrames()
|
|
|
|
-- These two could be collapsed to one shared function but it doesn't seem worth it.
|
|
-- Originally this was implemented with checks against UIParent but that seemed to
|
|
-- fall apart at really large resolutions. So instead we're using four invisible
|
|
-- boundary frames that share the same scale as the Bagshui inventory windows.
|
|
-- HOPEFULLY this will be accurate. (There's probably a better way to do this?)
|
|
|
|
if
|
|
requested
|
|
or self.uiFrame:GetTop() <= Bagshui.boundaryFrames.BOTTOM:GetBottom() + BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD
|
|
or self.uiFrame:GetBottom() >= Bagshui.boundaryFrames.TOP:GetTop() - BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD
|
|
then
|
|
self.settings:SetDefaults(true, nil, nil, "windowAnchorYOffset", true)
|
|
if
|
|
(self.settings.windowAnchorYPoint == "TOP" and self.settings.windowAnchorYOffset > 0)
|
|
or (self.settings.windowAnchorYPoint == "BOTTOM" and self.settings.windowAnchorYOffset < 0)
|
|
then
|
|
self.settings.windowAnchorYOffset = -self.settings.windowAnchorYOffset
|
|
end
|
|
rescued = true
|
|
end
|
|
|
|
if
|
|
requested
|
|
or self.uiFrame:GetRight() <= Bagshui.boundaryFrames.LEFT:GetLeft() + BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD
|
|
or self.uiFrame:GetLeft() >= Bagshui.boundaryFrames.RIGHT:GetRight() - BS_WINDOW_OFFSCREEN_RESCUE_THRESHOLD
|
|
then
|
|
self.settings:SetDefaults(true, nil, nil, "windowAnchorXOffset", true)
|
|
if
|
|
(self.settings.windowAnchorXPoint == "RIGHT" and self.settings.windowAnchorXOffset > 0)
|
|
or (self.settings.windowAnchorXPoint == "LEFT" and self.settings.windowAnchorXOffset < 0)
|
|
then
|
|
self.settings.windowAnchorXOffset = -self.settings.windowAnchorXOffset
|
|
end
|
|
rescued = true
|
|
end
|
|
|
|
-- Apply the new position.
|
|
if rescued then
|
|
Bagshui:Print(
|
|
requested and L.SettingReset_WindowPositionManual or L.SettingReset_WindowPositionAuto,
|
|
self.inventoryTypeLocalized
|
|
)
|
|
self:FixWindowPosition(true)
|
|
end
|
|
end
|
|
|
|
--- Record window offsets so they can be used by `Inventory:FixWindowPosition()`.
|
|
--- `GetLeft/Right/Top/Bottom()` always return values based on the bottom left of the screen,
|
|
--- so we need to do some work to figure out how these relate to the desired frame anchor.
|
|
--- Here's how it works, based on the frame's anchor:
|
|
--- - *Left/Bottom:* Easy - Use the returned value, adjusted by the frame scale
|
|
--- - *Right/Top:*
|
|
--- 1. Save the returned value, adjusted by frame scale.
|
|
--- 2. Get the appropriate UIParent dimension (Width/Height) and un-scale it.
|
|
--- 3. Subtract #1 from #2, then turn it negative.
|
|
function Inventory:SaveWindowPosition()
|
|
-- Prevent UpdateWindow() from being fired by settings changes so X and Y can both be saved
|
|
-- Without this, X will be saved but Y will be overwritten because FixWindowPosition() will
|
|
-- be called by UpdateWindow() before the Y setting is updated.
|
|
self.windowUpdateBlocked = true
|
|
|
|
-- Save new values.
|
|
self.settings.windowAnchorXOffset = self:GetWindowOffset(self.settings.windowAnchorXPoint)
|
|
self.settings.windowAnchorYOffset = self:GetWindowOffset(self.settings.windowAnchorYPoint)
|
|
|
|
-- Allow window updates again.
|
|
self.windowUpdateBlocked = false
|
|
end
|
|
|
|
--- Calculate the current window offset from the given point.
|
|
---@param point string LEFT/RIGHT/TOP/BOTTOM
|
|
---@return number
|
|
function Inventory:GetWindowOffset(point)
|
|
-- Calling, for example, self.uiFrame:GetLeft()
|
|
local offset = self.uiFrame["Get" .. BsUtil.Capitalize(point)](self.uiFrame) * self.uiFrame:GetScale()
|
|
|
|
if string.upper(point) == "RIGHT" or string.upper(point) == "TOP" then
|
|
-- Measure against the actual right/top edge of UIParent, which is what FixWindowPosition()
|
|
-- anchors to. (Dividing the width/height by the UI scale made the saved and restored
|
|
-- positions disagree whenever the UI scale wasn't 1, shoving the window to the left/bottom.)
|
|
local parentEdge = _G.UIParent["Get" .. BsUtil.Capitalize(point)](_G.UIParent)
|
|
if not parentEdge then
|
|
local dimension = (string.upper(point) == "RIGHT") and "Width" or "Height"
|
|
parentEdge = _G.UIParent["Get" .. dimension](_G.UIParent)
|
|
end
|
|
offset = offset - parentEdge
|
|
end
|
|
|
|
return offset
|
|
end
|
|
|
|
-- Let the skin know that the window position has changed.
|
|
function Inventory:NotifySkinOfPositionChange()
|
|
if type(BsSkin.inventoryPositionFunc) == "function" then
|
|
BsSkin.inventoryPositionFunc(self)
|
|
end
|
|
end
|
|
|
|
--- Do "cleanup" activities when we need to get rid of menus, de-focus the search box, etc.
|
|
--- **DO NOT RENAME** without updating `Ui:CloseMenusAndClearFocuses()`.
|
|
function Inventory:CloseMenusAndClearFocuses()
|
|
Bagshui:CloseMenus()
|
|
self.ui.frames.searchBox:ClearFocus()
|
|
end
|
|
|
|
--- Wrapper for `Bagshui:AddTooltipLine()` that automatically adds to the Bagshui info tooltip.
|
|
---@param text string Primary text value.
|
|
---@param label string Labels to display in front of text, if any.
|
|
---@param title boolean Use highlight color instead of normal color for `text`.
|
|
---@param indent boolean Indent the entire line.
|
|
function Inventory:AddBagshuiInfoTooltipLine(text, label, title, indent)
|
|
Bagshui:AddTooltipLine(BsInfoTooltip, text, label, title, indent)
|
|
end
|
|
|
|
--- Open the inventory window and set focus on the search box.
|
|
---@param searchString string? Search string to pre-populate.
|
|
function Inventory:Search(searchString)
|
|
self:Open()
|
|
self.ui.buttons.toolbar.search:Click()
|
|
if type(searchString) == "string" then
|
|
self.ui.frames.searchBox:SetText(searchString)
|
|
end
|
|
end
|
|
|
|
--- Clear the current search text and store it in history.
|
|
function Inventory:ClearSearch()
|
|
if self.searchText then
|
|
self.ui.frames.searchBox:AddHistoryLine(self.searchText)
|
|
self.ui.frames.searchBox:SetText("")
|
|
self.ui.frames.searchBox:GetScript("OnTextChanged")(self.ui.frames.searchBox)
|
|
end
|
|
end
|
|
|
|
--- Transfer the current search to the Catalog.
|
|
function Inventory:SearchCatalog()
|
|
if self.searchText then
|
|
local searchText = self.searchText
|
|
self:ClearSearch()
|
|
self:CloseMenusAndClearFocuses()
|
|
BsCatalog:Search(searchText)
|
|
end
|
|
end
|
|
|
|
--- Determine whether a given API function hook is enabled.
|
|
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
|
|
---@param bagNumParam number|boolean? `arg1` from the hooked API function, if applicable.
|
|
---@return boolean hookEnabled
|
|
function Inventory:GetHookEnabled(wowApiFunctionName, bagNumParam)
|
|
local hookPreference = self:GetHookSettingName(wowApiFunctionName, bagNumParam)
|
|
local hookPreferenceValue = true
|
|
if self.settings[hookPreference] ~= nil then
|
|
hookPreferenceValue = self.settings[hookPreference]
|
|
end
|
|
return hookPreferenceValue
|
|
end
|
|
|
|
--- Some hooked functions can be enabled/disabled through settings, but the setting
|
|
--- name isn't always the API function name. For example, Bags stores the
|
|
--- OpenBackpack/CloseBackpack/ToggleBackpack preference in hookBag0,
|
|
--- to match the autogenerated hookBagX settings. By configuring the
|
|
--- hookSettingTranslations class property, this substitution can be made.
|
|
---@param hookFunctionName string WoW API function being hooked.
|
|
---@param bagNumParam number|boolean? When given and there's no hookSettingTranslations match, return hookBagX, where X is bagNumParam.
|
|
---@return string settingName
|
|
function Inventory:GetHookSettingName(hookFunctionName, bagNumParam)
|
|
local retVal = nil
|
|
|
|
-- Find any special case matches
|
|
for pattern, preferenceName in pairs(self.hookSettingTranslations) do
|
|
if string.find(hookFunctionName, pattern) then
|
|
retVal = preferenceName
|
|
end
|
|
end
|
|
|
|
-- Make sure the bag number is within the declared range for this class.
|
|
-- Checking to ensure it's a number because some hooked bag functions
|
|
-- could be called with booleans (like OpenAllBags()).
|
|
if retVal == nil and type(bagNumParam) == "number" and self.myContainerIds[bagNumParam] then
|
|
retVal = "hookBag" .. bagNumParam
|
|
end
|
|
|
|
-- For anything else, the preference name is the hook function name.
|
|
if retVal == nil and self.hooks[hookFunctionName] then
|
|
retVal = hookFunctionName
|
|
end
|
|
|
|
---@diagnostic disable-next-line: return-type-mismatch
|
|
return retVal
|
|
end
|
|
end)
|