-- 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 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 ()` -- 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 `ReusableItemListFrameItemSlotButton`. 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 "" 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)