Files
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

411 lines
17 KiB
Lua

-- Bagshui Active Sort Order Management
-- Exposes: BsSortOrders (and Bagshui.components.SortOrders)
-- Raises: BAGSHUI_SORTORDER_UPDATE
Bagshui:AddComponent(function()
Bagshui:AddConstants({
-- Allowed properties and default values.
BS_SORT_ORDER_SKELETON = {
name = "",
fields = {
-- This permits an array of tables containing:
{
lookup = "",
field = "",
direction = "asc",
reverseWords = false,
},
},
},
-- Template for creating a new sort order.
BS_NEW_SORT_ORDER_TEMPLATE = {
name = "",
fields = {},
},
})
-- Key in _G.BagshuiData where sort orders will be stored.
local SORT_ORDERS_DATA_STORAGE_KEY = "sortOrders"
-- SortOrders class is built on the ObjectList prototype.
local SortOrders = Bagshui.prototypes.ObjectList:New({
dataStorageKey = SORT_ORDERS_DATA_STORAGE_KEY,
objectVersion = Bagshui.config.SortOrders.version,
objectMigrationFunction = Bagshui.config.SortOrders.migrate,
objectName = "SortOrder",
objectNamePlural = "SortOrders",
objectSkeleton = BS_SORT_ORDER_SKELETON,
objectTemplate = BS_NEW_SORT_ORDER_TEMPLATE,
defaults = Bagshui.config.SortOrders.defaults,
wikiPage = BS_WIKI_PAGES.SortOrders,
-- Can't initialize until Categories are done.
initEvent = "BAGSHUI_CATEGORIES_LOADED",
-- Lists of valid item fields, populated by AddValidItemFields.
validItemFields = {},
sortFields = {},
sortFieldsSortedByName = {},
-- Properties that need to be looked up in a table that isn't the item.
-- ```
-- <Lookup Name> = {
-- property = "",
-- table = <Lookup Table>,
-- }
-- ```
-- Populated by AddValidItemFields().
itemFieldLookups = {},
-- Debug.
debugResetOnLoad = false and BS_DEBUG,
})
Bagshui.environment.BsSortOrders = SortOrders
Bagshui.components.SortOrders = SortOrders
--- Initialization of Sort Orders depends on having the Categories class initialized
--- because of BS_CATEGORY_PROPERTIES_ALLOWED_IN_SORT_ORDERS. To deal with this,
--- Categories raises BAGSHUI_CATEGORIES_LOADED and we initialize based on that event.
function SortOrders:Init()
-- Build list of field names that are valid for use in sort orders.
-- Item properties.
self:AddValidItemFields(BS_ITEM_PROPERTIES_ALLOWED_IN_SORT_ORDERS, BS_ITEM_SKELETON)
-- Category properties as `Category.<property>`.
self:AddValidItemFields(
BS_CATEGORY_PROPERTIES_ALLOWED_IN_SORT_ORDERS,
BS_CATEGORY_SKELETON,
"Category",
"bagshuiCategoryId",
BsCategories.list
)
-- Load default sort orders and do other prototype class initialization.
-- IMPORTANT: This can't happen until after valid field lists are built.
-- Calls ObjectList:Init().
self._super.Init(self)
-- Ensure no invalid fields in Sort Orders.
self:CleanSortOrders()
-- Link the list of sort orders to the defaultSortOrder setting's list of valid choices.
-- This will let the Settings class know which sort orders are valid.
Bagshui.prototypes.Settings._settingInfo.defaultSortOrder.choices = self.list
-- Add the auto-split menus.
-- Sort Orders (used in Settings and Group menus).
Bagshui.prototypes.Menus:AddAutoSplitMenu(BS_AUTO_SPLIT_MENU_TYPE.SORT_ORDERS, {
defaultIdList = self.sortedIdLists.name,
sortFunc = function(sortOrderIds)
self:SortIdList(sortOrderIds)
end,
nameFunc = function(id)
return self:GetName(id) or tostring(id)
end,
tooltipTitleFunc = function()
-- Since sort order names can be long, just display "Details:" as the tooltip title.
return string.format(L.Symbol_Colon, L.Details)
end,
tooltipTextFunc = function(id, originalTooltipText)
if not id then
return originalTooltipText
end
-- Create a tooltip with one line per field, formatted like:
-- Category Name ASC
-- Type ASC
-- Subtype ASC
-- Quality DESC
-- Name [reversed] ASC
-- count DESC
-- bagNum ASC
-- slotNum ASC
local tooltipText
for _, sort in pairs(self.list[id].fields) do
local sortField = self:GetFieldIdentifier(sort.field, sort.lookup, "::")
local sortFieldFriendly = self.sortFields[sortField] and self.sortFields[sortField].friendlyName
or (sort.field .. (sort.lookup and ("." .. sort.lookup) or ""))
tooltipText = (tooltipText and tooltipText .. BS_NEWLINE or "") .. sortFieldFriendly
if sort.reverseWords then
tooltipText = string.format(L.Suffix_Reversed, tooltipText)
end
tooltipText = tooltipText
.. " "
.. LIGHTYELLOW_FONT_COLOR_CODE
.. string.lower(sort.direction)
.. FONT_COLOR_CODE_CLOSE
end
return tooltipText
end,
extraItems = {
{
text = string.format(L.Symbol_Ellipsis, L.Manage),
tooltipTitle = L.Manage,
tooltipText = string.format(L.Prefix_Manage, L.SortOrders),
checked = false,
value = {
func = function()
Bagshui:CloseMenus()
self:Open()
end,
},
},
},
})
-- Sort Fields (used by Sort Order editor).
Bagshui.prototypes.Menus:AddAutoSplitMenu(BS_AUTO_SPLIT_MENU_TYPE.SORT_FIELDS, {
defaultIdList = self.sortFieldsSortedByName,
sortFunc = function(sortFieldIds)
-- Don't need to do anything here - these don't ever change after startup.
end,
nameFunc = function(id, menuValue)
return self.sortFields[id].friendlyName
end,
})
end
-- Using a list of valid fields and an object template (table of `<fieldName> = <defaultValue>`), populate the validItemFields table.
---@param validFieldList string[] Array of fields that are allowed in sorting.
---@param objectTemplate table<string,string> List of `{ [fieldName] = [defaultValue] }`, used to figure out field type.
---@param lookupObjectName string? Name of the "lookup object" (for example, "Category").
---@param lookupProperty string? When lookup = lookupObjectName is encountered in a sort order, what item property should be used to find the lookup object (i.e. bagshuiCategoryId).
---@param lookupTable table? Where can the object identified by lookupObjectProperty be found? (for example, BsCategories.list).
function SortOrders:AddValidItemFields(validFieldList, objectTemplate, lookupObjectName, lookupProperty, lookupTable)
assert(
not lookupObjectName or (lookupObjectName and lookupProperty and lookupTable),
"SortOrders:AddValidItemFields(): All lookup parameters are required when one is provided"
)
-- This is a lookup object, so store lookup information.
if string.len(lookupObjectName or "") > 0 then
self.itemFieldLookups[lookupObjectName] = {
property = lookupProperty,
table = lookupTable,
}
end
-- Iterate the list of fields and save information.
for _, fieldName in pairs(validFieldList) do
-- Add to the lists of available fields.
local uniqueName = self:GetFieldIdentifier(fieldName, lookupObjectName)
self.sortFields[uniqueName] = {
friendlyName = self:GetFieldIdentifier(L["ItemPropFriendly_" .. fieldName], lookupObjectName, " "),
property = fieldName,
type = type(objectTemplate[fieldName]),
allowReverseWords = (fieldName == "name"), -- Hardcoding this as I can't currently think of any scenario where anything other than item name would makes sense for reversed words.
lookup = lookupObjectName,
lookupProperty = lookupProperty,
lookupTable = lookupTable,
}
table.insert(self.sortFieldsSortedByName, uniqueName)
end
table.sort(self.sortFieldsSortedByName)
end
-- Generate a unique ID for fields and to create their friendly name (when a space is passed in for separator).
---@param fieldName string Name of the field.
---@param lookupObjectName string? Name of the "lookup object" (for example, "Category"), if any.
---@param separator string? String to use between lookup object name and field name.
---@return string fieldIdentifier [lookupObjectName][separator][fieldName]
function SortOrders:GetFieldIdentifier(fieldName, lookupObjectName, separator)
return (lookupObjectName and (lookupObjectName .. (separator or "::")) or "") .. fieldName
end
--- See `ObjectList:GetUses()`.
---@param objectId string|number
---@param objectMetadata table Metadata about the object.
---@return boolean inUse Whether the object is being used.
function SortOrders:GetUses(objectId, objectMetadata)
return BsProfiles:GetProfilesUsingObject(objectId, objectMetadata, BsSortOrders)
end
-- Ensure the sort order is valid before saving.
---@param sortOrderId string|number ID of the sort order being saved.
---@param sortOrderInfo table Sort order object to be saved.
function SortOrders:DoPreSaveOperations(sortOrderId, sortOrderInfo)
if not self:ValidateSortOrder(sortOrderId, sortOrderInfo) then
return
end
end
--- Test whether the given sort order is valid.
--- Throws an error if it's invalid (unless `removeIfInvalid` passed).
---@param sortOrderId string|number
---@param sortOrderInfo table<string,any> Sort order object to validate.
---@param removeIfInvalid boolean? Remove the sort field instead of throwing an error when it's invalid.
---@return boolean
function SortOrders:ValidateSortOrder(sortOrderId, sortOrderInfo, removeIfInvalid)
assert(sortOrderId, "sortOrderId is required to perform validation")
local baseError = "Sort order " .. sortOrderId .. " failed validation: "
assert(sortOrderInfo.name, baseError .. "name is missing")
assert(sortOrderInfo.fields, baseError .. "fields is missing")
-- Walk backwards so we can use table.remove().
for index = table.getn(sortOrderInfo.fields), 1, -1 do
local sort = sortOrderInfo.fields[index]
local fieldId = self:GetFieldIdentifier(sort.field, sort.lookup)
if not self.sortFields[fieldId] then
if removeIfInvalid then
table.remove(sortOrderInfo.fields, index)
else
-- This should really never be hit if everything else is handled
-- well, so there's no need to make it pretty. It's really more
-- for the built-in sort orders than anything.
assert(
false,
baseError .. self:GetFieldIdentifier(sort.field, sort.lookup, ".") .. " is not a valid sort field"
)
end
end
self:ValidateSortFieldDirection(sort)
end
return true
end
--- Set the sort direction of a field to "asc" if it's not currently "desc".
---@param sortFieldTable table A table from the list of sort fields within a sort order. Should typically have `field` and `direction` keys.
function SortOrders:ValidateSortFieldDirection(sortFieldTable)
sortFieldTable.direction = BsUtil.Trim(string.lower(sortFieldTable.direction or "")) == "desc" and "desc" or "asc"
end
--- Loop through all Sort Orders and make sure they're good before trying to use them.
--- This basically just removes any invalid sort fields (anything not in
--- BS_ITEM_PROPERTIES_ALLOWED_IN_SORT_ORDERS), so be sure to take care of any migration
--- by updating Config\SortOrders.lua first.
function SortOrders:CleanSortOrders()
for id, sortOrderInfo in pairs(self.list) do
self:ValidateSortOrder(id, sortOrderInfo, true)
end
end
--- Helper function: Obtain either the normal or "Sort" version of a lookup table item property.
---@param lookupObject table Object to pull property values from.
---@param lookupProperty string Property to look up.
---@return any propertyValue Property value.
local function SortGroup_GetLookupFieldProperty(lookupObject, lookupProperty)
if lookupObject[lookupProperty .. "Sort"] then
return lookupObject[lookupProperty .. "Sort"]
end
return lookupObject[lookupProperty]
end
--- Helper function: Reverse a property, split by individual words.
--- Automatically handles item suffixes ("of the Whale", etc)
---@param item table Entry from the Bagshui inventory cache.
---@param propertyValue string String to be reversed.
---@param propertyName string Item property, used to determine whether to go into name suffix mode.
---@return string reversedString Reversed string.
local function SortGroup_GetReversedByWordsPropertyString(item, propertyValue, propertyName)
-- Turn "A B C of the X" into "C B A X the of".
if propertyName == "name" and string.len(item.suffixName or "") > 0 then
return BsUtil.ReverseStringByWords(item.baseName) .. " " .. BsUtil.ReverseStringByWords(item.suffixName)
end
return BsUtil.ReverseStringByWords(propertyValue)
end
--- Do the work of sorting the items in a group.
---@param groupItems table[] Array of Bagshui inventory cache items.
---@param groupConfig table Group settings from the layout structure.
---@param defaultSortOrderId string|number? ID of sort order to use if the group doesn't have one configured. Falls back to BS_DEFAULT_SORT_ORDER_ID if needed.
function SortOrders:SortGroup(groupItems, groupConfig, defaultSortOrderId)
-- Only declare the sorting function once.
-- This function consumes self._sortGroup_SortFields as a way to get another parameter into the function.
if not self._sortGroup_Sort then
self._sortGroup_Sort = function(itmA, itmB)
-- Loop through all sort fields in order and check each one to see if it's applicable
for i, sort in ipairs(self._sortGroup_SortFields) do
-- Start with some defaults.
local itemProperty = sort.field
local lookupTable = nil
-- Determine whether this is a lookup field.
if sort.lookup and self.itemFieldLookups[sort.lookup] then
itemProperty = self.itemFieldLookups[sort.lookup].property
lookupTable = self.itemFieldLookups[sort.lookup].table
end
-- Pull the value of the property we're sorting by.
local valA = itmA[itemProperty]
local valB = itmB[itemProperty]
-- Special cases.
if lookupTable then
-- For lookup mode, we need to replace valA and valB with the lookup property values.
valA = SortGroup_GetLookupFieldProperty(lookupTable[valA], sort.field)
valB = SortGroup_GetLookupFieldProperty(lookupTable[valB], sort.field)
elseif sort.reverseWords then
-- Reverse properties if requested.
valA = SortGroup_GetReversedByWordsPropertyString(itmA, valA, itemProperty)
valB = SortGroup_GetReversedByWordsPropertyString(itmB, valB, itemProperty)
end
-- Can't do anything if both are nil.
if type(valA) == "nil" and type(valB) == "nil" then
return false
end
-- Make sure sort is not case-sensitive.
if type(valA) == "string" then
valA = string.lower(valA)
end
if type(valB) == "string" then
valB = string.lower(valB)
end
-- Make sure we don't try to compare to nil.
if type(valA) ~= "nil" and type(valB) == "nil" then
valB = type(valA) == "number" and 0 or ""
elseif type(valA) == "nil" and type(valB) ~= "nil" then
valA = type(valB) == "number" and 0 or ""
end
-- Return the proper value depending on whether this is ascending or
-- descending sort (true if valA should come before valB).
if (sort.direction == "asc" and valA < valB) or (sort.direction == "desc" and valA > valB) then
return true
elseif (sort.direction == "asc" and valA > valB) or (sort.direction == "desc" and valA < valB) then
return false
end
end
return false
end
end
-- SortGroup() actually starts here.
-- Safety check.
if type(groupItems) ~= "table" then
return
end
-- Start by assuming the default sort order.
local sortOrderId = (defaultSortOrderId and self.list[defaultSortOrderId]) and defaultSortOrderId
or BS_DEFAULT_SORT_ORDER_ID
-- If the group has a valid sort order, change to that.
if type(groupConfig) == "table" and groupConfig.sortOrder then
if self.list[groupConfig.sortOrder] then
sortOrderId = groupConfig.sortOrder
else
-- If the configured sort order ID doesn't exist, reset it to to default.
groupConfig.sortOrder = nil
end
end
-- Sort the groupItems table.
self._sortGroup_SortFields = self.list[sortOrderId].fields
table.sort(groupItems, self._sortGroup_Sort)
end
end)