--[[ Each entry can contain the following: local entry = { control, -- Control to focus on. Can be nil. data, -- Data associated with the focus. Can be nil. highlight, -- Highlight texture that is alpha'ed in and out via the highlightFadeAnimation when focus is lost/received. Can be nil. highlightFadeAnimation, -- Animation to play on a highlight texture. If nil, and highlight exists, will create a FocusAlphaFadeAnimation connected to the highlight texture iconScaleAnimation, -- Animation to play on an icon. If nil, and entry.control.icon exists, will create a FocusIconScaleAnimation connected to entry.control.icon. If false, no animation will be created or played. activate = function(control, data) blah() end, -- Function that is called when focused is received. If nil, defaults to control:Activate() if that function exists. deactivate = function(control, data) blah() end, -- Function that is called when focused is lost. If nil, defaults to control:Deactivate() if that function exists. canFocus = function(control) return true end, -- Function that returns a boolean indicating whether the control can receive focus. If nil, defaults to true. } --]] --Layout consts-- ZO_GAMEPAD_FOCUS_HIGHLIGHT_INSIDE_PADDING = 4 ZO_GAMEPAD_FOCUS_HIGHLIGHT_WIDE_PADDING = ZO_GAMEPAD_FOCUS_HIGHLIGHT_INSIDE_PADDING + 5 local FOCUS_MOVEMENT_TYPES = { MOVE_NEXT = 1, MOVE_PREVIOUS = 2, } local function GamepadListPlaySound(movementType) if movementType == FOCUS_MOVEMENT_TYPES.MOVE_NEXT then PlaySound(SOUNDS.GAMEPAD_MENU_DOWN) elseif movementType == FOCUS_MOVEMENT_TYPES.MOVE_PREVIOUS then PlaySound(SOUNDS.GAMEPAD_MENU_UP) end end ZO_GamepadFocus = ZO_Object:Subclass() function ZO_GamepadFocus:New(...) local object = ZO_Object.New(self) object:Initialize(...) return object end --[[ Initializes a GamepadFocus with an optional movementController or direction. If movementController is not nil, one will be created using direction. If direction is not specified, it will default to vertical. ]] function ZO_GamepadFocus:Initialize(control, movementController, direction) self.data = {} self.control = control self.index = nil self.savedIndex = nil self:InitializeMovementController(movementController, direction) self:SetActive(false) self:SetPlaySoundFunction(GamepadListPlaySound) self.directionalInputEnabled = true end function ZO_GamepadFocus:InitializeMovementController(movementController, direction) self.movementController = movementController or ZO_MovementController:New(direction or MOVEMENT_CONTROLLER_DIRECTION_VERTICAL) end function ZO_GamepadFocus:SetActive(active, retainFocus) if self.active ~= active then self.active = active if self.active then self:SetFocusByIndex(self.savedIndex ~= 0 and self.savedIndex or 1) self:ValidateFocus() if self.directionalInputEnabled then DIRECTIONAL_INPUT:Activate(self, self.control) end else self.savedIndex = self.index if not retainFocus then self:ClearFocus() end DIRECTIONAL_INPUT:Deactivate(self) end end end function ZO_GamepadFocus:IsActive() return self.active end function ZO_GamepadFocus:SetFocusChangedCallback(onFocusChangedFunction) self.onFocusChangedFunction = onFocusChangedFunction end function ZO_GamepadFocus:SetLeaveFocusAtBeginningCallback(onLeaveFocusAtBeginningFunction) self.onLeaveFocusAtBeginningFunction = onLeaveFocusAtBeginningFunction end function ZO_GamepadFocus:Activate(retainFocus) self:SetActive(true, retainFocus) end function ZO_GamepadFocus:Deactivate(retainFocus) self:SetActive(false, retainFocus) end function ZO_GamepadFocus:AddEntry(entry) table.insert(self.data, entry) if not entry.highlightFadeAnimation and entry.highlight then entry.highlightFadeAnimation = ANIMATION_MANAGER:CreateTimelineFromVirtual("FocusAlphaFadeAnimation", entry.highlight) end if entry.control and entry.control.icon and entry.iconScaleAnimation == nil then entry.iconScaleAnimation = ANIMATION_MANAGER:CreateTimelineFromVirtual("FocusIconScaleAnimation", entry.control.icon) end if entry.highlightFadeAnimation then if entry.highlight then entry.highlight:SetHidden(false) end entry.highlightFadeAnimation:PlayInstantlyToStart() end if entry.iconScaleAnimation then entry.iconScaleAnimation:PlayInstantlyToStart() end end local function DefaultEqualityFunction(item, value) return value.control == item end function ZO_GamepadFocus:RemoveMatchingEntries(compareItem, equalityFunction) if not equalityFunction then equalityFunction = DefaultEqualityFunction end local focus = self:GetFocus() local shouldUpdateFocus = false for k, v in ipairs(self.data) do if equalityFunction(compareItem, v) then if focus and k <= focus then self:ClearFocus() shouldUpdateFocus = true -- k == focus is a special case if k < focus then focus = focus - 1 end end table.remove(self.data, k) end end if shouldUpdateFocus then self:SetFocusByIndex(zo_clamp(focus, 1, #self.data)) end end function ZO_GamepadFocus:RemoveAllEntries() self:ClearFocus() for k in ipairs(self.data) do self.data[k] = nil end self.savedIndex = nil end local function EnableFocus(data, index) if index and #data > 0 and index <= #data then local item = data[index] if item.activate then item.activate(item.control, item.data) elseif item.control and (item.control.Activate ~= nil) then item.control:Activate() end if item.highlightFadeAnimation then item.highlightFadeAnimation:PlayForward() end if item.iconScaleAnimation then item.iconScaleAnimation:PlayForward() end return true end end local function DisableFocus(data, index, noAnimations) if index and #data > 0 and (index <= #data) then local item = data[index] if item.deactivate then item.deactivate(item.control, item.data) elseif item.control and (item.control.Deactivate ~= nil) then item.control:Deactivate() end if item.highlightFadeAnimation then if noAnimations then item.highlightFadeAnimation:PlayInstantlyToStart() else item.highlightFadeAnimation:PlayBackward() end end if item.iconScaleAnimation then if noAnimations then item.iconScaleAnimation:PlayInstantlyToStart() else item.iconScaleAnimation:PlayBackward() end end return true end end function ZO_GamepadFocus:GetFocusItem(includeSavedFocus) local focusIndex = self:GetFocus(includeSavedFocus) return self:GetItem(focusIndex) end function ZO_GamepadFocus:GetItem(index) if index then return self.data[index] end return nil end function ZO_GamepadFocus:GetItemCount() return #self.data end function ZO_GamepadFocus:GetNarrationText() local focusItem = self:GetFocusItem() if focusItem and focusItem.narrationText then return focusItem.narrationText(focusItem) end end function ZO_GamepadFocus:GetAdditionalInputNarrationFunction() local focusItem = self:GetFocusItem() if focusItem then return focusItem.additionalInputNarrationFunction end end function ZO_GamepadFocus:GetHeaderNarration() local focusItem = self:GetFocusItem() if focusItem and focusItem.headerNarrationFunction then return focusItem.headerNarrationFunction() end end function ZO_GamepadFocus:GetSubHeaderNarration() local focusItem = self:GetFocusItem() if focusItem and focusItem.subHeaderNarrationFunction then return focusItem.subHeaderNarrationFunction(focusItem) end end function ZO_GamepadFocus:GetFooterNarration() local focusItem = self:GetFocusItem() if focusItem and focusItem.footerNarrationFunction then return focusItem.footerNarrationFunction() end end function ZO_GamepadFocus:IsFocused(control) local isControlFocused = false local focusItem = self:GetFocusItem() if focusItem and focusItem.control and focusItem.control == control then isControlFocused = true end return isControlFocused end function ZO_GamepadFocus:SetFocusToMatchingEntry(compareItem, equalityFunction) local didSetFocus if not equalityFunction then equalityFunction = DefaultEqualityFunction end local focus = self:GetFocus() local shouldUpdateFocus = false for k, v in ipairs(self.data) do if equalityFunction(compareItem, v) then self:SetFocusByIndex(k) didSetFocus = true break end end return didSetFocus end function ZO_GamepadFocus:SetFocusToFirstEntry(setIfInactive) self:SetFocusByIndex(1, setIfInactive) end function ZO_GamepadFocus:SetFocusByIndex(newIndex, setIfInactive) if self.index then local oldIndex = self.index self.index = nil -- Set to nil before calling DisableFocus() to guard against recursion. DisableFocus(self.data, oldIndex) end -- DisableFocus() could have triggered a SetFocus(), so we need to check whether self.index is nil before enabling focus again. if newIndex and not self.index then if (not setIfInactive) and (not self.active) then self.savedIndex = newIndex elseif EnableFocus(self.data, newIndex) then -- EnableFocus() could have triggered a SetFocus, so we need to check self.index again. if not self.index then self.index = newIndex self.savedIndex = newIndex -- Needed if setIfInactive so that activate "restores" focus properly. end end end if self.onFocusChangedFunction then self.onFocusChangedFunction(self:GetFocusItem()) end end function ZO_GamepadFocus:SetSelectedIndex(newIndex, setIfInactive) self:SetFocusByIndex(newIndex, setIfInactive) end function ZO_GamepadFocus:GetFocus(includeSavedFocus) if self.active then return self.index elseif includeSavedFocus then return self.savedIndex end end function ZO_GamepadFocus:ClearFocus() if self.index then local oldIndex = self.index self.index = nil local DISABLE_ANIMATIONS = true DisableFocus(self.data, oldIndex, DISABLE_ANIMATIONS) end if self.onFocusChangedFunction then self.onFocusChangedFunction(self:GetFocusItem()) end end local function FindPrevFocusIndex(oldIndex, focusItems) if not oldIndex then return nil end local newIndex = oldIndex - 1 while newIndex >= 1 do local data = focusItems[newIndex] if not data.canFocus or data.canFocus(data.control) then return newIndex end newIndex = newIndex - 1 end end local function FindNextFocusIndex(oldIndex, focusItems) if not oldIndex then return nil end local newIndex = oldIndex + 1 while newIndex <= #focusItems do local data = focusItems[newIndex] if not data.canFocus or data.canFocus(data.control) then return newIndex end newIndex = newIndex + 1 end end function ZO_GamepadFocus:MovePrevious() local index = FindPrevFocusIndex(self.index, self.data) if index then self:SetFocusByIndex(index) self.onPlaySoundFunction(FOCUS_MOVEMENT_TYPES.MOVE_PREVIOUS) return true else if self.onLeaveFocusAtBeginningFunction then self.onLeaveFocusAtBeginningFunction() end end return false end function ZO_GamepadFocus:MoveNext() local index = FindNextFocusIndex(self.index, self.data) if index then self:SetFocusByIndex(index) self.onPlaySoundFunction(FOCUS_MOVEMENT_TYPES.MOVE_NEXT) return true end return false end function ZO_GamepadFocus:UpdateDirectionalInput() local result = self.movementController:CheckMovement() if result == MOVEMENT_CONTROLLER_MOVE_NEXT then self:MoveNext() elseif result == MOVEMENT_CONTROLLER_MOVE_PREVIOUS then self:MovePrevious() end end function ZO_GamepadFocus:SetPlaySoundFunction(fn) self.onPlaySoundFunction = fn end function ZO_GamepadFocus:SetDirectionalInputEnabled(enabled) self.directionalInputEnabled = enabled if enabled then DIRECTIONAL_INPUT:Activate(self, self.control) else DIRECTIONAL_INPUT:Deactivate(self) end end function ZO_GamepadFocus:ValidateFocus() local data = self:GetFocusItem() --The entry that was already focused is no longer eligible for focus if data and data.canFocus and not data.canFocus(data.control) then --Try to select the previous thing if not self:MovePrevious() then --Nothing to select backward, so move forward if not self:MoveNext() then --We failed to select anything forward or backwards, so there is no valid focus return false end end end return true end