Files
Bagshui/Components/Inventory.Layout.lua
T
OEVGandClaude Opus 5.5 8f6277d521 Skip unchanged slots and lock-only changes in cache updates
- Normalize the locked flag from GetContainerItemInfo (1/nil) to the
  cached 1/0, so `preLocked ~= nowLocked` no longer treats every unlocked
  slot as changed. Every cache update was re-running ItemInfo:Get() and
  the tooltip scan for every slot.
- When only the locked state changes, record it directly and don't flag
  the cache for re-categorization; the window still redraws.
- Update() re-categorizes only when items changed (cacheNeedsCategorize),
  not for cosmetic lock changes.
- Build emptyGenericContainerSlots for every empty slot on each update,
  since unchanged slots no longer pass through the change branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
2026-11-01 20:46:22 +00:00

2630 lines
109 KiB
Lua

-- Bagshui Inventory Prototype: Window Layout
-- Categorizing, Sorting, Putting Things On The Screen, etc.
-- Most of the functions here shouldn't be called directly as Inventory:Update() handles the coordination.
Bagshui:AddComponent(function()
local Inventory = Bagshui.prototypes.Inventory
-- When deciding whether there are differences between a dry run and current state,
-- some tables need to be compared deeper than the default of 1 level. Set those here.
local LAYOUT_LOOKUP_TABLE_COMPARE_DEPTH = {
categoryIdsGroupedBySequence = 2,
groupItems = 2,
}
-- Reusable variable for use in `Inventory:Update()`.
local update_cascadeInventory
--- This is the main workhorse for orchestrating UI changes; it's called when almost anything happens.
---@param cascade boolean? After completing the update process for this inventory, trigger it for any docked inventory.
function Inventory:Update(cascade)
-- self:PrintDebug(string.format("Update() windowUpdateBlocked=%s", tostring(self.windowUpdateBlocked)))
-- Only update if we haven't been blocked from doing so.
-- One of the main reasons this can happen is when the window is being dragged --
-- see `Inventory:SaveWindowPosition()` for details.
if self.windowUpdateBlocked then
return
end
-- Don't redraw while the window is being dragged. A full redraw (for example from a BAG_UPDATE
-- while looting in combat) resizes and re-lays out every button of a frame that is moving,
-- which shows up as a hitch. The update runs as soon as the window is dropped.
if self:IsDragInProgress() then
self.updateDeferredByDrag = true
self.updateDeferredByDragCascade = self.updateDeferredByDragCascade or cascade
return
end
-- Update is only needed if the window is visible.
if not self:Visible() and not self.forceCacheUpdate then
self.windowUpdateBlocked = false
return
end
-- Decide whether we'll need to cascade, and pass everything along.
update_cascadeInventory = cascade and (self.dockedInventory or self.dockedToInventory)
if update_cascadeInventory then
update_cascadeInventory.forceCacheUpdate = update_cascadeInventory.forceCacheUpdate or self.forceCacheUpdate
update_cascadeInventory.forceResort = update_cascadeInventory.forceResort or self.forceResort
update_cascadeInventory.windowUpdateNeeded = update_cascadeInventory.windowUpdateNeeded or self.windowUpdateNeeded
end
-- Reset per-update transient flags before running the pipeline.
self.cacheChanged = false
self.cacheNeedsCategorize = false
self.lookupTablesWereRebuilt = false
-- We're updating!
self.windowUpdateBlocked = true
-- Before doing anything else, we need to know if we're online.
self:UpdateOnlineStatus()
-- Always update the cache if we get this far.
self:UpdateCache()
-- self.forceCacheUpdate was true, but the window might not be visible.
if not self:Visible() then
-- No need to redraw everything when we're not visible.
self.windowUpdateBlocked = false
return
end
-- Nothing changed and nothing is waiting for a redraw -- skip the entire pipeline.
-- This is the primary optimization for spurious BAG_UPDATE events.
if not self.cacheChanged and not self.windowUpdateNeeded and not self.forceResort and not self.lookupTablesStale then
self.windowUpdateBlocked = false
if update_cascadeInventory then
update_cascadeInventory:Update()
end
return
end
-- Perform all updates in the necessary order.
self:ValidateLayout()
-- Capture resort signals before ManageDryRun(true) clears forceResort.
local needsCategorizeAndSort = self.forceResort or self.resortNeeded
self:ManageDryRun(true) -- First call decides whether we're in dry run mode (`self.dryRun`).
self:UpdateLayoutLookupTables()
if self.cacheNeedsCategorize or self.lookupTablesWereRebuilt or needsCategorizeAndSort then
self:CategorizeAndSort()
end
self:ManageDryRun(false) -- Second call re-points lookup tables if needed and sets `self.enableResortIcon`.
-- If the dry run shows the layout is identical to last render, the window
-- update request came only from CategorizeAndSort, and no cache items actually
-- changed, skip the full redraw. The cache-changed guard is essential: both
-- proposedLayoutState and currentLayoutState hold the same live pointers into
-- self.inventory, so ObjectsEqual() always reports them as identical after
-- UpdateCache() writes new data in place -- even when item A replaced item B
-- in the same slot. Without the guard the texture would not update.
-- UpdateWindow() still runs its lightweight path (colors, border, strata).
if self.dryRun and not self.enableResortIcon and self.windowUpdateNeededByCategorize and not self.cacheChanged then
self.windowUpdateNeeded = false
end
self.windowUpdateNeededByCategorize = false
-- Always refresh item button visual state when the window is visible and
-- the full layout rebuild is being skipped (keeps search and highlights current).
if not self.windowUpdateNeeded and self:Visible() then
self:UpdateItemSlotColors()
end
self:FindSpecialItems()
self:UpdateWindow()
self:UpdateBagBar()
self:UpdateToolbar()
-- Reset status.
self.windowUpdateBlocked = false
-- Automatically cascade updates to docked inventory.
if update_cascadeInventory then
update_cascadeInventory:Update()
end
end
--- Force a window redraw with optional cache update.
---@param cascade boolean? Parameter for `Inventory:Update()`.
---@param updateCache boolean? `true` to trigger a cache update.
function Inventory:ForceUpdateWindow(cascade, updateCache)
self.cacheUpdateNeeded = updateCache or false
self.windowUpdateNeeded = true
self.forceResort = false
self:Update(cascade)
end
--- Refresh only item slot colors without all the overhead of calculating the window layout.
--- High-performance -- much better than `UpdateWindow()`/`ForceUpdateWindow()`, so it should be used for
--- changing highlights or opacity (searching, bag bar mouseover, Edit Mode highlighting, etc.).
function Inventory:UpdateItemSlotColors()
for _, button in ipairs(self.ui.buttons.itemSlots) do
if button:IsVisible() then
self.ui:UpdateItemButtonColorsAndBadges(button)
end
end
end
--- Refresh cooldown animations on all visible item slot buttons without rebuilding the window layout.
--- Called in response to BAG_UPDATE_COOLDOWN so cooldown timers appear immediately after item use.
function Inventory:UpdateItemSlotCooldowns()
local cooldownStart, cooldownDuration, isOnCooldown
for _, button in ipairs(self.ui.buttons.itemSlots) do
if button:IsVisible() and button.bagshuiData.item then
cooldownStart = 0
cooldownDuration = 0
isOnCooldown = nil
if button.bagshuiData.item.itemString ~= nil then
cooldownStart, cooldownDuration, isOnCooldown =
_G.GetContainerItemCooldown(button.bagshuiData.bagNum, button.bagshuiData.slotNum)
end
_G.CooldownFrame_SetTimer(
button.bagshuiData.buttonComponents.cooldown,
cooldownStart,
cooldownDuration,
isOnCooldown
)
end
end
end
--- Ensure there will be no errors when using the active layout.
function Inventory:ValidateLayout()
-- Completely empty layouts need at least a row and a group.
if table.getn(self.layout) == 0 then
table.insert(self.layout, {})
BsUtil.TableCopy(BS_NEW_PROFILE_TEMPLATE.structure.primary.layout, self.layout)
end
-- Validate categories assigned to the layout.
for row = 1, table.getn(self.layout) do
for group = 1, table.getn(self.layout[row]) do
local groupCategories = self.layout[row][group].categories
if groupCategories then
-- Remove invalid categories.
-- Need to step backwards so we can use table.remove().
for catNum = table.getn(groupCategories), 1, -1 do
local categoryId = groupCategories[catNum]
if not BsCategories.list[categoryId] then
table.remove(groupCategories, catNum)
end
end
else
-- This group needs a categories property.
self.layout[row][group].categories = {}
end
end
end
end
--- Control whether the current layout update is a dry run or not.
--- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process.
---
--- Doing a dry run means storing information in the `proposedLayoutState` tables
--- instead of `currentLayoutState`. The proposed state can then be compared
--- to current to decide whether resorting is needed.
---
--- This must be called twice during the `Update()` process - once prior to categorizing/sorting
--- with a parameter of `true` and again just after with `false` (or nil) before calling `UpdateWindow()`.
---@param phase1 boolean? Pass `true` the first time the function is called.
function Inventory:ManageDryRun(phase1)
if phase1 then
-- Determine whether this is real or if we're just simulating to decide whether there are changes.
self.dryRun = false
-- Ensure items don't shift around when the window is open under normal usage.
-- self.forceResort can be used to override (usually when the user clicks the Resort button).
if (not self.resortNeeded or self:Visible()) and not self.forceResort then
self.dryRun = true
-- self:PrintDebug("++++DRY RUN ON++++")
-- else
-- self:PrintDebug("----dry run off----")
end
-- Reset force resort flag.
self.forceResort = false
-- Point layout state tracking keys to the correct tables.
for key, _ in pairs(self.currentLayoutState) do
self[key] = self[self.dryRun and "proposedLayoutState" or "currentLayoutState"][key]
end
else
-- Resetting tables to current if needed.
-- Decide whether the resort icon needs to be enabled.
self.enableResortIcon = false
-- Special stuff for dry run mode:
-- - Manage the resort icon.
-- - Re-point layout state tables.
if
self.dryRun
-- Safety check to ensure pointers are always reset.
or self.groups ~= self.currentLayoutState.groups
then
-- We can immediately know to enable resorting if any empty slot is peeled off its stack.
self.enableResortIcon = (self.emptySlotStackingAllowed and self.hasSlotsWithStackingPrevented)
for key, _ in pairs(self.currentLayoutState) do
if
self.dryRun
and not self.enableResortIcon -- No need to keep checking once we've hit true.
and not BsUtil.ObjectsEqual(
self.proposedLayoutState[key],
self.currentLayoutState[key],
nil,
nil,
LAYOUT_LOOKUP_TABLE_COMPARE_DEPTH[key] or 1
)
then
self.enableResortIcon = true
end
-- Always swap back to the "current" layout state for UpdateWindow() instead
-- of the dry run one so that nothing moves around undesirably.
self[key] = self.currentLayoutState[key]
end
end
end
end
--- Assign inventory items to Bagshui categories and sort into the correct order for display.
--- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process.
---
--- Inventory cache to lookup table relationships:
--- - Cache is stored as `self.inventory[bagNum][slotNum]`
--- - Group items store references to the inventory cache items:
--- `self.groupItems[groupNum][position] = <reference to self.inventory[bagNum][slotNum]>`
function Inventory:CategorizeAndSort()
--self:PrintDebug("CategorizeAndSort()")
-- Categorize items and assign to groups.
self:CategorizeItems()
-- Sort items within groups.
self:SortGroups()
-- Reset flags.
self.resortNeeded = false
self.windowUpdateNeeded = true
self.windowUpdateNeededByCategorize = true
end
--- Fill the lookup tables that are used when building the interface.
--- ***Generally* should not be called directly!** Use `Inventory:Update()` to coordinate the process.
---
--- Lookup tables managed by this function:
--- - `activeGroups`: `{ <Group ID> = <Reference to group> }`
--- - `categoryIdsGroupedBySequence` is used to determine the order of execution for category matching:
--- ```
--- {
--- [<Sequence Num 1>] = {
--- [<Category ID 1>],
--- [<Category ID 2>],
--- },
--- [<Sequence Num 2>] = {
--- [<Category ID 3>],
--- },
--- }
--- ```
--- - `sortedCategorySequenceNumbers` is just an array of the sequence numbers from `categoryIdsGroupedBySequence`
--- so that it can easily be sorted. This avoids having to resort the sequence numbers in `categoryIdsGroupedBySequence`
--- on every call to `Categories:Categorize()`.
--- - `categoriesToGroups` is exactly what it sounds like - the mapping of category IDs to group IDs (which are simply row:position)
--- and is used by `CategorizeAndSort()` when filling the groupItems table.
function Inventory:UpdateLayoutLookupTables()
--self:PrintDebug("UpdateLayoutLookupTables()")
-- self:PrintDebug(BsCategories.list)
-- Only rebuild when the layout structure has actually changed.
-- Also rebuild if the current tables are unpopulated: ManageDryRun() can switch self.groups
-- (and the other layout-state tables) to the proposedLayoutState copies, which start empty
-- and won't have been filled yet if this is the first dry-run pass since the last rebuild.
if not self.lookupTablesStale and next(self.groups) ~= nil then
return
end
self.lookupTablesWereRebuilt = true
-- This will be used at the end if defaultCategoryFound is still false.
local lastGroupId = ""
local defaultCategoryFound = false
-- Wipe all the lookup tables managed by this function.
BsUtil.TableClear(self.activeCategoryIds)
BsUtil.TableClear(self.activeGroups)
BsUtil.TableClear(self.categoriesToGroups)
BsUtil.TableClear(self.categorySequenceNumbers)
BsUtil.TableClear(self.groups)
BsUtil.TableClear(self.sortedCategorySequenceNumbers)
-- Don't remove unused sequence number tables - no need to upset the garbage collector.
-- (This could be slightly more efficient with something like Compost, but it's not a huge deal.)
for _, categoryIds in pairs(self.categoryIdsGroupedBySequence) do
BsUtil.TableClear(categoryIds)
end
-- Process the layout and update lookup tables.
for rowNum, rowGroups in ipairs(self.layout) do
for groupNum, groupDetails in ipairs(rowGroups) do
local groupId = rowNum .. ":" .. groupNum
lastGroupId = groupId
-- Store reference to the group for easy access later.
self.groups[groupId] = groupDetails
-- Store the group ID on the group so it's easily accessible for use by Update()
groupDetails.groupId = groupId
-- Groups aren't technically active unless they have categories assigned and at least one category is valid.
if groupDetails.categories and table.getn(groupDetails.categories) > 0 then
for _, categoryId in ipairs(groupDetails.categories) do
self:AddCategoryToLookupTables(categoryId, groupId)
if categoryId == BsCategories.defaultCategory then
defaultCategoryFound = true
end
end
end
end
end
-- Ensure the default category is always assigned to a group. This is a failsafe and really shouldn't ever be reached.
if not defaultCategoryFound then
self:PrintDebug("WARNING: no default category found!")
self:AddCategoryToLookupTables(BsCategories.defaultCategory, lastGroupId)
end
-- Order sortedCategorySequenceNumbers by category sequence.
table.sort(self.sortedCategorySequenceNumbers)
self.lookupTablesStale = false
end
--- Helper function to add a category to the lookup tables.
---@param categoryId string ID of the category.
---@param groupId string ID of the group to associate with the category.
function Inventory:AddCategoryToLookupTables(categoryId, groupId)
-- Ensure this is a valid category.
-- Shouldn't ever really be an issue because ValidateLayout() is called first, but let's be safe.
if BsCategories.list[categoryId] then
-- Get info about the category.
local categoryDetails = BsCategories.list[categoryId]
-- This is an active category (used when checking for errors).
self.activeCategoryIds[categoryId] = true
-- We haven't seen this sequence number yet.
if self.categoryIdsGroupedBySequence[categoryDetails.sequence] == nil then
self.categoryIdsGroupedBySequence[categoryDetails.sequence] = {}
end
-- Add sequence number to sortedCategorySequenceNumbers so it can be used during categorization.
if not self.categorySequenceNumbers[categoryDetails.sequence] then
self.categorySequenceNumbers[categoryDetails.sequence] = true
table.insert(self.sortedCategorySequenceNumbers, categoryDetails.sequence)
end
-- Create the category-to-group relationship in categoryIdsGroupedBySequence[<sequence>][<categoryId].
self.categoryIdsGroupedBySequence[categoryDetails.sequence][categoryId] = groupId
-- Add to the flat category-to-group relationship table.
self.categoriesToGroups[categoryId] = groupId
-- This is a an active group.
self.activeGroups[groupId] = self.groups[groupId]
end
end
--- Assign categories and groups to all items in the inventory cache.
--- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process.
---
--- Lookup tables managed by this function:
--- - `groupItems` stores the sorted list of items that belong to each group.
---
--- Also updates each item's `bagshuiGroupId` and `bagshuiCategoryId` properties.
function Inventory:CategorizeItems()
-- groupItems stores the list of items that belongs to each group.
-- Start clean each time we categorize and sort.
-- groupItems is a table of tables, so only wipe the 2nd level tables to keep the garbage collector happy.
for groupId, _ in pairs(self.groupItems) do
BsUtil.TableClear(self.groupItems[groupId])
end
-- Using groups instead of activeGroups here just to ensure every possibility is initialized.
for groupId, _ in pairs(self.groups) do
if not self.groupItems[groupId] then
self.groupItems[groupId] = {}
end
end
local defaultGroupId = self.categoriesToGroups[BsCategories.defaultCategory]
local groupId
-- Perform the actual categorization.
for _, bagNum in ipairs(self.containerIds) do
-- Only process bags that have contents.
if self.containers[bagNum].numSlots > 0 and table.getn(self.inventory[bagNum]) > 0 then
-- Process all bag slots.
local bagNumSlots = self.containers[bagNum].numSlots
if bagNumSlots > 0 then
for slotNum = 1, bagNumSlots do
-- Categorize each item.
-- This must ALWAYS be performed so new items don't show up with an unknown category in the Bagshui tooltip.
BsCategories:Categorize(
self.inventory[bagNum][slotNum],
nil, -- Categorization as any alternate character is not currently enabled.
self.sortedCategorySequenceNumbers,
self.categoryIdsGroupedBySequence
)
-- Fall back to the default group if one wasn't assigned.
groupId = self.inventory[bagNum][slotNum].bagshuiGroupId
if string.len(tostring(groupId or "")) == 0 then
groupId = defaultGroupId
end
-- Empty slots are now allowed to stack since they've been through the categorizing process.
-- (When an empty slot is first seen during a cache update, it gets the _bagshuiPreventEmptySlotStack
-- property set so that empty slots don't just "disappear" into a stack when the window is open
-- and the user moves an item out of a slot.)
if not self.dryRun and self.inventory[bagNum][slotNum].emptySlot == 1 then
self.inventory[bagNum][slotNum]._bagshuiPreventEmptySlotStack = nil
end
-- Final safety check to ensure we don't somehow try to assign to a group that doesn't exist.
if not self.groupItems[groupId] then
groupId = defaultGroupId
end
-- Add to group positions lookup table.
table.insert(self.groupItems[groupId], self.inventory[bagNum][slotNum])
end -- bagNumSlots loop
end -- bagNumSlots > 0
end -- Known bag check
end -- self.containerIds loop
-- Show the error button if there were problems.
self.errorText = BsCategories:GetErrors(self.activeCategoryIds)
end
-- Sort each group's items in the configured order.
-- **Should not be called directly!** Use `Inventory:Update()` to coordinate the process.
function Inventory:SortGroups()
for groupId, groupConfig in pairs(self.activeGroups) do
BsSortOrders:SortGroup(self.groupItems[groupId], groupConfig, self.settings.defaultSortOrder)
end
end
--- Return approximate dimensions for a row of groups using the same slot/group math as UpdateWindow().
---@param rowGroups table[] Layout groups to measure.
---@param maxColumns number Maximum item columns allowed in the row.
---@param adjustedItemSlotSize number Scaled item slot size.
---@param itemSlotMargin number Space around item slots.
---@param groupPadding number Group padding.
---@return number actualWidth
---@return number actualHeight
---@return number visibleGroupCount
---@return number rowWidthInItems
function Inventory:GetLayoutRowDimensions(rowGroups, maxColumns, adjustedItemSlotSize, itemSlotMargin, groupPadding)
local numGroupsInRow = table.getn(rowGroups)
local totalItemsInRow = 0
local rowWidthInItems = 0
local rowHeightInItems = 1
local unshrinkableGroupCount = 0
local groupWidthsInItems = {}
local groupId
for columnNum = 1, numGroupsInRow do
groupId = rowGroups[columnNum].groupId
groupWidthsInItems[groupId] = self.groupItemCounts[groupId] or 0
totalItemsInRow = totalItemsInRow + groupWidthsInItems[groupId]
end
rowWidthInItems = totalItemsInRow
while rowWidthInItems > maxColumns do
rowHeightInItems = rowHeightInItems + 1
repeat
rowWidthInItems = 0
unshrinkableGroupCount = 0
for columnNum = 1, numGroupsInRow do
groupId = rowGroups[columnNum].groupId
if groupWidthsInItems[groupId] > 1 then
local newWidth = groupWidthsInItems[groupId] - 1
if self.groupItemCounts[groupId] / newWidth <= rowHeightInItems then
groupWidthsInItems[groupId] = newWidth
else
unshrinkableGroupCount = unshrinkableGroupCount + 1
end
else
unshrinkableGroupCount = unshrinkableGroupCount + 1
end
rowWidthInItems = rowWidthInItems + groupWidthsInItems[groupId]
end
until rowWidthInItems <= maxColumns or unshrinkableGroupCount >= numGroupsInRow
-- If every group is already at its minimum width, this row can't fit within
-- maxColumns. Stop measuring instead of looping forever.
if rowWidthInItems > maxColumns then
local canShrink = false
for columnNum = 1, numGroupsInRow do
if groupWidthsInItems[rowGroups[columnNum].groupId] > 1 then
canShrink = true
break
end
end
if not canShrink then
break
end
end
end
local actualWidth = 0
local visibleGroupCount = 0
for columnNum = 1, numGroupsInRow do
groupId = rowGroups[columnNum].groupId
if groupWidthsInItems[groupId] > 0 then
actualWidth = actualWidth
+ (groupWidthsInItems[groupId] * (adjustedItemSlotSize + itemSlotMargin))
+ itemSlotMargin
+ (groupPadding * 2)
visibleGroupCount = visibleGroupCount + 1
end
end
if totalItemsInRow == 0 then
return 0, 0, visibleGroupCount, rowWidthInItems
end
return
actualWidth,
(rowHeightInItems * (adjustedItemSlotSize + itemSlotMargin)) + itemSlotMargin + (groupPadding * 2),
visibleGroupCount,
rowWidthInItems
end
--- Build a temporary layout for rendering visible groups in a smaller footprint.
--- Does not mutate the saved Structure profile layout.
---@param maxColumns number Maximum item columns allowed by the window setting.
---@param adjustedItemSlotSize number Scaled item slot size.
---@param itemSlotMargin number Space around item slots.
---@param groupPadding number Group padding.
---@param groupXSpacing number Horizontal space between groups.
---@param groupYSpacing number Vertical space between groups.
---@return table renderLayout Temporary layout table.
function Inventory:BuildAutoCompactRenderLayout(
maxColumns,
adjustedItemSlotSize,
itemSlotMargin,
groupPadding,
groupXSpacing,
groupYSpacing
)
local visibleGroups = {}
local hideGroup
local groupId
for rowNum = 1, table.getn(self.layout) do
for columnNum = 1, table.getn(self.layout[rowNum]) do
groupId = self.layout[rowNum][columnNum].groupId
if groupId then
hideGroup = false
if self.groups[groupId] and self.groups[groupId].hide then
hideGroup = true
self.hasHiddenGroups = true
end
if not self.showHidden and hideGroup then
self.groupItemCounts[groupId] = self.highlightItemsInContainerLocked
and self:GetGroupItemCountForLayout(groupId, self.highlightItemsInContainerLocked)
or 0
else
self.groupItemCounts[groupId] = self:GetGroupItemCountForLayout(groupId)
end
if self.groupItemCounts[groupId] > 0 then
table.insert(visibleGroups, {
groupId = groupId,
name = self.layout[rowNum][columnNum].name,
sourceRowNum = rowNum,
sourceColumnNum = columnNum,
})
end
else
Bagshui:PrintWarning("Inventory update call should have forced resort!")
end
end
end
if table.getn(visibleGroups) == 0 then
return {}
end
local bestLayout
local bestArea
local bestWidth
for targetWidth = 1, maxColumns do
local candidateLayout = {}
local currentRow = {}
for groupNum = 1, table.getn(visibleGroups) do
local trialRow = {}
for currentRowGroupNum = 1, table.getn(currentRow) do
table.insert(trialRow, currentRow[currentRowGroupNum])
end
table.insert(trialRow, visibleGroups[groupNum])
local _, _, _, trialRowWidthInItems = self:GetLayoutRowDimensions(
trialRow,
targetWidth,
adjustedItemSlotSize,
itemSlotMargin,
groupPadding
)
if table.getn(currentRow) > 0 and trialRowWidthInItems > targetWidth then
table.insert(candidateLayout, currentRow)
currentRow = {}
end
table.insert(currentRow, visibleGroups[groupNum])
end
if table.getn(currentRow) > 0 then
table.insert(candidateLayout, currentRow)
end
local candidateWidth = 0
local candidateHeight = 0
local visibleRows = 0
for rowNum = 1, table.getn(candidateLayout) do
local rowWidth, rowHeight, visibleGroupCount = self:GetLayoutRowDimensions(
candidateLayout[rowNum],
maxColumns,
adjustedItemSlotSize,
itemSlotMargin,
groupPadding
)
if visibleGroupCount > 0 then
rowWidth = rowWidth + ((visibleGroupCount - 1) * groupXSpacing)
if rowWidth > candidateWidth then
candidateWidth = rowWidth
end
candidateHeight = candidateHeight + rowHeight
visibleRows = visibleRows + 1
end
end
if visibleRows > 1 then
candidateHeight = candidateHeight + ((visibleRows - 1) * groupYSpacing)
end
local candidateArea = candidateWidth * candidateHeight
if
not bestArea
or candidateArea < bestArea
or (candidateArea == bestArea and candidateWidth < bestWidth)
then
bestLayout = candidateLayout
bestArea = candidateArea
bestWidth = candidateWidth
end
end
return bestLayout
end
--- Actually do the work of updating the UI.
--- **Should not be called directly!** Use `Inventory:Update()` or `Inventory:ForceWindowUpdate()` to coordinate the process.
---
--- This function handles:
--- - Sizing and placing groups so everything fits within the allowed number of columns
--- - Calling `AssignItemsToSlots()` to fill item slot buttons
--- - Stacking/unstacking empty slots
--- - Hiding/showing components and setting window colors based on preferences
--- - Sizing the window
function Inventory:UpdateWindow()
-- self:PrintDebug("UpdateWindow()")
-- Clear the "never online" message once we're online.
if self.online then
Bagshui.currentCharacterData[self.inventoryTypeSavedVars].neverOnline = nil
end
-- Can only highlight changes if there's something to highlight.
self.highlightChangesEnabled = (
self.hasChanges
or (self.dockedInventory and self.dockedInventory.hasChanges)
or (self.dockedToInventory and self.dockedToInventory.hasChanges)
) or false
-- Safeguard: Turn off Highlight Changes if there's nothing to highlight.
if not self.highlightChangesEnabled then
self.highlightChanges = false
end
-- There are a few things that always need to happen even if we're not
-- doing a full layout update.
self.windowColor = self.settings.windowBackground
self.borderColor = self.settings.windowBorder
if self.settings.windowUseSkinColors and BsSkin.skinBackgroundColor then
self.windowColor = BsSkin.skinBackgroundColor
end
if self.settings.windowUseSkinColors and BsSkin.windowBackdrop then
self.windowColor = BsSkin.windowColor or { 1, 1, 1, 1 }
end
if not self.online then
self.borderColor = BS_COLOR.RED
elseif self.settings.windowUseSkinColors and BsSkin.skinBorderColor then
self.borderColor = BsSkin.skinBorderColor
elseif self.settings.windowUseSkinColors and BsSkin.windowBackdrop then
self.borderColor = BsSkin.windowBorderColor or { 1, 1, 1, 1 }
end
self.uiFrame:SetBackdropColor(self.windowColor[1], self.windowColor[2], self.windowColor[3], self.windowColor[4])
self.uiFrame:SetBackdropBorderColor(
self.borderColor[1],
self.borderColor[2],
self.borderColor[3],
self.borderColor[4]
)
self.uiFrame:SetFrameStrata(self.settings.windowStrata)
-- Update the utilization display setting.
-- Done here because it's consumed in `UpdateBagBar()` and `UpdateToolbar()`.
self.alwaysShowUsageSummary = (
self.settings.bagUsageDisplay == BS_INVENTORY_BAG_USAGE_DISPLAY.ALWAYS
or (self.settings.bagUsageDisplay == BS_INVENTORY_BAG_USAGE_DISPLAY.SMART and not self.settings.stackEmptySlots)
or false
)
-- Only do the intensive work of redrawing the UI if we have to.
if self.windowUpdateNeeded then
BsUtil.TableClear(self.groupsIdsToFrames)
local uiFrames = self.ui.frames
local groupFrames = uiFrames.groups
local uiButtons = self.ui.buttons
local itemSlotSize = self.settings.itemSize
local itemSlotScale = self.ui:GetItemButtonScale(self.ui.buttons.itemSlots[1], itemSlotSize)
local adjustedItemSlotSize = self.ui:GetItemButtonScaledSize(self.ui.buttons.itemSlots[1], itemSlotSize)
local itemSlotMargin = self.settings.itemMargin + (BsSkin.itemSlotMarginFudge or 0)
local groupMarginFudged = self.settings.groupMargin + BsSkin.groupMarginFudge
local groupLabelHeight = groupFrames[1].bagshuiData.text:GetHeight() + math.abs(BsSkin.groupLabelMinPadding)
local groupPadding = self.settings.groupPadding
+ (BsSkin.groupPaddingFudge or 0)
+ ((BsSkin.itemSlotMarginFudge / 2) * itemSlotScale)
local maxColumns = self.settings.windowMaxColumns
-- Edit Mode shows every group, including empty ones, so rows have many more groups than usual.
-- Squeezing them into the normal limit makes the window absurdly tall, so allow it to be wider.
if self.editMode and maxColumns < BS_EDIT_MODE_MIN_MAX_COLUMNS then
maxColumns = BS_EDIT_MODE_MIN_MAX_COLUMNS
end
local anchorLeft = self.settings.windowContentAlignment == "LEFT"
local showGroupLabels = (self.settings.showGroupLabels and not self.settings.hideGroupLabelsOverride)
or self.editMode
local firstFrame, widestRowLastFrame, topmostFrame
-- Window component visibility.
local showHeader = ((self.settings.showHeader and not self.dockTo) or self.temporarilyShowWindowHeader)
local showFooter = (
(self.settings.showFooter and not self.dockTo)
or self.temporarilyShowWindowFooter
or self.temporarilyShowBagBar
)
-- Reset hidden group and item tracking.
self.hasHiddenGroups = false -- Updated in this function.
self.hasHiddenItems = false -- Updated in GetGroupItemCountForLayout().
self.hasOpenables = false -- Updated in AssignItemsToSlots().
self.nextOpenableItemBagNum = nil -- Updated in AssignItemsToSlots().
self.nextOpenableItemSlotNum = nil -- Updated in AssignItemsToSlots().
self.nextOpenableItemSlotButton = nil -- Updated in AssignItemsToSlots().
-- Full reset of empty slot stack counts is needed so that the tracking of which
-- bag they represent is rebuilt.
self:ResetEmptySlotStackCounts(true)
-- Is empty slot stacking currently allowed?
self.emptySlotStackingAllowed = (
self.settings.stackEmptySlots -- User setting.
and not self.expandEmptySlotStacks -- Temporary expansion.
and not self.editMode -- Always expand in Edit Mode.
)
-- Used to track row changes so they include group move targets (the normal
-- rowNum variable is just an iteration over self.layout).
local uiRowNum = 0
-- These three rowGroup* variables control which direction we step though
-- the groups (left-to-right or right-to-left).
local rowGroupStart, rowGroupEnd, rowGroupStep
-- Exactly what it says on the tin.
local numGroupsInRow
local totalItemsInRow = 0
local rowWidthInItems = 0
local rowHeightInItems = 0
local finalWindowWidth = 0
local finalWindowHeight = 0
local hideGroup = false
-- Used to track which row is actually the widest after the frames are positioned.
local widestRowLastFrameOutsideX = 0
local currentRowLastFrameOutsideX
-- Used during row width calculations to determine the point when we need
-- to increase the row height again.
local unshrinkableGroupCount = 0
-- The uiGroupNum* variables are used to keep track of which group frame(s)
-- we should be using for each row and the overall progress as we step through the layout.
local uiGroupNum = 1 -- Need to have a default value here since we use uiGroupNum later to hide unused groups.
local uiGroupNumStart = 1
local uiGroupNumEnd
-- currentItemSlotButtonNum tracks the progress of assigning items to buttons.
local currentItemSlotButtonNum = 1
-- Groups can have some weird padding going on because of their edge textures,
-- so we need to adjust the group spacing so the visual outcome is correct.
local groupXSpacing = groupMarginFudged
-- Similarly, the initial position of everything isn't straightforward and
-- gets affected by Edit Mode.
local groupEdgeOffset = groupMarginFudged
-- Increase the space between the bottom edge and the first group when the footer is visible.
local groupEdgeYOffset = groupMarginFudged - (showFooter and BsSkin.groupMarginFudge or 0)
-- When group labels are shown, we may need more Y space.
local groupYSpacing = groupXSpacing
if showGroupLabels then
if groupYSpacing < groupLabelHeight then
groupYSpacing = groupLabelHeight
end
groupYSpacing = groupYSpacing + BsSkin.groupLabelYPadding
end
-- Don't need to calculate the row (horizontal) group move target width outside Edit Mode.
local rowGroupMoveTargetWidth = 0
-- Edit Mode spacing adjustments -- keep things as tight as possible since the window will
-- almost always grow due to more groups being visible.
if self.editMode then
groupXSpacing = -1
groupYSpacing = groupXSpacing
groupEdgeOffset = BsSkin.inventoryEditModeMargin
groupEdgeYOffset = groupEdgeOffset
-- The horizontal group move target is basically the width of a single group.
rowGroupMoveTargetWidth = adjustedItemSlotSize
+ (itemSlotMargin * 2)
+ (groupPadding * 2)
+ ((BsSkin.groupMoveTargetThinDimension + BsSkin.groupMoveTargetAnchorOffset) * 2)
end
-- Reset group move target count tracking for ShowNextGroupMoveTarget().
self.groupMoveTargetCount = 0
-- Column number adjustment for insertion via group move targets.
-- This is needed so that we get the correct insertion point regardless of
-- whether we're going from left to right or vice versa.
local groupMoveTargetLeadingColumnNumAdjustment = 0
local groupMoveTargetTrailingColumnNumAdjustment = 0
-- Prepare to use ShowFrameInNextPosition() for groups.
self:InitFramePositioningTable(
"UpdateWindow", -- Position tracking table.
groupXSpacing, -- Frame X spacing.
groupYSpacing, -- Frame Y spacing.
uiFrames.main, -- Initial frame to anchor to.
-groupEdgeOffset,
groupEdgeYOffset
)
-- Reset tracking tables.
BsUtil.TableClear(self.groupItemCounts) -- How many items are in each group.
BsUtil.TableClear(self.groupWidthsInItems) -- Group width in items (actual count EXCEPT in Edit Mode, where 0 becomes 1 to force the group to be visible when empty).
BsUtil.TableClear(self.actualGroupWidths) -- Group width in screen units.
local renderLayout = self.layout
if self.settings.autoCompactLayout and not self.editMode then
renderLayout = self:BuildAutoCompactRenderLayout(
maxColumns,
adjustedItemSlotSize,
itemSlotMargin,
groupPadding,
groupXSpacing,
groupYSpacing
)
end
-- Groups are placed starting at the bottom of the main frame and working upwards.
-- Since we're starting at the bottom of the window, we need to start at the last row of the layout table.
for rowNum = table.getn(renderLayout), 1, -1 do
-- self:PrintDebug("ROW "..rowNum)
-- Get the number of groups in this row.
numGroupsInRow = table.getn(renderLayout[rowNum])
-- Use that information to figure out the number of the end group.
uiGroupNumEnd = uiGroupNumStart + numGroupsInRow - 1
-- Determine anchoring.
if anchorLeft then
rowGroupStart = 1
rowGroupEnd = numGroupsInRow
rowGroupStep = 1
groupMoveTargetLeadingColumnNumAdjustment = 0
groupMoveTargetTrailingColumnNumAdjustment = 1
else
rowGroupStart = numGroupsInRow
rowGroupEnd = 1
rowGroupStep = -1
groupMoveTargetLeadingColumnNumAdjustment = 1
groupMoveTargetTrailingColumnNumAdjustment = 0
end
-- Populate lists that are used to determine how tall each group needs to
-- be in order to fit all items within the maximum column count.
totalItemsInRow = 0
for columnNum = rowGroupStart, rowGroupEnd, rowGroupStep do
local groupId = renderLayout[rowNum][columnNum].groupId
if groupId then
hideGroup = false
-- Determine whether this group is visible and update hasHiddenGroups
-- to control whether the Show/Hide toolbar button is enabled.
if self.groups[groupId] and self.groups[groupId].hide then
hideGroup = true
self.hasHiddenGroups = true
end
-- Determine how many items are in each group in this row based on group visibility.
if not self.editMode and not self.showHidden and hideGroup then
-- Set the groupItemCounts for this group to 0 if it's not visible unless it contains
-- items from the bag that has highlight lock set.
self.groupItemCounts[groupId] = self.highlightItemsInContainerLocked
and self:GetGroupItemCountForLayout(groupId, self.highlightItemsInContainerLocked)
or 0
else
self.groupItemCounts[groupId] = self:GetGroupItemCountForLayout(groupId)
end
-- Increment total for this row.
totalItemsInRow = totalItemsInRow + self.groupItemCounts[groupId]
-- Starting width for all groups is the number of items in the group.
self.groupWidthsInItems[groupId] = self.groupItemCounts[groupId]
-- groupWidthsInItems gets fudged for Edit Mode to make all groups visible, even if they're empty.
if self.groupWidthsInItems[groupId] == 0 and self.editMode then
self.groupWidthsInItems[groupId] = 1
end
else
Bagshui:PrintWarning("Inventory update call should have forced resort!")
end
end
-- Starting values for row height/width.
rowWidthInItems = totalItemsInRow
rowHeightInItems = 1
-- Shrink group widths until everything fits within the configured column limit. Basically:
-- 1. Increment the row height by 1.
-- 2. Loop through all columns and decrease width of each by 1 (so long as that's possible and won't make it too tall).
-- 3. See if everything fits now. If not, go back to step 2 unless all groups are now too tall, in which case, go back to step 1.
-- (No need to do any of this unless there are too many items in the row to fit in the configured number of columns.)
while rowWidthInItems > maxColumns do
rowHeightInItems = rowHeightInItems + 1
repeat
rowWidthInItems = 0
unshrinkableGroupCount = 0
for columnNum = rowGroupStart, rowGroupEnd, rowGroupStep do
local groupId = renderLayout[rowNum][columnNum].groupId
-- Shrink the group by 1, if possible.
if self.groupWidthsInItems[groupId] > 1 then
local newWidth = self.groupWidthsInItems[groupId] - 1
-- Make sure this width would't make the group too tall for this row height.
if self.groupItemCounts[groupId] / newWidth <= rowHeightInItems then
self.groupWidthsInItems[groupId] = newWidth
else
unshrinkableGroupCount = unshrinkableGroupCount + 1
end
else
unshrinkableGroupCount = unshrinkableGroupCount + 1
end
rowWidthInItems = rowWidthInItems + self.groupWidthsInItems[groupId]
end
until rowWidthInItems <= maxColumns or unshrinkableGroupCount >= numGroupsInRow
-- Failsafe for impossible rows: if there are more visible groups than
-- allowed columns, every group bottoms out at width 1 and can't shrink
-- any further.
if rowWidthInItems > maxColumns then
local canShrink = false
for columnNum = rowGroupStart, rowGroupEnd, rowGroupStep do
if self.groupWidthsInItems[renderLayout[rowNum][columnNum].groupId] > 1 then
canShrink = true
break
end
end
if not canShrink then
break
end
end
end
-- Is there anything to display on this row?
if totalItemsInRow == 0 and not self.editMode then
-- All groups are not visible.
for groupNum = uiGroupNumStart, uiGroupNumEnd do
-- This check doesn't seem like it should be necessary, but https://github.com/veechs/Bagshui/issues/172
-- says otherwise.
if groupFrames[groupNum] then
groupFrames[groupNum]:Hide()
end
end
else
-- There are groups to display, so let's continue.
-- Calculate the actual group widths, which we need to know when we make the group frame visible.
local totalActualGroupWidths = 0 -- Will be used to determine how wide the window needs to be.
for columnNum = rowGroupStart, rowGroupEnd, rowGroupStep do
local group = renderLayout[rowNum][columnNum]
-- Make sure this group will be displayed before adding it to the totals.
if self.groupWidthsInItems[group.groupId] > 0 or self.editMode then
self.actualGroupWidths[group.groupId] = (
-- Item slot widths, margin included.
(self.groupWidthsInItems[group.groupId] * (adjustedItemSlotSize + itemSlotMargin))
-- This accounts for the space on the opposite side of the last item.
+ itemSlotMargin
-- Left and right group padding.
+ (groupPadding * 2)
)
totalActualGroupWidths = totalActualGroupWidths + self.actualGroupWidths[group.groupId]
else
self.actualGroupWidths[group.groupId] = 0
end
end
-- Every group on the row gets the same height even if it's not full of items.
local actualGroupHeight =
-- Item slot heights, margin included.
(rowHeightInItems * (adjustedItemSlotSize + itemSlotMargin))
-- This accounts for the space above the last item.
+ itemSlotMargin
-- Top and bottom group padding
+ (groupPadding * 2)
-- Edit Mode accommodations
if self.editMode then
-- Group labels move inside in Edit Mode and need to be added to the height.
actualGroupHeight = actualGroupHeight + groupLabelHeight
-- Show the between-row group move target below this row.
uiRowNum = uiRowNum + 1
self:ShowNextGroupMoveTarget(
rowGroupMoveTargetWidth,
BsSkin.groupMoveTargetThinDimension,
uiRowNum,
rowNum + 1,
1,
BS_INVENTORY_LAYOUT_DIRECTION.ROW
)
end
-- Put the groups in their places.
uiGroupNum = uiGroupNumStart
uiRowNum = uiRowNum + 1
for columnNum = rowGroupStart, rowGroupEnd, rowGroupStep do
local groupId = renderLayout[rowNum][columnNum].groupId
-- Ensure the group exists.
self.ui:CreateGroup(uiGroupNum)
local group = groupFrames[uiGroupNum]
-- Store a reference to the group widget so it's easily accessible via group ID.
-- Primarily used when an item needs to be assigned to a new group in ManagePendingGroupAssignments().
self.groupsIdsToFrames[groupId] = group
-- Show leading group move target for this group in Edit Mode.
if self.editMode then
self:ShowNextGroupMoveTarget(
BsSkin.groupMoveTargetThinDimension,
actualGroupHeight,
uiRowNum,
rowNum,
columnNum + groupMoveTargetLeadingColumnNumAdjustment,
BS_INVENTORY_LAYOUT_DIRECTION.COLUMN
)
-- In Edit Mode, the group move target should be used for height/width calculations.
if not firstFrame then
firstFrame = self.positioningTables.UpdateWindow.anchorToFrame
end
end
-- Display the group.
if self.groupWidthsInItems[groupId] > 0 then
-- Update info on the UI object.
group.bagshuiData.groupId = groupId
group.bagshuiData.rowNum = renderLayout[rowNum][columnNum].sourceRowNum or rowNum
group.bagshuiData.columnNum = renderLayout[rowNum][columnNum].sourceColumnNum or columnNum
-- Add the group to the window.
self:ShowFrameInNextPosition(
"UpdateWindow",
uiRowNum,
group,
self.actualGroupWidths[groupId],
actualGroupHeight
)
-- This is the group that should be used for height/width calculations.
if not firstFrame then
firstFrame = self.positioningTables.UpdateWindow.anchorToFrame
end
-- Set background and border colors.
self.ui:SetGroupColors(group)
-- Labels.
if showGroupLabels then
-- Move to the correct location depending on whether we're in Edit Mode.
group.bagshuiData.labelFrame:SetHeight(groupLabelHeight)
group.bagshuiData.labelFrame:ClearAllPoints()
if self.editMode then
group.bagshuiData.labelFrame:SetWidth(
self.actualGroupWidths[groupId] - (groupPadding * 2) - itemSlotMargin
)
group.bagshuiData.labelFrame:SetPoint(
"TOPLEFT",
group,
"TOPLEFT",
BsSkin.groupLabelEditModeXOffset,
BsSkin.groupLabelEditModeYOffset
)
group.bagshuiData.labelFrame:EnableMouse(false)
else
group.bagshuiData.labelFrame:SetWidth(
self.actualGroupWidths[groupId] + BsSkin.groupLabelRelativeWidth
)
group.bagshuiData.labelFrame:SetPoint(
"BOTTOMLEFT",
group,
"TOPLEFT",
BsSkin.groupLabelXOffset,
BsSkin.groupLabelYOffset
)
group.bagshuiData.labelFrame:EnableMouse(true)
end
-- Set text to group name, or a single space if the group name is empty.
-- The single space is necessary so that the height can be measured properly.
group.bagshuiData.text:SetText(
string.len(renderLayout[rowNum][columnNum].name or "") > 0 and renderLayout[rowNum][columnNum].name
or " "
)
group.bagshuiData.labelFrame:Show()
else
-- Labels are turned off.
group.bagshuiData.labelFrame:Hide()
end
-- Assign items to the group (only if there are items in the group).
if self.groupItemCounts[groupId] > 0 then
currentItemSlotButtonNum = self:AssignItemsToSlots(
groupId,
uiGroupNum,
currentItemSlotButtonNum,
self.groupWidthsInItems[groupId],
groupPadding,
itemSlotSize,
itemSlotMargin
)
end
-- Give groups mouse input and display group manager buttons in Edit Mode.
self.ui:SetGroupInteractivityEnabled(group, self.editMode)
else
-- Group should not be visible.
group:Hide()
end
uiGroupNum = uiGroupNum + 1
end
-- Display the trailing group move target for this row.
if self.editMode then
self:ShowNextGroupMoveTarget(
BsSkin.groupMoveTargetThinDimension,
actualGroupHeight,
uiRowNum,
rowNum,
rowGroupEnd + groupMoveTargetTrailingColumnNumAdjustment,
BS_INVENTORY_LAYOUT_DIRECTION.COLUMN
)
end
-- Update the widest row record so we can make the window the correct width.
currentRowLastFrameOutsideX = (
anchorLeft and self.positioningTables.UpdateWindow.anchorToFrame:GetRight()
or self.positioningTables.UpdateWindow.anchorToFrame:GetLeft()
)
if
widestRowLastFrameOutsideX == 0
or (
anchorLeft and widestRowLastFrameOutsideX < currentRowLastFrameOutsideX
or widestRowLastFrameOutsideX > currentRowLastFrameOutsideX
)
then
widestRowLastFrameOutsideX = currentRowLastFrameOutsideX
widestRowLastFrame = self.positioningTables.UpdateWindow.anchorToFrame
end
end
uiGroupNumStart = uiGroupNumStart + numGroupsInRow
end
-- Add the final group move target above the last row.
if self.editMode then
uiRowNum = uiRowNum + 1
self:ShowNextGroupMoveTarget(
rowGroupMoveTargetWidth,
BsSkin.groupMoveTargetThinDimension,
uiRowNum,
1,
1,
BS_INVENTORY_LAYOUT_DIRECTION.ROW
)
end
-- Hide unused item slot buttons, groups, and group move targets.
local itemSlotButtonCount = table.getn(uiButtons.itemSlots)
if currentItemSlotButtonNum <= itemSlotButtonCount then
for buttonNum = currentItemSlotButtonNum, itemSlotButtonCount do
uiButtons.itemSlots[buttonNum]:Hide()
end
end
for i = uiGroupNum, table.getn(uiFrames.groups) do
uiFrames.groups[i]:Hide()
end
for i = self.groupMoveTargetCount + 1, table.getn(uiFrames.groupMoveTargets) do
uiFrames.groupMoveTargets[i]:Hide()
end
-- The last frame to be placed is always going to be the highest one vertically,
-- unlike the frame used for horizontal sizing, which is dependent on which row is the widest.
topmostFrame = self.positioningTables.UpdateWindow.anchorToFrame
-- Show the "No inventory data" message for when the cache has never been populated.
if not self.editMode and BsUtil.TrueTableSize(self.inventory) == 0 then
self.ui.text.noData:Show()
else
self.ui.text.noData:Hide()
end
-- Calculate what we always know for width and height (header/footer are added next).
finalWindowWidth =
-- Inventory content.
((firstFrame and widestRowLastFrame) and math.abs(firstFrame:GetRight() - widestRowLastFrame:GetLeft()) or 0)
-- Outer group margins.
+ groupEdgeOffset * 2
-- Window padding.
+ BsSkin.inventoryWindowPadding * 2
finalWindowHeight =
-- Inventory content.
((topmostFrame and firstFrame) and (topmostFrame:GetTop() - firstFrame:GetBottom()) or 0)
-- Bottom group margin.
+ groupEdgeYOffset
-- Edit Mode messes with the sizing.
+ (
self.editMode
and BsSkin.groupMoveTargetThinDimension
or (
-- Top group margin is the standard one.
groupEdgeOffset
-- Need some breathing room above the topmost labels.
+ (showGroupLabels and (groupLabelHeight - BsSkin.groupMarginFudge) or 0)
)
)
-- "No data" message
+ (self.ui.text.noData:IsShown() and self.ui.text.noData:GetHeight() or 0)
-- Window padding.
+ BsSkin.inventoryWindowPadding * 2
-- Show/hide header/footer (toolbars) and add to window dimensions.
if showHeader then
-- Ensure main frame anchor is correct.
uiFrames.main:SetPoint("TOP", uiFrames.header, "BOTTOM", 0, 0)
uiFrames.header:Show()
-- Update window height.
finalWindowHeight = finalWindowHeight
+ (uiFrames.header:GetHeight())
+ math.abs(BsSkin.inventoryHeaderFooterYAdjustment)
else
-- When the header is hidden, the main frame needs to anchor to the UI frame instead.
uiFrames.main:SetPoint("TOP", self.uiFrame, "TOP", 0, BsSkin.inventoryWindowPadding)
uiFrames.header:Hide()
end
if showFooter then
-- Ensure main frame anchor is correct.
uiFrames.main:SetPoint("BOTTOM", uiFrames.footer, "TOP", 0, 0)
-- Show/hide Bag Bar and mini utilization summary.
if self.settings.showBagBar or self.temporarilyShowBagBar then
uiFrames.bagBar:Show()
self.ui.frames.miniSpaceSummaryBottom:Hide()
else
uiFrames.bagBar:Hide()
if self.alwaysShowUsageSummary then
self.ui.frames.miniSpaceSummaryBottom:Show()
end
end
-- Remove space summary from top toolbar.
self.ui.frames.miniSpaceSummaryTop:Hide()
-- Apply bag bar scaling and opacity.
local bagBarScale = (itemSlotSize / uiButtons.itemSlots[1].bagshuiData.originalSizeAdjusted)
* BsSkin.bagBarScale
uiFrames.bagBar:SetScale(bagBarScale)
-- Invert scaling for available space display so text is the normal size.
uiFrames.spaceSummary:SetScale(1 / bagBarScale)
-- Invert scaling for borders if needed.
-- If bag bar scaling was reworked to scale the individual buttons, Ui:SetItemButtonSize()
-- could be used to handle this automatically, but that refactor is not happening now.
if BsSkin.itemSlotBorderInverseScale then
for _, bagSlotButton in ipairs(self.ui.buttons.bagSlots) do
bagSlotButton.bagshuiData.buttonComponents.border:SetScale(1 / bagBarScale)
end
end
-- Determine footer height based on whether the bag bar is visible.
local footerHeight = math.max(
(
-- Bag bar visible.
self.settings.showBagBar and (uiButtons.itemSlots[1].bagshuiData.originalSizeAdjusted * bagBarScale)
or 0
),
BsSkin.inventoryHeaderFooterHeight
)
uiFrames.footer:SetHeight(footerHeight)
uiFrames.footer:Show()
-- Update window height.
finalWindowHeight = finalWindowHeight + footerHeight + BsSkin.inventoryHeaderFooterYAdjustment
else
-- When the footer is hidden, the main frame needs to anchor to the UI frame instead.
uiFrames.main:SetPoint("BOTTOM", self.uiFrame, "BOTTOM", 0, BsSkin.inventoryWindowPadding)
uiFrames.footer:Hide()
end
-- We can finally size the window frame.
self.uiFrame:SetWidth(math.max(finalWindowWidth, BsSkin.inventoryWindowMinWidth))
self.uiFrame:SetHeight(finalWindowHeight)
-- Set scale and anchor for docked frame.
if self.dockTo then
-- Scaling of docked frames is accomplished by scaling the Main frame, then changing the
-- dimensions of the window frame. This is done instead of just scaling self.uiFrame to keep
-- the window border looking the same as the frame it's docked to; it looks a little weird if
-- the border of the docked frame is shrunk.
local dockedFrameScale = BsSkin.inventoryDockedScale * Bagshui.components[self.dockTo].uiFrame:GetScale()
self.ui.frames.main:SetScale(dockedFrameScale)
self.uiFrame:SetWidth(finalWindowWidth * dockedFrameScale)
self.uiFrame:SetHeight(finalWindowHeight * dockedFrameScale)
-- Determine how to anchor the docked window based on how the parent window is docked.
self.uiFrame:ClearAllPoints()
local anchorY = "BOTTOM"
local anchorX = "RIGHT"
local anchorToY = "TOP"
local anchorToX = "RIGHT"
local xOffset = 0
local yOffset = -4
if Bagshui.components[self.dockTo].settings.windowAnchorXPoint == "LEFT" then
anchorX = "LEFT"
anchorToX = "LEFT"
end
if Bagshui.components[self.dockTo].settings.windowAnchorYPoint == "TOP" then
anchorY = "TOP"
anchorToY = "BOTTOM"
yOffset = -yOffset
end
self.uiFrame:SetPoint(
anchorY .. anchorX,
Bagshui.components[self.dockTo].uiFrame,
anchorToY .. anchorToX,
xOffset,
yOffset
)
else
-- Set scale and anchor for non-docked frame.
self.uiFrame:SetScale(self.settings.windowScale)
-- Ensure the window stays in the right place as it resizes.
-- WoW will change the anchor point to TOPLEFT when the frame is dragged, so we need to reset it.
self:FixWindowPosition()
end
end
-- Check for mouse over state for interact-able elements to ensure tooltips stay current.
Bagshui:QueueClassCallback(self, self.ItemSlotAndGroupMouseOverCheck)
-- Reset statuses.
self.lastExpandEmptySlotStacks = self.expandEmptySlotStacks
self.windowUpdateNeeded = false
end
--- Fill the given UI group frame with the items assigned to the given group.
---@param groupId number ID of the group to which items should be assigned.
---@param uiGroupNum number Frame in the `self.ui.frames.groups` table to use for display.
---@param currentItemSlotButtonNum number Frame in the `self.ui.frames.buttons.itemSlots` table to use next.
---@param groupWidthInItems number Count of items to place on each row of the group.
---@param groupPadding number Padding between group edge and item slots.
---@param itemSlotSize number Height/width of the item slot button.
---@param itemSlotMargin number Margin around the outer edge of each item slot button.
---@return any currentItemSlotButtonNum Index of the last frame from `self.ui.frames.buttons.itemSlots` that was used.
function Inventory:AssignItemsToSlots(
groupId,
uiGroupNum,
currentItemSlotButtonNum,
groupWidthInItems,
groupPadding,
itemSlotSize,
itemSlotMargin
)
-- self:PrintDebug(string.format("AssignItemsToSlots() groupId=%s uiGroupNum=%s groupWidthInItems=%s", groupId, uiGroupNum, groupWidthInItems))
local itemsPlacedInCurrentRow = 0
local rowNum = 0
local genericBagType
local button
local isEmptySlotStack
-- We need the real item count here to be able to step through all items.
-- (Using the count for layout from upstream won't work if empty slot
-- stacking is on because we can end up missing items).
local groupItemCount = table.getn(self.groupItems[groupId])
-- Make sure there are items to assign in this group.
if groupItemCount > 0 then
local buttons = self.ui.buttons
local frames = self.ui.frames
local groupFrame = frames.groups[uiGroupNum]
-- Prepare for layout
local initialOffset = groupPadding + itemSlotMargin
self:InitFramePositioningTable(
"AssignItemsToSlots", -- Position tracking table
itemSlotMargin, -- Frame X spacing
itemSlotMargin, -- Frame Y spacing
groupFrame, -- Initial frame to anchor to
-initialOffset, -- Initial X offset
initialOffset -- Initial Y offset
)
-- Update empty slot stack count for this group.
if self.emptySlotStackingAllowed then
self:CountEmptySlots(groupId)
end
-- Need to go in reverse because we're building right to left.
local firstItem = groupItemCount
local lastItem = 1
local step = -1
local item = nil
local hideItem = false
local bagIsHidden = false
for position = firstItem, lastItem, step do
-- Grab the item info.
item = self.groupItems[groupId][position]
genericBagType = self.containers[item.bagNum].genericType
-- Item hiding and empty slot handling
hideItem = false
isEmptySlotStack = false
-- Items (and empty slots) in bags the user has hidden.
bagIsHidden = self:IsBagHiddenInLayout(item.bagNum)
if bagIsHidden then
hideItem = true
-- Empty slot stacking.
elseif
self.emptySlotStackingAllowed -- Empty slot stacking must be on.
and item.emptySlot == 1 -- This item must be an empty slot.
and not item._bagshuiPreventEmptySlotStack -- This slot must not be temporarily blocked from stacking (usually if it just became empty and re-sorting hasn't occurred yet).
then
-- We've already shown one empty slot of this type in this group, so hide all the others.
if self.emptySlotStacks[genericBagType].displayed then
hideItem = true
else
-- Prevent any more empty slots of this generic container type from appearing in this group.
self.emptySlotStacks[genericBagType].displayed = true
-- Override the normal empty slot item with the stack so the count displays.
item = self.emptySlotStacks[genericBagType]
isEmptySlotStack = true
end
end
-- Determine whether this item is hidden.
if
not self.editMode
and not self.showHidden
and self.highlightItemsInContainerLocked ~= item.bagNum -- Unhide items when bag highlighting is locked on.
and self.hideItems[item.id]
then
hideItem = true
end
if not hideItem then
-- Ensure slot button exists.
self.ui:CreateInventoryItemSlotButton(currentItemSlotButtonNum)
button = buttons.itemSlots[currentItemSlotButtonNum]
-- Ensure parentage of item slot button is set to current group frame.
-- This is required to get the layering right -- without it, stuff can end up behind groups.
button:SetParent(groupFrame)
-- Record details of this button's assignments so we can access
-- them during UI events.
button.bagshuiData.groupId = groupId
button.bagshuiData.position = position
-- These always need to refer to the original item's location so
-- empty slot stacks magically work with click events
button.bagshuiData.bagNum = self.groupItems[groupId][position].bagNum
button.bagshuiData.slotNum = self.groupItems[groupId][position].slotNum
button.bagshuiData.isEmptySlotStack = isEmptySlotStack
-- Display the item slot button.
self:ShowFrameInNextPosition("AssignItemsToSlots", rowNum, button, itemSlotSize)
-- Add the item to the button (texture, tooltip, etc.).
self.ui:AssignItemToItemButton(button, item, groupId)
-- Should the Clam button be enabled?
if item.openable == 1 then
self.hasOpenables = true
-- The first openable item we come across will be the one the Clam button targets.
if not self.nextOpenableItemBagNum then
self.nextOpenableItemBagNum = item.bagNum
self.nextOpenableItemSlotNum = item.slotNum
self.nextOpenableItemSlotButton = button
end
end
-- Increment counters.
itemsPlacedInCurrentRow = itemsPlacedInCurrentRow + 1
currentItemSlotButtonNum = currentItemSlotButtonNum + 1
-- When we reach the end of a row, move to the next one.
if itemsPlacedInCurrentRow == groupWidthInItems then
itemsPlacedInCurrentRow = 0
rowNum = rowNum + 1
end
end
end
end
return currentItemSlotButtonNum
end
--- Call this before using `UpdateEmptySlotStackCount()` to zero the counts on the self.emptySlotStacks items
---@param resetBagsRepresented boolean? Clear the `_bagsRepresented` property of the empty slot stack items. Should only be called with `true` once per window update.
function Inventory:ResetEmptySlotStackCounts(resetBagsRepresented)
for _, emptySlotStack in pairs(self.emptySlotStacks) do
emptySlotStack.count = 0
emptySlotStack.displayed = false
if resetBagsRepresented then
BsUtil.TableClear(emptySlotStack._bagsRepresented)
end
end
end
--- Loop through all items in a group and add their counts to the appropriate empty
--- slot stack using `UpdateEmptySlotStackCount()`
---@param groupId number ID of group to count empty slots for.
function Inventory:CountEmptySlots(groupId)
self:ResetEmptySlotStackCounts()
for _, item in pairs(self.groupItems[groupId]) do
self:UpdateEmptySlotStackCount(item)
end
end
--- Has the user hidden this bag's items (right-click or Ctrl+Click on its bag button)?
---@param bagNum number
---@return boolean
function Inventory:IsBagHidden(bagNum)
local settingName = Bagshui.config.HideBagSettings[bagNum]
return settingName ~= nil and self.settings[settingName] == true
end
--- Should this bag's items be left out of the window right now?
--- Same exceptions as individually hidden items: Edit Mode, Show Hidden, and a locked bag highlight.
---@param bagNum number
---@return boolean
function Inventory:IsBagHiddenInLayout(bagNum)
return not self.editMode
and not self.showHidden
and self.highlightItemsInContainerLocked ~= bagNum
and self:IsBagHidden(bagNum)
end
--- Determine whether a slot is empty, and if so, add 1 to the appropriate per-bag-type empty slot stack.
---@param item table Bagshui inventory cache entry.
---@return boolean # true if the item is an empty slot.
function Inventory:UpdateEmptySlotStackCount(item)
-- Only increment the count for slots that are empty AND not temporarily prevented from stacking.
if item.emptySlot == 1 and not item._bagshuiPreventEmptySlotStack then
-- Empty slots in bags the user has hidden don't count toward any stack.
if self:IsBagHiddenInLayout(item.bagNum) then
return true
end
-- genericType SHOULD be filled in, but let's just be sure.
local genericBagType = self.containers[item.bagNum].genericType or BsGameInfo.itemSubclasses.Container.Bag
-- Increment the count.
self.emptySlotStacks[genericBagType].count = self.emptySlotStacks[genericBagType].count + 1
-- Record that the bag containing this item is represented by this stack
-- (used during container mouseover slot highlighting).
self.emptySlotStacks[genericBagType]._bagsRepresented[item.bagNum] = true
-- Return true so calling functions can know the item is an empty slot.
return true
end
return false
end
--- Return the number of items in a group that will be visible, accounting for stacked slots and hidden items.
--- Also updates `self.hasHiddenItems`.
---@param groupId number ID of group to get the number of items for.
---@param zeroUnlessHasItemsInBagNum number? Return 0 unless the group has items that are in the specified bag number -- in that case, return the normal count.
---@return integer visibleItemCount
function Inventory:GetGroupItemCountForLayout(groupId, zeroUnlessHasItemsInBagNum)
-- No items.
if not self.groupItems[groupId] then
return 0
end
-- Quickly return real count when we're in Edit Mode since nothing can be hidden.
if self.editMode then
return table.getn(self.groupItems[groupId])
end
-- Slot stacking / item hiding is enabled, so prepare for more complex counting.
local visibleItemCount = 0
local itemVisible
local hasEmptySlots = false
local hasItemsInFilterBag = false
-- We'll be using the permanent self.emptySlotStacks items, so reset their counts first.
self:ResetEmptySlotStackCounts()
-- Count up the number of visible items while figuring out if there are any empty slots or hidden items.
for _, item in ipairs(self.groupItems[groupId]) do
-- Assume the item will be counted
itemVisible = true
-- Have we found an item in the bag that will require a full count to be returned?
if zeroUnlessHasItemsInBagNum and item.bagNum == zeroUnlessHasItemsInBagNum then
hasItemsInFilterBag = true
end
-- Empty slot stacks and hidden items.
if self:IsBagHiddenInLayout(item.bagNum) then
-- This item is in a bag the user has hidden. Mark that hidden items exist so the
-- toolbar's Show Hidden button is enabled.
self.hasHiddenItems = true
itemVisible = false
elseif self:UpdateEmptySlotStackCount(item) then
-- This is an empty slot stack, which we know because in addition to
-- updating the appropriate empty slot stack proxy item in self.emptySlotsStacks,
-- UpdateEmptySlotStackCount() returns true if the given item is an empty slot.
if self.emptySlotStackingAllowed then
itemVisible = false
hasEmptySlots = true
end
elseif self.hideItems[item.id] then
-- Hidden items.
-- We need to update hasHiddenItems even if we're not actually hiding this item so that the toolbar icon will be enabled.
self.hasHiddenItems = true
-- Need to count the item when in Edit Mode or hidden items are shown.
itemVisible = (self.editMode or self.showHidden or self.highlightItemsInContainerLocked == item.bagNum)
end
-- Increment count if visible.
if itemVisible then
visibleItemCount = visibleItemCount + 1
end
end
-- Add 1 for each empty slot stack that contains 1 or more slots.
if hasEmptySlots then
for _, emptySlotStack in pairs(self.emptySlotStacks) do
if emptySlotStack.count > 0 then
visibleItemCount = visibleItemCount + 1
end
end
end
-- We were asked for a full count ONLY If this group has items in a specific bag.
if zeroUnlessHasItemsInBagNum and not hasItemsInFilterBag then
return 0
end
return visibleItemCount
end
--- Prepare a layout table to be used by `ShowFrameInNextPosition()`.
---@param positioningTableName string Identifier for this positioning table that will also be passed to `ShowFrameInNextPosition()`.
---@param frameXSpacing number Horizontal space between frames.
---@param frameYSpacing number Vertical space between frames.
---@param initialAnchorToFrame table Frame to which the first frame should be attached.
---@param initialXOffset number Horizontal space between initialAnchorToFrame and first frame.
---@param initialYOffset number Vertical space between initialAnchorToFrame and first frame.
function Inventory:InitFramePositioningTable(
positioningTableName,
frameXSpacing,
frameYSpacing,
initialAnchorToFrame,
initialXOffset,
initialYOffset
)
-- Set up positioning variable storage.
-- The table doesn't need to be cleared if it already exists because all its values will be reset below.
if self.positioningTables[positioningTableName] == nil then
self.positioningTables[positioningTableName] = {}
end
local positioningTable = self.positioningTables[positioningTableName]
-- Define anchor points and spacing.
positioningTable.anchorXPoint = self.settings.windowContentAlignment
positioningTable.anchorLeft = positioningTable.anchorXPoint == "LEFT"
positioningTable.anchorToPointXRowStart = "TOP" .. positioningTable.anchorXPoint
positioningTable.anchorToPointXSubsequent = "BOTTOM" .. BsUtil.FlipAnchorPoint(positioningTable.anchorXPoint)
positioningTable.frameXSpacing = frameXSpacing
positioningTable.frameYSpacing = frameYSpacing
-- Track the current row so we know when to move to a new one.
positioningTable.currentRow = nil
-- Each time we move to a new row, this is the frame the first element of the new row will anchor to.
positioningTable.firstVisibleFrameInPreviousRow = nil
-- The very first frame anchors to the bottom corner of the given frame.
positioningTable.anchorPoint = "BOTTOM" .. positioningTable.anchorXPoint
positioningTable.initialAnchorToFrame = initialAnchorToFrame
positioningTable.anchorToFrame = initialAnchorToFrame
positioningTable.anchorToPoint = positioningTable.anchorPoint
-- These are the initial values for X and Y offsets.
positioningTable.anchorXOffset = initialXOffset
positioningTable.anchorYOffset = initialYOffset
end
--- Helper function for `UpdateWindow()` to manage the process of displaying the grid of
--- groups and items within those groups at the correct location.
---@param positioningTableName string Identifier for this positioning table that was prepared in `InitFramePositioningTable()`.
---@param rowNum number Current row number.
---@param frame table Frame to display.
---@param width number Frame width.
---@param height number? Frame height (will use width if not specified).
---@param scale number? Frame scale.
function Inventory:ShowFrameInNextPosition(positioningTableName, rowNum, frame, width, height, scale)
local positioningTable = self.positioningTables[positioningTableName]
assert(
positioningTable,
"Inventory:ShowFrameInNextPosition(): Positioning table '"
.. tostring(positioningTableName)
.. "' has not been initialized; must call InitFramePositioningTable() first!"
)
-- self:PrintDebug(
-- "Showing " .. frame:GetName() .. " for row " .. rowNum
-- .. "\nAnchor point:" .. positioningTable.anchorPoint
-- .. "\nAnchor frame:" .. positioningTable.anchorToFrame:GetName()
-- .. "\nAnchor frame point:" .. positioningTable.anchorToPoint
-- .. "\nX offset:" .. (positioningTable.anchorLeft and -positioningTable.anchorXOffset or positioningTable.anchorXOffset)
-- .. "\nY offset:" .. positioningTable.anchorYOffset
-- .. "\nWidth" .. width
-- .. "\nHeight" .. height
-- .. "\nScale" .. tostring(scale)
-- )
-- Is this a new row?
-- nil check is so this only happens once we're above the first row.
if positioningTable.currentRow ~= nil and positioningTable.currentRow ~= rowNum then
-- Anchor to the first frame in the previous row.
positioningTable.anchorToFrame = positioningTable.firstVisibleFrameInPreviousRow
positioningTable.anchorToPoint = positioningTable.anchorToPointXRowStart
positioningTable.anchorXOffset = 0
-- First frame in the row needs to be higher up.
positioningTable.anchorYOffset = positioningTable.frameYSpacing
end
-- Display the frame.
self:SizeAndShowFrame(
frame,
positioningTable.anchorPoint,
positioningTable.anchorToFrame,
positioningTable.anchorToPoint,
positioningTable.anchorLeft and -positioningTable.anchorXOffset or positioningTable.anchorXOffset,
positioningTable.anchorYOffset,
width,
height,
scale,
-positioningTable.frameXSpacing -- HitRectInsets
)
-- The next element in a row always anchors to the previous one.
positioningTable.anchorToFrame = frame
-- When we move to a new row, update positioning variables to the proper state for anchoring within the same row.
if positioningTable.currentRow ~= rowNum then
-- Update the row number of the tracking variable so we know we've found the first visible element in this row.
positioningTable.currentRow = rowNum
-- Change the anchor point for subsequent frames.
positioningTable.anchorToPoint = positioningTable.anchorToPointXSubsequent
-- Subsequent frames in this row will be spaced using the frameXSpacing set by InitFramePositioningTable().
positioningTable.anchorXOffset = -positioningTable.frameXSpacing
-- Subsequent frames in this same row should be at the same vertical offset.
positioningTable.anchorYOffset = 0
-- Keep a note of the first frame int this row because the first frame in the next row will anchor to it.
positioningTable.firstVisibleFrameInPreviousRow = frame
end
end
-- SizeAndShowFrame() gets called a LOT so let's reuse variables to cut down on garbage collection.
local sizeAndShow_frame, sizeAndShow_finalScale, sizeAndShow_inverseScale, sizeAndShow_sizeWasSetViaScaling
--- Display the given frame at the specified position and size.
---@param frameObjOrName table|string Frame or name of frame.
---@param frameAttachPoint string Point on `frameObjOrName` that should be be attached.
---@param attachToFrame table|string Frame to which `frameObjOrName` should be attached.
---@param attachToPoint string Point on `attachToFrame` to which `frameObjOrName` should be attached..
---@param xOffset number? Horizontal offset for `frameObjOrName` (negative for left).
---@param yOffset number? Vertical offset for `frameObjOrName` (negative for down).
---@param width number Width of the frame.
---@param height number? Height of the frame (will use width if not specified).
---@param scale number? Scaling factor (0 - 1) that should be applied to the frame.
---@param hitRectInsets number? Hit rectangle insets to should be applied to the frame.
function Inventory:SizeAndShowFrame(
frameObjOrName,
frameAttachPoint,
attachToFrame,
attachToPoint,
xOffset,
yOffset,
width,
height,
scale,
hitRectInsets
)
-- Get the frame object.
sizeAndShow_frame = frameObjOrName
if type(frameObjOrName) == "string" then
sizeAndShow_frame = _G[frameObjOrName]
end
assert(
sizeAndShow_frame ~= nil,
"Invalid frame '" .. tostring(frameObjOrName) .. "' provided to SizeAndShowFrame()."
)
-- When an item slot button is sized by calling SetItemButtonSize, calling SetWidth/SetHeight isn't necessary.
sizeAndShow_sizeWasSetViaScaling = false
-- Item slot buttons should always be sized by scaling if possible, so do it automatically.
if
not scale
and sizeAndShow_frame.bagshuiData
and sizeAndShow_frame.bagshuiData.type == BS_UI_ITEM_BUTTON_TYPE.ITEM
then
scale = self.ui:SetItemButtonSize(sizeAndShow_frame, width)
sizeAndShow_sizeWasSetViaScaling = scale ~= nil
end
-- We need to know both the scale factor to apply to the frame and the inverse scale,
-- because SetPoint() uses the frame scale for offsets, so we need to un-scale the offsets.
sizeAndShow_finalScale = scale or 1
sizeAndShow_inverseScale = 1 / sizeAndShow_finalScale
-- Normal behavior -- set height, width, and scale.
if not sizeAndShow_sizeWasSetViaScaling then
sizeAndShow_frame:SetWidth(width)
sizeAndShow_frame:SetHeight(height or width)
sizeAndShow_frame:SetScale(sizeAndShow_finalScale)
end
-- Move frame to the specified point.
sizeAndShow_frame:ClearAllPoints()
sizeAndShow_frame:SetPoint(
frameAttachPoint,
attachToFrame,
attachToPoint,
xOffset * sizeAndShow_inverseScale,
yOffset * sizeAndShow_inverseScale
)
-- Adjust hit rectangle insets.
if hitRectInsets then
sizeAndShow_frame:SetHitRectInsets(hitRectInsets, hitRectInsets, hitRectInsets, hitRectInsets)
end
sizeAndShow_frame:Show()
-- For some reason, an initial call to GetTop() is sometimes necessary to prevent it
-- from returning nil later.
sizeAndShow_frame:GetTop()
end
--- Automatically select the next Group Move Target based on groupMoveTargetCount,
--- creating it if needed, then display using `ShowFrameInNextPosition()`
---@param width number Desired width of the group move target.
---@param height number Desired height of the group move target.
---@param uiRowNum number Current row number within the UI layout.
---@param layoutRowNum number Current layout/structure row number that the group move target should act on.
---@param layoutColumnNum number Current layout/structure column number that the group move target should act on.
---@param insertType BS_INVENTORY_LAYOUT_DIRECTION Whether the roup move target should act on rows or columns.
function Inventory:ShowNextGroupMoveTarget(width, height, uiRowNum, layoutRowNum, layoutColumnNum, insertType)
-- Increment count of targets and create a new one if needed.
self.groupMoveTargetCount = self.groupMoveTargetCount + 1
self.ui:CreateGroupMoveTarget(self.groupMoveTargetCount)
local groupMoveTarget = self.ui.frames.groupMoveTargets[self.groupMoveTargetCount]
-- Snag the button within the group move target frame so we can resize it and set properties.
local button = self.ui.frames.groupMoveTargets[self.groupMoveTargetCount].bagshuiData.button
-- This information will be pulled when the group move target button is clicked.
button.bagshuiData.insert = insertType
button.bagshuiData.rowNum = layoutRowNum
button.bagshuiData.columnNum = layoutColumnNum
-- Select button offsets based on whether the group is horizontal or vertical (start by assuming vertical).
local xOffsetLeft = BsSkin.groupMoveTargetAnchorOffset
local xOffsetRight = xOffsetLeft
local yOffset = BsSkin.groupMoveTargetLongDimensionSubtraction
if width > height then
-- This one is horizontal.
xOffsetRight = -(BsSkin.groupMoveTargetLongDimensionSubtraction + BsSkin.groupMoveTargetThinDimension + 1)
xOffsetLeft = xOffsetRight - BsSkin.groupMoveTargetAnchorOffset * 2
yOffset = -BsSkin.groupMoveTargetAnchorOffset
end
-- Set button offsets so the move target is visually shorter than the height/width of the groups.
button:SetPoint("TOPLEFT", groupMoveTarget, "TOPLEFT", -xOffsetLeft, -yOffset)
button:SetPoint("BOTTOMRIGHT", groupMoveTarget, "BOTTOMRIGHT", xOffsetRight, yOffset)
-- Display the frame.
self:ShowFrameInNextPosition("UpdateWindow", uiRowNum, groupMoveTarget, width, height)
end
--- Update free/available slot counts. Subclasses should override for special bag bar stuff.
function Inventory:UpdateBagBar()
local showBagUsage = self.settings.bagUsageAlwaysShow
local showUsageSummary = false
-- We always need to do one loop through the bag buttons to refresh the appearance and make some decisions.
for _, bagSlotButton in ipairs(self.ui.buttons.bagSlots) do
-- Set button textures -- necessary for them to be correct when viewing another character's inventory.
_G.SetItemButtonTexture(bagSlotButton, self.containers[bagSlotButton.bagshuiData.bagNum].texture)
-- Locked highlight border.
if self.highlightItemsInContainerLocked == bagSlotButton.bagshuiData.bagNum then
bagSlotButton.bagshuiData.buttonComponents.border:SetBackdropBorderColor(
BS_COLOR.ITEM_SLOT_HIGHLIGHT[1],
BS_COLOR.ITEM_SLOT_HIGHLIGHT[2],
BS_COLOR.ITEM_SLOT_HIGHLIGHT[3],
BsSkin.itemSlotBorderContainerHighlightOpacity
)
bagSlotButton.bagshuiData.buttonComponents.innerGlow:SetVertexColor(
BS_COLOR.ITEM_SLOT_HIGHLIGHT[1],
BS_COLOR.ITEM_SLOT_HIGHLIGHT[2],
BS_COLOR.ITEM_SLOT_HIGHLIGHT[3],
BsSkin.itemSlotInnerGlowOpacity + 0.2
)
bagSlotButton.bagshuiData.highlightBorderApplied = true
elseif self.highlightItemsInContainerLocked ~= bagSlotButton.bagshuiData.bagNum then
bagSlotButton.bagshuiData.buttonComponents.border:SetBackdropBorderColor(
BsSkin.itemSlotBorderDefaultColor.r,
BsSkin.itemSlotBorderDefaultColor.g,
BsSkin.itemSlotBorderDefaultColor.b,
BsSkin.itemSlotBorderOpacity
)
bagSlotButton.bagshuiData.buttonComponents.innerGlow:SetVertexColor(0, 0, 0, 0)
bagSlotButton.bagshuiData.highlightBorderApplied = false
end
-- Offline icon texture opacity.
bagSlotButton.bagshuiData.buttonComponents.iconTexture:SetVertexColor(
BS_COLOR.ITEM_SLOT_STATE_NORMAL[1],
BS_COLOR.ITEM_SLOT_STATE_NORMAL[2],
BS_COLOR.ITEM_SLOT_STATE_NORMAL[3],
(not self.online and BsSkin.itemSlotTextureOfflineOpacity or 1)
-- Dim bags whose items are hidden.
* (self:IsBagHidden(bagSlotButton.bagshuiData.bagNum) and 0.4 or 1)
)
-- Overall button opacity.
bagSlotButton:SetAlpha(
self.editMode and 0.2
-- When an item in the inventory is moused over, we want to dim everything but the container it's in.
or (
self.hoveredItem
and (
(
-- Empty slot stack.
self.hoveredItem._bagsRepresented
and self.hoveredItem._bagsRepresented[bagSlotButton.bagshuiData.bagNum]
-- Normal item.
or self.hoveredItem.bagNum == bagSlotButton.bagshuiData.bagNum
)
and 1
or 0.4
)
)
-- When the container highlight is locked on, dim other bags.
or ((self.highlightItemsInContainerLocked and self.highlightItemsInContainerLocked ~= bagSlotButton.bagshuiData.bagNum) and 0.7)
-- No item currently hovered.
or 1
)
-- Display free space information when the mouse is over any button.
if bagSlotButton.bagshuiData.mouseIsOver then
showBagUsage = true
showUsageSummary = true
end
end
-- Also display free space information when the mouse is over the summary area.
if self.alwaysShowUsageSummary or self.ui.frames.spaceSummary.bagshuiData.mouseIsOver then
showUsageSummary = true
end
-- Cancel free space display in Edit Mode.
if self.editMode then
showBagUsage = false
showUsageSummary = false
end
-- Width of bag space summary text area to the right of the bag bar that will be added
-- to the overall bag bar width.
local summaryWidth = 0
-- Reset space tracking - don't need to worry about whether the table members
-- are initialized because that's handled below. Here we're only iterating over
-- what already exists.
for _, spaceInfo in pairs(self.containerSpace) do
spaceInfo.available = 0
spaceInfo.used = 0
spaceInfo.total = 0
end
self.availableSlots, self.usedSlots, self.totalSlots = 0, 0, 0
-- Gether free space information.
for _, bagSlotButton in ipairs(self.ui.buttons.bagSlots) do
-- Step through bag slot buttons and determine available/used/total space.
local container = self.containers[bagSlotButton.bagshuiData.bagNum]
local available, used = 0, 0
local slotText = ""
if container.numSlots > 0 then
used = container.slotsFilled
if container.slotsFilled < container.numSlots then
available = container.numSlots - used
slotText = string.format("%s/%s", used, container.numSlots)
else
slotText = L.Full
end
end
if showBagUsage then
bagSlotButton.bagshuiData.buttonComponents.stock:SetText(slotText)
bagSlotButton.bagshuiData.buttonComponents.stock:Show()
else
bagSlotButton.bagshuiData.buttonComponents.stock:Hide()
end
local genericType = container.genericType or BsGameInfo.itemSubclasses["Container"]["Bag"]
-- Initialize space tracking if needed.
if not self.containerSpace[genericType] then
self.containerSpace[genericType] = {
available = 0,
used = 0,
total = 0,
}
end
-- Update calculations.
local spaceInfo = self.containerSpace[genericType]
spaceInfo.available = spaceInfo.available + available
spaceInfo.used = spaceInfo.used + used
spaceInfo.total = spaceInfo.total + container.numSlots
self.availableSlots = self.availableSlots + available
self.usedSlots = self.usedSlots + used
self.totalSlots = self.totalSlots + container.numSlots
end
-- Figure out what to display in the summary based on settings.
local textParts = BsUtil.Split(self.settings.bagUsageFormat, "_")
local mainText = tostring(textParts[1] == "Used" and self.usedSlots or self.availableSlots)
local miniText = mainText
local subText
if textParts[2] then
if textParts[2] == "Total" then
subText = self.totalSlots
elseif textParts[3] == "Total" then
subText =
string.format("%s/%s", (textParts[2] == "Used" and self.usedSlots or self.availableSlots), self.totalSlots)
-- miniText = miniText .. " (" .. subText .. ")"
end
end
if showUsageSummary then
self.ui.frames.spaceSummary.bagshuiData.text:SetText(mainText)
self.ui.frames.spaceSummary.bagshuiData.text:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.text:SetText(miniText)
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.text:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
self.ui.frames.miniSpaceSummaryTop.bagshuiData.text:SetText(miniText)
self.ui.frames.miniSpaceSummaryTop.bagshuiData.text:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
if subText then
self.ui.frames.spaceSummary.bagshuiData.text:ClearAllPoints()
self.ui.frames.spaceSummary.bagshuiData.text:SetPoint("BOTTOM", self.ui.frames.spaceSummary, "CENTER", 0, -2)
self.ui.frames.spaceSummary.bagshuiData.subtext:SetText(subText)
self.ui.frames.spaceSummary.bagshuiData.subtext:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
self.ui.frames.spaceSummary.bagshuiData.subtext:Show()
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:SetText(subText)
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:Show()
self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:SetText(subText)
self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:Show()
else
self.ui.frames.spaceSummary.bagshuiData.subtext:Hide()
self.ui.frames.spaceSummary.bagshuiData.text:ClearAllPoints()
self.ui.frames.spaceSummary.bagshuiData.text:SetPoint("CENTER", self.ui.frames.spaceSummary, "CENTER")
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:Hide()
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:SetText("")
self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:Hide()
self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:SetText("")
end
-- Resize and show/hide.
summaryWidth = math.max(
self.ui.frames.spaceSummary.bagshuiData.text:GetStringWidth(),
self.ui.frames.spaceSummary.bagshuiData.subtext:GetStringWidth()
)
self.ui.frames.spaceSummary:SetWidth(summaryWidth)
self.ui.frames.spaceSummary:SetAlpha(1) -- Reversing the SetAlpha(0) call that happens when we hide the summary.
self.ui.frames.miniSpaceSummaryBottom:SetWidth(
self.ui.frames.miniSpaceSummaryBottom.bagshuiData.text:GetStringWidth()
+ self.ui.frames.miniSpaceSummaryBottom.bagshuiData.subtext:GetStringWidth()
+ 2
)
self.ui.frames.miniSpaceSummaryTop:SetWidth(
self.ui.frames.miniSpaceSummaryTop.bagshuiData.text:GetStringWidth()
+ self.ui.frames.miniSpaceSummaryTop.bagshuiData.subtext:GetStringWidth()
+ 2
)
else
-- Hide space summary.
self.ui.frames.spaceSummary:SetAlpha(0) -- Using SetAlpha() instead of Hide() so it's still responsive to mouseover.
end
-- Set the Bag Bar to the correct width.
self.ui.frames.bagBar:SetWidth(
self.ui.frames.bagBar.bagshuiData.baseWidth
+ (math.max(summaryWidth, 15) * self.ui.frames.spaceSummary:GetScale())
+ 10 -- Buffer so nothing gets cut off.
)
end
--- Enable/disable/highlight toolbar buttons as appropriate.
function Inventory:UpdateToolbar()
local toolbarButtons = self.ui.buttons.toolbar
-- Resort icon.
self:SetToolbarButtonState(
toolbarButtons.resort,
nil,
(
self.enableResortIcon
or (
-- Docked inventory needs resorting.
self.dockedInventory and self.dockedInventory.enableResortIcon
)
)
)
-- Restack icon.
self:SetToolbarButtonState(
toolbarButtons.restack,
nil,
(
self.multiplePartialStacks
or (
-- Docked inventory has needs restack.
self.dockedInventory and self.dockedInventory.multiplePartialStacks
)
)
)
-- Show/Hide icon.
self:SetToolbarButtonState(
toolbarButtons.showHide,
nil,
(self.hasHiddenGroups or self.hasHiddenItems) and not self.editMode,
self.showHidden,
L.Toolbar_Hide_TooltipTitle,
L.Toolbar_Show_TooltipTitle
)
-- Highlight Changes icon.
self:SetToolbarButtonState(
toolbarButtons.highlightChanges,
nil,
self.highlightChangesEnabled and not self.editMode,
self.highlightChanges,
L.Toolbar_UnHighlightChanges_TooltipTitle,
L.Toolbar_HighlightChanges_TooltipTitle
)
-- Character icon.
self:SetToolbarButtonState(
toolbarButtons.character,
nil,
table.getn(BsCharacterData.characterIdList) > 1, -- Only enable when there's more than one character.
self.activeCharacterId ~= Bagshui.currentCharacterId
)
-- Lock highlights for inventories when they're open.
for inventoryType, _ in self:OtherInventoryTypesInToolbarIconOrder(true) do
self:SetToolbarButtonState(toolbarButtons[inventoryType], nil, true, Bagshui.components[inventoryType]:Visible())
end
-- Catalog.
self:SetToolbarButtonState(toolbarButtons.catalog, nil, true, BsCatalog:Visible())
-- Refresh toolbar button colors in case they've changed.
for _, button in pairs(toolbarButtons) do
self.ui:SetIconButtonColors(button)
end
-- State buttons.
self:SetToolbarButtonState(toolbarButtons.offline, not self.online)
self:SetToolbarButtonState(toolbarButtons.error, (string.len(self.errorText or "") > 0))
self:SetToolbarButtonState(toolbarButtons.editMode, self.editMode, nil, self.editMode)
-- Error button needs its tooltip updated.
toolbarButtons.error.bagshuiData.tooltipText = self.errorText
-- Update status text.
if self.editMode then
-- Show active Structure profile name when editing.
self.ui.text.status:SetText(
NORMAL_FONT_COLOR_CODE
.. string.format(L.Symbol_Colon, L.Profile_Structure)
.. FONT_COLOR_CODE_CLOSE
.. " "
.. BsProfiles:GetName(self.settings.profileStructure)
)
self.ui.frames.status:Show()
-- Move the offline character name into the tooltip in edit mode.
toolbarButtons.offline.bagshuiData.tooltipText = GRAY_FONT_COLOR_CODE
.. self.activeCharacterId
.. FONT_COLOR_CODE_CLOSE
else
self.ui.text.status:SetText(self.activeCharacterId)
if self.activeCharacterId ~= Bagshui.currentCharacterId then
self.ui.frames.status:Show()
else
self.ui.frames.status:Hide()
end
-- Don't need the offline character name in the tooltip normally.
toolbarButtons.offline.bagshuiData.tooltipText = nil
end
-- Money frame.
if self.settings.showMoney then
self.ui.frames.money:Show()
self.ui.frames.money:SetAlpha(self.editMode and 0.2 or 1)
-- Use label colors for money text.
for _, text in pairs(self.ui.frames.money.bagshuiData.texts) do
text:SetTextColor(
self.settings.groupLabelDefault[1],
self.settings.groupLabelDefault[2],
self.settings.groupLabelDefault[3],
self.settings.groupLabelDefault[4]
)
end
else
self.ui.frames.money:Hide()
end
-- Hearthstone button.
if self.hearthButton and self.settings.showFooter and self.settings.showHearthstone and self.hearthstoneItemRef then
toolbarButtons.hearthstone:Show()
-- Display cooldown.
local cooldownStart, cooldownDuration, isOnCooldown =
_G.GetContainerItemCooldown(self.hearthstoneItemRef.bagNum, self.hearthstoneItemRef.slotNum)
self.ui:SetIconButtonCooldown(toolbarButtons.hearthstone, cooldownStart, cooldownDuration, isOnCooldown)
else
toolbarButtons.hearthstone:Hide()
end
-- Clam (open container) button.
self:SetToolbarButtonState(
toolbarButtons.clam,
(self.clamButton and self.settings.showClam or false),
(
self.hasOpenables
and not self.editMode
-- Don't allow at Bank because UseContainerItem() moves the item
-- instead of opening it.
and not Bagshui.components.Bank.atBank
-- Avoid messing with item highlighting during a pending sale.
and not self.itemPendingSale
)
)
-- Disenchant button.
self:SetToolbarButtonState(
toolbarButtons.disenchant,
(
self.disenchantButton
and BsCharacter.spellNamesToIds[toolbarButtons.disenchant.bagshuiData.spellName]
and self.settings.showDisenchant
or false
),
not self.editMode
)
-- Pick Lock button.
self:SetToolbarButtonState(
toolbarButtons.pickLock,
(
self.pickLockButton
and BsCharacter.spellNamesToIds[toolbarButtons.pickLock.bagshuiData.spellName]
and self.settings.showPickLock
or false
),
not self.editMode
)
-- Utilization summaries.
if
self.alwaysShowUsageSummary
and (not self.settings.showFooter or (self.settings.showFooter and not self.settings.showBagBar))
then
if self.settings.showFooter then
self.ui.frames.miniSpaceSummaryBottom:Show()
self.ui.frames.miniSpaceSummaryTop:Hide()
else
self.ui.frames.miniSpaceSummaryBottom:Hide()
self.ui.frames.miniSpaceSummaryTop:Show()
end
else
self.ui.frames.miniSpaceSummaryBottom:Hide()
self.ui.frames.miniSpaceSummaryTop:Hide()
end
-- Re-anchor all toolbar widgets based on visibility.
self:UpdateToolbarAnchoring("topLeftToolbar", "LEFT")
self:UpdateToolbarAnchoring("topRightToolbar", "RIGHT")
self:UpdateToolbarAnchoring("bottomRightToolbar", "RIGHT")
-- Disable unusable stuff in Edit Mode.
local editModeState = self.editMode and "Disable" or "Enable"
-- Hearthstone
if self.hearthButton then
self.ui.buttons.toolbar.hearthstone[editModeState](self.ui.buttons.toolbar.hearthstone)
end
-- Search
self.ui.buttons.toolbar.search[editModeState](self.ui.buttons.toolbar.search)
-- Parent toolbar needs to sync state.
if self.dockedToInventory then
self.dockedToInventory:UpdateToolbar()
end
end
--- Control the state and appearance of a toolbar button.
---@param button table Button object.
---@param visible boolean? Whether the button should be displayed.
---@param enable boolean? Whether the button should be enabled.
---@param lockHighlight boolean? Whether the button should be highlighted.
---@param lockedHighlightTooltip string? Tooltip title to display when the button highlight is locked.
---@param unlockedHighlightTooltip string? Tooltip title to display when the button highlight is unlocked.
function Inventory:SetToolbarButtonState(
button,
visible,
enable,
lockHighlight,
lockedHighlightTooltip,
unlockedHighlightTooltip
)
if visible == nil then
visible = true
end
button[visible and "Show" or "Hide"](button)
if button.Enable then
if enable == nil then
enable = true
end
button[enable and "Enable" or "Disable"](button)
end
if button.LockHighlight then
if lockHighlight then
if lockedHighlightTooltip then
button.bagshuiData.tooltipTitle = lockedHighlightTooltip
end
button:LockHighlight()
else
if unlockedHighlightTooltip then
button.bagshuiData.tooltipTitle = unlockedHighlightTooltip
end
button:UnlockHighlight()
end
end
-- Update tooltip if it's visible so that text stays current.
if
button.bagshuiData.isIconButton
and button.bagshuiData.mouseIsOver
and BsIconButtonTooltip:IsVisible()
and BsIconButtonTooltip:IsOwned(button)
then
self.ui:ShowIconButtonTooltip(button, 0)
end
end
--- Toolbar icons need their anchors updated based on what's shown.
---@param widgetOrderTable string Name of table in `self.ui.ordering`, which is an array of WoW UI widgets, in display order. Can also include numbers as spacing directives that override the default.
---@param anchorPoint "LEFT"|"RIGHT" Place to anchor each widget. This point will be anchored to the opposing point of the previous widget.
function Inventory:UpdateToolbarAnchoring(widgetOrderTable, anchorPoint)
local widgetList = self.ui.ordering[widgetOrderTable]
local invert = (anchorPoint == "RIGHT" and -1 or 1)
local defaultOffset = invert * BsSkin.toolbarSpacing
local nextOffset
-- Go through the list of widgets in order, but skip the first one since
-- that's only there as the left/rightmost anchor.
for widgetPosition = 2, table.getn(widgetList) do
local widget = widgetList[widgetPosition]
-- Ignore spacing directives when finding widgets.
if type(widget) == "table" and widget:IsShown() then
-- Assume default spacing, adjusted if customized for the widget.
nextOffset = defaultOffset + (widget.bagshuiData and widget.bagshuiData.autoLayoutXOffset or 0)
-- Walk backwards through the list of widgets and anchor to the first visible one.
for anchorPosition = widgetPosition - 1, 1, -1 do
if type(widgetList[anchorPosition]) ~= "table" then
-- Spacing directive found. Use this instead of the default.
if type(widgetList[anchorPosition]) == "number" then
nextOffset = widgetList[anchorPosition]
end
else
-- Check visibility and update anchoring.
local anchorButton = widgetList[anchorPosition]
if anchorButton:IsShown() then
widget:SetPoint(anchorPoint, anchorButton, BsUtil.FlipAnchorPoint(anchorPoint), nextOffset, 0)
break
end
end
end -- Inner anchor finding loop.
end
end -- Widget iteration.
end
--- It seems like the client doesn't always catch situations where frames move out from
--- under the cursor. When empty slots are expanded/collapsed, that's exactly what happens, so
--- we have to manually hide the tooltip (handled in `ItemButton_OnClick()`) and we also need to determine
--- if the mouse is over an item button **after** the UI is redrawn. To do that, `UpdateWindow()` queues a
--- call to this function on the next frame update. Then we can evaluate whether the mouse is present,
--- and if so, update the UI to reflect that.
--- This is also an issue when picking up and putting down Categories/Groups/Items in Edit Mode,
--- where we manually hide tooltips and need to make them reappear when something is put back down.
function Inventory:ItemSlotAndGroupMouseOverCheck()
local mouseOverFound = false
for _, button in self.ui.buttons.itemSlots do
-- The IsVisible() check is needed because item slot frames that are hidden will still return true for MouseIsOver().
if _G.MouseIsOver(button) and button:IsVisible() then
self:ItemButton_OnEnter(button)
mouseOverFound = true
break
end
end
if self.editMode and not mouseOverFound then
for _, group in self.ui.frames.groups do
if _G.MouseIsOver(group) and group:IsVisible() then
self:Group_OnEnter(group)
end
end
end
end
--- Look through the inventory cache to find and record references to any special items.
--- Currently only used for the Hearthstone.
function Inventory:FindSpecialItems()
-- Reset Hearthstone item tracking.
self.hearthstoneItemRef = nil
self.hideItems[BS_INVENTORY_HEARTHSTONE_ITEM_ID] = (
self.hearthButton
and self.settings.showHearthstone
and self.settings.showFooter
) or nil
for _, bagContents in pairs(self.inventory) do
for __, item in ipairs(bagContents) do
-- Check for Hearthstone.
if self.hearthButton and item.id == BS_INVENTORY_HEARTHSTONE_ITEM_ID then
self.hearthstoneItemRef = item
end
end
end
end
--- Toggle an inventory class property.
--- Used to display/hide hidden items, enable/disable highlighting of changes,
--- maybe more in the future?
---@param propertyName any
function Inventory:ToggleProperty(propertyName)
if not propertyName then
return
end
_G.PlaySound("igMainMenuOptionCheckBox" .. (self[propertyName] and "Off" or "On"))
self[propertyName] = not self[propertyName]
self:ForceUpdateWindow()
if self.dockedInventory then
self.dockedInventory:ToggleProperty(propertyName)
end
end
end)