Files
Bagshui/Components/Inventory.EditMode.lua
T
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

669 lines
25 KiB
Lua

-- Bagshui Inventory Prototype: Edit Mode
-- Most Edit Mode related things live here, although there are a few bits in
-- OnClick/OnEnter/OnLeave events for item buttons and groups.
Bagshui:AddComponent(function()
local Inventory = Bagshui.prototypes.Inventory
--- Enter or exit Edit Mode.
---@param enable boolean
function Inventory:SetEditMode(enable)
self:ClearEditModeCursor()
self:ManagePendingGroupAssignments(nil, false) -- Discard any pending group assignments.
self.highlightItemsInContainerId = nil
self.highlightItemsInContainerLocked = nil
self.editMode = (enable == true)
self:ClearSearch()
self:UpdateEditModeUiStateBasedOnCursor()
self.windowUpdateNeeded = true
self.lookupTablesStale = true
self.forceResort = true
self:Update()
end
--- Turn Edit Mode on if it's off and vice versa.
function Inventory:ToggleEditMode()
self:SetEditMode(not self.editMode)
end
--- Trigger window update only if Edit Mode is enabled.
---@param forceResort boolean? Trigger a full re-sort.
---@param itemSlotColorsOnly boolean? High performance update mode.
---@param calledFromEvent boolean? This is coming from a BAGSHUI_PROFILE_UPDATE event raised by another inventory class.
function Inventory:EditModeWindowUpdate(forceResort, itemSlotColorsOnly, calledFromEvent)
if not (self.editMode or calledFromEvent) then
return
end
if itemSlotColorsOnly and forceResort ~= true then
-- High-performance window redraw to update category highlighting.
-- Using normal Update() is way too slow to keep pace with mouse movements.
self:UpdateItemSlotColors()
else
-- Normal Edit Mode update.
self.cacheUpdateNeeded = false
self.windowUpdateNeeded = true
if forceResort then
self.lookupTablesStale = true
end
self.forceResort = (forceResort == true)
self:Update()
-- Don't cause a stack overflow with recursive events.
if not calledFromEvent then
Bagshui:RaiseEvent("BAGSHUI_INVENTORY_EDIT_MODE_UPDATE", nil, self, forceResort)
end
end
end
--- Since we're faking cursor attachment via a tooltip, we need a way to determine
--- whether something has been picked up.
---@return boolean # true if there's an item on the cursor in Edit Mode.
function Inventory:EditModeCursorHasItem()
return (self.editMode and self.editState.cursorItem ~= nil)
end
-- Intelligently display the Edit Mode cursor if there's an item to be held.
function Inventory:ShowEditModeCursor()
if self.editMode and self.editState.cursorItem ~= nil and self.editState.cursorTooltipTitle ~= nil then
-- This is a quick hack to enable Edit Mode cursor clearing on Escape press.
-- See Bagshui:CloseAllWindows() for details.
self.uiFrame.bagshuiData.hasCursorItem = self
local cursorTooltip = BsCursorTooltip
cursorTooltip:SetOwner(self.uiFrame, "ANCHOR_CURSOR")
cursorTooltip:ClearLines()
-- Add text.
cursorTooltip:AddLine(self.editState.cursorTooltipTitle)
if self.editState.cursorTooltipText then
cursorTooltip:AddLine(self.editState.cursorTooltipText)
end
-- Display correct icon.
local cursorTooltipIcon = "Icons\\Question"
if self.editState.cursorItemType == BS_INVENTORY_OBJECT_TYPE.GROUP then
cursorTooltipIcon = "Icons\\Group"
elseif self.editState.cursorItemType == BS_INVENTORY_OBJECT_TYPE.CATEGORY then
cursorTooltipIcon = "Icons\\Category"
elseif self.editState.cursorItemType == BS_INVENTORY_OBJECT_TYPE.ITEM then
if self.editState.cursorItem.emptySlot == 1 then
cursorTooltipIcon = self:GetEmptySlotTexture(self.editState.cursorItem)
else
cursorTooltipIcon = self.editState.cursorItem.texture
end
end
cursorTooltip.bagshuiData.texture1:SetTexture(BsUtil.GetFullTexturePath(cursorTooltipIcon))
-- Display the tooltip so it will calculate its initial width based on the text.
cursorTooltip:Show()
-- Adjust width to account for icon.
local currentWidth = cursorTooltip:GetWidth()
local firstLineWidthWithIcon = cursorTooltip.bagshuiData.textLeft1:GetStringWidth() + 36
if firstLineWidthWithIcon > currentWidth then
cursorTooltip:SetWidth(firstLineWidthWithIcon)
end
-- Make the cursor glow.
_G.SetCursor("CAST_CURSOR")
-- Make sure tooltips aren't fighting.
BsInfoTooltip:Hide()
self:UpdateEditModeUiStateBasedOnCursor()
end
end
--- Hide the Edit Mode cursor tooltip without clearing it (used when the cursor
--- leaves the inventory window).
function Inventory:HideEditModeCursor()
BsCursorTooltip:Hide()
_G.SetCursor(nil)
end
--- Drop whatever the Edit Mode cursor is carrying without doing anything with it.
function Inventory:ClearEditModeCursor()
if self.editState.cursorItem ~= nil then
-- This is part of a quick hack to enable Edit Mode cursor clearing on Escape press.
-- See Bagshui:CloseAllWindows() for details.
self.uiFrame.bagshuiData.hasCursorItem = nil
-- Reset all editState variables.
self.editState.cursorItem = nil
self.editState.cursorItemType = nil
self.editState.cursorTooltipTitle = nil
self.editState.cursorTooltipText = nil
self.editState.highlightCategory = nil
self.editState.highlightItem = nil
self:HideEditModeCursor()
self:UpdateEditModeUiStateBasedOnCursor()
self:EditModeWindowUpdate()
end
end
--- Make changes to UI based on picking up/putting down elements.
--- - Set the correct text on group move targets based on what's on the cursor.
--- - Enable/disable group/item slot interactivity.
function Inventory:UpdateEditModeUiStateBasedOnCursor()
local cursorHasItem = self:EditModeCursorHasItem()
local cursorItemIsGroup = (cursorHasItem and self.editState.cursorItemType == BS_INVENTORY_OBJECT_TYPE.GROUP)
-- Update group move target text and colors.
local groupMoveTargetText = cursorItemIsGroup and "•" or "+"
for _, groupMoveTarget in ipairs(self.ui.frames.groupMoveTargets) do
groupMoveTarget.bagshuiData.text:SetText(groupMoveTargetText)
self.ui:SetGroupColors(groupMoveTarget.bagshuiData.button, false)
end
-- Enable/disable group interactivity.
for _, group in ipairs(self.ui.frames.groups) do
group:Enable()
end
-- Item slots should NOT get mouse input when there's something on the cursor,
-- since we always want it directed to the group.
for _, itemSlotButton in pairs(self.ui.buttons.itemSlots) do
itemSlotButton:EnableMouse(not cursorHasItem)
end
end
--- Build the correct text for the Edit Mode cursor tooltip.
---@param objectName string
---@param objectType string
---@return string tooltipTitle
function Inventory:FormatEditModeCursorTooltipTitle(objectName, objectType)
return BS_FONT_COLOR.BAGSHUI
.. string.format(L.Symbol_Colon, objectType)
.. FONT_COLOR_CODE_CLOSE
.. " "
.. HIGHLIGHT_FONT_COLOR_CODE
.. objectName
.. FONT_COLOR_CODE_CLOSE
end
--- "Pick up" an item and place it on the cursor.
---@param item table Inventory item cache entry.
function Inventory:EditModePickUpItem(item)
assert(item, "Inventory<" .. tostring(self.inventoryType) .. ">:EditModePickUpItem() - item is required")
if item.id == 0 then
return
end
self.editState.cursorItem = item
self.editState.cursorItemType = BS_INVENTORY_OBJECT_TYPE.ITEM
self.editState.cursorTooltipTitle =
self:FormatEditModeCursorTooltipTitle(BsItemInfo:GetQualityColoredName(item), L.Item)
self.editState.cursorTooltipText = string.format(L.EditMode_Tooltip_SelectNew, string.lower(L.Location))
self.editState.highlightItem = item.id
_G.PlaySound("INTERFACESOUND_CURSORGRABOBJECT")
self:ShowEditModeCursor()
end
--- When a group is clicked with an item on the cursor in Edit Mode, display the
--- menu so the item can be assigned to a category within the group.
---@param item table Inventory item cache entry.
---@param groupWidget table Group UI frame.
function Inventory:ShowItemCategoryAssignmentMenuForGroup(item, groupWidget)
self:ClearEditModeCursor()
local groupId = groupWidget.bagshuiData.groupId
-- Already in the correct place; nothing to do.
if groupId == item.bagshuiGroupId then
return
end
if not self:GroupExists(groupId) then
return
end
-- Show menu at cursor if mouse is over the group widget,
-- or force it to be at the widget otherwise.
self.menus:OpenMenu("Item", item, groupId, groupWidget.mouseIsOver and "cursor" or groupWidget)
end
--- "Pick up" a category and place it on the cursor.
--- Named EditModePickUpCategory to fit with EditModePickUpItem.
---@param categoryId string|number
function Inventory:EditModePickUpCategory(categoryId)
if not categoryId then
return
end
local categoryName = BsCategories:GetName(categoryId)
if categoryName then
Bagshui:CloseMenus()
-- Grab category info.
self.editState.cursorItem = categoryId
self.editState.cursorItemType = BS_INVENTORY_OBJECT_TYPE.CATEGORY
self.editState.cursorTooltipTitle = self:FormatEditModeCursorTooltipTitle(categoryName, L.Category)
self.editState.cursorTooltipText = string.format(L.EditMode_Tooltip_SelectNew, string.lower(L.Group))
self.editState.highlightCategory = categoryId
-- Attach the category to the cursor.
self:ShowEditModeCursor()
_G.PlaySound("INTERFACESOUND_CURSORGRABOBJECT")
end
end
-- Assign a category to a group (move from previous group or assign for the first time).
---@param categoryId string|number
---@param groupId string Group identifier in `"row:column"` format.
---@param noHighlight boolean? When true, do not highlight `categoryId` when the window update occurs.
function Inventory:AssignCategoryToGroup(categoryId, groupId, noHighlight)
if not self:GroupExists(groupId) then
return
end
-- Parse the destination location.
local destRow, destColumn = unpack(BsUtil.Split(groupId, ":"))
destRow = tonumber(destRow)
destColumn = tonumber(destColumn)
-- Remove from previous location, if present.
self:RemoveCategoryFromGroup(categoryId, self.categoriesToGroups[categoryId], true)
-- Assign to the destination location.
table.insert(self.layout[destRow][destColumn].categories, categoryId)
self:ClearEditModeCursor()
if not noHighlight then
self.editState.highlightCategory = categoryId
end
-- Update window to show new organization
self:EditModeWindowUpdate(true)
end
--- Unassign a category from a group.
---@param categoryId string|number
---@param groupId string Group identifier in `"row:column"` format.
---@param noWindowUpdate boolean? When true, don't call `EditModeWindowUpdate()` after making changes.
function Inventory:RemoveCategoryFromGroup(categoryId, groupId, noWindowUpdate)
if not self:GroupExists(groupId) then
return
end
-- Parse the group location.
local sourceRow, sourceColumn = unpack(BsUtil.Split(groupId, ":"))
sourceRow = tonumber(sourceRow)
sourceColumn = tonumber(sourceColumn)
-- Find the category in the categories array and remove it.
for index, groupCategoryId in ipairs(self.layout[sourceRow][sourceColumn].categories) do
if groupCategoryId == categoryId then
table.remove(self.layout[sourceRow][sourceColumn].categories, index)
break
end
end
-- Update window to show new organization.
if not noWindowUpdate then
self:EditModeWindowUpdate(true)
end
end
--- Assign/unassign a group to/from a category.
---@param categoryId any
---@param groupId string Group identifier in `"row:column"` format.
---@param noHighlight boolean? When true, do not highlight `categoryId` when the window update occurs.
function Inventory:ToggleCategoryGroupAssignment(categoryId, groupId, noHighlight)
if not self:GroupExists(groupId) then
return
end
if self.categoriesToGroups[categoryId] == groupId then
self:RemoveCategoryFromGroup(categoryId, groupId)
else
self:AssignCategoryToGroup(categoryId, groupId, noHighlight)
end
end
--- "Pick up" a group so it can be moved to a new location.
--- Named EditModePickUpGroup to fit with EditModePickUpItem.
---@param groupId string Group identifier in `"row:column"` format.
function Inventory:EditModePickUpGroup(groupId)
assert(groupId, "Inventory<" .. tostring(self.inventoryType) .. ">:EditModePickUpGroup() - groupId is required")
self.editState.cursorItem = groupId
self.editState.cursorItemType = BS_INVENTORY_OBJECT_TYPE.GROUP
self.editState.cursorTooltipTitle = self:FormatEditModeCursorTooltipTitle(
((string.len(self.groups[groupId].name or "") > 0) and self.groups[groupId].name or L.UnnamedGroup),
L.Group
)
self.editState.cursorTooltipText = string.format(L.EditMode_Tooltip_SelectNew, string.lower(L.Location))
-- Attach the group to the cursor
_G.PlaySound("INTERFACESOUND_CURSORGRABOBJECT")
self:ShowEditModeCursor()
end
--- Assign a group to the current layout. This includes:
--- - Moving a group from its current location to a new one.
--- - Creating a new group when a category or item is on the cursor and a group move target is clicked.
---@param objectId string|number
---@param objectType BS_INVENTORY_OBJECT_TYPE
---@param insertType BS_INVENTORY_LAYOUT_DIRECTION
---@param destRow number Row to place the group in.
---@param destColumn number Column to place the group in.
function Inventory:AssignGroupToLayout(objectId, objectType, insertType, destRow, destColumn)
self:ClearEditModeCursor()
local group = nil
local groupIsNew = false
if objectType == BS_INVENTORY_OBJECT_TYPE.GROUP then
-- This is a group that is already assigned to the layout in another spot.
-- Get info about the group we need to move.
local sourceRow, sourceColumn = unpack(BsUtil.Split(objectId, ":"))
sourceRow = tonumber(sourceRow)
sourceColumn = tonumber(sourceColumn)
-- Can't move a group onto itself or just next ot itself.
if
insertType == BS_INVENTORY_LAYOUT_DIRECTION.COLUMN
and sourceRow == destRow
and (destColumn == sourceColumn or destColumn == sourceColumn + 1)
then
return
end
-- Remove group from old position.
group, destRow, destColumn = self:RemoveGroupFromLayout(sourceRow, sourceColumn, destRow, destColumn, true)
else
-- When a category or item is placed on a group move target, a new group needs to be created.
if objectType == BS_INVENTORY_OBJECT_TYPE.CATEGORY or objectType == BS_INVENTORY_OBJECT_TYPE.ITEM then
-- Set variables that will eventually be consumed by ManagePendingGroupAssignments().
self.pendingGroupAssignmentObject = objectId
self.pendingGroupAssignmentObjectType = objectType
end
-- Create a new empty group. ManagePendingGroupAssignments() will take care of filling it if needed.
group = {
categories = {},
name = "",
}
groupIsNew = true
end
-- Add group to new position.
if insertType == BS_INVENTORY_LAYOUT_DIRECTION.ROW then
table.insert(self.layout, destRow, { group })
else
table.insert(self.layout[destRow], destColumn, group)
end
self:EditModeWindowUpdate(true)
-- Prompt to name new groups.
if groupIsNew then
self:RenameGroup(destRow .. ":" .. destColumn, true)
end
end
--- Apply or clear the pending group assignments from `self.pendingGroupAssignmentObject`/`pendingGroupAssignmentObjectType`.
---@param groupId string? Group to which the pending assignment should apply.
---@param apply boolean? If false, discard the pending assignments instead of applying them.
function Inventory:ManagePendingGroupAssignments(groupId, apply)
if apply then
local objectId = self.pendingGroupAssignmentObject
local objectType = self.pendingGroupAssignmentObjectType
if objectType == BS_INVENTORY_OBJECT_TYPE.CATEGORY then
self:AssignCategoryToGroup(objectId, groupId, true)
elseif objectType == BS_INVENTORY_OBJECT_TYPE.ITEM then
self:ShowItemCategoryAssignmentMenuForGroup(objectId, self.groupsIdsToFrames[groupId])
end
end
self.pendingGroupAssignmentObject = nil
self.pendingGroupAssignmentObjectType = nil
end
--- Prompt for group name and update if changed.
---@param groupId string Group identifier in `"row:column"` format.
---@param groupIsNew boolean? When true, delete the group when the Undo (Cancel) button is clicked.
function Inventory:RenameGroup(groupId, groupIsNew)
if not self:GroupExists(groupId) then
return
end
local dialogName = "BAGSHUI_RENAME_GROUP"
if not _G.StaticPopupDialogs[dialogName] then
_G.StaticPopupDialogs[dialogName] = {
text = "",
button1 = _G.OKAY,
button2 = _G.CANCEL,
hasEditBox = true,
timeout = 0,
whileDead = true,
hideOnEscape = true,
EditBoxOnEnterPressed = BsUtil.StaticPopupDialogs_EnterClicksFirstButton,
-- Extra property to be used in OnShow to fill the edit box.
-- Blizzard doesn't provide an easy way to do this in Vanilla as the
-- data property isn't accessible in OnShow, and all the FrameXML code uses
-- global variables and functions.
_bagshuiDefaultEditBoxText = "",
-- Set the default value for the edit box.
OnShow = function()
_G[_G.this:GetName() .. "EditBox"]:SetText(_G.StaticPopupDialogs[dialogName]._bagshuiDefaultEditBoxText)
_G[_G.this:GetName() .. "EditBox"]:SetFocus()
end,
--- Okay was clicked, so change the group name.
---@param data table Reference to `self.renameGroup_Data`, passed through via the dialog's `data` property.
OnAccept = function(data)
self.editModeGroupHighlight = nil
if self.groups[data.groupId] then
self.groups[data.groupId].name = _G[_G.this:GetParent():GetName() .. "EditBox"]:GetText()
end
self:ManagePendingGroupAssignments(data.groupId, true)
self:EditModeWindowUpdate()
end,
--- Cancel was clicked.
--- For new groups, this means the group is no longer wanted.
--- For existing groups, just remove highlighting.
---@param data table Reference to `self.renameGroup_Data`, passed through via the dialog's `data` property.
OnCancel = function(data)
if data.groupIsNew then
local row, column = self:GroupIdToRowColumn(data.groupId)
if row and column then
self:RemoveGroupFromLayout(row, column)
end
end
self.editModeGroupHighlight = nil
self:ManagePendingGroupAssignments(data.groupId, false)
self:EditModeWindowUpdate()
end,
-- Clear text on hide so it doesn't bleed over to other dialogs.
OnHide = BsUtil.StaticPopupDialogs_ClearTextOnHide,
}
end
-- Current group name and text to prompt user with.
local currentName = self.groups[groupId].name
local prompt = groupIsNew and L.EditMode_Prompt_NewGroupName
or string.format(
L.EditMode_Prompt_RenameGroup,
(string.len(currentName or "") > 0 and currentName or L.UnnamedGroup)
)
-- Update dialog properties.
local dialogProps = _G.StaticPopupDialogs[dialogName]
dialogProps.text = prompt
dialogProps._bagshuiDefaultEditBoxText = currentName or ""
-- Lock group highlight so it's more clear what group is being renamed.
self.editModeGroupHighlight = groupId
self:EditModeWindowUpdate()
-- This seems to get removed sometimes so we'll make sure it stays.
if not self.renameGroup_Data then
self.renameGroup_Data = {}
end
-- Set properties we need the dialog scripts to know.
self.renameGroup_Data.groupId = groupId
self.renameGroup_Data.groupIsNew = groupIsNew
-- Change button labels based on what is happening.
if groupIsNew then
_G.StaticPopupDialogs[dialogName].button1 = L.Create
_G.StaticPopupDialogs[dialogName].button2 = L.Undo
else
_G.StaticPopupDialogs[dialogName].button1 = _G.OKAY
_G.StaticPopupDialogs[dialogName].button2 = _G.CANCEL
end
-- Show the dialog.
local dialog = _G.StaticPopup_Show(dialogName)
-- Pass stuff to dialog functions via the magic data property.
if dialog then
dialog.data = self.renameGroup_Data
end
end
--- Delete a group after confirmation.
---@param groupId string Group identifier in `"row:column"` format.
function Inventory:DeleteGroup(groupId)
if not self:GroupExists(groupId) then
return
end
local dialogName = "BAGSHUI_DELETE_GROUP"
if not _G.StaticPopupDialogs[dialogName] then
_G.StaticPopupDialogs[dialogName] = {
text = L.EditMode_Prompt_DeleteGroup,
button1 = L.Delete,
button2 = L.Cancel,
showAlert = true,
timeout = 0,
whileDead = true,
hideOnEscape = true,
-- Delete the group.
-- The groupId parameter comes from the dialog's data property which is set below.
OnAccept = function(groupId)
local row, column = self:GroupIdToRowColumn(groupId)
self.editModeGroupHighlight = nil
self:RemoveGroupFromLayout(row, column)
end,
-- Remove highlighting.
OnCancel = function()
self.editModeGroupHighlight = nil
self:EditModeWindowUpdate()
end,
}
end
-- Lock group highlight so it's more clear which group is up for deletion.
self.editModeGroupHighlight = groupId
self:EditModeWindowUpdate()
-- Display the dialog and provide the object type and object name to the prompt text.
local dialog = _G.StaticPopup_Show(dialogName)
-- Pass groupId through to StaticPopupDialog via the magical data property.
if dialog then
dialog.data = groupId
end
end
--- Remove a group from the current layout, returning adjusted destination row/column values if provided.
--- These adjusted values are needed when a group is moved, because removing it from its current position
--- can change where it needs to be inserted. For example, moving a group from 1:1 to 1:4 will mean it
--- actually needs to be inserted at 1:3, since the removal shifts everything down by 1. Similarly,
--- 2:4 to 4:1 requires inserting at 3:1 if the group being moved was the only one on row 2.
---@param row number Row of the group being deleted.
---@param column number Column of the group being deleted.
---@param destRow number? Desired destination row to be adjusted.
---@param destColumn number? Desired destination column to be adjusted.
---@param noWindowUpdate boolean? When true, don't call EditModeWindowUpdate() after performing the removal.
---@return table? group # Group object.
---@return number? destRow # Adjusted destination row.
---@return number? destColumn # Adjusted destination column.
function Inventory:RemoveGroupFromLayout(row, column, destRow, destColumn, noWindowUpdate)
assert(row and column, "Inventory:RemoveGroupFromLayout(): row and column are both required.")
local group = table.remove(self.layout[row], column)
-- Remove empty row.
local rowRemoved = false
if table.getn(self.layout[row]) == 0 then
table.remove(self.layout, row)
rowRemoved = true
end
-- Destination layout adjustments.
if destRow ~= nil and destColumn ~= nil then
-- Decrement destination column number by 1 if staying on the same row since
-- everything is shifted down by removal (but only if the destination column
-- is higher than the source).
if destRow == row and destColumn > column then
destColumn = destColumn - 1
end
-- Decrement destination row number by 1 (same reasoning as column adjustment above).
if rowRemoved and destRow > row then
destRow = destRow - 1
end
end
-- Update window to show new organization.
if not noWindowUpdate then
self:EditModeWindowUpdate(true)
end
return group, destRow, destColumn
end
--- Parse row and column numbers from a "row:column" string.
---@param groupId string Group identifier in `"row:column"` format.
---@return number? row
---@return number? column
function Inventory:GroupIdToRowColumn(groupId)
local row, column = unpack(BsUtil.Split(groupId, ":"))
row = tonumber(row)
column = tonumber(column)
return row, column
end
--- Assign a sort order to a group.
---@param sortOrderId string|number|nil Sort order identifier, or nil to reset to default.
---@param groupId string Group identifier in `"row:column"` format.
function Inventory:AssignSortOrderToGroup(sortOrderId, groupId)
if not self:GroupExists(groupId) then
return
end
self.groups[groupId].sortOrder = sortOrderId
-- Update window to show new organization.
self:EditModeWindowUpdate(true)
end
--- Test whether a group exists.
---@param groupId string Group identifier in `"row:column"` format.
---@return boolean
function Inventory:GroupExists(groupId)
if not groupId then
return false
end
if not self.groups[groupId] then
Bagshui:PrintError(string.format(L.Error_GroupNotFound, tostring(groupId)))
return false
end
return true
end
end)