- CategorizeItems() keeps each item's category unless something it was based on changed (item, bag/slot, bag type, count, charges, stock state, tooltip). A full pass runs on window open, forced resort, rebuilt lookup tables, character switch and any non-bag event. - ActiveQuestItems: raise BAGSHUI_ACTIVE_QUEST_ITEM_UPDATE only when the quest item list or counts changed, not after every QUEST_LOG_UPDATE. - Item buttons: OnUpdate is set on mouse enter and removes itself after the mouse leaves; the base template OnUpdate is cleared. Stock badges are refreshed once a minute from the window's OnUpdate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
1309 lines
56 KiB
Lua
1309 lines
56 KiB
Lua
-- Bagshui Inventory Prototype: Item Slot Buttons
|
|
|
|
Bagshui:AddComponent(function()
|
|
local Inventory = Bagshui.prototypes.Inventory
|
|
local InventoryUi = Bagshui.prototypes.InventoryUi
|
|
|
|
--- Special OnHide for Inventory item slot buttons to also hide tooltips and
|
|
--- the stack split frame.
|
|
local function InventoryItemButton_OnHide()
|
|
-- oldOnHide was captured during button creation in CreateInventoryItemSlotButton().
|
|
_G.this.bagshuiData.oldOnHide()
|
|
|
|
if BsInfoTooltip:IsOwned(_G.this) then
|
|
BsInfoTooltip:Hide()
|
|
end
|
|
|
|
-- If the stack splitting frame is visible and owned by this inventory window, hide it.
|
|
-- (this.hasStackSplit is managed by Blizzard's FrameXML code).
|
|
if _G.this.hasStackSplit == 1 then
|
|
_G.StackSplitFrame:Hide()
|
|
end
|
|
end
|
|
|
|
--- Create a new item slot button.
|
|
--- This is a separate function instead of overriding InventoryUi:CreateItemSlotButton()
|
|
--- because they need to take completely different parameters.
|
|
---@param buttonNum number? When provided, only create the group if it doesn't exist.
|
|
function InventoryUi:CreateInventoryItemSlotButton(buttonNum)
|
|
local ui = self
|
|
local inventory = self.inventory
|
|
|
|
-- Wrapper functions so we can reference self.
|
|
if not inventory._itemSlotButton_ScriptWrapper_OnClick then
|
|
function inventory._itemSlotButton_ScriptWrapper_OnClick()
|
|
inventory:ItemButton_OnClick(_G.arg1)
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnDragStart()
|
|
inventory:ItemButton_OnClick("LeftButton", true)
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnReceiveDrag()
|
|
if _G.CursorHasItem() then
|
|
inventory:ItemButton_OnClick("LeftButton", true)
|
|
end
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnUpdate()
|
|
inventory:ItemButton_OnUpdate(_G.arg1)
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnEnter()
|
|
inventory:ItemButton_OnEnter()
|
|
-- OnUpdate only runs while the mouse is over the button (tooltip refresh on cooldown and
|
|
-- modifier keys, Edit Mode cursor). Running it on every button every frame was a constant
|
|
-- cost with the Bank open. It removes itself once the mouse has left (see ItemButton_OnUpdate).
|
|
_G.this:SetScript("OnUpdate", inventory._itemSlotButton_ScriptWrapper_OnUpdate)
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnLeave()
|
|
inventory:ItemButton_OnLeave()
|
|
end
|
|
function inventory._itemSlotButton_ScriptWrapper_OnHide()
|
|
inventory:ItemButton_OnEnter()
|
|
end
|
|
end
|
|
|
|
ui:CreateIfNotExists(buttonNum, ui.buttons.itemSlots, function(elementNum)
|
|
-- The button is created normally, then customized for Inventory use.
|
|
local slotButton = ui:CreateItemSlotButton("Item" .. elementNum, inventory.uiFrame)
|
|
ui.buttons.itemSlots[elementNum] = slotButton
|
|
|
|
-- Used by OnUpdate to manage real-time stock badge fading.
|
|
slotButton.bagshuiData.lastStockStateRefresh = _G.GetTime()
|
|
|
|
-- Consumed by InventoryItemButton_OnHide(). Must be captured before changing OnHide
|
|
-- to the Inventory-specific version.
|
|
slotButton.bagshuiData.oldOnHide = slotButton:GetScript("OnHide")
|
|
|
|
-- Inventory button events.
|
|
slotButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
slotButton:RegisterForDrag("LeftButton")
|
|
|
|
slotButton:SetScript("OnClick", inventory._itemSlotButton_ScriptWrapper_OnClick)
|
|
slotButton:SetScript("OnDragStart", inventory._itemSlotButton_ScriptWrapper_OnDragStart)
|
|
slotButton:SetScript("OnReceiveDrag", inventory._itemSlotButton_ScriptWrapper_OnReceiveDrag)
|
|
-- No OnUpdate until the mouse enters the button (see _itemSlotButton_ScriptWrapper_OnEnter).
|
|
-- Clear the base template's OnUpdate too (the inventory wrapper used to replace it).
|
|
slotButton:SetScript("OnUpdate", nil)
|
|
slotButton:SetScript("OnEnter", inventory._itemSlotButton_ScriptWrapper_OnEnter)
|
|
slotButton:SetScript("OnLeave", inventory._itemSlotButton_ScriptWrapper_OnLeave)
|
|
slotButton:SetScript("OnHide", InventoryItemButton_OnHide)
|
|
|
|
self:AddItemSlotButtonGetIdProxy(slotButton)
|
|
end)
|
|
end
|
|
|
|
--- ### Now it's time for fun with metatables!
|
|
---
|
|
--- We have a little problem when it comes to ensuring maximum interoperability
|
|
--- and compatibility with other addons and it's called GetID(). See, the
|
|
--- Blizzard code in ContainerFrameItemButton_OnClick/OnEnter uses
|
|
--- <itemButton>:GetParent():GetID()and <itemButton>:GetID() to determine
|
|
--- bag and slot number, respectively. We need to call those functions
|
|
--- because they're hooked by other addons (which probably should be hooking
|
|
--- other things instead, but we can't do anything about that).
|
|
---
|
|
--- Slot number isn't an issue -- we can easily and safely set that in
|
|
--- OnEnter (and OnClick). Bag number is a problem though, because slots
|
|
--- from different bags are going to share the same parent (Group) frame.
|
|
--- This means that if you click an item and then mouse over another before
|
|
--- the <itemButton>:GetParent():GetID() call is made, the wrong bag
|
|
--- number is likely to be returned.
|
|
---
|
|
--- The bag number problem was first identified with stack splitting,
|
|
--- so a solution that only applied to stack splitting was put in place.
|
|
--- A more generalized approach is desirable though, and so here it is.
|
|
---
|
|
--- The code below sets up two proxy frames that are outfitted with metatables
|
|
--- so that they act as if they are the item button, but with overridden
|
|
--- functions that give us what we need to ensure code outside our control
|
|
--- receives the correct bag and slot numbers.
|
|
---
|
|
--- These proxies are picked up by Inventory:ItemButton_OnClick/OnEnter()
|
|
--- and set up so Blizzard's GetID() calls will be intercepted and redirected
|
|
--- to our custom functions.
|
|
---
|
|
--- (Now it's entirely possible there's a simpler way to do this, and
|
|
--- if there is, I'd love to know it. There were past experiments with
|
|
--- intermediate parent frames, but frame levels between item buttons
|
|
--- and groups are slightly finicky and led to item buttons disappearing.
|
|
--- Another attempt was made along those lines in the disastrous 1.2.17
|
|
--- release. So having something that works is and doesn't seem to cause
|
|
--- any side effects is preferable.)
|
|
---
|
|
--- ### All that to say...
|
|
--- What this function does is create a proxy button frame and assign it to the
|
|
--- `slotButton.bagshuiData.getIdProxy' property. It's then available for other
|
|
--- code to use and pass upstream (or is it downstream?) to Blizzard code.
|
|
---@param slotButton table Button to create proxy frames for.
|
|
function InventoryUi:AddItemSlotButtonGetIdProxy(slotButton)
|
|
if not slotButton.bagshuiData then
|
|
slotButton.bagshuiData = {}
|
|
end
|
|
|
|
-- Proxy for `slotButton` that was created above.
|
|
-- Will override `GetID()` and `GetParent()`.
|
|
--
|
|
-- This is deliberately a plain table, NOT a frame made with CreateFrame(). Its `[0]` is pointed at
|
|
-- another frame's userdata (below), and EnumerateFrames() follows `[0]`: a real proxy frame would send
|
|
-- any addon that walks every frame back to the item button forever, freezing the game.
|
|
-- Frame methods only need `[0]`, so a table with `[0]` works the same.
|
|
local slotButtonProxy = {}
|
|
-- Store the proxy so it can be used by `Inventory:ItemButton_OnClick/OnEnter()`
|
|
slotButton.bagshuiData.getIdProxy = slotButtonProxy
|
|
-- Proxy for `slotButton`'s parent frame.
|
|
-- Will override `GetID()` only.
|
|
-- (Also a plain table, for the same reason as `slotButtonProxy`.)
|
|
local parentProxy = {}
|
|
-- Reference to `slotButton`'s actual parent frame, updated
|
|
-- by `getItemButtonParent()`.
|
|
local realParent
|
|
|
|
--- Proxy function for `slotButton:GetID()`.
|
|
--- Always returns the slot number of the item assigned to the button.
|
|
local function getItemSlot(_)
|
|
return slotButton.bagshuiData.slotNum
|
|
end
|
|
|
|
--- Proxy function for `slotButton:GetParent()`.
|
|
--- Stores the real parent frame so the metatable knows where to
|
|
--- redirect everything other than GetID().
|
|
local function getItemButtonParent(_)
|
|
realParent = slotButton:GetParent()
|
|
-- Update userdata property of proxy frame.
|
|
-- rawset() because the proxy has no `[0]` of its own and `__newindex` would redirect the write.
|
|
rawset(parentProxy, 0, realParent[0])
|
|
return parentProxy
|
|
end
|
|
|
|
--- Proxy function for `slotButton:GetParent():GetID()`.
|
|
--- Always returns the bag number of the item assigned to the button.
|
|
local function getItemBag(_)
|
|
return slotButton.bagshuiData.bagNum
|
|
end
|
|
|
|
-- Metatable that makes `slotButtonProxy` work.
|
|
local slotButtonMetatable = {
|
|
__index = function(_, key)
|
|
if key == "GetID" then
|
|
return getItemSlot
|
|
elseif key == "GetParent" then
|
|
return getItemButtonParent
|
|
else
|
|
return slotButton[key]
|
|
end
|
|
end,
|
|
__newindex = function(_, key, val)
|
|
slotButton[key] = val
|
|
end,
|
|
}
|
|
setmetatable(slotButtonProxy, slotButtonMetatable)
|
|
-- Redirect frame userdata to avoid errors.
|
|
-- Credit: https://www.wowinterface.com/forums/showthread.php?t=53928
|
|
-- rawset() because the proxy has no `[0]` of its own and `__newindex` would redirect the write.
|
|
rawset(slotButtonProxy, 0, slotButton[0])
|
|
|
|
-- Metatable that makes `parentProxy` work.
|
|
local parentMetatable = {
|
|
__index = function(_, key)
|
|
if key == "GetID" then
|
|
return getItemBag
|
|
else
|
|
return realParent[key]
|
|
end
|
|
end,
|
|
__newindex = function(_, key, val)
|
|
realParent[key] = val
|
|
end,
|
|
}
|
|
-- The userdata (table key 0) is updated in getItemButtonParent() since it changes.
|
|
setmetatable(parentProxy, parentMetatable)
|
|
end
|
|
|
|
--- OnEnter mostly handles tooltip stuff.
|
|
---@param itemButton table? Item slot button widget.
|
|
function Inventory:ItemButton_OnEnter(itemButton)
|
|
itemButton = itemButton or _G.this
|
|
|
|
-- Cases when nothing should happen.
|
|
if
|
|
-- OnEnter still fires even if mouse events are disabled so we need to avoid doing anything when we're in that state.
|
|
not itemButton:IsMouseEnabled()
|
|
-- Don't pop up tooltips or do anything else when a menu is open.
|
|
or self.menus:IsMenuOpen()
|
|
then
|
|
return
|
|
end
|
|
|
|
local buttonInfo = itemButton.bagshuiData
|
|
local item = itemButton.bagshuiData.item or self.inventory[buttonInfo.bagNum][buttonInfo.slotNum]
|
|
|
|
-- Record that the mouse has moved over this button (used by OnUpdate to determine
|
|
-- whether the tooltip should be shown when the Edit Mode cursor puts down an item).
|
|
-- This is used instead of MouseIsOver() because that function returns true even
|
|
-- when the frame is behind other frames.
|
|
buttonInfo.mouseIsOver = true
|
|
|
|
-- Highlight all items belonging to a category on mouseover in Edit Mode,
|
|
-- but only if an object hasn't been picked up.
|
|
if self.editState.cursorItem == nil then
|
|
if _G.IsAltKeyDown() and item.id > 0 then
|
|
self.editState.highlightItem = item.id
|
|
self.editState.highlightCategory = nil
|
|
else
|
|
self.editState.highlightItem = nil
|
|
self.editState.highlightCategory = item.bagshuiCategoryId
|
|
end
|
|
end
|
|
|
|
-- Nothing further to do when there's an Edit Mode item on the cursor,
|
|
-- This can't be combined with the initial "return immediately" checks above
|
|
-- because we want to trigger Edit Mode highlighting.
|
|
if self:EditModeCursorHasItem() then
|
|
return
|
|
end
|
|
|
|
-- Turn off the Bagshui tooltip hint once Alt is held.
|
|
if not self.settings.hint_bagshuiTooltip and _G.IsAltKeyDown() then
|
|
self.settings.hint_bagshuiTooltip = true
|
|
end
|
|
|
|
-- Record current modifier key state (referenced by OnUpdate() to decide if it has changed).
|
|
-- Translating return values from 1/nil to true/false so the *KeyDown properties don't have
|
|
-- to be initialized to non-nil.
|
|
buttonInfo.altKeyDown = (_G.IsAltKeyDown() == 1)
|
|
buttonInfo.controlKeyDown = (_G.IsControlKeyDown() == 1)
|
|
buttonInfo.shiftKeyDown = (_G.IsShiftKeyDown() == 1)
|
|
|
|
-- In Edit Mode and/or "Bagshui Info" mode with Alt down, special behavior needs to be enabled:
|
|
-- - Tooltips get truncated at the first blank line so there's more room for the Bagshui tooltip (if enabled).
|
|
local truncateTooltip = (
|
|
(BS_INVENTORY_TRUNCATE_TOOLTIPS_EDIT_MODE and self.editMode)
|
|
or (BS_INVENTORY_TRUNCATE_TOOLTIPS_INFO_MODE and _G.IsAltKeyDown())
|
|
)
|
|
-- Empty slots deserve a tooltip too sometimes.
|
|
local displayTooltipForEmptySlots = (_G.IsAltKeyDown() or self.editMode)
|
|
|
|
-- When Alt is held in Edit Mode, swap from the Category tooltip to the Item tooltip.
|
|
local editModeShowItemTooltip = self.editMode and _G.IsAltKeyDown()
|
|
|
|
-- GameTooltip normally anchors to item frame, but in Edit Mode it attaches to the group frame.
|
|
local gameTooltipAnchorPoint = BsUtil.FlipAnchorPoint(self.settings.windowAnchorXPoint)
|
|
local tooltipAnchorFrame = self.editMode and itemButton:GetParent() or itemButton
|
|
local tooltipOffset = BsSkin.tooltipExtraOffset
|
|
_G.GameTooltip:ClearAllPoints()
|
|
_G.GameTooltip:SetOwner(
|
|
itemButton,
|
|
-- Using ANCHOR_PRESERVE here for Edit Mode instead of ANCHOR_NONE
|
|
-- so pfUI doesn't mess with the position.
|
|
"ANCHOR_" .. (self.editMode and "PRESERVE" or gameTooltipAnchorPoint),
|
|
-tooltipOffset,
|
|
tooltipOffset
|
|
)
|
|
if self.editMode then
|
|
_G.GameTooltip:SetPoint(
|
|
"BOTTOM" .. BsUtil.FlipAnchorPoint(gameTooltipAnchorPoint),
|
|
tooltipAnchorFrame,
|
|
"TOP" .. gameTooltipAnchorPoint,
|
|
-tooltipOffset,
|
|
tooltipOffset
|
|
)
|
|
end
|
|
|
|
-- When true, the class instance had a `itemSlotTooltipFunction` defined
|
|
-- and we need to reapply our preferred tooltip position.
|
|
local setTooltipPositionAgain = false
|
|
|
|
-- Populate GameTooltip with Item or Edit Mode information, as appropriate.
|
|
|
|
if not self.editMode or editModeShowItemTooltip then
|
|
-- Item tooltip outside Edit Mode, or if Alt is held in Edit Mode.
|
|
|
|
-- Empty slots have special handling, as usual.
|
|
if item.emptySlot == 1 then
|
|
-- This is an empty slot.
|
|
|
|
_G.GameTooltip:ClearLines()
|
|
|
|
-- Add name.
|
|
if (displayTooltipForEmptySlots or self.settings.stackEmptySlots) and item.name ~= nil then
|
|
_G.GameTooltip:AddLine(BsItemInfo:GetQualityColoredName(item), 1, 1, 1, true)
|
|
end
|
|
|
|
-- Empty slot stacking/unstacking instructions should only be added when appropriate.
|
|
if not self.editMode and self.settings.stackEmptySlots and not _G.CursorHasItem() then
|
|
_G.GameTooltip:AddLine(
|
|
string.format(
|
|
L.Tooltip_Inventory_ToggleEmptySlotStacking,
|
|
L.Click,
|
|
string.lower(self.expandEmptySlotStacks and L.Stack or L.Unstack)
|
|
),
|
|
nil,
|
|
nil,
|
|
nil,
|
|
true
|
|
)
|
|
end
|
|
else
|
|
-- There's an item in this slot.
|
|
|
|
-- These will be populated with the return values of ItemInfo:LoadTooltip().
|
|
local hasCooldown, repairCost
|
|
|
|
-- Populate the tooltip.
|
|
-- There doesn't seem to be a way to get hasCooldown/repairCost without
|
|
-- loading the tooltip, so we have to call this even if there's an
|
|
-- itemSlotTooltipFunction that's going to load it again.
|
|
hasCooldown, repairCost = BsItemInfo:LoadTooltip(_G.GameTooltip, item, self)
|
|
|
|
-- This class instance has a custom tooltip loading function.
|
|
-- Spoiler: It's probably ContainerFrameItemButton_OnEnter().
|
|
-- (itemSlotTooltipFunction is the function name, not the actual function
|
|
-- see declaration in Inventory.lua for reasoning.)
|
|
if
|
|
self.online -- Definitely have to be online for this.
|
|
and not self.editMode -- Not calling in Edit Mode so we cut out some hooks and have a little more control.
|
|
and not truncateTooltip -- Also, we don't need extra stuff if we're just going to truncate anyway.
|
|
and self.itemSlotTooltipFunction
|
|
and type(_G[self.itemSlotTooltipFunction]) == "function"
|
|
then
|
|
-- Ugh, this is stupid, but the GFW tooltip hooking code doesn't
|
|
-- have `ContainerFrameItemButton_OnEnter()`'s button parameter,
|
|
-- so the Blizzard function ends up pulling the global `this`
|
|
-- instead of the passed parameter. When `Inventory:ItemSlotAndGroupMouseOverCheck()`
|
|
-- calls us, global `this` isn't the item slot button and
|
|
-- `ContainerFrameItemButton_OnEnter()`'s call to `GetRight()`
|
|
-- returns nil, throwing an error. We can work around their bug by
|
|
-- temporarily changing the value of global `this`, then restoring it.
|
|
local oldGlobalThis = _G.this
|
|
-- In addition to the above, we need to use our metatable'd proxy frame
|
|
-- (set up in InventoryUi:CreateInventoryItemSlotButton()) so the
|
|
-- GetID() and GetParent():GetID() functions will be overridden.
|
|
_G.this = itemButton.bagshuiData.getIdProxy or _G.this
|
|
_G[self.itemSlotTooltipFunction](itemButton.bagshuiData.getIdProxy)
|
|
_G.this = oldGlobalThis
|
|
-- `ContainerFrameItemButton_OnEnter()` will change the tooltip position
|
|
-- to something that ignores our custom offsets, so we need to fix that.
|
|
setTooltipPositionAgain = true
|
|
end
|
|
|
|
-- There are situations where we may want to truncate the tooltip
|
|
-- at the first empty line to leave more room for the Bagshui tooltip.
|
|
-- This code was originally written when information was being added
|
|
-- to GameTooltip instead of a separate tooltip, so it was very
|
|
-- necessary due to the tooltip line limit. Now that the Bagshui Info
|
|
-- tooltip is used instead, it's not as relevant, but it's still nice to
|
|
-- not have GameTooltip so giant when the Bagshui Info tooltip is shown.
|
|
-- The BS_INVENTORY_TRUNCATE_TOOLTIPS_*_MODE constants control whether
|
|
-- tooltip truncation is enabled.
|
|
if truncateTooltip then
|
|
local firstBlankLine, ttTextFrame, ttText, leftText, rightText
|
|
for ttLineNum = 1, _G.GameTooltip:NumLines() do
|
|
leftText = nil
|
|
rightText = nil
|
|
for i, lr in ipairs(BsHiddenTooltip.bagshuiData.textFieldsPerLine) do
|
|
ttTextFrame = _G["GameTooltipText" .. lr .. ttLineNum]
|
|
if ttTextFrame ~= nil and ttTextFrame:IsVisible() then
|
|
ttText = ttTextFrame:GetText()
|
|
|
|
if ttText ~= nil and not string.find(ttText, "^" .. BS_NEWLINE) then
|
|
if i == 1 then
|
|
leftText = BsUtil.Trim(ttText)
|
|
else
|
|
rightText = BsUtil.Trim(ttText)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- We found a blank line and can truncate here..
|
|
if string.len(leftText or "") == 0 and string.len(rightText or "") == 0 then
|
|
firstBlankLine = ttLineNum
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Hide lines.
|
|
if firstBlankLine then
|
|
for line = firstBlankLine, _G.GameTooltip:NumLines() do
|
|
for _, lr in ipairs(BsHiddenTooltip.bagshuiData.textFieldsPerLine) do
|
|
if _G["GameTooltipText" .. lr .. line] then
|
|
_G["GameTooltipText" .. lr .. line]:SetText(nil)
|
|
_G["GameTooltipText" .. lr .. line]:Hide()
|
|
end
|
|
end
|
|
end
|
|
_G.GameTooltipMoneyFrame:Hide()
|
|
end
|
|
end
|
|
|
|
-- Don't want to apply this stuff in Edit Mode since normal functionality isn't available.
|
|
if not self.editMode then
|
|
_G.ResetCursor()
|
|
|
|
-- Trigger ItemButton_OnUpdate to reload the tooltip after 1 second if there's an active cooldown.
|
|
-- (In other words, the 1 here represents number of seconds, NOT `true`).
|
|
buttonInfo.tooltipCooldownUpdate = hasCooldown and 1 or nil
|
|
|
|
-- Special cases.
|
|
if _G.InRepairMode() and (repairCost and repairCost > 0) then
|
|
-- Repair mode.
|
|
_G.GameTooltip:AddLine(_G.TEXT(_G.REPAIR_COST), 1, 1, 1)
|
|
_G.SetTooltipMoney(_G.GameTooltip, repairCost)
|
|
elseif self.ui:IsFrameVisible(_G.MerchantFrame) and _G.MerchantFrame.selectedTab ~= 2 then
|
|
-- At merchant (not on buyback tab).
|
|
_G.ShowContainerSellCursor(item.bagNum, item.slotNum)
|
|
elseif (item.readable == 1 or (_G.IsControlKeyDown() and not _G.IsAltKeyDown())) and item.emptySlot ~= 1 then
|
|
-- Readable items (books, etc) / Control key dressup.
|
|
_G.ShowInspectCursor()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Show Alt+Click Trade hint when appropriate.
|
|
if (self.settings.rightClickAttach or self.settings.altClickAttach) and self:IsInitiateTradeAllowed(item) then
|
|
_G.GameTooltip:AddLine(string.format(L.Tooltip_Inventory_TradeShortcut, L.AltClick, _G.UnitName("target")))
|
|
end
|
|
elseif self.editMode then
|
|
-- By default in Edit Mode, show category info as the primary tooltip.
|
|
-- This gets overridden when Alt is held.
|
|
_G.GameTooltip:AddLine(
|
|
Bagshui:FormatTooltipLine(
|
|
(BsCategories:GetName(item.bagshuiCategoryId) or L.Error_ItemCategoryUnknown),
|
|
string.format(L.Symbol_Colon, L.Category),
|
|
self.editMode
|
|
)
|
|
)
|
|
end
|
|
|
|
-- Bagshui Info Tooltip
|
|
if
|
|
self.editMode
|
|
or _G.IsAltKeyDown()
|
|
or (BsSettings.showInfoTooltipsWithoutAlt and not _G.IsShiftKeyDown())
|
|
or self.itemPendingSale
|
|
then
|
|
-- 4th parameter: Always place the info tooltip under GameTooltip in edit mode.
|
|
Bagshui:SetInfoTooltipPosition(itemButton, true, self.editMode)
|
|
|
|
if not self.editMode then
|
|
-- There's no real information to show for empty slot stacks.
|
|
if not buttonInfo.isEmptySlotStack then
|
|
-- Populate the tooltip.
|
|
if _G.IsControlKeyDown() and not self.itemPendingSale then
|
|
-- All item properties.
|
|
BsItemInfo:AddTooltipInfo(item, BsInfoTooltip)
|
|
else
|
|
-- Normal mode.
|
|
|
|
-- Sell protection confirmation instructions.
|
|
if self.ui:IsFrameVisible("MerchantFrame") and self.itemPendingSale == item then
|
|
self:AddBagshuiInfoTooltipLine(
|
|
BS_FONT_COLOR.UI_ORANGE .. L.Inventory_Item_SellProtection_ConfirmSale .. FONT_COLOR_CODE_CLOSE
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(
|
|
LIGHTYELLOW_FONT_COLOR_CODE
|
|
.. string.format(L.Inventory_Item_SellProtection_Reason, self:GetItemSellProtectionReason(item))
|
|
.. FONT_COLOR_CODE_CLOSE
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(
|
|
GRAY_FONT_COLOR_CODE .. L.Inventory_Item_SellProtection_OverrideHint .. FONT_COLOR_CODE_CLOSE
|
|
)
|
|
end
|
|
|
|
-- Inventory counts.
|
|
BsCatalog:AddTooltipInfo(item.itemString, BsInfoTooltip)
|
|
|
|
-- Only show more in-depth info when Alt is held, even if showInfoTooltipsWithoutAlt is on.
|
|
if _G.IsAltKeyDown() then
|
|
-- Active quest info.
|
|
if Bagshui.activeQuestItems[item.name] then
|
|
if BsInfoTooltip:NumLines() > 0 then
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
end
|
|
self:AddBagshuiInfoTooltipLine(
|
|
tostring(Bagshui.activeQuestItems[item.name].obtained or "?")
|
|
.. "/"
|
|
.. tostring(Bagshui.activeQuestItems[item.name].needed or "?"),
|
|
string.format(L.Symbol_Colon, L.ItemPropFriendly_activeQuest)
|
|
)
|
|
end
|
|
|
|
-- Stock state.
|
|
if
|
|
(item.bagshuiDate or 0) > 0
|
|
and item.bagshuiStockState ~= nil
|
|
and item.bagshuiStockState ~= BS_ITEM_STOCK_STATE.NO_CHANGE
|
|
then
|
|
if BsInfoTooltip:NumLines() > 0 then
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
end
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L["Stock_" .. item.bagshuiStockState],
|
|
string.format(L.Symbol_Colon, L.StockState)
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(
|
|
BsUtil.FormatTimeRemainingString(_G.time() - item.bagshuiDate),
|
|
string.format(L.Symbol_Colon, L.StockLastChange)
|
|
)
|
|
end
|
|
|
|
-- Assigned category and bag name.
|
|
if BsInfoTooltip:NumLines() > 0 then
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
end
|
|
self:AddBagshuiInfoTooltipLine(
|
|
(BsCategories:GetName(item.bagshuiCategoryId) or L.Error_ItemCategoryUnknown),
|
|
string.format(L.Symbol_Colon, L.Category),
|
|
false
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(
|
|
(self.containers[item.bagNum].name or L.Unknown),
|
|
string.format(L.Symbol_Colon, L.Bag)
|
|
)
|
|
|
|
-- Instructions.
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.AltRightClick,
|
|
string.format(L.Symbol_Colon, string.format(L.Suffix_Menu, L.Item))
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(L.HoldControlAlt, string.format(L.Symbol_Colon, L.MoreInformation))
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- Edit Mode.
|
|
|
|
if editModeShowItemTooltip then
|
|
-- Item instructions.
|
|
|
|
if item.id > 0 then
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.AltClick,
|
|
string.format(L.Symbol_Colon, string.format(L.Prefix_Move, L.Item))
|
|
)
|
|
end
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.RightClick,
|
|
string.format(L.Symbol_Colon, string.format(L.Suffix_Menu, L.CategorySlashItem))
|
|
)
|
|
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
|
|
-- Category name.
|
|
self:AddBagshuiInfoTooltipLine(
|
|
(BsCategories:GetName(item.bagshuiCategoryId) or L.Error_ItemCategoryUnknown),
|
|
string.format(L.Symbol_Colon, L.Category),
|
|
self.editMode
|
|
)
|
|
-- Target Category: Release Alt.
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.ReleaseAlt,
|
|
string.format(L.Symbol_Colon, string.format(L.Prefix_Target, L.Category))
|
|
)
|
|
else
|
|
-- Category instructions.
|
|
|
|
-- Move Category: Click.
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.Click,
|
|
string.format(L.Symbol_Colon, string.format(L.Prefix_Move, L.Category))
|
|
)
|
|
-- Category/Item Menu: Right-Click.
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.RightClick,
|
|
string.format(L.Symbol_Colon, string.format(L.Suffix_Menu, L.CategorySlashItem))
|
|
)
|
|
self:AddBagshuiInfoTooltipLine(" ")
|
|
|
|
-- Item name.
|
|
if string.len(item.name or "") > 0 then
|
|
self:AddBagshuiInfoTooltipLine(
|
|
BsItemInfo:GetQualityColoredName(item),
|
|
string.format(L.Symbol_Colon, L.Item),
|
|
true
|
|
)
|
|
end
|
|
|
|
-- Target Item: Hold Alt.
|
|
self:AddBagshuiInfoTooltipLine(
|
|
L.HoldAlt,
|
|
string.format(L.Symbol_Colon, string.format(L.Prefix_Target, L.Item))
|
|
)
|
|
end
|
|
end
|
|
elseif not self.settings.hint_bagshuiTooltip then
|
|
-- Show tip about holding alt to display Bagshui info tooltip.
|
|
Bagshui:SetInfoTooltipPosition(itemButton, true)
|
|
self:AddBagshuiInfoTooltipLine(L.HoldAlt, string.format(L.Symbol_Colon, L.BagshuiTooltipIntro))
|
|
else
|
|
-- Clear the tooltip so we know not to display it.
|
|
BsInfoTooltip:ClearLines()
|
|
end
|
|
|
|
-- Normal tooltip.
|
|
if _G.GameTooltip:NumLines() > 0 or _G.GameTooltip:IsVisible() then
|
|
-- Quality color tooltip borders (currently a hidden setting).
|
|
if
|
|
self.settings.qualityColorTooltipBorders
|
|
and type(buttonInfo.qualityColor) == "table"
|
|
and item.quality > -1
|
|
and itemButton.bagshuiData.buttonComponents
|
|
and itemButton.bagshuiData.buttonComponents.border
|
|
then
|
|
_G.GameTooltip:SetBackdropBorderColor(
|
|
buttonInfo.qualityColor.r,
|
|
buttonInfo.qualityColor.g,
|
|
buttonInfo.qualityColor.b
|
|
)
|
|
end
|
|
|
|
-- Need to reapply customized tooltip position.
|
|
-- See setTooltipPositionAgain declaration for details.
|
|
if setTooltipPositionAgain then
|
|
_G.GameTooltip:ClearAllPoints()
|
|
_G.GameTooltip:SetPoint(
|
|
"BOTTOM" .. BsUtil.FlipAnchorPoint(gameTooltipAnchorPoint),
|
|
tooltipAnchorFrame,
|
|
"TOP" .. gameTooltipAnchorPoint,
|
|
-tooltipOffset,
|
|
tooltipOffset
|
|
)
|
|
end
|
|
|
|
_G.GameTooltip:Show()
|
|
else
|
|
_G.GameTooltip:Hide()
|
|
end
|
|
|
|
-- Bagshui info tooltip.
|
|
if BsInfoTooltip:NumLines() > 0 then
|
|
Bagshui:ShowInfoTooltip()
|
|
else
|
|
BsInfoTooltip:Hide()
|
|
end
|
|
|
|
-- If the mouse is obscuring the tooltip, move the tooltip to the other side of the item button.
|
|
local offset = 10
|
|
if
|
|
(
|
|
_G.MouseIsOver(_G.GameTooltip, offset, offset, offset, -offset)
|
|
or (BsInfoTooltip:IsVisible() and _G.MouseIsOver(BsInfoTooltip, offset, offset, offset, -offset))
|
|
) and (_G.GameTooltip:GetNumPoints() or 0) > 0
|
|
then
|
|
local point, anchorTo, anchorToPoint, xOffset, yOffset = _G.GameTooltip:GetPoint(1)
|
|
_G.GameTooltip:ClearAllPoints()
|
|
_G.GameTooltip:SetPoint(
|
|
BsUtil.FlipAnchorPointComponent(point, 2),
|
|
anchorTo,
|
|
BsUtil.FlipAnchorPointComponent(anchorToPoint, 2),
|
|
-xOffset,
|
|
yOffset
|
|
)
|
|
|
|
if BsInfoTooltip:IsVisible() then
|
|
point, anchorTo, anchorToPoint, xOffset, yOffset = BsInfoTooltip:GetPoint(1)
|
|
BsInfoTooltip:ClearAllPoints()
|
|
BsInfoTooltip:SetPoint(
|
|
BsUtil.FlipAnchorPointComponent(point, 2),
|
|
anchorTo,
|
|
BsUtil.FlipAnchorPointComponent(anchorToPoint, 2),
|
|
-xOffset,
|
|
yOffset
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Edit Mode highlighting.
|
|
self:EditModeWindowUpdate(nil, true)
|
|
|
|
-- Highlight bag this item is in.
|
|
self.hoveredItem = item
|
|
self:UpdateBagBar()
|
|
end
|
|
|
|
--- OnLeave: Reset everything and hide the tooltip.
|
|
function Inventory:ItemButton_OnLeave(itemButton)
|
|
itemButton = itemButton or _G.this
|
|
|
|
-- Clear Edit Mode category highlighting.
|
|
if self.editState.cursorItemType ~= BS_INVENTORY_OBJECT_TYPE.CATEGORY or self.editState.cursorItem == nil then
|
|
self.editState.highlightCategory = nil
|
|
end
|
|
if self.editState.cursorItemType ~= BS_INVENTORY_OBJECT_TYPE.ITEM or self.editState.cursorItem == nil then
|
|
self.editState.highlightItem = nil
|
|
end
|
|
|
|
-- Reset tracking properties.
|
|
itemButton.bagshuiData.tooltipCooldownUpdate = nil
|
|
itemButton.bagshuiData.altKeyDown = nil
|
|
itemButton.bagshuiData.controlKeyDown = nil
|
|
itemButton.bagshuiData.shiftKeyDown = nil
|
|
|
|
-- Record that the mouse has left this button (used by OnUpdate to determine
|
|
-- whether the tooltip should be shown when the Edit Mode cursor puts down an item).
|
|
-- This is used instead of MouseIsOver() because that function returns true
|
|
-- even when the frame is behind other frames.
|
|
itemButton.bagshuiData.mouseIsOver = false
|
|
|
|
-- Hide the main tooltip.
|
|
if _G.GameTooltip:IsOwned(itemButton) then
|
|
_G.GameTooltip:Hide()
|
|
-- Go back to the normal cursor unless the Edit Mode cursor is holding something.
|
|
if not BsCursorTooltip:IsVisible() then
|
|
_G.ResetCursor()
|
|
end
|
|
end
|
|
|
|
-- Hide our tooltip.
|
|
if BsInfoTooltip:IsOwned(itemButton) then
|
|
BsInfoTooltip:Hide()
|
|
end
|
|
|
|
-- Update Edit Mode colors.
|
|
self:EditModeWindowUpdate(nil, true)
|
|
|
|
-- Un-highlight bag this item is in.
|
|
self.hoveredItem = nil
|
|
self:UpdateBagBar()
|
|
end
|
|
|
|
-- Store reusable variable outside OnUpdate to keep the garbage collector happy.
|
|
local itemButton_OnUpdate_RefreshTooltip
|
|
|
|
-- The main goal of OnUpdate is to keep the tooltip up to date by calling OnEnter once
|
|
-- a second (if needed) so long as the mouse remains over the item slot. It also
|
|
-- does a couple of other things.
|
|
--
|
|
-- We need a fresh call to OnEnter when his itemButton owns the tooltip and one
|
|
-- of the following is true:
|
|
-- - The `bagshuiData.tooltipCooldownUpdate` property exists and it's been more than 1 second since it was set.
|
|
-- - The `bagshuiData.<modifier>KeyDown` property doesn't match `Is<Modifier>KeyDown()`.
|
|
---@param elapsed number? Time since the last OnUpdate call.
|
|
function Inventory:ItemButton_OnUpdate(elapsed)
|
|
-- Refresh stock state every 60 seconds so badges fade in almost real-time.
|
|
if
|
|
_G.this.bagshuiData
|
|
and _G.this.bagshuiData.item
|
|
and _G.this.bagshuiData.item.bagshuiStockState ~= BS_ITEM_STOCK_STATE.NO_CHANGE
|
|
and _G.GetTime() - _G.this.bagshuiData.lastStockStateRefresh > 60
|
|
and _G.this.bagshuiData.type == BS_UI_ITEM_BUTTON_TYPE.ITEM
|
|
then
|
|
self.ui:UpdateItemButtonStockState(_G.this)
|
|
self.ui:UpdateItemButtonColorsAndBadges(_G.this)
|
|
_G.this.bagshuiData.lastStockStateRefresh = _G.GetTime()
|
|
end
|
|
|
|
-- Edit Mode help - make tooltips disappear as soon as the cursor holds an item.
|
|
if
|
|
self.editMode
|
|
and _G.this.bagshuiData.mouseIsOver
|
|
and self:EditModeCursorHasItem()
|
|
and _G.GameTooltip:IsOwned(_G.this)
|
|
then
|
|
self:ItemButton_OnLeave()
|
|
return
|
|
end
|
|
|
|
-- Clear state tracking variables when the mouse isn't present, and stop running every frame
|
|
-- until the mouse enters again.
|
|
if not _G.this.bagshuiData.mouseIsOver then
|
|
_G.this.bagshuiData.tooltipCooldownUpdate = nil
|
|
_G.this.bagshuiData.altKeyDown = nil
|
|
_G.this.bagshuiData.controlKeyDown = nil
|
|
_G.this.bagshuiData.shiftKeyDown = nil
|
|
_G.this:SetScript("OnUpdate", nil)
|
|
return
|
|
end
|
|
|
|
-- Does the tooltip need to be updated?
|
|
itemButton_OnUpdate_RefreshTooltip = false
|
|
|
|
-- More Edit Mode help - display tooltips as soon as the cursor no longer holds an item.
|
|
if self.editMode and _G.this.bagshuiData.mouseIsOver and not _G.GameTooltip:IsOwned(_G.this) then
|
|
itemButton_OnUpdate_RefreshTooltip = true
|
|
end
|
|
|
|
-- Update cooldown info in tooltip.
|
|
if _G.this.bagshuiData.tooltipCooldownUpdate ~= nil then
|
|
-- tooltipCooldownUpdate is initially set to 1 by OnEnter when there's a cooldown.
|
|
-- Here we subtract the elapsed time in seconds, which will eventually go below 0
|
|
-- so long as the property isn't wiped by moving the mouse off this item.
|
|
_G.this.bagshuiData.tooltipCooldownUpdate = _G.this.bagshuiData.tooltipCooldownUpdate - elapsed
|
|
|
|
-- Don't proceed until it's been more than 1 second.
|
|
if _G.this.bagshuiData.tooltipCooldownUpdate < 0 then
|
|
itemButton_OnUpdate_RefreshTooltip = true
|
|
end
|
|
end
|
|
|
|
-- Show/hide tooltip when modifier key state changes.
|
|
-- The 1/nil to true/false translation was done in our ItemButton_OnEnter
|
|
-- for reasons explained there, so we need to mirror it here.
|
|
if
|
|
(_G.this.bagshuiData.altKeyDown ~= nil and _G.this.bagshuiData.altKeyDown ~= (_G.IsAltKeyDown() == 1))
|
|
or (_G.this.bagshuiData.controlKeyDown ~= nil and _G.this.bagshuiData.controlKeyDown ~= (_G.IsControlKeyDown() == 1))
|
|
or (_G.this.bagshuiData.shiftKeyDown ~= nil and _G.this.bagshuiData.shiftKeyDown ~= (_G.IsShiftKeyDown() == 1))
|
|
then
|
|
itemButton_OnUpdate_RefreshTooltip = true
|
|
end
|
|
|
|
-- Time to update the tooltip.
|
|
if itemButton_OnUpdate_RefreshTooltip then
|
|
self:ItemButton_OnEnter(_G.this)
|
|
end
|
|
end
|
|
|
|
--- Left and right click and all the modifier key combinations.
|
|
---@param mouseButton string
|
|
---@param isDrag number|nil|boolean
|
|
function Inventory:ItemButton_OnClick(mouseButton, isDrag)
|
|
local itemButton = _G.this
|
|
|
|
local buttonInfo = itemButton.bagshuiData
|
|
local item = self.inventory[buttonInfo.bagNum][buttonInfo.slotNum]
|
|
|
|
-- Nothing normal should happen in Edit Mode.
|
|
if self.editMode then
|
|
if mouseButton == "LeftButton" then
|
|
-- When a menu is open, just close it and don't do anything else.
|
|
-- Without this, you can accidentally pick up a group/category/item when trying to close a menu.
|
|
if self.menus:IsMenuOpen() then
|
|
Bagshui:CloseMenus()
|
|
-- Need to update state so that things know the cursor is present.
|
|
self:ItemButton_OnEnter()
|
|
return
|
|
end
|
|
|
|
if self:EditModeCursorHasItem() then
|
|
-- Something has been picked up. We can only take action here if it's a category.
|
|
if self.editState.cursorItemType == BS_INVENTORY_OBJECT_TYPE.CATEGORY then
|
|
self:AssignCategoryToGroup(self.editState.cursorItem, buttonInfo.groupId)
|
|
end
|
|
else
|
|
-- Nothing on the cursor, so pick up the Item/Category.
|
|
if _G.IsAltKeyDown() then
|
|
self:EditModePickUpItem(item)
|
|
else
|
|
self:EditModePickUpCategory(item.bagshuiCategoryId)
|
|
end
|
|
end
|
|
elseif mouseButton == "RightButton" then
|
|
-- Just open the item menu on right-click.
|
|
self:ClearEditModeCursor()
|
|
self.menus:OpenMenu("Item", item, nil, "cursor")
|
|
end
|
|
else
|
|
-- Normal processing (non-Edit Mode).
|
|
|
|
-- Make sure there's an item of some sort to work with (can be an empty slot representation).
|
|
if item then
|
|
-- Clear stock state when interacting with an item.
|
|
if self.settings.itemStockChangeClearOnInteract then
|
|
item.bagshuiDate = -1
|
|
end
|
|
|
|
-- Flip the expandEmptySlotStacks flag when clicking empty slots if stacking is enabled.
|
|
if
|
|
self.settings.stackEmptySlots
|
|
and not isDrag
|
|
and mouseButton == "LeftButton"
|
|
and not _G.CursorHasItem()
|
|
and item.emptySlot == 1
|
|
then
|
|
self.expandEmptySlotStacks = buttonInfo.isEmptySlotStack
|
|
self:ItemButton_OnLeave()
|
|
self:ForceUpdateWindow()
|
|
return
|
|
end
|
|
|
|
-- Can't do anything else offline.
|
|
if not self.online then
|
|
return
|
|
end
|
|
|
|
-- Reset pending item sale.
|
|
if self.itemPendingSale and (mouseButton == "LeftButton" or self.itemPendingSale ~= item) then
|
|
self:ClearItemPendingSale(itemButton)
|
|
return
|
|
end
|
|
|
|
-- Bag swapping.
|
|
if
|
|
Bagshui.cursorBagSlotNum
|
|
and BsItemInfo:IsContainer(item)
|
|
and self.inventoryIdsToContainerIds[Bagshui.cursorBagSlotNum]
|
|
then
|
|
self:SwapBag(item, self.inventoryIdsToContainerIds[Bagshui.cursorBagSlotNum])
|
|
return
|
|
end
|
|
|
|
-- Try to do something with the item. When that fails, go on to default actions.
|
|
local clickHandled = true
|
|
local callPickupContainerItemFromBagshuiPickupItem = false
|
|
if
|
|
-- Drag action.
|
|
-- Don't allow anything else to happen when a drag action has been initiated,
|
|
-- since this should always pick up the item, no matter what modifiers are pressed.
|
|
isDrag
|
|
then
|
|
-- Use the default left button handler but go directly to PickupContainerItem()
|
|
-- instead of proxying through ContainerFrameItemButton_OnClick() since we
|
|
-- absolutely know we want to pick up the item immediately.
|
|
mouseButton = "LeftButton"
|
|
clickHandled = false
|
|
callPickupContainerItemFromBagshuiPickupItem = true
|
|
elseif
|
|
-- Item information menu Alt+right-click blocks everything else.
|
|
mouseButton == "RightButton"
|
|
and _G.IsAltKeyDown()
|
|
and not _G.IsControlKeyDown()
|
|
and not _G.IsShiftKeyDown()
|
|
then
|
|
self.menus:OpenMenu("Item", item, nil, "cursor")
|
|
-- Makes sense to return here since there's no need for a window update.
|
|
return
|
|
elseif
|
|
-- Sell protection: Trigger confirmation prompt.
|
|
self.ui:IsFrameVisible("MerchantFrame")
|
|
and mouseButton == "RightButton"
|
|
and not _G.IsControlKeyDown()
|
|
and not (_G.IsControlKeyDown() and _G.IsAltKeyDown() and _G.IsShiftKeyDown())
|
|
and self:GetItemSellProtectionReason(item)
|
|
then
|
|
self.itemPendingSale = item
|
|
self.highlightItemsInContainerId = item.bagNum
|
|
self.highlightItemsContainerSlot = item.slotNum
|
|
self:ItemButton_OnEnter(itemButton)
|
|
self:UpdateItemSlotColors()
|
|
-- Don't let default actions happen because we don't want the tooltip hidden.
|
|
return
|
|
elseif
|
|
-- Sell protection: Sale confirmed.
|
|
self.ui:IsFrameVisible("MerchantFrame")
|
|
and (
|
|
-- Confirming the previously-selected item.
|
|
(self.itemPendingSale == item and mouseButton == "RightButton" and _G.IsControlKeyDown())
|
|
-- All modifiers down to bypass sale protection.
|
|
or (_G.IsControlKeyDown() and _G.IsAltKeyDown() and _G.IsShiftKeyDown())
|
|
)
|
|
then
|
|
self:ContainerItemAction(item, "Use", false)
|
|
elseif
|
|
-- Blizzard Mail Attachments - Right-click/Alt+click.
|
|
self:IsItemClickActionAllowed(mouseButton, "InboxFrame", "SendMailFrame")
|
|
-- Doing two checks here for "Mail" addon in case another addon ends up replicating
|
|
-- the way it handles attachments.
|
|
and not _G.IsAddOnLoaded("Mail")
|
|
and _G.SendMailPackageButton:IsEnabled() == 1
|
|
then
|
|
-- Switch to the Send Mail tab and attach the item.
|
|
self:AttachItem(item, _G.MailFrameTab_OnClick, 2, _G.ClickSendMailItemButton)
|
|
elseif
|
|
-- "Mail" addon - Alt+click (it only provides right-click).
|
|
(mouseButton == "LeftButton" and _G.IsAltKeyDown())
|
|
and self.settings.altClickAttach
|
|
and self.ui:IsFrameVisible("MailFrame")
|
|
and _G.IsAddOnLoaded("Mail")
|
|
then
|
|
-- Pretend Alt isn't down so Mail's UseContainerItem will do the work.
|
|
self:ContainerItemAction(item, "Use", false)
|
|
elseif
|
|
-- CT_MailMod, Postal, and Postal Returned [Postal is descended from CT_MailMod] -
|
|
-- Right-click (they only provide Alt+click).
|
|
mouseButton == "RightButton"
|
|
and self.settings.rightClickAttach
|
|
and (
|
|
(_G.IsAddOnLoaded("CT_MailMod") and self.ui:IsFrameVisible("CT_MailFrame"))
|
|
or (
|
|
(
|
|
-- The Postal family.
|
|
_G.IsAddOnLoaded("Postal")
|
|
or _G.IsAddOnLoaded("Postal Returned")
|
|
-- IsAddOnLoaded goes by TOC file name, and Postal Returned
|
|
-- can be loaded from Postal-Returned.toc as well
|
|
-- due to <https://github.com/veechs/Postal-Returned/issues/2>.
|
|
or _G.IsAddOnLoaded("Postal-Returned")
|
|
) and self.ui:IsFrameVisible("PostalFrame")
|
|
)
|
|
)
|
|
then
|
|
-- Pretend Alt is down so their code will do the item attachment work.
|
|
self:ContainerItemAction(item, "Pickup", true)
|
|
elseif
|
|
-- "Old" Aux <https://github.com/mrrosh/aux-addon_old-interface/>.
|
|
-- Unlike the newer aux, old Aux has Alt/Control/Shift+click handling
|
|
-- but does *not* do right-click. It also hooks ContainerFrameItemButton_OnClick()
|
|
-- instead of UseContainerItem() so it really is totally different.
|
|
-- Must come before Blizzard Auction House because the old
|
|
-- version doesn't have its own frame and uses the Blizz one.
|
|
_G.IsAddOnLoaded("aux-addon")
|
|
and _G.AuxVersion
|
|
and _G.Aux
|
|
and _G.Aux_ContainerFrameItemButton_OnClick
|
|
and self.ui:IsFrameVisible("AuctionFrame")
|
|
then
|
|
if mouseButton == "RightButton" and self.settings.rightClickAttach then
|
|
-- Make right-clicking behave like all other auction house right-clicks (send to Sell tab).
|
|
local oldGlobalThis = _G.this
|
|
local oldIsAltKeyDown = _G.IsAltKeyDown
|
|
_G.this = itemButton.bagshuiData.getIdProxy or _G.this -- Have to force the use of the item button proxy for GetID()/GetParent():GetID().
|
|
_G.IsAltKeyDown = BsUtil.ReturnTrue
|
|
_G.Aux_ContainerFrameItemButton_OnClick("LeftButton")
|
|
_G.IsAltKeyDown = oldIsAltKeyDown
|
|
_G.this = oldGlobalThis
|
|
else
|
|
-- Let things fall through the normal path so `Aux_ContainerFrameItemButton_OnClick()`
|
|
-- will handle Alt/Control/Shift+click.
|
|
clickHandled = false
|
|
end
|
|
elseif
|
|
-- Blizzard Auction House - Right-click/Alt+click.
|
|
self:IsItemClickActionAllowed(mouseButton, "AuctionFrame")
|
|
then
|
|
-- Switch to the Auctions tab and attach the item.
|
|
self:AttachItem(item, _G.AuctionFrameTab_OnClick, 3, _G.ClickAuctionSellItemButton)
|
|
elseif
|
|
-- aux - Alt+click (it only provides right-click).
|
|
(mouseButton == "LeftButton" and _G.IsAltKeyDown())
|
|
and self.settings.altClickAttach
|
|
and _G.IsAddOnLoaded("aux-addon")
|
|
and self.ui:IsFrameVisible("aux_frame")
|
|
then
|
|
-- Pretend Alt isn't down so aux's UseContainerItem will do the work.
|
|
self:ContainerItemAction(item, "Use", false)
|
|
elseif
|
|
-- Trade - Right-click/Alt+click.
|
|
self:IsItemClickActionAllowed(mouseButton, "TradeFrame")
|
|
then
|
|
local tradeSlot = _G.TradeFrame_GetAvailableSlot()
|
|
if tradeSlot then
|
|
self:AttachItem(item, nil, nil, _G.ClickTradeButton, tradeSlot)
|
|
end
|
|
elseif
|
|
-- Initiate Trade on Al+click when friendly unit is targeted.
|
|
(self.settings.rightClickAttach or self.settings.altClickAttach)
|
|
and mouseButton == "LeftButton"
|
|
and _G.IsAltKeyDown()
|
|
and self:IsInitiateTradeAllowed(item)
|
|
then
|
|
-- We've already hooked TradeFrame_OnShow so that once the Trade window opens,
|
|
-- Inventory:TradeQueuedItem() can add self.queuedTradeItem.
|
|
self.queuedTradeItem = item
|
|
_G.InitiateTrade("target")
|
|
else
|
|
-- No conditions were met, so go on to default actions.
|
|
clickHandled = false
|
|
end
|
|
|
|
-- Keeping this as a separate statement instead of putting it in the else
|
|
-- clause for the other actions above in case there's a situation where an
|
|
-- action might want to fall through to default behavior when it fails.
|
|
if not clickHandled then
|
|
-- We need to ensure use our metatable'd proxy frame (set up in
|
|
-- InventoryUi:CreateInventoryItemSlotButton()) is used so the
|
|
-- GetID() and GetParent():GetID() functions will be overridden.
|
|
-- The best way to guarantee this is to temporarily change the
|
|
-- meaning of global this while any code outside our control
|
|
-- is executed.
|
|
local oldGlobalThis = _G.this
|
|
_G.this = itemButton.bagshuiData.getIdProxy or _G.this
|
|
|
|
if mouseButton == "LeftButton" then
|
|
-- Normal left-click.
|
|
-- This will eventually become a call to ContainerFrameItemButton_OnClick(), which can handle:
|
|
-- - Control+click dress-up.
|
|
-- - Shift+click chat links.
|
|
-- - Shift+click stack splitting.
|
|
-- - Calling PickupContainerItem() if none of the above are met.
|
|
-- It also allows hooks to both ContainerFrameItemButton_OnClick() and PickupContainerItem() to work.
|
|
-- (See the declaration of Bagshui:PickupItem() for details about why it exists.)
|
|
Bagshui:PickupItem(item, self, itemButton, callPickupContainerItemFromBagshuiPickupItem)
|
|
else
|
|
-- Normal right-click.
|
|
-- ContainerFrameItemButton_OnClick() for right button will handle:
|
|
-- - Shift+right-click stack splitting at merchant.
|
|
-- - Otherwise calling UseContainerItem().
|
|
-- It also allows hooks to both ContainerFrameItemButton_OnClick() and UseContainerItem() to work.
|
|
_G.ContainerFrameItemButton_OnClick(mouseButton)
|
|
end
|
|
-- Restore global this.
|
|
_G.this = oldGlobalThis
|
|
end
|
|
|
|
-- Clear pending sale item (must happen before window update).
|
|
self:ClearItemPendingSale(nil, true)
|
|
-- Hide tooltip on click -- UpdateWindow() will call ItemSlotAndGroupMouseOverCheck(), which will show it again if needed.
|
|
self:ItemButton_OnLeave()
|
|
-- Something was clicked, so make sure the window is up to date.
|
|
-- Queued instead of immediate: the item usually hasn't moved yet (a bank/bag move needs a
|
|
-- server round-trip), and the bag/lock events that follow queue an update too. Queuing lets
|
|
-- them collapse into one full update instead of two.
|
|
self.windowUpdateNeeded = true
|
|
self:QueueUpdate()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Refresh the stock badges ("new"/"up"/"down") of visible item buttons so they fade over time.
|
|
--- Called once a minute from the window's OnUpdate.
|
|
function Inventory:RefreshItemStockBadges()
|
|
if not self.ui or not self.ui.buttons or not self.ui.buttons.itemSlots then
|
|
return
|
|
end
|
|
local now = _G.GetTime()
|
|
for _, button in ipairs(self.ui.buttons.itemSlots) do
|
|
local buttonInfo = button.bagshuiData
|
|
if
|
|
buttonInfo
|
|
and buttonInfo.item
|
|
and buttonInfo.type == BS_UI_ITEM_BUTTON_TYPE.ITEM
|
|
and buttonInfo.item.bagshuiStockState ~= BS_ITEM_STOCK_STATE.NO_CHANGE
|
|
and button:IsVisible()
|
|
then
|
|
self.ui:UpdateItemButtonStockState(button)
|
|
self.ui:UpdateItemButtonColorsAndBadges(button)
|
|
buttonInfo.lastStockStateRefresh = now
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Return the appropriate empty slot texture (generic or profession-specific).
|
|
---@param emptySlot table Bagshui item table for the empty slot.
|
|
---@return string texturePath
|
|
function Inventory:GetEmptySlotTexture(emptySlot)
|
|
local texture = BS_INVENTORY_EMPTY_SLOT_TEXTURE[L.Bag]
|
|
if self.containers[emptySlot.bagNum] then
|
|
texture = BS_INVENTORY_EMPTY_SLOT_TEXTURE[self.containers[emptySlot.bagNum].genericType]
|
|
end
|
|
return texture
|
|
end
|
|
|
|
--- Call `PickupContainerItem()` for the given item, forcing Alt to be up, and
|
|
--- optionally calling the specified function(s) before/after.
|
|
--- The cursor will be cleared afterwards if an item is present, since the whole
|
|
--- point of this function is to get an item attached. To pick up an item, use
|
|
--- Bagshui:PickupItem() instead.
|
|
---@param item table Bagshui item.
|
|
---@param beforePickupFunction function? Function to call before `PickupContainerItem()`.
|
|
---@param beforePickupParam any? Parameter to pass to `beforePickupFunction()`.
|
|
---@param afterPickupFunction function? Function to call after `PickupContainerItem()`.
|
|
---@param afterPickupParam any? Parameter to pass to `afterPickupFunction()`.
|
|
function Inventory:AttachItem(item, beforePickupFunction, beforePickupParam, afterPickupFunction, afterPickupParam)
|
|
if type(beforePickupFunction) == "function" then
|
|
beforePickupFunction(beforePickupParam)
|
|
end
|
|
|
|
-- Ensure Alt is NOT down so addons like CT_MailMod, Postal, and Postal Returned that hook
|
|
-- PickupContainerItem and alter its behavior when Alt is down won't mess things up.
|
|
self:ContainerItemAction(item, "Pickup", false)
|
|
|
|
if type(afterPickupFunction) == "function" then
|
|
afterPickupFunction(afterPickupParam)
|
|
end
|
|
|
|
-- Put item back if our action wasn't successful or the item was swapped with another.
|
|
if _G.CursorHasItem() then
|
|
_G.ClearCursor()
|
|
end
|
|
end
|
|
|
|
--- Call `PickupContainerItem()` or `UseContainerItem()` for the given item while forcing
|
|
--- the Alt key to be in a specific state by temporarily overriding `IsAltKeyDown()`.
|
|
---@param item table Bagshui item.
|
|
---@param action string `"Pickup"` or `"Use"` as in `PickupContainerItem` or `UseContainerItem`.
|
|
---@param altDown boolean? `true` for Alt down, `false` for Alt up.
|
|
function Inventory:ContainerItemAction(item, action, altDown)
|
|
local oldIsAltKeyDown = _G.IsAltKeyDown
|
|
_G.IsAltKeyDown = altDown and BsUtil.ReturnTrue or BsUtil.ReturnFalse
|
|
_G[(string.lower(action or "") == "use" and "UseContainerItem" or "PickupContainerItem")](item.bagNum, item.slotNum)
|
|
_G.IsAltKeyDown = oldIsAltKeyDown
|
|
end
|
|
|
|
--- Determine whether an Alt+click or right-click action is allowed.
|
|
--- If there's currently an item on the cursor, the answer will be no, regardless of anything else.
|
|
---@param mouseButton string OnClick arg1.
|
|
---@param frame1 string? Any of the frameN parameters must be visible for this to return `true`.
|
|
---@param frame2 string?
|
|
---@param frame3 string?
|
|
---@param frame4 string?
|
|
---@return boolean
|
|
function Inventory:IsItemClickActionAllowed(mouseButton, frame1, frame2, frame3, frame4)
|
|
return not _G.CursorHasItem()
|
|
and ((self.settings.altClickAttach and mouseButton == "LeftButton" and _G.IsAltKeyDown()) or (self.settings.rightClickAttach and mouseButton == "RightButton"))
|
|
and (
|
|
self.ui:IsFrameVisible(frame1)
|
|
or self.ui:IsFrameVisible(frame2)
|
|
or self.ui:IsFrameVisible(frame3)
|
|
or self.ui:IsFrameVisible(frame4)
|
|
)
|
|
end
|
|
|
|
--- Determine whether trade can be initiated with the targeted unit.
|
|
---@param item table Bagshui item.
|
|
---@return boolean
|
|
function Inventory:IsInitiateTradeAllowed(item)
|
|
return not _G.CursorHasItem()
|
|
-- Must be a valid item, not an empty slot.
|
|
and item
|
|
and item.emptySlot ~= 1
|
|
and item.itemString
|
|
and item.itemString ~= ""
|
|
-- Can't trade soulbound items.
|
|
and not string.find(item.tooltip, _G.ITEM_SOULBOUND)
|
|
-- No "action" frames visible.
|
|
and not self.ui:IsFrameVisible("TradeFrame")
|
|
and not self.ui:IsFrameVisible("AuctionFrame")
|
|
and not self.ui:IsFrameVisible("MailFrame")
|
|
-- Can't trade with yourself.
|
|
and not _G.UnitIsUnit("target", "player")
|
|
-- Friendly units only.
|
|
and _G.UnitExists("target")
|
|
and _G.UnitIsFriend("player", "target")
|
|
and _G.UnitIsPlayer("target")
|
|
-- Must be close enough.
|
|
and _G.CheckInteractDistance("target", 2)
|
|
end
|
|
|
|
--- Hook for trade frame so we can add the Alt+clicked item after it opens.
|
|
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
|
|
function Inventory:TradeFrame_OnShow(wowApiFunctionName)
|
|
self.hooks:OriginalHook(wowApiFunctionName)
|
|
Bagshui:QueueClassCallback(self, self.TradeQueuedItem, 0.05)
|
|
end
|
|
|
|
--- Helper for TradeFrame_OnShow to try and ensure the item we want to Alt+click trade
|
|
--- actually gets traded, even if things are running slow.
|
|
---@param attemptCount number How many passes we've made while trying to trade the item.
|
|
function Inventory:TradeQueuedItem(attemptCount)
|
|
attemptCount = attemptCount or 1
|
|
|
|
-- Reasons to stop:
|
|
if
|
|
-- Too many tries.
|
|
attemptCount > 30
|
|
-- Nothing has been queued to trade.
|
|
or not self.queuedTradeItem
|
|
-- There's an item on the cursor that ISN'T the one we're trying to put in the trade window.
|
|
or (_G.CursorHasItem() and (Bagshui.cursorItem ~= self.queuedTradeItem or Bagshui.cursorItem == nil))
|
|
-- First slot in the trade window has been filled.
|
|
or (_G.TradePlayerItem1ItemButton and _G.TradePlayerItem1ItemButton.hasItem)
|
|
-- Trade frame was closed.
|
|
or not self.ui:IsFrameVisible("TradeFrame")
|
|
then
|
|
self.queuedTradeItem = nil
|
|
return
|
|
end
|
|
|
|
-- We might have the item we want already on the cursor based on the checks above.
|
|
if not _G.CursorHasItem() then
|
|
-- Calling PickupContainerItem() directly since we don't need any of the things
|
|
-- Bagshui:PickupItem() provides (and calling ContainerFrameItemButton_OnClick()
|
|
-- will generate an error now since _G.this isn't the item button.)
|
|
self:ContainerItemAction(self.queuedTradeItem, "Pickup", false)
|
|
end
|
|
_G.ClickTradeButton(1)
|
|
|
|
-- Do another pass to make sure it worked.
|
|
Bagshui:QueueClassCallback(self, self.TradeQueuedItem, 0.05, nil, attemptCount + 1)
|
|
end
|
|
end)
|