Skip to content

Commit

Permalink
Merge pull request #95 from veechs/containerframe-getid-compat
Browse files Browse the repository at this point in the history
Rework how item buttons provide bag and slot number to Blizzard code
  • Loading branch information
veechs authored Feb 22, 2025
2 parents 55015a6 + 15d2153 commit f248770
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 107 deletions.
24 changes: 24 additions & 0 deletions Components/Bagshui.BlizzFixes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,28 @@ function Bagshui:MoneyFrame_UpdateMoney(wowApiFunctionName)
end



--- Ensure the stack split frame stays onscreen.
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
---@param maxStack any `OpenStackSplitFrame()` parameter.
---@param parent any `OpenStackSplitFrame()` parameter.
---@param anchor any `OpenStackSplitFrame()` parameter.
---@param anchorTo any `OpenStackSplitFrame()` parameter.
function Bagshui:OpenStackSplitFrame(wowApiFunctionName, maxStack, parent, anchor, anchorTo)
-- Pass along to the normal `OpenStackSplitFrame()` to handle everything.
self.hooks:OriginalHook(wowApiFunctionName, maxStack, parent, anchor, anchorTo)
-- Reposition if needed.
if BsUtil.GetFrameOffscreenAmount(_G.StackSplitFrame, "y") < 0 then
self:PrintDebug(anchor)
self:PrintDebug(BsUtil.FlipAnchorPointComponent(anchor, 1))
_G.StackSplitFrame:ClearAllPoints()
_G.StackSplitFrame:SetPoint(
BsUtil.FlipAnchorPointComponent(anchor, 1),
parent,
BsUtil.FlipAnchorPointComponent(anchorTo, 1),
0, 0
)
end
end

end)
66 changes: 0 additions & 66 deletions Components/Bagshui.Cursor.lua
Original file line number Diff line number Diff line change
Expand Up @@ -253,70 +253,4 @@ function Bagshui:PickupInventoryItem(wowApiFunctionName, invSlotId)
end



--- Does the given object have the necessary properties to be used to derive
--- bag/slot numbers for stack splitting?
--- This logic needs to be used twice - once in `Bagshui:OpenStackSplitFrame()`
--- and again in `SplitItemButtonStack()`, so it's centralized here.
---@param frame any
---@return false
local function FrameIsBagshuiItemButton(frame)
return (
type(frame) == "table"
and frame.bagshuiData
and frame.bagshuiData.item
and frame.bagshuiData.bagNum ~= BS_ITEM_SKELETON.bagNum
and frame.bagshuiData.slotNum ~= BS_ITEM_SKELETON.slotNum
)
end



--- Callback used for stack splitting. Assigned to an item button's `SplitStack`
--- property by `Bagshui:OpenStackSplitFrame()`.
---@param button any
---@param amount any
local function SplitItemButtonStack(button, amount)
if FrameIsBagshuiItemButton(button) then
_G.SplitContainerItem(
button.bagshuiData.bagNum,
button.bagshuiData.slotNum,
amount
)
elseif button._bagshuiOldSplitStack then
button._bagshuiOldSplitStack(button, amount)
end
end


--- Hack to make stack splitting work consistently.
---
--- Without this, mouseover events can reset the ID of a group's parent frame,
--- changing the bag number of the pending split. As a result, the split ends
--- up targeting the wrong item.
---
--- The workaround is pretty simple - just reference the cache item assigned
--- to the button to figure out the correct bag and slot number.
---
--- We trigger this by intercepting the `OpenStackSplitFrame()` call from
--- `ContainerFrameItemButton_OnClick()` and reassign the frame's `SplitStack`
--- property to our `SplitItemButtonStack()` function, which becomes the callback
--- used when the split is invoked.
---@param wowApiFunctionName string Hooked WoW API function that triggered this call.
---@param maxStack any `OpenStackSplitFrame()` parameter.
---@param parent any `OpenStackSplitFrame()` parameter.
---@param anchor any `OpenStackSplitFrame()` parameter.
---@param anchorTo any `OpenStackSplitFrame()` parameter.
function Bagshui:OpenStackSplitFrame(wowApiFunctionName, maxStack, parent, anchor, anchorTo)
-- When an item button owns the stack split frame, override the SplitStack
-- property (which is an ad-hoc function) created by ContainerFrameItemButton_OnClick().
if FrameIsBagshuiItemButton(parent) then
self:PrintDebug(" !!!!!!!!!!!!!! OVERRIDE")
parent.SplitStack = SplitItemButtonStack
end
-- Pass along to the normal `OpenStackSplitFrame()` to handle everything.
self.hooks:OriginalHook(wowApiFunctionName, maxStack, parent, anchor, anchorTo)
end


end)
167 changes: 126 additions & 41 deletions Components/Inventory.Ui.ItemButton.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ local Inventory = Bagshui.prototypes.Inventory
local InventoryUi = Bagshui.prototypes.InventoryUi


