Files
Bagshui/Components/Ui.ScrollableList.lua
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

1967 lines
86 KiB
Lua

-- Bagshui UI Class: Scrollable Lists
-- Warning: Messy code ahead.
Bagshui:AddComponent(function()
local Ui = Bagshui.prototypes.Ui
-- Inter-column and ScrollFrame edge padding.
local COLUMN_SPACING = 5
local HEADER_SPACING = 6
--- Create a new scrollable list.
---
--- `params` table values:
--- ```
--- {
--- ---@type string Unique string that will be suffixed per-element and passed to `Ui:CreateElementName()`.
--- namePrefix,
--- ---@type number Width of the scrollable list.
--- width,
--- ---@type table? Parent frame.
--- parent,
--- ---@type BS_UI_SCROLLABLE_LIST_TYPE?? Type of the list.
--- listType,
--- ---@type boolean? Can list entries be selected?
--- selectable,
--- ---@type boolean? Can multiple list entries be selected simultaneously using control/shift?
--- multiSelect,
--- ---@type boolean? When `selectable` and `multiSelect` are true, show a checkbox next to each entry.
--- checkboxes,
--- ---@type boolean? When `checkboxes` is true, headers will get checkboxes too unless this is false.
--- headerCheckboxes,
--- ---@type boolean? List is fully read-only and cannot be changed. Gets stored in the list's `bagshuiData.readOnly` property.
--- readOnly,
--- ---@type string? ASC/DESC
--- initialSortOrder,
--- ---@type number?
--- rowHeight,
--- ---@type number?
--- rowSpacing,
--- ---@type (table|string)?
--- font,
--- ---@type (table|string)?
--- headerFont,
--- ---@type table[]? Array of columns to display (see below), if more than one field needs to be displayed.
--- entryColumns,
--- ---@type boolean? `true` to disable display of column names above the list.
--- hideColumnHeaders,
--- ---@type function(entry)? -> table|string Given a list entry value, return display information about that entry. Can return a table whose values should correspond to `entryColumns` fields, with `entryDisplayProperty` being the primary (first) value.
--- entryInfoFunc,
--- ---@type string? When `entryInfoFunc` returns a table, this property value will be placed in the first column.
--- entryDisplayProperty,
--- ---@type function(entryFrameName, entryFrame, ui)? Called after each entry frame is created to allow for custom modifications.
--- entryFrameCreationFunc,
--- ---@type function(listFrame, entryFrame, entry, ui, entryFrameCallbacksExtraParam)? Called after each entry frame is populated to allow for custom modifications.
--- entryFramePopulateFunc,
--- ---@type any? Additional parameter for `entryFramePopulateFunc()`.
--- entryFrameCallbacksExtraParam,
--- ---@type function(entryDisplayProperty, entry, entryInfo, entryPrimaryText)? -> string When `entryInfoFunc()` returns a table, this will be called to further refine the display value.
--- entryColumnTextFunc
--- ---@type function(sortField, listFrame)? Triggered when the sort field is changed by clicking a column header. Must call `Ui:PopulateScrollableList()` with the new list of entries, **sorted ascending**.
--- onSortFieldChanged,
--- ---@type table? Alternate parent frame when `buttons` and the search box should be parented differently than the scrollable list itself.
--- buttonAndSearchBoxParent,
--- ---@type table[]? Array of buttons to create (see below).
--- buttons,
--- ---@type number? Custom x offset of first button that gets created.
--- firstButtonXOffset,
--- ---@type boolean? Don't create a search box.
--- noSearchBox,
--- ---@type string? Placeholder text for empty search box.
--- searchPlaceholderText,
--- ---@type function(entries, entryFrame)? Called after the entire list is updated.
--- onChangeFunc,
--- ---@type function(listFrame)? Callback triggered after a selection is made and the list has been updated to the new state.
--- onSelectionChangedFunc,
--- ---@type function? Called when a list entry is double-clicked.
--- onDoubleClickFunc,
--- ---@type function(entryFrame, modifierKeyRefresh)? Called when the mouse enters a list frame (this is already handled for item lists and will be ignored).
--- entryOnEnterFunc,
--- ---@type function(entryFrame)? Called when the mouse leaves a list frame (this is already handled for item lists and will be ignored).
--- entryOnLeaveFunc,
--- ---@type table? Frame that should receive drag events for item lists.
--- itemDragTarget
--- }
--- ```
--- `entryColumns` is an array of:
--- ```
--- {
--- ---@type string Property in the table returned from `entryInfoFunc()` to display.
--- field = "name",
--- ---@type string Column title to display in the UI.
--- title = L.CategoryManager_Name,
--- ---@type number Absolute width of this column. Either `width` or `widthPercent` is required, with the former prioritized over the latter.
--- width = 200,
--- ---@type number Relative width of this column.
--- widthPercent = 90,
--- ---@type string Only one column should have this property. This will be the initial sort order of the list.
--- currentSortOrder = "ASC",
--- ---@type string Starting sort order for this column if the user chooses to sort by it.
--- lastSortOrder = "ASC",
--- ---@type boolean Disallow changing sort order for this column.
--- lockSortOrder = true,
--- }
--- ```
---
--- `buttons` is an array of `Ui:CreateIconButton()` `buttonOpts` parameter values, plus the special properties below.
--- Note that the `*_Disable` properties can be updated at any time before calling `PopulateScrollableList()`
--- and the changes will be taken into account.
--- ```
--- {
--- ---@type boolean? Don't create this button.
--- skip,
--- ---@type BS_UI_SCROLLABLE_LIST_BUTTON_NAME? One of the pre-configured scrollable list button types, some of which come with built-in behaviors.
--- scrollableList_ButtonName,
--- ---@type boolean? When true, anchor this button to the previous one in the array.
--- scrollableList_AutomaticAnchor,
--- ---@type boolean? When true, anchor this button to the search box for the list frame.
--- scrollableList_AnchorToSearchBox,
--- ---@type boolean? When true, anchor this button to the scroll frame itself (technically anchors to the background since that's the visual representation of the ScrollFrame).
--- scrollableList_AnchorToScrollFrame,
--- ---@type boolean? When true, disable the button.
--- scrollableList_Disable,
--- ---@type boolean? When true, disable the button if nothing in the list is selected.
--- scrollableList_DisableIfNothingSelected,
--- ---@type boolean? When true, disable the button if multiple items in the list are selected.
--- scrollableList_DisableIfMultipleSelected,
--- ---@type boolean? When true, disable the button when the first item in the list is selected.
--- scrollableList_DisableIfFirstEntrySelected,
--- ---@type boolean? When true, disable the button only when the last item in the list is selected.
--- scrollableList_DisableIfLastEntrySelected,
--- ---@type boolean? When true, disable the button if the list entry information has a `readOnly` property that is true.
--- scrollableList_DisableIfReadOnly,
--- ---@type function(scrollableListEntryInfo) -> boolean? When true is returned, disable the button.
--- scrollableList_DisableFunc,
--- }
---```
---@param params table
---@return table scrollFrame
---@return table scrollChild
---@return table listFrame
function Ui:CreateScrollableList(params)
assert(type(params) == "table", "Parameter list for Ui:CreateScrollableList() must be a table")
assert(params.namePrefix, "CreateScrollableItemList(): namePrefix is required")
assert(params.width, "CreateScrollableItemList(): width is required")
-- Default to text list if not specified.
local listType = params.listType or BS_UI_SCROLLABLE_LIST_TYPE.TEXT
-- Prepare list of available buttons (this can't be done sooner due to localization stuff).
-- Using `Ui` instead of `self` here so it's only done once -- this is constant across all instances of the Ui class.
if not Ui._createScrollableList_AvailableButtons then
Ui._createScrollableList_AvailableButtons = {
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.NEW] = {
name = "New",
texture = "Add",
tooltipTitle = L.New,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.ADD] = {
name = "Add",
texture = "Add",
tooltipTitle = L.Add,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DELETE] = {
name = "Delete",
texture = "Delete",
tooltipTitle = L.Delete,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfReadOnly = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DOWN] = {
name = "Down",
texture = "Down",
tooltipTitle = L.MoveDown,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfLastEntrySelected = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DUPLICATE] = {
name = "Duplicate",
texture = "Duplicate",
tooltipTitle = L.Duplicate,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfMultipleSelected = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.EDIT] = {
name = "Edit",
texture = "Edit",
tooltipTitle = L.Edit,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfMultipleSelected = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.IMPORT] = {
name = "Import",
texture = "Import",
tooltipTitle = L.Import,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.REMOVE] = {
name = "Remove",
texture = "Remove",
tooltipTitle = L.Remove,
scrollableList_DisableIfNothingSelected = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.REPLACE] = {
name = "Replace",
texture = "Replace",
tooltipTitle = L.Replace,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfMultipleSelected = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.SHARE] = {
name = "Share",
texture = "Share",
tooltipTitle = L.Share,
scrollableList_DisableIfEmptyList = true,
},
[BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP] = {
name = "Up",
texture = "Up",
tooltipTitle = L.MoveUp,
scrollableList_DisableIfNothingSelected = true,
scrollableList_DisableIfFirstEntrySelected = true,
},
}
end
-- Helper functions -- declared here to capture `self`.
if not self._scrollableList_SetSelection then
-- OnClick helper function for list selection and pulling items from the cursor.
self._scrollableList_SetSelection = function()
-- If there is an item on the cursor, add that to the list.
local item
if _G.this.bagshuiData.listFrame.bagshuiData.scrollableListType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
item = Bagshui:GetCursorItem()
end
if item then
-- The cursor held an item.
self:ItemListAdd(_G.this.bagshuiData.listFrame, item.id)
_G.ClearCursor()
else
-- Normal behavior.
self:CloseMenusAndClearFocuses(true, true, false)
self:SetScrollableListSelection(_G.this.bagshuiData.listFrame, _G.this)
end
end
-- OnMouseUp/OnReceiveDrag helper function for item lists to pull items from the cursor.
self._scrollableItemList_ReceivedItem = function()
if not _G.this.bagshuiData then
return
end
local item = Bagshui:GetCursorItem()
local targetListFrame = _G.this.bagshuiData.itemDragTargetListFrame
or _G.this.bagshuiData.listFrame
or (_G.this.bagshuiData.entries and _G.this)
if item and targetListFrame then
self:ItemListAdd(targetListFrame, item.id)
_G.ClearCursor()
end
end
-- OnClick helper function for column headers.
self._scrollableList_SetSortOrder = function()
-- Changing the sort field is only possible if there's a callback function
-- to re-sort the entry list. As noted in the `params` table description for
-- `Ui:CreateScrollableList()`, this function is responsible for calling
-- `Ui:PopulateScrollableList()` with the updated entry list, **sorted ascending**.
if _G.this.bagshuiData.listFrame.bagshuiData.onSortFieldChanged then
_G.PlaySound("igMainMenuOptionCheckBoxOn")
-- Get the sort field name and the most recent way it was sorted.
local sortField = _G.this.bagshuiData.columnParams.sortField or _G.this.bagshuiData.columnParams.field
local newSortOrder = _G.this.bagshuiData.columnParams.lastSortOrder
-- If the field is the current sort order field, reverse it.
if
_G.this.bagshuiData.columnParams.currentSortOrder and not _G.this.bagshuiData.columnParams.lockSortOrder
then
if _G.this.bagshuiData.columnParams.currentSortOrder == "ASC" then
newSortOrder = "DESC"
else
newSortOrder = "ASC"
end
end
-- Update the state of all sort fields
for _, col in ipairs(_G.this.bagshuiData.listFrame.bagshuiData.entryColumns) do
if (col.sortField or col.field) == sortField then
col.currentSortOrder = newSortOrder
col.lastSortOrder = newSortOrder
else
col.currentSortOrder = nil
end
end
-- Store the sort order so `PopulateScrollableList()` can decide whether
-- it's working forwards or backwards through the entry list.
_G.this.bagshuiData.listFrame.bagshuiData.sortOrder = newSortOrder
-- Trigger the callback.
_G.this.bagshuiData.listFrame.bagshuiData.onSortFieldChanged(sortField, _G.this.bagshuiData.listFrame)
end
end
end
-- Create the scrollable list components and store cross-references everywhere for easy access.
local scrollFrame, scrollChild, listFrame = self:CreateScrollableContent(params.namePrefix, params.parent)
scrollChild.bagshuiData.listFrame = listFrame
scrollFrame.bagshuiData.listFrame = listFrame
scrollFrame.bagshuiData.scrollableListType = listType
scrollChild.bagshuiData.scrollableListType = listType
listFrame.bagshuiData.scrollableListType = listType
-- Initialize Bagshui properties.
-- Array of list entry values, as provided to `Ui:PopulateScrollableList()`.
---@type any[]
listFrame.bagshuiData.entries = {}
-- All frames that are currently populated and displayed in the list.
listFrame.bagshuiData.entryFrames = {}
-- Key-value table representing all currently selected objects in the list.
-- Keys are object IDs and values are always `true`.
---@type table<any, true>
listFrame.bagshuiData.selectedEntries = {}
-- When only one item in the list is selected, this will be populated with
-- its object ID (it will also have an entry in `selectedEntries`; this is
-- provided for convenience so functions that only deal with one selected
-- item don't need to iterate and do extra work).
---@type any
listFrame.bagshuiData.selectedEntry = nil
-- Pass parameters through for later use (see this function's definition for details).
listFrame.bagshuiData.entryInfoFunc = params.entryInfoFunc
listFrame.bagshuiData.entryColumnTextFunc = params.entryColumnTextFunc
listFrame.bagshuiData.sortOrder = params.initialSortOrder or "ASC"
listFrame.bagshuiData.onSortFieldChanged = params.onSortFieldChanged
listFrame.bagshuiData.entryFrameCreationFunc = params.entryFrameCreationFunc
listFrame.bagshuiData.entryColumns = params.entryColumns
listFrame.bagshuiData.entryFramePopulateFunc = params.entryFramePopulateFunc
listFrame.bagshuiData.entryFrameCallbacksExtraParam = params.entryFrameCallbacksExtraParam
listFrame.bagshuiData.entryColumns = params.entryColumns
listFrame.bagshuiData.entryDisplayProperty = (params.entryColumns and params.entryColumns[1].field)
or params.entryDisplayProperty
or "Name"
listFrame.bagshuiData.rowHeight = params.rowHeight
or (params.listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM and 24 or 16)
listFrame.bagshuiData.rowSpacing = params.rowSpacing
or (params.listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM and 3 or 0)
listFrame.bagshuiData.font = params.font or "GameFontHighlight"
listFrame.bagshuiData.headerFont = params.headerFont or "GameFontNormal"
listFrame.bagshuiData.selectable = params.selectable or params.multiSelect
listFrame.bagshuiData.multiSelect = params.multiSelect
listFrame.bagshuiData.checkboxes = params.multiSelect and params.checkboxes
listFrame.bagshuiData.headerCheckboxes = params.headerCheckboxes
listFrame.bagshuiData.readOnly = params.readOnly
listFrame.bagshuiData.onSelectionChangedFunc = params.onSelectionChangedFunc
listFrame.bagshuiData.onDoubleClickFunc = params.onDoubleClickFunc
listFrame.bagshuiData.entryOnEnterFunc = params.entryOnEnterFunc
listFrame.bagshuiData.entryOnLeaveFunc = params.entryOnLeaveFunc
listFrame.bagshuiData.onChangeFunc = params.onChangeFunc
-- Add drag handling for item lists.
if listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM and params.itemDragTarget then
if not params.itemDragTarget.bagshuiData then
params.itemDragTarget.bagshuiData = {}
end
-- Required by `self._scrollableItemList_ReceivedItem()` for the itemDragTarget.
-- (Not needed for the other scrollable list components -- see logic in the aforementioned ReceivedItem function.)
params.itemDragTarget.bagshuiData.itemDragTargetListFrame = listFrame
-- Drag target + scrollable list components should respond to drag events.
params.itemDragTarget:EnableMouse(true)
params.itemDragTarget:SetScript("OnReceiveDrag", self._scrollableItemList_ReceivedItem)
listFrame:SetScript("OnReceiveDrag", self._scrollableItemList_ReceivedItem)
scrollFrame:SetScript("OnReceiveDrag", self._scrollableItemList_ReceivedItem)
scrollChild:SetScript("OnReceiveDrag", self._scrollableItemList_ReceivedItem)
-- In addition to drag events, everybody needs to receive MouseUp so that
-- clicking with an item on the cursor works. Capturing the existing OnMouseUp
-- so nothing gets broken.
local oldItemDragTargetOnMouseUp = params.itemDragTarget:GetScript("OnMouseUp")
params.itemDragTarget:SetScript("OnMouseUp", function()
self._scrollableItemList_ReceivedItem()
if oldItemDragTargetOnMouseUp then
oldItemDragTargetOnMouseUp()
end
end)
local oldListFrameOnMouseUp = listFrame:GetScript("OnMouseUp")
listFrame:SetScript("OnMouseUp", function()
self._scrollableItemList_ReceivedItem()
if oldListFrameOnMouseUp then
oldListFrameOnMouseUp()
end
end)
local scrollChildOnMouseUp = scrollChild:GetScript("OnMouseUp")
scrollChild:SetScript("OnMouseUp", function()
self._scrollableItemList_ReceivedItem()
if scrollChildOnMouseUp then
scrollChildOnMouseUp()
end
end)
local scrollFrameOnMouseUp = scrollFrame:GetScript("OnMouseUp")
scrollFrame:SetScript("OnMouseUp", function()
self._scrollableItemList_ReceivedItem()
if scrollFrameOnMouseUp then
scrollFrameOnMouseUp()
end
end)
end
-- Set initial width.
self:SetWidth(scrollFrame, params.width) -- Using Ui:SetWidth for ScrollFrame as explained in the function declaration.
listFrame:SetWidth(params.width)
-- Add headers if there are columns.
if params.entryColumns then
local nextAnchorToFrame = scrollFrame.bagshuiData.background
local nextAnchorToPoint = "TOPLEFT"
listFrame.bagshuiData.columnHeaders = {}
-- Populate listFrame.bagshuiData.columnHeaders with a table of { fieldName = columnFrame }.
for i, col in ipairs(params.entryColumns) do
assert(
col.field,
"Column " .. i .. " in the " .. params.namePrefix .. " scrollable list does not have a field property"
)
assert(
col.width or col.widthPercent,
"Column "
.. i
.. " in the "
.. params.namePrefix
.. " scrollable list does not have a width or widthPercent property"
)
local columnFrame = _G.CreateFrame("Button", params.namePrefix .. col.field .. "ColumnHeader", params.parent)
listFrame.bagshuiData.columnHeaders[col.field] = columnFrame
columnFrame.bagshuiData = {
listFrame = listFrame,
columnNum = i,
columnParams = col,
text = self:CreateShadowedFontString(columnFrame, nil, "GameFontNormalSmall"),
}
columnFrame.bagshuiData.text:SetText(col.title or col.name or col.field)
-- headerHeight is used by `Ui:SetPoint()` to account for headers when
-- setting the top point of the scrollFrame.
if scrollFrame.bagshuiData.headerHeight == 0 and not params.hideColumnHeaders then
scrollFrame.bagshuiData.headerHeight = columnFrame.bagshuiData.text:GetHeight() + 2
end
-- Formatting.
columnFrame.bagshuiData.text:SetJustifyH("LEFT")
columnFrame.bagshuiData.text:SetJustifyV("MIDDLE")
columnFrame.bagshuiData.text:SetPoint("TOPLEFT", columnFrame, COLUMN_SPACING, 0)
columnFrame.bagshuiData.text:SetPoint("BOTTOMRIGHT", columnFrame, -COLUMN_SPACING, 0)
-- Calculate width and set size/position.
local columnWidth = col.width or (scrollFrame:GetWidth() * (col.widthPercent / 100))
params.entryColumns[i].actualWidth = columnWidth - 10
columnFrame:SetWidth(columnWidth)
columnFrame:SetHeight(scrollFrame.bagshuiData.headerHeight)
columnFrame:SetPoint("BOTTOMLEFT", nextAnchorToFrame, nextAnchorToPoint, 0, 1)
columnFrame:SetScript("OnClick", self._scrollableList_SetSortOrder)
nextAnchorToFrame = columnFrame
nextAnchorToPoint = "BOTTOMRIGHT"
end
-- Set initial column header state.
self:UpdateScrollableListColumnHeaders(listFrame)
end
-- Create search box
if not params.noSearchBox then
local searchBox = self:CreateSearchBox(
params.namePrefix .. "SearchBox",
params.buttonAndSearchBoxParent or params.parent,
nil, -- Width.
nil, -- Height.
function() -- OnTextChanged.
self:ShowScrollableListEntries(listFrame, _G.this.bagshuiData.searchText)
end,
nil, -- OnEnterPressed.
nil, -- OnIconClick.
params.searchPlaceholderText
)
listFrame.bagshuiData.searchBox = searchBox
end
-- Create buttons (add, remove, etc.).
listFrame.bagshuiData.buttons = {}
if type(params.buttons) == "table" then
-- Reusable table.
if not self._createScrollableList_ButtonParams then
self._createScrollableList_ButtonParams = {}
end
local lastCreatedButton
local buttonParams = self._createScrollableList_ButtonParams
for _, button in ipairs(params.buttons) do
if not button.skip then
BsUtil.TableClear(buttonParams)
local buttonName = button.scrollableList_ButtonName or button.name
-- Set initial parameters if available.
if Ui._createScrollableList_AvailableButtons[buttonName] then
BsUtil.TableCopy(Ui._createScrollableList_AvailableButtons[buttonName], buttonParams)
end
for key, val in pairs(button) do
buttonParams[key] = val
end
buttonParams.name = params.namePrefix .. buttonParams.name
buttonParams.parentFrame = params.buttonAndSearchBoxParent or params.parent
-- Adjust anchor as requested.
if button.scrollableList_AnchorToScrollFrame then
-- Need to anchor to background since that's the visual representation of the ScrollFrame.
buttonParams.anchorToFrame = scrollFrame.bagshuiData.background
elseif button.scrollableList_AnchorToSearchBox and listFrame.bagshuiData.searchBox then
buttonParams.anchorToFrame = listFrame.bagshuiData.searchBox
elseif button.scrollableList_AutomaticAnchor then
buttonParams.anchorToFrame = lastCreatedButton or buttonParams.parentFrame
buttonParams.anchorPoint = buttonParams.anchorPoint or "LEFT"
buttonParams.anchorToPoint = buttonParams.anchorToPoint or (not lastCreatedButton and "LEFT" or "RIGHT")
end
-- Automatic Add and Copy button handling for Item lists.
if listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
if button.scrollableList_ButtonName == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.ADD then
buttonParams.onClick = function()
self:ItemListPromptForNew(listFrame)
end
end
if button.scrollableList_ButtonName == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.SHARE then
buttonParams.onClick = function()
self:ItemListOpenCopyDialog(listFrame)
end
end
end
-- Remove, Up, and Down buttons always do the same thing.
if button.scrollableList_ButtonName == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.REMOVE then
-- Temporary table for removals to avoid errors.
-- Without this, pairs() can fail depending on the removal order.
local scrollableList_EntriesToRemove = {}
buttonParams.onClick = function()
-- Build temporary list of entries to remove.
BsUtil.TableClear(scrollableList_EntriesToRemove)
for selectedEntry, _ in pairs(listFrame.bagshuiData.selectedEntries) do
table.insert(scrollableList_EntriesToRemove, selectedEntry)
end
-- Perform removals.
for _, selectedEntry in ipairs(scrollableList_EntriesToRemove) do
self:ScrollableListRemove(listFrame, selectedEntry)
end
BsUtil.TableClear(scrollableList_EntriesToRemove)
end
end
if
button.scrollableList_ButtonName == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP
or button.scrollableList_ButtonName == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DOWN
then
local upDown = button.scrollableList_ButtonName
buttonParams.onClick = function()
self:ScrollableListMove(listFrame, listFrame.bagshuiData.selectedEntry, upDown)
end
end
-- Pick up additional/override parameters.
for param, value in pairs(button) do
if param == "onClick" then
local onClick = value
buttonParams[param] = function()
onClick(listFrame)
end
else
buttonParams[param] = value
end
end
-- Adjust X offset when this is the first button and automatic anchoring is enabled.
if buttonParams.scrollableList_AutomaticAnchor and params.firstButtonXOffset and not lastCreatedButton then
buttonParams.xOffset = params.firstButtonXOffset
end
-- Create button.
listFrame.bagshuiData.buttons[buttonName] = self:CreateIconButton(buttonParams)
listFrame.bagshuiData.buttons[buttonName].bagshuiData.listFrame = listFrame
listFrame.bagshuiData.buttons[buttonName].bagshuiData.scrollableList_Disable = buttonParams.scrollableList_Disable
or buttonParams.disable
listFrame.bagshuiData.buttons[buttonName].bagshuiData.scrollableList_DisableFunc = buttonParams.scrollableList_DisableFunc
or buttonParams.disableFunc
for prop, val in pairs(buttonParams) do
if string.find(prop, "^scrollableList_") then
listFrame.bagshuiData.buttons[buttonName].bagshuiData[prop] = val
end
end
lastCreatedButton = listFrame.bagshuiData.buttons[buttonName]
end
end
end
-- Same return values as CreateScrollableContent().
return scrollFrame, scrollChild, listFrame
end
--- Fill or refresh a Bagshui scrollable list frame built by `CreateScrollableList()`.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entries table? Array of list entry values.
---@param refresh boolean? When `entries` is nil, don't clear the list and just refresh it.
---@param preserveSelection boolean? Don't de-select the currently selected list entry.
---@param preserveSearch boolean? Don't clear search text.
function Ui:PopulateScrollableList(listFrame, entries, refresh, preserveSelection, preserveSearch)
assert(
listFrame.bagshuiData.scrollableListType,
"Ui:PopulateScrollableList(): Given listFrame is not a scrollable list"
)
-- Clear search text.
if listFrame.bagshuiData.searchBox and not preserveSearch then
listFrame.bagshuiData.searchBox:SetText("")
end
-- Save list of entries to `listFrame.bagshuiData.entries` so they're accessible for filtering.
if entries then
-- We've been given a fresh set of entries, so replace the existing ones.
BsUtil.TableCopy(entries, listFrame.bagshuiData.entries)
elseif not refresh and not listFrame.bagshuiData.refreshAfterUnknownItems then
-- This is not a refresh scenario and no entries were provided, so clear the list.
-- Regarding `refreshAfterUnknownItems`, see the `listFrame.bagshuiData.hasUnknownItems`
-- comment just below.
BsUtil.TableClear(listFrame.bagshuiData.entries)
end
-- Release any existing item list frames
self:ReleaseScrollableListFrames(listFrame.bagshuiData.entryFrames)
BsUtil.TableClear(listFrame.bagshuiData.entryFrames)
-- Used to track whether we saw any unknown items for item-type lists and schedule a refresh
-- callback if we did.
-- * It's updated by `PopulateScrollableListEntry()` if `ItemInfo:Get()` can't obtain
-- information about an item.
-- * If it's true at the end of this function, a refresh pass will be queued and
-- `refreshAfterUnknownItems` will be set to true. This will hopefully give
-- to receive information about the unknown item(s).
listFrame.bagshuiData.hasUnknownItems = false
-- This will be set to `true` in `PopulateScrollableListEntry()` if applicable.
listFrame.bagshuiData.hasHeaders = false
local listType = listFrame.bagshuiData.scrollableListType
local entryFrames = listFrame.bagshuiData.entryFrames
-- Prepare frames for everything in the list.
if table.getn(listFrame.bagshuiData.entries) > 0 then
-- Display in descending order if required.
local entryStart = 1
local entryEnd = table.getn(listFrame.bagshuiData.entries)
local entryStep = 1
if listFrame.bagshuiData.sortOrder == "DESC" then
entryStart = entryEnd
entryEnd = 1
entryStep = -1
end
-- Used by `Ui:SetScrollableListSelection()` when Shift is down to select
-- a range of frames.
local sequentialFrameNum = 1
-- Associate child frames with their headers.
local lastHeaderFrame
for i = entryStart, entryEnd, entryStep do
local entry = listFrame.bagshuiData.entries[i]
-- Grab an entry frame, reusing existing if possible.
local entryFrame =
self:GetAvailableScrollableListEntryFrame(listFrame, listType, listFrame.bagshuiData.entryFrameCreationFunc)
entryFrame.bagshuiData.scrollableListEntry = entry
entryFrame.bagshuiData.sequentialFrameNum = sequentialFrameNum
-- Store data needed for list selection behavior.
entryFrame.bagshuiData.listFrame = listFrame
if entryFrame.bagshuiData.itemButton then
entryFrame.bagshuiData.itemButton.bagshuiData.listFrame = listFrame
end
-- Add to this item list's array of item frames.
table.insert(entryFrames, entryFrame)
-- Update list entry with entry info.
self:PopulateScrollableListEntry(listFrame, entryFrame, entry)
-- Header tracking.
if entryFrame.bagshuiData.isHeader then
lastHeaderFrame = entryFrame
entryFrame.bagshuiData.headerFrame = nil
else
entryFrame.bagshuiData.headerFrame = lastHeaderFrame
end
-- Manage selection and double-click behavior.
local onMouseDown = nil
if listFrame.bagshuiData.selectable then
entryFrame:RegisterForClicks("LeftButtonUp")
onMouseDown = self._scrollableList_SetSelection
else
entryFrame:RegisterForClicks(nil)
end
entryFrame:SetScript("OnMouseDown", onMouseDown)
-- Item lists.
if entryFrame.bagshuiData.itemButton then
entryFrame.bagshuiData.itemButton:SetScript("OnMouseDown", onMouseDown)
end
entryFrame:SetScript("OnDoubleClick", listFrame.bagshuiData.onDoubleClickFunc)
sequentialFrameNum = sequentialFrameNum + 1
end
end
-- Make frames visible in the UI.
self:ShowScrollableListEntries(
listFrame,
(listFrame.bagshuiData.searchBox and listFrame.bagshuiData.searchBox.bagshuiData.searchText)
)
-- Update selection and button states.
self:SetScrollableListSelection(listFrame, (preserveSelection and listFrame.bagshuiData.lastSelection or nil))
-- Operations that only need to happen once and not after a second refresh for unknown items.
if not listFrame.bagshuiData.refreshAfterUnknownItems then
-- Update headers.
self:UpdateScrollableListColumnHeaders(listFrame)
-- Notify of change.
if listFrame.bagshuiData.onChangeFunc then
listFrame.bagshuiData.onChangeFunc(listFrame.bagshuiData.entries, listFrame)
end
end
-- Refresh again if there were unknown items.
if listFrame.bagshuiData.hasUnknownItems and not listFrame.bagshuiData.refreshAfterUnknownItems then
listFrame.bagshuiData.hasUnknownItems = false
listFrame.bagshuiData.refreshAfterUnknownItems = true
Bagshui:QueueClassCallback(self, self.PopulateScrollableList, 0.5, false, listFrame, nil)
return
end
-- Reset callback flag state if we make it this far.
listFrame.bagshuiData.refreshAfterUnknownItems = nil
end
--- Add content to a scrollable list entry frame.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entryFrame table Frame from `GetAvailableScrollableListEntryFrame()`.
---@param entry any List entry pulled from the `entries` provided to `CreateScrollableList()`.
function Ui:PopulateScrollableListEntry(listFrame, entryFrame, entry)
local listType = listFrame.bagshuiData.scrollableListType
local entryPrimaryText
local entryInfo = entry
local textFrames = entryFrame.bagshuiData.textFrames
entryFrame.bagshuiData.isHeader = false -- Not currently supported for ITEM type lists.
if listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
-- Additional work for item lists -- get item information, assign to item slot button.
-- entryInfo becomes an alias to the item button's reusable item info table.
entryInfo = entryFrame.bagshuiData.itemButton.bagshuiData.item
-- Always reset the item info table. Without this, switching from a known
-- to an unknown item doesn't work because entryInfo.name still has a value.
BsItemInfo:InitializeItem(entryInfo, true)
-- When entry is already a Bagshui item, don't call GetItemInfo again.
if type(entry) == "table" and type(entry.bagshuiInventoryType) then
BsUtil.TableCopy(entry, entryInfo)
else
-- Obtain item info - reuse the itemButton's info table for this
BsItemInfo:Get(
entry, -- itemIdentifier
entryInfo, -- itemInfoTable
true, -- initialize
false, -- reinitialize
true -- forceIntoLocalGameCache
)
end
-- If we didn't get a name, set it to `Unknown (<itemID or itemString>)`
-- since we need something to put on the label.
if string.len(entryInfo.name or "") == 0 then
if not listFrame.bagshuiData.refreshAfterUnknownItems then
listFrame.bagshuiData.hasUnknownItems = true -- Consumed by PopulateScrollableList() to trigger a refresh of item info.
BsItemInfo:LoadItemIntoLocalGameCache(entry)
end
entryInfo.name = L.Unknown .. " (" .. tostring(entry) .. ")"
end
-- Assign to item slot button.
self:AssignItemToItemButton(entryFrame.bagshuiData.itemButton, entryInfo)
-- Change label to show item name.
entryPrimaryText = entryInfo.name
else
-- All other list types.
-- Figure out the primary entry text, using entry information functions if available.
if listFrame.bagshuiData.entryInfoFunc then
entryInfo = listFrame.bagshuiData.entryInfoFunc(entry, listFrame.bagshuiData.entryFrameCallbacksExtraParam)
end
if type(entryInfo) == "table" then
entryPrimaryText = entryInfo[listFrame.bagshuiData.entryDisplayProperty]
if listFrame.bagshuiData.entryColumnTextFunc then
entryPrimaryText = listFrame.bagshuiData.entryColumnTextFunc(
listFrame.bagshuiData.entryDisplayProperty,
entry,
entryInfo,
entryPrimaryText,
listFrame.bagshuiData.entryFrameCallbacksExtraParam
)
end
entryFrame.bagshuiData.isHeader = entryInfo.scrollableList_Header
else
entryPrimaryText = tostring(entryInfo)
end
end
-- Store entry information so it's accessible elsewhere.
entryFrame.bagshuiData.scrollableListEntry = entry
entryFrame.bagshuiData.scrollableListEntryInfo = entryInfo
-- Set primary text.
textFrames[1]:SetText(entryPrimaryText)
-- Track which column frames to hide. Start at 2 since the first text frame is always visible.
local frameHideStart = 2
if listFrame.bagshuiData.entryColumns then
-- This list has multiple columns.
local nextAnchorToFrame = textFrames[1]
for i, col in ipairs(listFrame.bagshuiData.entryColumns) do
local columnWidth = col.actualWidth
if i == 1 then
-- The first frame has already been populated above and we just
-- need to adjust the width.
columnWidth = columnWidth - entryFrame.bagshuiData.widthOffset
else
-- Add another column text frame if needed.
if not textFrames[i] then
textFrames[i] = self:CreateShadowedFontString(entryFrame)
end
-- Get text and populate the frame.
local columnText = entryInfo[col.field] ~= nil and tostring(entryInfo[col.field]) or ""
if listFrame.bagshuiData.entryColumnTextFunc then
columnText = listFrame.bagshuiData.entryColumnTextFunc(col.field, entry, entryInfo, columnText)
end
textFrames[i]:SetText(columnText)
textFrames[i]:SetPoint("LEFT", nextAnchorToFrame, "RIGHT", 10, 0)
textFrames[i]:SetJustifyH(col.align or "LEFT")
nextAnchorToFrame = textFrames[i]
end
textFrames[i]:SetWidth(columnWidth)
textFrames[i]:SetFontObject(listFrame.bagshuiData.font)
frameHideStart = frameHideStart + 1
end
else
-- Single column.
textFrames[1]:SetWidth(listFrame:GetWidth() - entryFrame.bagshuiData.widthOffset)
end
-- Various other parts of ScrollableList need to know whether any headers exist.
if entryFrame.bagshuiData.isHeader then
listFrame.bagshuiData.hasHeaders = true
end
-- Hide unused frames.
for i = frameHideStart, table.getn(textFrames) do
textFrames[i]:Hide()
end
-- Show/hide checkboxes and apply header styling as needed.
self:UpdateScrollableListEntryFrame(listFrame, entryFrame)
-- Custom callback.
if listFrame.bagshuiData.entryFramePopulateFunc then
listFrame.bagshuiData.entryFramePopulateFunc(
listFrame,
entryFrame,
entry,
self,
listFrame.bagshuiData.entryFrameCallbacksExtraParam
)
end
end
-- Display scrollable item list entries in the UI, filtering by searchText if provided.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param searchText string? Filter text. *Currently only works against the primary text (first column).*
function Ui:ShowScrollableListEntries(listFrame, searchText)
local listType = listFrame.bagshuiData.scrollableListType
local entryFrames = listFrame.bagshuiData.entryFrames
local rowHeight = listFrame.bagshuiData.rowHeight
local rowSpacing = listFrame.bagshuiData.rowSpacing
local contentHeight = 0
local contentFrameWidth = listFrame.bagshuiData.scrollChild:GetWidth()
local nextAnchor = listFrame
local nextAnchorPoint = "TOPLEFT"
local nextYOffset = -5
for _, entryFrame in ipairs(entryFrames) do
entryFrame.bagshuiData.show = true
if searchText then
if listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
-- Item list.
entryFrame.bagshuiData.show =
BsRules:Match(searchText, entryFrame.bagshuiData.itemButton.bagshuiData.item, nil, nil, true)
else
-- Text list.
entryFrame.bagshuiData.show = (
string.find(
string.lower(BsUtil.RemoveUiEscapes(entryFrame.bagshuiData.textFrames[1]:GetText())),
string.lower(searchText)
) ~= nil
)
end
end
end
-- Show headers when child frames are shown.
if listFrame.bagshuiData.hasHeaders then
for headerIndex, headerFrame in ipairs(entryFrames) do
if headerFrame.bagshuiData.isHeader then
headerFrame.bagshuiData.show = false
-- Scan forward until we reach the next header frame.
-- Within this range are the entry frames that belong to the current header.
for child = headerIndex + 1, table.getn(entryFrames) do
if entryFrames[child].bagshuiData.isHeader then
break
end
if entryFrames[child].bagshuiData.show then
headerFrame.bagshuiData.show = true
break
end
end
end
end
end
-- Show/hide entries.
for i, entryFrame in ipairs(entryFrames) do
if entryFrame.bagshuiData.show then
entryFrame:ClearAllPoints()
entryFrame:SetParent(listFrame)
entryFrame:SetPoint(
"TOPLEFT",
nextAnchor,
nextAnchorPoint,
0,
nextYOffset - ((i > 1 and entryFrame.bagshuiData.isHeader) and HEADER_SPACING or 0)
)
entryFrame:SetWidth(contentFrameWidth)
entryFrame:SetHeight(rowHeight + (entryFrame.bagshuiData.isHeader and 1 or 0))
entryFrame:Show()
-- Make sure item slot button and checkbox are above the frame so they can receive events.
if entryFrame.bagshuiData.itemButton then
entryFrame.bagshuiData.itemButton:SetFrameLevel(entryFrame:GetFrameLevel() + 5)
end
if entryFrame.bagshuiData.checkbox and entryFrame.bagshuiData.checkbox:IsShown() then
entryFrame.bagshuiData.checkbox:SetFrameLevel(entryFrame:GetFrameLevel() + 5)
end
-- Prepare for next loop
nextAnchor = entryFrame
nextAnchorPoint = "BOTTOMLEFT"
nextYOffset = -rowSpacing
contentHeight = contentHeight + rowHeight + rowSpacing
else
entryFrame:Hide()
end
end
-- Update height of associated scrollChild.
listFrame.bagshuiData.scrollChild:SetHeight(contentHeight + 10)
end
--- Reusable OnEnter function for item list entry frames, used in `GetAvailableScrollableListEntryFrame()`.
---@param targetEntryFrame table? Alternate frame to use instead of global `this`.
---@param fromChildElement boolean? This is being called as a result of the mouse entering an associated child element like and item button or checkbox (explained in the comment above `if fromChildElement then...`).
---@param modifierKeyRefresh boolean? Updating because there was a modifier key change.
local function ScrollableListEntryFrame_OnEnter(targetEntryFrame, fromChildElement, modifierKeyRefresh)
local this = targetEntryFrame or _G.this
this.bagshuiData.mouseIsOver = true
if not modifierKeyRefresh then
-- Coordinate OnEnter with the item slot button so that mousing over the
-- list entry triggers the hover state for the item slot button.
-- See `ItemButton_OnEnter()` for the other side.
if fromChildElement then
this:LockHighlight()
elseif this.bagshuiData.checkbox then
this.bagshuiData.checkbox:GetScript("OnEnter")(this.bagshuiData.checkbox, true)
elseif this.bagshuiData.itemButton then
this.bagshuiData.itemButton:GetScript("OnEnter")(this.bagshuiData.itemButton, true)
end
end
if type(this.bagshuiData.listFrame.bagshuiData.entryOnEnterFunc) == "function" then
this.bagshuiData.listFrame.bagshuiData.entryOnEnterFunc(this)
end
end
--- Reusable OnLeave function for item list entry frames, used in `GetAvailableScrollableListEntryFrame()`.
---@param targetEntryFrame table? Alternate frame to use instead of global `this`.
---@param fromChildElement boolean? This is being called as a result of the mouse leaving an associated child element like and item button or checkbox (explained in the comment above `if fromItemButton then...`).
local function ScrollableListEntryFrame_OnLeave(targetEntryFrame, fromChildElement)
local this = targetEntryFrame or _G.this
this.bagshuiData.mouseIsOver = false
-- Coordinate OnLeave with the item slot button so that the mouse leaving the
-- list entry removes the hover state for the item slot button.
-- See `ItemButton_OnLeave()` for the other side.
if fromChildElement then
if not this.bagshuiData.selected then
this:UnlockHighlight()
end
elseif this.bagshuiData.checkbox then
this.bagshuiData.checkbox:GetScript("OnLeave")(this.bagshuiData.checkbox, true)
elseif this.bagshuiData.itemButton then
this.bagshuiData.itemButton:GetScript("OnLeave")(this.bagshuiData.itemButton, true)
end
if this.bagshuiData.listFrame and type(this.bagshuiData.listFrame.bagshuiData.entryOnLeaveFunc) == "function" then
this.bagshuiData.listFrame.bagshuiData.entryOnLeaveFunc(this)
end
end
--- Call the entry frame OnEnter again when modifier key states change so tooltips can be refreshed if needed.
local function ScrollableListEntryFrame_OnUpdate()
if not _G.this.bagshuiData.mouseIsOver then
return
end
if
_G.this.bagshuiData.altKeyState ~= _G.IsAltKeyDown()
or _G.this.bagshuiData.controlKeyState ~= _G.IsControlKeyDown()
or _G.this.bagshuiData.shiftKeyState ~= _G.IsShiftKeyDown()
then
ScrollableListEntryFrame_OnEnter(_G.this, false, true)
_G.this.bagshuiData.altKeyState = _G.IsAltKeyDown()
_G.this.bagshuiData.controlKeyState = _G.IsControlKeyDown()
_G.this.bagshuiData.shiftKeyState = _G.IsShiftKeyDown()
end
end
--- Obtain an item list frame, either by reusing an available one or creating new if needed.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param listType BS_UI_SCROLLABLE_LIST_TYPE
---@param entryFrameCreationFunc function? Callback that can be used to modify the entry frame after creation.
---@return table entryFrame
function Ui:GetAvailableScrollableListEntryFrame(listFrame, listType, entryFrameCreationFunc)
-- This is where we'll store frames so they can be reused.
-- The stringified pointer to the `entryFrameCreationFunc` is tagged on the end so that
-- customized frames are stored separately.
local listFrameTableName = listType .. (entryFrameCreationFunc and tostring(entryFrameCreationFunc) or "")
-- Ensure frame storage table exists.
if not self._reusableListFrames[listFrameTableName] then
self._reusableListFrames[listFrameTableName] = {}
end
-- Find a reusable frame if we can.
local reusableFrameTable = self._reusableListFrames[listFrameTableName]
assert(reusableFrameTable, "Failed to find entry frame table " .. listFrameTableName .. " (this shouldn't happen!)")
for i = 1, table.getn(reusableFrameTable) do
if not reusableFrameTable[i].bagshuiData.listFrame then
-- Bagshui:PrintDebug("reusing " .. listFrameTableName .. i)
return reusableFrameTable[i]
end
end
-- Bagshui:PrintDebug("creating new " .. listFrameTableName)
-- Nothing found - create a new frame.
local entryFrameNum = table.getn(reusableFrameTable) + 1
local entryFrameName = "Reusable" .. listType .. "Frame" .. entryFrameNum
local entryFrame = _G.CreateFrame("Button", entryFrameName, _G.UIParent)
entryFrame.bagshuiData = {
scrollableListEntry = nil, -- Will be filled by `PopulateScrollableList()`.
textFrames = {},
}
self:SetFrameBackdrop(entryFrame, "NONE")
entryFrame:SetBackdropColor(0, 0, 0, 0)
entryFrame:SetHitRectInsets(-1, -1, -1, -1)
-- Header background.
entryFrame:SetNormalTexture("Interface\\QuestFrame\\UI-QuestLogTitleHighlight")
entryFrame:GetNormalTexture():SetBlendMode("ADD")
entryFrame:GetNormalTexture():SetAlpha(0)
entryFrame:GetNormalTexture():SetVertexColor(0.5, 0.5, 0)
-- Highlight background.
entryFrame:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
entryFrame:GetHighlightTexture():SetBlendMode("ADD")
-- Default opacity for mouseover highlight. This also gets reset in `SetScrollableListSelection()`.
entryFrame:GetHighlightTexture():SetAlpha(0.5)
local text = self:CreateShadowedFontString(entryFrame)
entryFrame.bagshuiData.textFrames[1] = text
text:SetPoint("LEFT", entryFrame, "LEFT", COLUMN_SPACING, 0)
-- Consumed by `Ui:GetAvailableScrollableListEntryFrame()` to figure out how wide the text frame(s) should be.
entryFrame.bagshuiData.widthOffset = 5
-- Coordinate OnEnter/OnLeave with child elements.
entryFrame:SetScript("OnEnter", ScrollableListEntryFrame_OnEnter)
entryFrame:SetScript("OnLeave", ScrollableListEntryFrame_OnLeave)
entryFrame:SetScript("OnUpdate", ScrollableListEntryFrame_OnUpdate)
if listType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
-- Item lists require more work.
local itemButtonMargin = 5 + BsSkin.itemSlotMarginFudge
-- Create item slot button.
-- Intentionally using `Ui` here instead of `self` so the name is `ReusableItemListFrame<Number>ItemSlotButton`.
local itemButton = Ui:CreateItemSlotButton(entryFrameName .. "ItemSlotButton", entryFrame)
entryFrame.bagshuiData.itemButton = itemButton
itemButton:SetPoint("LEFT", entryFrame, "LEFT", itemButtonMargin, 0)
itemButton.bagshuiData.item = {} -- Will be filled by PopulateScrollableList()
itemButton.bagshuiData.entryFrame = entryFrame
itemButton.bagshuiData.tooltipAnchor = "ANCHOR_PRESERVE"
itemButton.bagshuiData.tooltipAnchorPoint = "TOPRIGHT"
itemButton.bagshuiData.tooltipAnchorToPoint = "TOPLEFT"
itemButton.bagshuiData.tooltipXOffset = -(3 + BsSkin.tooltipExtraOffset)
itemButton.bagshuiData.tooltipYOffset = 3
itemButton.bagshuiData.showBagshuiInfoWithoutAlt = true
itemButton.bagshuiData.colorBorders = true
-- Avoid highlighting the item button on click since it's already selecting the row.
itemButton.bagshuiData.buttonComponents.pushedTexture:SetTexture("")
-- Move left anchor of label.
text:SetPoint("LEFT", entryFrame.bagshuiData.itemButton, "RIGHT", itemButtonMargin - 2, 0)
entryFrame.bagshuiData.widthOffset = entryFrame.bagshuiData.widthOffset
+ entryFrame.bagshuiData.itemButton:GetWidth()
+ itemButtonMargin
-- Set default height and handle future size changes.
-- Height is updated when the frame is displayed by `ShowScrollableListEntries()`.
entryFrame:SetScript("OnSizeChanged", function()
self:SetItemButtonSize(itemButton, _G.this:GetHeight() - 2)
end)
entryFrame:SetHeight(30)
else
-- All other list types are much simpler.
-- Set the default height (updated when the frame is displayed by `ShowScrollableListEntries()`).
entryFrame:SetHeight(18)
-- Custom events are handled in ScrollableListEntryFrame_OnEnter/OnLeave.
end
-- Custom callback.
if entryFrameCreationFunc then
entryFrameCreationFunc(entryFrameName, entryFrame, self)
end
-- Save to reusable frame list.
table.insert(reusableFrameTable, entryFrame)
return entryFrame
end
--- Reusable OnEnter function for item list entry frames, applied in `Ui:UpdateScrollableListEntryFrame()`.
---@param this table? Alternate frame to use instead of global `this`.
---@param fromEntryFrame boolean? This is being called as a result of the mouse entering an associated list entry frame.
local function ScrollableListCheckbox_OnEnter(this, fromEntryFrame)
this = this or _G.this
if fromEntryFrame then
this:LockHighlight()
else
this.bagshuiData.entryFrame:GetScript("OnEnter")(this.bagshuiData.entryFrame, true)
end
end
--- Reusable OnLeave function for item list entry frames, applied in `Ui:UpdateScrollableListEntryFrame()`.
---@param this table? Alternate frame to use instead of global `this`.
---@param fromEntryFrame boolean? This is being called as a result of the mouse leaving an associated list entry frame.
local function ScrollableListCheckbox_OnLeave(this, fromEntryFrame)
this = this or _G.this
if fromEntryFrame then
this:UnlockHighlight()
else
this.bagshuiData.entryFrame:GetScript("OnLeave")(this.bagshuiData.entryFrame, true)
end
end
--- Reusable OnClick function for item list entry frames, applied in `Ui:UpdateScrollableListEntryFrame()`.
local ScrollableListCheckbox_OnClick
--- Add checkboxes and header styling to entries.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entryFrame table List entry frame from `Ui:GetAvailableScrollableListEntryFrame()`.
function Ui:UpdateScrollableListEntryFrame(listFrame, entryFrame)
-- Special styling isn't currently supported for item lists.
if listFrame.bagshuiData.scrollableListType == BS_UI_SCROLLABLE_LIST_TYPE.ITEM then
return
end
-- Grab the default text color so we can reapply it in when frames switch
-- between header and non-header mode.
if not listFrame.bagshuiData.textColorR then
listFrame.bagshuiData.textColorR, listFrame.bagshuiData.textColorG, listFrame.bagshuiData.textColorB, listFrame.bagshuiData.textColorA =
entryFrame.bagshuiData.textFrames[1]:GetTextColor()
end
if type(ScrollableListCheckbox_OnClick) ~= "function" then
-- Reusable function for checkbox OnClick.
function ScrollableListCheckbox_OnClick()
self:SetScrollableListSelection(_G.this.bagshuiData.listFrame, _G.this.bagshuiData.entryFrame)
end
end
if
listFrame.bagshuiData.checkboxes
and (
not entryFrame.bagshuiData.isHeader
or (entryFrame.bagshuiData.isHeader and listFrame.bagshuiData.headerCheckboxes ~= false)
)
then
-- Create checkbox.
if not entryFrame.bagshuiData.checkbox then
entryFrame.bagshuiData.checkbox = self:CreateCheckbox(
entryFrame:GetName() .. "Checkbox",
entryFrame, -- Parent.
nil, -- Template.
nil, -- Text.
ScrollableListCheckbox_OnClick
)
entryFrame.bagshuiData.checkbox.bagshuiData.listFrame = listFrame
entryFrame.bagshuiData.checkbox.bagshuiData.entryFrame = entryFrame
entryFrame.bagshuiData.checkbox:SetScript("OnEnter", ScrollableListCheckbox_OnEnter)
entryFrame.bagshuiData.checkbox:SetScript("OnLeave", ScrollableListCheckbox_OnLeave)
entryFrame.bagshuiData.checkbox:Hide()
end
-- Display checkbox.
if not entryFrame.bagshuiData.checkbox:IsShown() then
entryFrame.bagshuiData.checkbox:Show()
entryFrame.bagshuiData.checkbox:SetHeight(entryFrame:GetHeight() + 2)
entryFrame.bagshuiData.checkbox:SetWidth(entryFrame:GetHeight() + 2)
end
-- Update checkbox and text position.
entryFrame.bagshuiData.checkbox:ClearAllPoints()
if entryFrame.bagshuiData.isHeader then
-- Checkboxes for headers are positioned on the right.
-- entryFrame.bagshuiData.checkbox:SetPoint("RIGHT", entryFrame, "RIGHT", 0, 0)
self:SetPoint(entryFrame.bagshuiData.checkbox, "RIGHT", entryFrame, "RIGHT", 0, nil)
entryFrame.bagshuiData.textFrames[1]:SetPoint("LEFT", entryFrame, "LEFT", COLUMN_SPACING, 0)
entryFrame.bagshuiData.textFrames[1]:SetPoint(
"RIGHT",
entryFrame.bagshuiData.checkbox,
"LEFT",
-COLUMN_SPACING,
0
)
else
entryFrame.bagshuiData.checkbox:SetPoint("LEFT", entryFrame, "LEFT", 0, 0)
-- self:SetPoint(entryFrame.bagshuiData.checkbox, "LEFT", entryFrame, "LEFT", 0, nil)
entryFrame.bagshuiData.textFrames[1]:SetPoint("LEFT", entryFrame.bagshuiData.checkbox, "RIGHT", 0, 0)
entryFrame.bagshuiData.textFrames[1]:SetPoint("RIGHT", entryFrame, "RIGHT", -COLUMN_SPACING, 0)
end
elseif entryFrame.bagshuiData.checkbox then
entryFrame.bagshuiData.checkbox:Hide()
entryFrame.bagshuiData.textFrames[1]:SetPoint("LEFT", entryFrame, "LEFT", COLUMN_SPACING, 0)
entryFrame.bagshuiData.textFrames[1]:SetPoint("RIGHT", entryFrame, "RIGHT", -COLUMN_SPACING, 0)
end
-- Apply header or normal styling.
if entryFrame.bagshuiData.isHeader then
-- Display background.
entryFrame:GetNormalTexture():SetAlpha(0.7)
-- Change fonts.
if not listFrame.bagshuiData.headerFontPath then
entryFrame.bagshuiData.textFrames[1]:SetFontObject(listFrame.bagshuiData.headerFont)
listFrame.bagshuiData.headerFontPath, listFrame.bagshuiData.headerFontSize, listFrame.bagshuiData.headerFontStyle =
entryFrame.bagshuiData.textFrames[1]:GetFont()
listFrame.bagshuiData.headerFontSize = math.floor(listFrame.bagshuiData.headerFontSize) + 2
listFrame.bagshuiData.headerTextColorR, listFrame.bagshuiData.headerTextColorG, listFrame.bagshuiData.headerTextColorB, listFrame.bagshuiData.headerTextColorA =
entryFrame.bagshuiData.textFrames[1]:GetTextColor()
end
entryFrame.bagshuiData.textFrames[1]:SetFont(
listFrame.bagshuiData.headerFontPath,
listFrame.bagshuiData.headerFontSize,
listFrame.bagshuiData.headerFontStyle
)
entryFrame.bagshuiData.textFrames[1]:SetTextColor(
listFrame.bagshuiData.headerTextColorR,
listFrame.bagshuiData.headerTextColorG,
listFrame.bagshuiData.headerTextColorB,
listFrame.bagshuiData.headerTextColorA
)
else
entryFrame.bagshuiData.textFrames[1]:SetFontObject(listFrame.bagshuiData.font)
entryFrame.bagshuiData.textFrames[1]:SetTextColor(
listFrame.bagshuiData.textColorR,
listFrame.bagshuiData.textColorG,
listFrame.bagshuiData.textColorB,
listFrame.bagshuiData.textColorA
)
entryFrame:GetNormalTexture():SetAlpha(0)
end
end
--- Select/deselect an item in a scrollable list.
--- Two properties detailing the current selection state are exposed on the list's
--- `bagshuiData` table: `selectedEntries` and `selectedEntry`. See the comments
--- in `Ui:CreateScrollableList()` for more information.
--- This could probably use a refactor at some point -- it's gotten messy since
--- multi-select was added and there should be a way to pass a list of multiple
--- entries to select/deselect.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entryToSelect any? `entryFrame` UI element (or an element within one) *or* list entry value to select *or* nil to deselect.
---@param selectionState boolean? Force `entryToSelect` to be selected or deselected.
---@param scrollTo boolean? Scroll the list to the selected element when true.
function Ui:SetScrollableListSelection(listFrame, entryToSelect, selectionState, scrollTo)
-- Used to preserve selection.
listFrame.bagshuiData.lastSelection = entryToSelect
-- Consumed by `Ui:SetScrollableListEntrySelectionState()` to determine whether
-- things should be reset.
listFrame.bagshuiData._setScrollableListSelection_InProgress = true
local entryFrameToSelect
-- Flag to track when the selection was set by a user click as opposed to a programmatic value
-- so that we know whether to toggle selection if Control is down and the frame is already selected.
local entryToSelectWasFrame = false
local forceSelect = (selectionState == true)
local forceDeselect = (selectionState == false)
-- Sometimes we need to deselect everything.
-- Primarily for multi-select situations where the clicked entry was deselected
-- and a range select was then initiated.
local deselect = false
-- Invert the selection state instead of just selecting or deselecting.
local toggleSelection = (_G.IsControlKeyDown() or (listFrame.bagshuiData.checkboxes and not _G.IsShiftKeyDown()))
-- Figure out what we need to do.
if type(entryToSelect) == "table" and entryToSelect.GetParent then
-- entryToSelect is a UI element: Walk upwards to find the actual entry frame if this isn't one.
entryFrameToSelect = entryToSelect
while not entryFrameToSelect.bagshuiData.scrollableListEntry and entryFrameToSelect ~= _G.UIParent do
entryFrameToSelect = entryFrameToSelect:GetParent()
end
entryToSelectWasFrame = true
-- When true, the shift-key range selection tracking will be reset.
local updateSelectionRangeTracking = false
if (listFrame.bagshuiData.multiSelect and toggleSelection) or forceSelect or forceDeselect then
updateSelectionRangeTracking = true
-- Nothing else to do here, just preserve the existing selection.
elseif listFrame.bagshuiData.multiSelect and _G.IsShiftKeyDown() then
if listFrame.bagshuiData.lastUserSelectionAnchorStart then
-- Deselect the most recent set of shift-selected frames, if applicable.
-- This is what allows the behavior of decreasing or reversing the selection
-- range from the previous anchor.
if listFrame.bagshuiData.lastUserSelectionAnchorEnd then
self:SetScrollableListSelectionRange(
listFrame,
listFrame.bagshuiData.lastUserSelectionAnchorStart,
listFrame.bagshuiData.lastUserSelectionAnchorEnd,
nil
)
end
-- When the last action was to deselect, a shift+click range
-- should deselect all frames in the range.
if not listFrame.bagshuiData.lastUserSelectionAnchorWasSelected then
deselect = true
end
-- Select the current range of frames -- this updates the `listFrame.bagshuiData.selectedEntries` table.
-- Highlights will be applied updated the "Set selection" loop below.
self:SetScrollableListSelectionRange(
listFrame,
entryFrameToSelect.bagshuiData.sequentialFrameNum,
listFrame.bagshuiData.lastUserSelectionAnchorStart,
not deselect
)
-- This will be used on the next call to deselect the previous shift-selected range, if applicable.
listFrame.bagshuiData.lastUserSelectionAnchorEnd = entryFrameToSelect.bagshuiData.sequentialFrameNum
end
else
updateSelectionRangeTracking = true
-- List was not multi-select or Control/Shift were not pressed, so clear the existing selection.
BsUtil.TableClear(listFrame.bagshuiData.selectedEntries)
end
-- Change the start anchor and clear the end anchor so shift-selection behavior will reset.
if updateSelectionRangeTracking then
listFrame.bagshuiData.lastUserSelectionAnchorStart = entryFrameToSelect.bagshuiData.sequentialFrameNum
listFrame.bagshuiData.lastUserSelectionAnchorEnd = nil
end
-- Track the most recently-performed action so we know whether the next
-- shift+click range should select or deselect.
listFrame.bagshuiData.lastUserSelectionAnchorWasSelected = (
not entryFrameToSelect.bagshuiData.selected or not _G.IsShiftKeyDown()
)
else
-- When either nil or the value of an entry within the list is provided,
-- multi-select will not be an option and the existing selection will be cleared,
-- unless the selection state is being forced.
if not (forceSelect or forceDeselect) then
BsUtil.TableClear(listFrame.bagshuiData.selectedEntries)
end
-- This was a programmatic value, not a frame clicked by the user.
listFrame.bagshuiData.lastUserSelectionAnchorStart = nil
listFrame.bagshuiData.lastUserSelectionAnchorEnd = nil
-- entryToSelect was not a frame, so we need to -- find the entry frame that owns this value.
if entryToSelect then
for _, entryFrame in ipairs(listFrame.bagshuiData.entryFrames) do
if entryFrame.bagshuiData.scrollableListEntry == entryToSelect then
entryFrameToSelect = entryFrame
break
end
end
end
end
-- Couldn't find the right thing to select.
if entryToSelect and not entryFrameToSelect then
Bagshui:PrintError(
"Failed to find selection for entry " .. tostring(entryToSelect) .. " -- this shouldn't happen!"
)
return
end
-- Tracking variables for managing button state.
local readOnly = false -- One or more read-only entries are selected.
local firstEntry = false -- The selection includes the first entry.
local lastEntry = false -- The selection includes the last entry.
local numEntries = table.getn(listFrame.bagshuiData.entryFrames)
-- Set selection.
for listPosition, entryFrame in ipairs(listFrame.bagshuiData.entryFrames) do
if
(
(
-- This is the frame being selected.
(entryFrame == entryFrameToSelect)
-- Frame was already selected and selection is preserved.
or listFrame.bagshuiData.selectedEntries[entryFrame.bagshuiData.scrollableListEntry]
)
-- Frame was previously selected and control key is held or in checkbox mode -- toggle selection state.
and not ((entryFrame == entryFrameToSelect) and listFrame.bagshuiData.multiSelect and entryToSelectWasFrame and entryFrame.bagshuiData.selected and toggleSelection)
and not deselect
) or ((entryFrame == entryFrameToSelect) and forceSelect and not forceDeselect)
then
listFrame.bagshuiData.selectedEntry = entryFrame.bagshuiData.scrollableListEntry
self:SetScrollableListEntrySelectionState(listFrame, entryFrame, true)
if
type(entryFrame.bagshuiData.scrollableListEntryInfo) == "table"
and entryFrame.bagshuiData.scrollableListEntryInfo.readOnly
then
readOnly = true
end
if listPosition == 1 then
firstEntry = true
elseif listPosition == numEntries then
lastEntry = true
end
else
self:SetScrollableListEntrySelectionState(listFrame, entryFrame, false)
end
-- When a header is selected/deselected in a multiSelect list, set the
-- same state for all children. Since frames are processed in order,
-- within SetScrollableListSelection(), changes made here to
-- listFrame.bagshuiData.selectedEntries will be seen by the child frames
-- once they're reached.
if
listFrame.bagshuiData.hasHeaders
and entryFrameToSelect
and entryFrame == entryFrameToSelect
and listFrame.bagshuiData.multiSelect
and listFrame.bagshuiData.headerCheckboxes ~= false
and entryFrameToSelect.bagshuiData.isHeader
then
-- Scan forward until we reach the next header frame.
-- Within this range are the entry frames that belong to the current header.
for child = listPosition + 1, table.getn(listFrame.bagshuiData.entryFrames) do
if listFrame.bagshuiData.entryFrames[child].bagshuiData.isHeader then
break
end
listFrame.bagshuiData.selectedEntries[listFrame.bagshuiData.entryFrames[child].bagshuiData.scrollableListEntry] =
listFrame.bagshuiData.selectedEntries[entryFrame.bagshuiData.scrollableListEntry]
end
end
end
-- Sync header selection state with that of children.
local childCount = 0
local selectedChildren = 0
local lastHeader
for listPosition, entryFrame in ipairs(listFrame.bagshuiData.entryFrames) do
-- Increment child count.
if lastHeader and not entryFrame.bagshuiData.isHeader then
childCount = childCount + 1
if entryFrame.bagshuiData.selected then
selectedChildren = selectedChildren + 1
end
end
-- Set header selection state.
if
lastHeader
and (
-- This is the next header.
(entryFrame ~= lastHeader and entryFrame.bagshuiData.isHeader and childCount > 0)
-- We've reached the end of the list.
or listPosition == table.getn(listFrame.bagshuiData.entryFrames)
)
then
self:SetScrollableListEntrySelectionState(listFrame, lastHeader, (selectedChildren >= childCount))
end
-- Reset at next header.
if entryFrame.bagshuiData.isHeader then
childCount = 0
selectedChildren = 0
lastHeader = entryFrame
end
end
local numSelectedEntries = BsUtil.TrueTableSize(listFrame.bagshuiData.selectedEntries)
-- The `selectedEntry` property is a convenience for when there is only one entry selected.
if numSelectedEntries == 1 then
for selectedEntry, _ in pairs(listFrame.bagshuiData.selectedEntries) do
listFrame.bagshuiData.selectedEntry = selectedEntry
end
else
listFrame.bagshuiData.selectedEntry = nil
end
-- Enable/disable buttons based on special properties.
local enableDisableFunction
for _, button in pairs(listFrame.bagshuiData.buttons) do
enableDisableFunction = "Enable"
-- Disable buttons for special scenarios:
-- - Nothing is selected and button should only be enabled when something is selected.
-- - First/last entries.
-- - List entry has a readOnly property and button is flagged for read-only disable.
-- - Button is flagged to stay disabled.
if
(numSelectedEntries < 1 and button.bagshuiData.scrollableList_DisableIfNothingSelected)
or (numSelectedEntries > 1 and button.bagshuiData.scrollableList_DisableIfMultipleSelected)
or (firstEntry and button.bagshuiData.scrollableList_DisableIfFirstEntrySelected)
or (lastEntry and button.bagshuiData.scrollableList_DisableIfLastEntrySelected)
or (readOnly and button.bagshuiData.scrollableList_DisableIfReadOnly)
or (numEntries == 0 and button.bagshuiData.scrollableList_DisableIfEmptyList)
or (type(button.bagshuiData.scrollableList_DisableFunc) == "function" and button.bagshuiData.scrollableList_DisableFunc(
listFrame.bagshuiData.selectedEntries
))
or button.bagshuiData.scrollableList_Disable
then
enableDisableFunction = "Disable"
end
button[enableDisableFunction](button)
end
-- Done with internal updates and getting ready to invoke callback.
listFrame.bagshuiData._setScrollableListSelection_InProgress = false
-- Invoke callback.
-- Need to track whether we've just called it to prevent stack overflow.
if
not listFrame.bagshuiData.onSelectionChangedFunc_Called
and type(listFrame.bagshuiData.onSelectionChangedFunc) == "function"
then
listFrame.bagshuiData.onSelectionChangedFunc_Called = true
listFrame.bagshuiData.onSelectionChangedFunc(listFrame)
listFrame.bagshuiData.onSelectionChangedFunc_Called = false
end
-- Scroll to selected item if requested
if scrollTo then
-- Need a little delay for the ScrollFrame to know things have changed
Bagshui:QueueClassCallback(self, self.ScrollToListEntry, 0.005, false, listFrame, entryFrameToSelect)
end
end
--- Helper for `Ui:SetScrollableListSelection()` to update the selection state of an entry frame.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entryFrame table Frame within the list that should be updated.
---@param select boolean? `true` to select, 'false' or `nil` to deselect.
function Ui:SetScrollableListEntrySelectionState(listFrame, entryFrame, select)
if select then
entryFrame.bagshuiData.selected = true
listFrame.bagshuiData.selectedEntries[entryFrame.bagshuiData.scrollableListEntry] = true
entryFrame:GetHighlightTexture():SetAlpha(1)
entryFrame:LockHighlight()
else
entryFrame.bagshuiData.selected = false
listFrame.bagshuiData.selectedEntries[entryFrame.bagshuiData.scrollableListEntry] = nil
entryFrame:UnlockHighlight()
entryFrame:GetHighlightTexture():SetAlpha(0.5)
end
-- Sync checkbox state.
if entryFrame.bagshuiData.checkbox then
if listFrame.bagshuiData._setScrollableListSelection_InProgress then
entryFrame.bagshuiData.checkbox:Enable()
end
entryFrame.bagshuiData.checkbox:SetChecked(entryFrame.bagshuiData.selected)
end
end
--- Helper for `Ui:SetScrollableListSelection()` to select or deselect a range of frames.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param startNum number First frame to select or deselect. Can be a higher OR lower number than `endNum`.
---@param endNum number Last frame to select or deselect. Can be a higher OR lower number than `startNum`.
---@param select boolean? `true` to select, 'false' or `nil` to deselect.
function Ui:SetScrollableListSelectionRange(listFrame, startNum, endNum, select)
for i = startNum, endNum, (startNum < endNum and 1 or -1) do
listFrame.bagshuiData.selectedEntries[listFrame.bagshuiData.entryFrames[i].bagshuiData.scrollableListEntry] = (
select == true
) or nil
end
end
--- Scroll the ScrollFrame so the currently selected list item is visible.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entry any Value from the list's `bagshuiData.entries` array to scroll to, OR frame to scroll to.
function Ui:ScrollToListEntry(listFrame, entry)
-- Make sure there's something selected.
if not listFrame.bagshuiData.selectedEntry then
return
end
-- Find the frame for the selected entry.
local frameToShow
for i, entryFrame in ipairs(listFrame.bagshuiData.entryFrames) do
if entryFrame.bagshuiData.scrollableListEntry == entry or entryFrame == entry then
frameToShow = i
end
end
-- Couldn't find it - just bail.
if not frameToShow then
return
end
-- Find out how much we can scroll.
local scrollRange = listFrame.bagshuiData.scrollFrame:GetVerticalScrollRange()
-- There needs to be scrolling available.
if scrollRange == 0 then
return
end
-- Calculate where we need to scroll to.
-- There is probably a better/more accurate way to do this, but it seems to work.
local scrollHeight = listFrame.bagshuiData.scrollFrame:GetHeight()
local entryFrameHeight = listFrame.bagshuiData.rowHeight + listFrame.bagshuiData.rowSpacing
local entryFrameTop = (entryFrameHeight * (frameToShow - 1))
-- Start by assuming we're at the last frame, then calculate the correct location if needed.
local verticalScroll = scrollRange
if frameToShow < table.getn(listFrame.bagshuiData.entryFrames) then
verticalScroll = (entryFrameTop / (scrollHeight + scrollRange)) * scrollRange
end
-- Do the scroll.
listFrame.bagshuiData.scrollFrame:SetVerticalScroll(verticalScroll)
end
--- Put column header sort indicators in the correct state.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
function Ui:UpdateScrollableListColumnHeaders(listFrame)
if not listFrame.bagshuiData.entryColumns then
return
end
for i, col in ipairs(listFrame.bagshuiData.entryColumns) do
if listFrame.bagshuiData.columnHeaders[col.field] then
local columnFrame = listFrame.bagshuiData.columnHeaders[col.field]
local scrollFrame = listFrame.bagshuiData.scrollFrame
if col.currentSortOrder then
-- This is the colum we're sorting by, so show the sort indicator.
self:SetIconButtonTexture(
columnFrame,
"UI\\SortColumn" .. (col.currentSortOrder == "ASC" and "Asc" or "Desc")
)
-- Make sure all textures are sized and positioned correctly.
for _, textureName in ipairs(BS_UI_BUTTON_TEXTURES) do
local texture = columnFrame["Get" .. textureName .. "Texture"](columnFrame)
if texture then
texture:ClearAllPoints()
-- Manually position just to the right of the column header.
texture:SetPoint("LEFT", columnFrame, "LEFT", columnFrame.bagshuiData.text:GetStringWidth() + 8, 0)
texture:SetWidth(scrollFrame.bagshuiData.headerHeight - 2)
texture:SetHeight(scrollFrame.bagshuiData.headerHeight - 2)
end
end
else
-- Hide the sort indicator for all other columns by removing the texture.
self:SetIconButtonTexture(columnFrame, nil)
end
end
end
end
--- Mark all scrollable list entry frames in given array as reusable.
--- Reusing frames is necessary because they can't be destroyed.
---@param frameList table[] Array of scrollable list entry frames.
function Ui:ReleaseScrollableListFrames(frameList)
if type(frameList) ~= "table" then
return
end
for i = 1, table.getn(frameList) do
if frameList[i].bagshuiData and frameList[i].Hide then
frameList[i].bagshuiData.listFrame = nil -- This is the primary marker of reusability.
frameList[i].bagshuiData.scrollableListEntry = nil
frameList[i]:Hide()
frameList[i]:ClearAllPoints()
frameList[i]:SetParent(_G.UIParent)
end
end
end
--- Remove an entry from a scrollable list.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entry any Value from the list's `bagshuiData.entries` array to remove.
function Ui:ScrollableListRemove(listFrame, entry)
if not entry or string.len(tostring(entry)) == 0 then
return
end
-- Make sure we're working with a valid list.
self:CheckScrollableListInitialized(listFrame)
-- Find the location of the entry we're removing.
local arrayPosition = BsUtil.TableContainsValue(listFrame.bagshuiData.entries, entry)
-- Remove it and refresh the UI.
table.remove(listFrame.bagshuiData.entries, arrayPosition)
self:PopulateScrollableList(listFrame, nil, true, false)
-- Select the previous entry in the list.
if arrayPosition > 1 then
arrayPosition = arrayPosition - 1
end
if arrayPosition > table.getn(listFrame.bagshuiData.entries) then
arrayPosition = table.getn(listFrame.bagshuiData.entries)
end
self:SetScrollableListSelection(listFrame, listFrame.bagshuiData.entries[arrayPosition])
end
--- Move an entry in a scrollable list up or down.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param entry any Value from the list's `bagshuiData.entries` array to move.
---@param direction string BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP or BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DOWN
function Ui:ScrollableListMove(listFrame, entry, direction)
if not entry or string.len(tostring(entry)) == 0 then
return
end
if direction ~= BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP and direction ~= BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DOWN then
return
end
-- Make sure we're working with a valid list.
self:CheckScrollableListInitialized(listFrame)
-- Find the location of the entry we're moving.
local arrayPosition = BsUtil.TableContainsValue(listFrame.bagshuiData.entries, entry)
-- Make sure it's not already at the top/bottom when being moved up/down.
if
(direction == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP and arrayPosition == 1)
or (
direction == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.DOWN
and arrayPosition == table.getn(listFrame.bagshuiData.entries)
)
then
return
end
-- Move it, refresh the UI, and re-select it in the new position..
local newPosition = direction == BS_UI_SCROLLABLE_LIST_BUTTON_NAME.UP and arrayPosition - 1 or arrayPosition + 1
table.remove(listFrame.bagshuiData.entries, arrayPosition)
table.insert(listFrame.bagshuiData.entries, newPosition, entry)
self:PopulateScrollableList(listFrame, nil, true, false)
self:SetScrollableListSelection(listFrame, entry, nil, true)
end
--- Prompt for item IDs, links, or item strings to add to a scrollable item list.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
function Ui:ItemListPromptForNew(listFrame)
local dialogName = "itemListNew"
if not self.dialogProperties[dialogName] then
self:AddMultilineDialog(dialogName, {
prompt = L.ItemList_NewPrompt,
button1 = L.Add,
button1DisableOnEmptyText = true,
OnAccept = function(dialog)
self:ItemListAdd(dialog.data.listFrame, dialog:GetText())
end,
})
end
local dialog = self:ShowDialog(dialogName, nil, nil, nil, nil, self:FindWindowFrame(listFrame:GetParent()))
-- Pass stuff to dialog callbacks.
if dialog then
dialog.data.listFrame = listFrame
end
end
--- Display a read-only dialog containing the IDs of items in an item-type list.
--- There will be one ID per line followed by a Lua-style comment with the name:
--- ```
--- 8932 -- Alterac Swiss
--- 13935 -- Baked Salmon
--- ```
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
function Ui:ItemListOpenCopyDialog(listFrame)
local dialogName = "itemListCopy"
if not self.dialogProperties[dialogName] then
self:AddMultilineDialog(dialogName, {
prompt = L.ItemList_CopyPrompt,
readOnly = true,
button1 = "",
button2 = L.Close,
})
end
local items = ""
for _, entryFrame in ipairs(listFrame.bagshuiData.entryFrames) do
local itemInfo = entryFrame.bagshuiData.itemButton.bagshuiData.item
items = items
.. tostring(itemInfo.id)
.. string.rep(" ", 8 - string.len(tostring(itemInfo.id)))
.. "-- "
.. tostring(itemInfo.name)
.. BS_NEWLINE
end
items = string.sub(items, 1, -2) -- Remove trailing newline.
self:ShowDialog(dialogName, nil, nil, nil, items, self:FindWindowFrame(listFrame:GetParent()))
end
--- Add the given item to the specified scrollable item list.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param itemList string|number One or more item IDs, links, or itemStrings. Multiple should be separated by whitespace, commas, semicolons, ore newlines.
function Ui:ItemListAdd(listFrame, itemList)
if not itemList or not listFrame.bagshuiData or listFrame.bagshuiData.readOnly then
return
end
-- Make sure we're working with a valid list.
self:CheckScrollableListInitialized(listFrame, BS_UI_SCROLLABLE_LIST_TYPE.ITEM)
-- Remove comments.
itemList = string.gsub(itemList, "%-%-.-\n", BS_NEWLINE) -- Per-line.
itemList = string.gsub(itemList, "%-%-.-$", "") -- Very last line.
-- Break up the item list.
local items = BsUtil.Split(itemList, "[%s,;\n]+", true)
-- Add all items to the list.
for _, item in ipairs(items) do
-- Chat links pasted from databases will have a string like this:
-- /script DEFAULT_CHAT_FRAME:AddMessage("\124cffffffff\124Hitem:8932::::::::60:::::\124h[Alterac Swiss]\124h\124r");
-- We need to force the \124 into a pipe so it's parsed as an item.
item = string.gsub(item, "\\124", "|")
-- Parse out an item's ID from an item link.
local _, _, itemNum = string.find(item, "item:(%d+)")
-- Grab the first number in the provided string (or the item ID we found above).
item = BsUtil.ExtractNumber(itemNum or item)
-- Add to list.
if item then
BsUtil.TableInsertArrayItemUnique(listFrame.bagshuiData.entries, item)
end
end
-- Refresh UI.
self:PopulateScrollableList(listFrame, nil, true)
end
--- Ensure the supposed scrollable list frame passed to various functions is a valid Bagshui scrollable list frame.
---@param listFrame table `listFrame` return value from `Ui:CreateScrollableList()`.
---@param listType BS_UI_SCROLLABLE_LIST_TYPE? `listFrame` must be of this type or an error will be thrown.
function Ui:CheckScrollableListInitialized(listFrame, listType)
assert(type(listFrame) == "table", "Ui:CheckScrollableListInitialized(): listFrame is not a table")
assert(listFrame.GetName, "Ui:CheckScrollableListInitialized(): listFrame is not a frame")
local frameName = listFrame:GetName() or "<Unnamed Frame>"
assert(listFrame.bagshuiData, frameName .. " is not a Bagshui frame")
assert(listFrame.bagshuiData.entries, frameName .. " is not a Bagshui list frame")
assert(listFrame.bagshuiData.scrollableListType, frameName .. " is not a Bagshui scrollable list frame")
if listType then
assert(
listFrame.bagshuiData.scrollableListType == listType,
frameName .. " is not a Bagshui scrollable " .. tostring(listType) .. " list frame"
)
end
end
end)