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