-- Disabled in favor of ContainerFrameItemButton_OnClick().
-- local function ItemButton_SplitStack(button, split)
-- -- These functions are NOT captured into the Bagshui environment to ensure that if they're
-- -- hooked by another addon, we call the hooked version instead of the original.
-- _G.SplitContainerItem(button.bagshuiData.bagNum, button.bagshuiData.slotNum, split)
-- -- Instead of redefining the button.SplitStack function every time,
-- -- we're using an additional property to track whether this is a
-- -- right-click sell split at the merchant or a normal one.
-- if button.bagshuiData.splitStackAtMerchant then
-- _G.MerchantItemButton_OnClick("LeftButton")
-- end
-- end


--- Special OnHide for Inventory item slot buttons to also hide tooltips and
--- the stack split frame.
local function InventoryItemButton_OnHide()
Expand Down Expand Up @@ -103,9 +89,114 @@ function InventoryUi:CreateInventoryItemSlotButton(buttonNum)
slotButton:SetScript("OnLeave", inventory._itemSlotButton_ScriptWrapper_OnLeave)
slotButton:SetScript("OnHide", InventoryItemButton_OnHide)

-- SplitStack() as a button property is required to reuse Blizzard's OpenStackSplitFrame().
-- Disabled in favor of ContainerFrameItemButton_OnClick().
-- slotButton.SplitStack = ItemButton_SplitStack

-- 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.)

-- Proxy for `slotButton` that was created above.
-- Will override `GetID()` and `GetParent()`.
local slotButtonProxy = _G.CreateFrame("Frame")
-- 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.
local parentProxy = _G.CreateFrame("Frame")
-- 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.item.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.
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.item.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
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
)
end
Expand All @@ -130,12 +221,6 @@ function Inventory:ItemButton_OnEnter(itemButton)
local buttonInfo = itemButton.bagshuiData
local item = itemButton.bagshuiData.item or self.inventory[buttonInfo.bagNum][buttonInfo.slotNum]

-- Update IDs so ContainerFrameItemButton_OnEnter() will work.
-- Disabled because we're currently not actually calling ContainerFrameItemButton_OnEnter,
-- but this could probably be added in the future if it's found to be necessary for compatibility
-- with other addons.
self:UpdateItemButtonIDs(itemButton, item)

-- 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
Expand Down Expand Up @@ -288,8 +373,11 @@ function Inventory:ItemButton_OnEnter(itemButton)
-- 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
_G.this = itemButton
_G[self.itemSlotTooltipFunction](itemButton)
-- 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
_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.
Expand Down Expand Up @@ -362,8 +450,8 @@ function Inventory:ItemButton_OnEnter(itemButton)
_G.GameTooltip:AddLine(_G.TEXT(_G.REPAIR_COST), 1, 1, 1)
_G.SetTooltipMoney(_G.GameTooltip, repairCost)

elseif self.ui:IsFrameVisible(_G.MerchantFrame) then
-- At merchant.
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 or (_G.IsControlKeyDown() and not _G.IsAltKeyDown())) and item.emptySlot ~= 1 then
Expand Down Expand Up @@ -759,9 +847,6 @@ function Inventory:ItemButton_OnClick(mouseButton, isDrag)
local buttonInfo = itemButton.bagshuiData
local item = self.inventory[buttonInfo.bagNum][buttonInfo.slotNum]

-- Update IDs so ContainerFrameItemButton_OnClick() will work.
self:UpdateItemButtonIDs(itemButton, item)

-- Nothing normal should happen in Edit Mode.
if self.editMode then

Expand Down Expand Up @@ -962,6 +1047,15 @@ function Inventory:ItemButton_OnClick(mouseButton, isDrag)
-- 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

if mouseButton == "LeftButton" then
-- Normal left-click.
-- This will eventually become a call to ContainerFrameItemButton_OnClick(), which can handle:
Expand All @@ -981,6 +1075,8 @@ function Inventory:ItemButton_OnClick(mouseButton, isDrag)
-- It also allows hooks to both ContainerFrameItemButton_OnClick() and UseContainerItem() to work.
_G.ContainerFrameItemButton_OnClick(mouseButton)
end
-- Restore global this.
_G.this = oldGlobalThis
end

-- Hide tooltip on click -- UpdateWindow() will call ItemSlotAndGroupMouseOverCheck(), which will show it again if needed.
Expand All @@ -993,17 +1089,6 @@ end



--- Set item slot button and parent frame ID to the given item's bagNum and slotNum, respectively.
--- This provides compatibility with Blizzard's ContainerFrameItemButton_OnClick(), etc.
---@param button table Item slot button.
---@param item table Bagshui item.
function Inventory:UpdateItemButtonIDs(button, item)
button:SetID(item and item.slotNum or -99)
button:GetParent():SetID(item and item.bagNum or -99)
end



--- Return the appropriate empty slot texture (generic or profession-specific).
---@param emptySlot table Bagshui item table for the empty slot.
---@return string texturePath
Expand Down

0 comments on commit f248770

Please sign in to comment.