local DEFAULT_TEMPLATE = "ZO_GamepadItemSubEntryTemplate" local DEFAULT_HEADER_TEMPLATE = "ZO_GamepadMenuEntryHeaderTemplate" ZO_GamepadInventoryList = ZO_InitializingCallbackObject:Subclass() --[[ Initializes the ZO_GamepadInventoryList. This should not be called directly, as it will be called by New(). control must be an XML control for intializing a parameteric list. inventoryType must be one of the Bag enum values, or a table containing multiple bag enum values. selectedDataCallback may be a function to call when the selected item has changed. May be nil. entryEditCallback may be a function to call when initializing the ZO_GamepadEntryData for display. If specified, it should take a single argument which will be the ZO_GamepadEntryData, and will be called after entry:InitializeInventoryVisualData() and entry.itemData is set. May be nil. categorizationFunction may be a function to call to retrieve the category string for an inventory item. If specified, should take a inventoryData (as returned by SHARED_INVENTORY:GenerateSingleSlotData) and return a string category. If nil, defaults to ZO_InventoryUtils_Gamepad_GetBestItemCategoryDescription(). sortFunction may be a function that is passed to table.sort to sort the entries for display. If nil, a default will be used tgat will sort alphabetically by category than by name. useTriggers: Should the control bind the triggers to jump categories when activated? If nil, defaults to true. ]] function ZO_GamepadInventoryList:Initialize(control, inventoryType, slotType, selectedDataCallback, entrySetupCallback, categorizationFunction, sortFunction, useTriggers, template, templateSetupFunction) self.control = control self.selectedDataCallback = selectedDataCallback self.entrySetupCallback = entrySetupCallback self.categorizationFunction = categorizationFunction self.sortFunction = sortFunction self.dataByBagAndSlotIndex = {} self.useTriggers = useTriggers ~= false -- nil => true self.template = template or DEFAULT_TEMPLATE self.currentSortType = ITEM_LIST_SORT_TYPE_CATEGORY if type(inventoryType) == "table" then self.inventoryTypes = inventoryType else self.inventoryTypes = { inventoryType } end for _, bagId in ipairs(self.inventoryTypes) do self.dataByBagAndSlotIndex[bagId] = {} end local function VendorEntryTemplateSetup(entryControl, data, selected, selectedDuringRebuild, enabled, activated) ZO_Inventory_BindSlot(data, slotType, data.slotIndex, data.bagId) ZO_SharedGamepadEntry_OnSetup(entryControl, data, selected, selectedDuringRebuild, enabled, activated) end self.list = ZO_GamepadVerticalParametricScrollList:New(self.control) self.list:AddDataTemplate(self.template, templateSetupFunction or VendorEntryTemplateSetup, ZO_GamepadMenuEntryTemplateParametricListFunction) self.list:AddDataTemplateWithHeader(self.template, templateSetupFunction or VendorEntryTemplateSetup, ZO_GamepadMenuEntryTemplateParametricListFunction, nil, DEFAULT_HEADER_TEMPLATE) -- generate the trigger keybinds so we can add/remove them later when necessary self.triggerKeybinds = {} ZO_Gamepad_AddListTriggerKeybindDescriptors(self.triggerKeybinds, self.list) local function SelectionChangedCallback(list, selectedData, oldSelectedData) if self.selectedDataCallback then self.selectedDataCallback(list, selectedData, oldSelectedData) end if selectedData then GAMEPAD_INVENTORY:PrepareNextClearNewStatus(selectedData) self:GetParametricList():RefreshVisible() end end local function OnEffectivelyShown() if self.selectedDataCallback then self.selectedDataCallback(self.list, self.list:GetTargetData()) end -- Don't activate on show in case there is another element that is intended to be active (ie. when the list is empty and the header should be selected instead) end local function OnEffectivelyHidden() GAMEPAD_INVENTORY:TryClearNewStatusOnHidden() -- To ensure the list isn't left greyed-out once we've entered due to factors such as pending animations that would -- cause an effective hidden state, no longer calling self:Deactivate() here. Expecting managing classes to handle this. end local function OnInventoryUpdated(bagId) for _, currentInventoryType in ipairs(self.inventoryTypes) do if bagId == currentInventoryType then self:RefreshList() break end end end local function OnSingleSlotInventoryUpdate(bagId, slotIndex) if not self.control:IsHidden() then for _, currentInventoryType in ipairs(self.inventoryTypes) do if bagId == currentInventoryType then local bag = self.dataByBagAndSlotIndex[bagId] --we should always have a bag table to match all entries in self.inventoryTypes but this will catch any issue with that internalassert(bag ~= nil) if bag then local entry = bag[slotIndex] if entry then local itemData = SHARED_INVENTORY:GenerateSingleSlotData(currentInventoryType, slotIndex) if itemData then itemData.bestGamepadItemCategoryName = ZO_InventoryUtils_Gamepad_GetBestItemCategoryDescription(itemData) self:SetupItemEntry(entry, itemData) self.list:RefreshVisible() else -- The item was removed. self:RefreshList() end else -- The item is new. self:RefreshList() end -- don't loop over any more inventoryTypes, we've handled the slot update break end end end end end self:SetOnSelectedDataChangedCallback(SelectionChangedCallback) self.refresh = ZO_Refresh:New() self.refresh:AddRefreshGroup("list", { RefreshAll = function() self:OnRefreshList(self.shouldTriggerRefreshCallback) self.shouldTriggerRefreshCallback = nil end, }) local function OnUpdate() self.list:OnUpdate() self.refresh:UpdateRefreshGroups() end self.control:SetHandler("OnUpdate", OnUpdate) self.control:SetHandler("OnEffectivelyShown", OnEffectivelyShown) self.control:SetHandler("OnEffectivelyHidden", OnEffectivelyHidden) SHARED_INVENTORY:RegisterCallback("FullInventoryUpdate", OnInventoryUpdated) SHARED_INVENTORY:RegisterCallback("SingleSlotInventoryUpdate", OnSingleSlotInventoryUpdate) end function ZO_GamepadInventoryList:ClearInventoryTypes() self.inventoryTypes = {} self:RefreshList() end function ZO_GamepadInventoryList:SetInventoryTypes(inventoryTypes) local newInventoryTypes if type(inventoryTypes) == "table" then newInventoryTypes = inventoryTypes else newInventoryTypes = { inventoryTypes } end if newInventoryTypes then local sameBags = true for i, newBag in ipairs(newInventoryTypes) do if self.inventoryTypes[i] ~= newBag then sameBags = false break end end if not sameBags then self.inventoryTypes = newInventoryTypes --Refresh list will also regenerate these tables for each bag, but if the inventory list is hidden it will set a dirty flag instead and do it when it is effectively shown. This is a problem --when a single slot update occurs because it checks self.inventoryTypes to know if we have a bag table in dataByBagAndSlotIndex to work with but we haven't rebuilt dataByBagAndSlotIndex yet -- so we end up with an index on a bag table that doesn't exist. So we rebuild dataByBagAndSlotIndex immediately here. self.dataByBagAndSlotIndex = {} for _, bagId in ipairs(self.inventoryTypes) do self.dataByBagAndSlotIndex[bagId] = {} end self:RefreshList() return true end end return false end function ZO_GamepadInventoryList:AddInventoryType(inventoryType) if self.inventoryTypes then table.insert(self.inventoryTypes, inventoryType) else self.inventoryTypes = {inventoryType} end self:RefreshList() end function ZO_GamepadInventoryList:RegisterForScreenNarration(listScreen) SCREEN_NARRATION_MANAGER:RegisterParametricListScreen(self.list, listScreen) end function ZO_GamepadInventoryList:UnregisterForScreenNarration() SCREEN_NARRATION_MANAGER:UnregisterParametricList(self.list) end --[[ Add a function called when the selected item is changed. ]]-- function ZO_GamepadInventoryList:SetOnSelectedDataChangedCallback(selectedDataCallback) self.list:SetOnSelectedDataChangedCallback(selectedDataCallback) end --[[ Remove a function called when the selected item is changed. ]]-- function ZO_GamepadInventoryList:RemoveOnSelectedDataChangedCallback(selectedDataCallback) self.list:RemoveOnSelectedDataChangedCallback(selectedDataCallback) end --[[ Add a function called when the target data is changed. ]]-- function ZO_GamepadInventoryList:SetOnTargetDataChangedCallback(selectedDataCallback) self.list:SetOnTargetDataChangedCallback(selectedDataCallback) end --[[ Remove a function called when the target data is changed. ]]-- function ZO_GamepadInventoryList:RemoveOnTargetDataChangedCallback(selectedDataCallback) self.list:RemoveOnTargetDataChangedCallback(selectedDataCallback) end --[[ categorizationFunction function may be a function which takes a inventory data and returns a category string. ]]-- function ZO_GamepadInventoryList:SetCategorizationFunction(categorizationFunction) self.categorizationFunction = categorizationFunction self:RefreshList() end --[[ Sets the function which is passed to table.sort() when sorting the inventory inventory items. ]]-- function ZO_GamepadInventoryList:SetSortFunction(sortFunction) self.sortFunction = sortFunction self:RefreshList() end --[[ entryEditCallback may be a function to call when initializing the ZO_GamepadEntryData for display. If specified, it should take a single argument which will be the ZO_GamepadEntryData, and will be called after entry:InitializeInventoryVisualData() and entry.itemData is set. May be nil. ]]-- function ZO_GamepadInventoryList:SetEntrySetupCallback(entrySetupCallback) self.entrySetupCallback = entrySetupCallback self:RefreshList() end --[[ itemFilterFunction function may be a function which takes an inventory data and returns whether to include the item in the inventory list. If set to nil, all items will be included. ]]-- function ZO_GamepadInventoryList:SetItemFilterFunction(itemFilterFunction) self.itemFilterFunction = itemFilterFunction self:RefreshList() end --[[ SetOnRefreshListCallback sets a function that will be called whenever the list is refreshed. ]]-- function ZO_GamepadInventoryList:SetOnRefreshListCallback(onRefreshListCallback) self.onRefreshListCallback = onRefreshListCallback end --[[ Sets whether to bind the triggers to jump categories while the list is active. If the list is currently active, this will add/remove the bindings immediately. ]]-- function ZO_GamepadInventoryList:SetUseTriggers(useTriggers) if self.useTriggers == useTriggers then -- Exit out if no change, to simplify later logic. return end self.useTriggers = useTriggers if self.list:IsActive() then if useTriggers then KEYBIND_STRIP:AddKeybindButtonGroup(self.triggerKeybinds) else KEYBIND_STRIP:RemoveKeybindButtonGroup(self.triggerKeybinds) end end end --[[ Returns the currently selected entry's data. ]]-- function ZO_GamepadInventoryList:GetTargetData() return self.list:GetTargetData() end --[[ Returns the currently selected entry's control. ]]-- function ZO_GamepadInventoryList:GetTargetControl() return self.list:GetTargetControl() end --[[ Returns the underlying parameteric list. ]]-- function ZO_GamepadInventoryList:GetParametricList() return self.list end --[[ Moves the selection to the next item. ]]-- function ZO_GamepadInventoryList:MoveNext() return self.list:MoveNext() end --[[ Moves the selection to the previous item. ]]-- function ZO_GamepadInventoryList:MovePrevious() return self.list:MovePrevious() end --[[ Query if the inventory list is empty ]]-- do local function HasSlotData(inventoryType, slotIndex, filterFunction) local slotData = SHARED_INVENTORY:GenerateSingleSlotData(inventoryType, slotIndex) if slotData then if (not filterFunction) or filterFunction(slotData) then return true end end return false end function ZO_GamepadInventoryList:IsEmpty() for _, bagId in ipairs(self.inventoryTypes) do local filterFunction = self.itemFilterFunction for slotIndex in ZO_IterateBagSlots(bagId) do if HasSlotData(bagId, slotIndex, filterFunction) then return false end end end return true end end --[[ Passthrough functions for operating on the parametric list itself ]]-- function ZO_GamepadInventoryList:SetFirstIndexSelected(...) self.list:SetFirstIndexSelected(...) end function ZO_GamepadInventoryList:SetLastIndexSelected(...) self.list:SetLastIndexSelected(...) end function ZO_GamepadInventoryList:SetPreviousSelectedDataByEval(...) return self.list:SetPreviousSelectedDataByEval(...) end function ZO_GamepadInventoryList:SetNextSelectedDataByEval(...) return self.list:SetNextSelectedDataByEval(...) end --[[ Moves the selection to the specified item. The same arguments can be provided as ZO_ParametricScrollList.SetSelectedIndex() accepts. ]]-- function ZO_GamepadInventoryList:SetSelectedIndex(...) self.list:SetSelectedIndex(...) end --[[ Activates the inventory list. ]]-- function ZO_GamepadInventoryList:Activate() self.list:Activate() if self.useTriggers then KEYBIND_STRIP:AddKeybindButtonGroup(self.triggerKeybinds) end end --[[ Deactivates the inventory list. ]]-- function ZO_GamepadInventoryList:Deactivate() if self.useTriggers then KEYBIND_STRIP:RemoveKeybindButtonGroup(self.triggerKeybinds) end self.list:Deactivate() end --[[ GetNumItems the inventory list. ]]-- function ZO_GamepadInventoryList:GetNumItems() return self.list:GetNumItems() end --[[ GetSelectedIndex the inventory list. ]]-- function ZO_GamepadInventoryList:GetSelectedIndex() return self.list:GetSelectedIndex() end --[[ An internal helper function used to initialize or update a ZO_GamepadEntryData with itemData. ]]-- function ZO_GamepadInventoryList:SetupItemEntry(entry, itemData) entry:InitializeInventoryVisualData(itemData) entry.itemData = itemData if self.entrySetupCallback then self.entrySetupCallback(entry) end end local DEFAULT_GAMEPAD_ITEM_SORT = { bestGamepadItemCategoryName = { tiebreaker = "name" }, name = { tiebreaker = "requiredLevel" }, requiredLevel = { tiebreaker = "requiredChampionPoints", isNumeric = true }, requiredChampionPoints = { tiebreaker = "iconFile", isNumeric = true }, iconFile = { tiebreaker = "uniqueId" }, uniqueId = { isId64 = true }, } local function ItemSortFunc(data1, data2) return ZO_TableOrderingFunction(data1, data2, "bestGamepadItemCategoryName", DEFAULT_GAMEPAD_ITEM_SORT, ZO_SORT_ORDER_UP) end function ZO_GamepadInventoryList:AddSlotDataToTable(slotsTable, inventoryType, slotIndex) local itemFilterFunction = self.itemFilterFunction local categorizationFunction = self.categorizationFunction or ZO_InventoryUtils_Gamepad_GetBestItemCategoryDescription local slotData = SHARED_INVENTORY:GenerateSingleSlotData(inventoryType, slotIndex) if slotData then if (not itemFilterFunction) or itemFilterFunction(slotData) then -- itemData is shared in several places and can write their own value of bestItemCategoryName. -- We'll use bestGamepadItemCategoryName instead so there are no conflicts. slotData.bestGamepadItemCategoryName = categorizationFunction(slotData) table.insert(slotsTable, slotData) end end end function ZO_GamepadInventoryList:GenerateSlotTable() local slots = {} for _, bagId in ipairs(self.inventoryTypes) do for slotIndex in ZO_IterateBagSlots(bagId) do self:AddSlotDataToTable(slots, bagId, slotIndex) end end table.sort(slots, self.sortFunction or ItemSortFunc) return slots end do local function IsInFilteredCategories(filterCategories, itemData) -- No category selected, don't filter out anything. if ZO_IsTableEmpty(filterCategories) then return true end for _, filterData in ipairs(itemData.filterData) do if filterCategories[filterData] then return true end end return false end --[[ If the list is hidden, queues a refresh for the next time the list is shown. Otherwise, clears and fully refreshes the list. ]]-- function ZO_GamepadInventoryList:RefreshList(shouldTriggerRefreshListCallback) self.shouldTriggerRefreshCallback = self.shouldTriggerRefreshCallback or shouldTriggerRefreshListCallback self.refresh:RefreshAll("list") end function ZO_GamepadInventoryList:OnRefreshList(shouldTriggerRefreshListCallback) self.list:Clear() for _, bagId in ipairs(self.inventoryTypes) do self.dataByBagAndSlotIndex[bagId] = {} end local slots = self:GenerateSlotTable() local currentBestCategoryName = nil for _, itemData in ipairs(slots) do local passesTextFilter = TEXT_SEARCH_MANAGER:IsItemInSearchTextResults(self.searchContext, BACKGROUND_LIST_FILTER_TARGET_BAG_SLOT, itemData.bagId, itemData.slotIndex) local passesCategoryFilter = IsInFilteredCategories(self.filterCategories, itemData) if passesTextFilter and passesCategoryFilter then local entry = ZO_GamepadEntryData:New(itemData.name, itemData.iconFile) self:SetupItemEntry(entry, itemData) if self.currentSortType == ITEM_LIST_SORT_TYPE_CATEGORY and itemData.bestGamepadItemCategoryName ~= currentBestCategoryName then currentBestCategoryName = itemData.bestGamepadItemCategoryName entry:SetHeader(currentBestCategoryName) self.list:AddEntryWithHeader(self.template, entry) else self.list:AddEntry(self.template, entry) end self.dataByBagAndSlotIndex[itemData.bagId][itemData.slotIndex] = entry end end self.list:Commit() if shouldTriggerRefreshListCallback and self.onRefreshListCallback then self.onRefreshListCallback(self.list) end end end function ZO_GamepadInventoryList:SetSearchContext(context) self.searchContext = context end function ZO_GamepadInventoryList:GetSearchContext() return self.searchContext end --[[ Refreshes the appearance of the list without clearing and fully refreshing the list ]]-- function ZO_GamepadInventoryList:RefreshVisible() self.list:RefreshVisible() end --[[ Enables or disables direcitonal input to the list. enable must be a boolean. ]]-- function ZO_GamepadInventoryList:SetDirectionalInputEnabled(enable) self.list:SetDirectionalInputEnabled(enable) end --[[ Sets if the inventory list is aligned to the screen center. Does not need an expectedEntryHeight ]]-- function ZO_GamepadInventoryList:SetAlignToScreenCenter(alignToScreenCenter, expectedEntryHeight) self.list:SetAlignToScreenCenter(alignToScreenCenter, expectedEntryHeight) end --[[ Gets the control of the list ]]-- function ZO_GamepadInventoryList:GetControl() return self.list:GetControl() end --[[ Returns true if the list is active ]]-- function ZO_GamepadInventoryList:IsActive() return self.list:IsActive() end function ZO_GamepadInventoryList:SetNoItemText(noItemText) self.list:SetNoItemText(noItemText) end function ZO_GamepadInventoryList:ClearList() self.list:Clear() end