Script v1_7_1_0
- AI
- Animals
- Collections
- Contracts
- Debug
- Economy
- Elements
- EnvironmentalScore
- Errors
- Events
- GUI
- AnimalScreen
- AnimationElement
- BitmapElement
- BoxLayoutElement
- BreadcrumbsElement
- ButtonElement
- ButtonOverlay
- ChatWindow
- CheckedOptionElement
- ClearElement
- ColorPickButtonElement
- ConstructionScreen
- ContextActionDisplay
- ControlBarDisplay
- DialogElement
- DynamicFadedBitmapElement
- FillLevelsDisplay
- FlowLayoutElement
- FocusManager
- FrameElement
- FrameReferenceElement
- GameInfoDisplay
- GameInfoDisplayMobile
- GamePausedDisplay
- Gui
- GuiDataSource
- GuiElement
- GuiMixin
- GuiOverlay
- GuiProfile
- GuiSoundPlayer
- GuiTopDownCamera
- GuiTopDownCursor
- GuiUtils
- HUDDisplayElement
- HUDElement
- HUDFrameElement
- HUDPopupMessage
- HUDSliderElement
- HUDTextDisplay
- IndexChangeSubjectMixin
- IndexStateElement
- InfoDisplay
- InfoHUDBox
- InGameIcon
- IngameMap
- IngameMapElement
- InputGlyphElement
- InputGlyphElementUI
- InputHelpDisplay
- KeyValueInfoHUDBox
- ListElement
- ListItemElement
- MapOverlayGenerator
- MixerWagonHUDExtension
- MultiTextOptionElement
- MultiValueTween
- Overlay
- PagingElement
- PictureElement
- PlatformIconElement
- PlayerControlPadDisplay
- PlaySampleMixin
- RenderElement
- RoundStatusBar
- ScreenElement
- ScrollingLayoutElement
- SettingsModel
- SideNotification
- SideNotificationMobile
- SliderElement
- SpeakerDisplay
- SpeedMeterDisplay
- SpeedSliderDisplay
- StatusBar
- SteeringSliderDisplay
- TabbedMenu
- TabbedMenuFrameElement
- TabbedMenuWithDetails
- TableElement
- TableHeaderElement
- TerrainLayerElement
- TextBackdropElement
- TextElement
- TextInputElement
- ThreePartBitmapElement
- TimerElement
- ToggleButtonElement
- TopNotification
- Tween
- TweenSequence
- VariableWorkWidthHUDExten...
- VehicleHUDExtension
- VehicleSchemaDisplay
- VideoElement
- WardrobeScreen
- Handtools
- Hud
- I3d
- Input
- Jobs
- Maps
- Materials
- Misc
- Objects
- Parameters
- Placeables
- Placement
- Player
- Shop
- Sounds
- Specialization
- Specializations
- StateMachine
- Statistics
- Tasks
- Triggers
- Utils
- Vehicles
Engine v1_7_1_0
- AI
- Animation
- Camera
- Entity
- Fillplanes
- general
- General
- I3D
- Input
- Lighting
- Math
- Network
- Node
- NoteNode
- Overlays
- Particle System
- Physics
- Rendering
- Scenegraph
- Shape
- Sound
- Spline
- String
- Terrain Detail
- Text Rendering
- Tire Track
- VoiceChat
- XML
Foundation Reference
FocusManager
DescriptionThe FocusManager controls which element in the menu system is currently focused and allows menu control with only keyboard or gamepad. For each participating gui element the focus state and the next focused gui element in each direction is stored. This data is set up directly in the xml file of the gui screen and loaded through the loadElementFromXML() function or manually loaded within code by using the loadElementFromCustomValues() method. Focus handling is independent for every screen in the GUI. To swap screens the setGui() method has to be used. The focus system is then controlled with 5 actions: MENU_UP, MENU_DOWN, MENU_RIGHT and MENU_LEFT to change the currently focused element in the specified direction and MENU_ACCEPT to activate the currently focused element. When using dynamically changing objects which cannot be set directly in the XML file of the screen the method createLinkageSystemForElements() can be used to set up direction links between the passed elements automatically.Functions
- checkElementDistance
- getClosestPointOnBoundingBox
- getDirectionForAxisValue
- getElementById
- getFocusedElement
- getFocusOverrideFunction
- getNestedFocusTarget
- getNextFocusElement
- getShortestBoundingBoxVector
- hasFocus
- inputEvent
- isDirectionLocked
- isFocusInputLocked
- isLocked
- linkElements
- loadElementFromCustomValues
- loadElementFromXML
- lockFocusInput
- releaseLock
- releaseMovementFocusInput
- removeElement
- requireLock
- resetFocusInputLocks
- serveAutoFocusId
- setElementFocusOverlayState
- setFocus
- setGui
- setHighlight
- setSoundPlayer
- unsetFocus
- unsetHighlight
- updateFocus
checkElementDistance
DescriptionChecks the distance between two GuiElements with the aim of incrementally finding the closest other element in a direction within a screen view.Definition
checkElementDistance(curElement Current, other Other, dirX Scan, dirY Scan, curElementOffsetY Position, closestOther Previously, closestDistanceSq Squared)Arguments
curElement | Current | checking GuiElement |
other | Other | GuiElement to compare |
dirX | Scan | direction vector x component, normalized to unit length |
dirY | Scan | direction vector y component, normalized to unit length |
curElementOffsetY | Position | y offset of current element's bounding volume, used when checking for wrap-around |
closestOther | Previously | closest other GuiElement |
closestDistanceSq | Squared | distance from the current checking element to the previously closest other GuiElement |
505 | function FocusManager.checkElementDistance(curElement, other, dirX, dirY, curElementOffsetY, closestOther, closestDistanceSq) |
506 | local retOther = closestOther |
507 | local retDistSq = closestDistanceSq |
508 | |
509 | local elementBox = curElement:getBorders() |
510 | elementBox[2] = elementBox[2] + curElementOffsetY |
511 | elementBox[4] = elementBox[4] + curElementOffsetY |
512 | local elementCenter = curElement:getCenter() |
513 | elementCenter[2] = elementCenter[2] + curElementOffsetY |
514 | |
515 | if other ~= curElement and not other.disabled and other:getIsVisible() and other:canReceiveFocus() and not (other:isChildOf(curElement) or curElement:isChildOf(other)) then |
516 | local otherBox = other:getBorders() |
517 | local otherCenter = other:getCenter() |
518 | |
519 | -- get vector between bounding box points |
520 | local elementDirX, elementDirY = FocusManager.getShortestBoundingBoxVector(elementBox, otherBox, otherCenter) |
521 | |
522 | -- test direction and distance of bounding box points |
523 | local boxDistanceSq = MathUtil.vector2LengthSq(elementDirX, elementDirY) |
524 | local dot = MathUtil.dotProduct(elementDirX, elementDirY, 0, dirX, dirY, 0) |
525 | if boxDistanceSq < FocusManager.EPSILON then -- boundaries touch, use center points for direction check |
526 | dot = MathUtil.dotProduct(otherCenter[1] - elementCenter[1], otherCenter[2] - elementCenter[2], 0, dirX, dirY, 0) |
527 | end |
528 | |
529 | if dot > 0 then -- other element lies in scanning direction |
530 | local useOther = false |
531 | |
532 | -- when two elements are equally close, choose the one further up (-y) and/or further left (-x) |
533 | if closestOther and math.abs(closestDistanceSq - boxDistanceSq) < FocusManager.EPSILON then |
534 | -- also compare dot products |
535 | local closestBox = closestOther:getBorders() |
536 | local closestCenter = closestOther:getCenter() |
537 | local toClosestX, toClosestY = FocusManager.getShortestBoundingBoxVector(elementBox, closestBox, closestCenter) |
538 | local closestDot = MathUtil.dotProduct(toClosestX, toClosestY, 0, dirX, dirY, 0) |
539 | |
540 | if math.abs(closestDot - dot) < FocusManager.EPSILON then -- same distance and angle as previous best |
541 | -- when going up, go right first, etc. --> ensure symmetric paths in all directions |
542 | if dirY > 0 then |
543 | useOther = other.absPosition[1] > closestOther.absPosition[1] |
544 | elseif dirY < 0 then |
545 | useOther = other.absPosition[1] < closestOther.absPosition[1] |
546 | elseif dirX > 0 then |
547 | useOther = other.absPosition[2] > closestOther.absPosition[2] |
548 | elseif dirX < 0 then |
549 | useOther = other.absPosition[2] < closestOther.absPosition[2] |
550 | end |
551 | elseif dot > closestDot then -- when distance is equal and angles differ, prefer the one closer to the movement direction |
552 | useOther = true |
553 | end |
554 | elseif boxDistanceSq < closestDistanceSq then |
555 | useOther = true |
556 | end |
557 | |
558 | if useOther then |
559 | retOther = other |
560 | retDistSq = boxDistanceSq |
561 | end |
562 | end |
563 | end |
564 | |
565 | return retOther, retDistSq |
566 | end |
getClosestPointOnBoundingBox
DescriptionGiven a point and bounding box, get the closest other point on the bounding box circumference. If the point lies within the bounding box, it is returned unchanged.Definition
getClosestPointOnBoundingBox(x Point, y Point, boxMinX Bounding, boxMinY Bounding, boxMaxX Bounding, boxMaxY Bounding)Arguments
x | Point | X |
y | Point | Y |
boxMinX | Bounding | box minimum point X |
boxMinY | Bounding | box minimum point Y |
boxMaxX | Bounding | box maximum point X |
boxMaxY | Bounding | box maximum point Y |
Closest | point | x, y |
459 | function FocusManager.getClosestPointOnBoundingBox(x, y, boxMinX, boxMinY, boxMaxX, boxMaxY) |
460 | local px, py = x, y |
461 | if x < boxMinX then |
462 | px = boxMinX |
463 | elseif x > boxMaxX then |
464 | px = boxMaxX |
465 | end |
466 | |
467 | if y < boxMinY then |
468 | py = boxMinY |
469 | elseif y > boxMaxY then |
470 | py = boxMaxY |
471 | end |
472 | |
473 | return px, py |
474 | end |
getDirectionForAxisValue
DescriptionGet a direction value for a given menu input action and valueDefinition
getDirectionForAxisValue()Code
362 | function FocusManager.getDirectionForAxisValue(inputAction, value) |
363 | if value == nil then |
364 | return nil |
365 | end |
366 | |
367 | local direction = nil |
368 | if inputAction == InputAction.MENU_AXIS_UP_DOWN then |
369 | if value < 0 then |
370 | direction = FocusManager.BOTTOM |
371 | elseif value > 0 then |
372 | direction = FocusManager.TOP |
373 | end |
374 | elseif inputAction == InputAction.MENU_AXIS_LEFT_RIGHT then |
375 | if value < 0 then |
376 | direction = FocusManager.LEFT |
377 | elseif value > 0 then |
378 | direction = FocusManager.RIGHT |
379 | end |
380 | end |
381 | |
382 | return direction |
383 | end |
getElementById
DescriptionGet a focusable GuiElement in the current view by its ID.Definition
getElementById()Code
111 | function FocusManager:getElementById(id) |
112 | return self.currentFocusData.idToElementMapping[id] |
113 | end |
getFocusedElement
DescriptionGet the currently focused GuiElementDefinition
getFocusedElement()Code
117 | function FocusManager:getFocusedElement() |
118 | return self.currentFocusData.focusElement |
119 | end |
getFocusOverrideFunction
DescriptionGet a closure override function for elements' getFocusOverride() methods.Definition
getFocusOverrideFunction(forDirections List, substitute Element, useSubstituteForFocus (Optional))Arguments
forDirections | List | of directions to override |
substitute | Element | to substitute as focus target in overridden direction |
useSubstituteForFocus | (Optional) | If true, the substitute parameter will be used as the origin for finding the next focus target in the overridden direction. |
902 | function FocusManager:getFocusOverrideFunction(forDirections, substitute, useSubstituteForFocus) |
903 | if forDirections == nil or #forDirections < 1 then |
904 | return function(elementSelf, dir) return false, nil end |
905 | end |
906 | |
907 | local f = function(elementSelf, dir) |
908 | for _, overrideDirection in pairs(forDirections) do |
909 | if dir == overrideDirection then |
910 | if useSubstituteForFocus then |
911 | local next = self:getNextFocusElement(substitute, dir) |
912 | if next then |
913 | return true, next |
914 | end |
915 | else |
916 | return true, substitute |
917 | end |
918 | end |
919 | end |
920 | |
921 | return false, nil |
922 | end |
923 | |
924 | return f |
925 | end |
getNestedFocusTarget
DescriptionGet an element's focus target at the deepest nesting depth, e.g. when multiple nested layouts point down to their child elements until only a single element is left which points to itself.Definition
getNestedFocusTarget(element GuiElement, direction Focus)Arguments
element | GuiElement | whose focus target needs to be retrieved |
direction | Focus | navigation direction |
Focus | target |
627 | function FocusManager.getNestedFocusTarget(element, direction) |
628 | local target = element |
629 | local prevTarget = nil |
630 | while target and prevTarget ~= target do |
631 | prevTarget = target |
632 | target = target:getFocusTarget(FocusManager.OPPOSING_DIRECTIONS[direction], direction) |
633 | end |
634 | |
635 | return target |
636 | end |
getNextFocusElement
DescriptionFind the next other element to the one provided in a given navigation directionDefinition
getNextFocusElement(element GUI, direction Direction)Arguments
element | GUI | element which needs a focus link |
direction | Direction | constant [TOP | BOTTOM | LEFT | RIGHT] |
Next | GUI | element in given direction which can be linked, actual scanning direction used (may change in wrap around scenarios) |
573 | function FocusManager:getNextFocusElement(element, direction) |
574 | -- if there is a configured next element, return that |
575 | local nextFocusId = element.focusChangeData[direction] |
576 | if nextFocusId then |
577 | return self.currentFocusData.idToElementMapping[nextFocusId], direction |
578 | end |
579 | -- otherwise, find the next one based on proximity: |
580 | local dirX, dirY = unpack(FocusManager.DIRECTION_VECTORS[direction]) |
581 | |
582 | local closestOther = nil |
583 | local closestDistance = math.huge |
584 | |
585 | for _, other in pairs(self.currentFocusData.idToElementMapping) do |
586 | closestOther, closestDistance = FocusManager.checkElementDistance(element, other, dirX, dirY, 0, closestOther, closestDistance) |
587 | end |
588 | |
589 | if closestOther == nil then |
590 | -- wrap around |
591 | if direction == FocusManager.LEFT then |
592 | -- look up instead |
593 | closestOther, direction = self:getNextFocusElement(element, FocusManager.TOP) |
594 | elseif direction == FocusManager.RIGHT then |
595 | -- look down instead |
596 | closestOther, direction = self:getNextFocusElement(element, FocusManager.BOTTOM) |
597 | else |
598 | -- get the right test elements |
599 | local validWrapElements = self.currentFocusData.idToElementMapping -- screen wrap around |
600 | if element.parent and element.parent.wrapAround then -- local box/area wrap around if required |
601 | validWrapElements = element.parent.elements |
602 | end |
603 | |
604 | local wrapOffsetY = 0 |
605 | if direction == FocusManager.TOP then |
606 | wrapOffsetY = -1.2 - element.size[2] -- below screen must be <-1 to work in all cases, even though screen space is defined within [0, 1] |
607 | elseif direction == FocusManager.BOTTOM then |
608 | wrapOffsetY = 1.2 + element.size[2] -- above screen |
609 | end |
610 | |
611 | -- try wrapping around |
612 | for _, other in pairs(validWrapElements) do |
613 | closestOther, closestDistance = FocusManager.checkElementDistance(element, other, dirX, dirY, wrapOffsetY, closestOther, closestDistance) |
614 | end |
615 | end |
616 | end |
617 | |
618 | return closestOther, direction |
619 | end |
getShortestBoundingBoxVector
DescriptionCalculate the shortest connecting line segment between two bounding boxes. Overlapping boxes will result in flipped directions, so take care.Definition
getShortestBoundingBoxVector()Code
479 | function FocusManager.getShortestBoundingBoxVector(elementBox, otherBox, otherCenter) |
480 | local ePointX, ePointY = FocusManager.getClosestPointOnBoundingBox( |
481 | otherCenter[1], otherCenter[2], |
482 | elementBox[1], elementBox[2], elementBox[3], elementBox[4]) |
483 | |
484 | local oPointX, oPointY = FocusManager.getClosestPointOnBoundingBox( |
485 | ePointX, ePointY, -- use the previously calculated bounding box point here to get the closest boundary distance |
486 | otherBox[1], otherBox[2], otherBox[3], otherBox[4]) |
487 | |
488 | -- get vector between bounding box points |
489 | local elementDirX = oPointX - ePointX |
490 | local elementDirY = oPointY - ePointY |
491 | |
492 | return elementDirX, elementDirY |
493 | end |
hasFocus
DescriptionDetermine if a GuiElement is currently focused.Definition
hasFocus()Code
892 | function FocusManager:hasFocus(element) |
893 | return ((self.currentFocusData.focusElement == element) and (element.focusActive)) |
894 | end |
inputEvent
DescriptionHandles input and changes focus if required and possible.Definition
inputEvent(action Name, value Input, eventUsed Usage)Arguments
action | Name | of navigation action which triggered the event, see InputAction |
value | Input | value [-1, 1] |
eventUsed | Usage | flag, no action is taken if this is true |
True | if | the input event has been consumed, false otherwise |
328 | function FocusManager:inputEvent(action, value, eventUsed) |
329 | local element = self.currentFocusData.focusElement |
330 | |
331 | local pressedAccept = false |
332 | local pressedUp = action == InputAction.MENU_AXIS_UP_DOWN and value > g_analogStickVTolerance |
333 | local pressedDown = action == InputAction.MENU_AXIS_UP_DOWN and value < -g_analogStickVTolerance |
334 | local pressedLeft = action == InputAction.MENU_AXIS_LEFT_RIGHT and value < -g_analogStickHTolerance |
335 | local pressedRight = action == InputAction.MENU_AXIS_LEFT_RIGHT and value > g_analogStickHTolerance |
336 | |
337 | if action == InputAction.MENU_AXIS_UP_DOWN then |
338 | self:updateFocus(element, pressedUp, FocusManager.TOP, eventUsed) |
339 | self:updateFocus(element, pressedDown, FocusManager.BOTTOM, eventUsed) |
340 | elseif action == InputAction.MENU_AXIS_LEFT_RIGHT then |
341 | self:updateFocus(element, pressedLeft, FocusManager.LEFT, eventUsed) |
342 | self:updateFocus(element, pressedRight, FocusManager.RIGHT, eventUsed) |
343 | end |
344 | |
345 | if not eventUsed and element ~= nil and not element.needExternalClick then |
346 | pressedAccept = action == InputAction.MENU_ACCEPT |
347 | if pressedAccept and not self:isFocusInputLocked(action) then |
348 | -- elements can get unfocused, accept is only allowed for currently focused and visible elements |
349 | if element.focusActive and element:getIsVisible() then |
350 | self.focusSystemMadeChanges = true |
351 | element:onFocusActivate() |
352 | self.focusSystemMadeChanges = false |
353 | end |
354 | end |
355 | end |
356 | |
357 | return eventUsed or pressedUp or pressedDown or pressedLeft or pressedRight or pressedAccept |
358 | end |
isDirectionLocked
DescriptionDetermine if focus navigation in a given direction is currently locked.Definition
isDirectionLocked(direction Navigation)Arguments
direction | Navigation | direction as defined in constants |
True | if | navigation in given direction is locked |
886 | function FocusManager:isDirectionLocked(direction) |
887 | return self.lastInput[direction] ~= nil |
888 | end |
isFocusInputLocked
DescriptionChecks if the focus manager has an input lock on input.Definition
isFocusInputLocked(inputAxis InputAction, value Axis, True if)Arguments
inputAxis | InputAction | axis or action code |
value | Axis | value [-1, 1] or nil if not a directional axis |
True | if | locked, false otherwise |
390 | function FocusManager:isFocusInputLocked(inputAxis, value) |
391 | local key = FocusManager.getDirectionForAxisValue(inputAxis, value) |
392 | if key == nil and inputAxis ~= InputAction.MENU_AXIS_UP_DOWN and inputAxis ~= InputAction.MENU_AXIS_LEFT_RIGHT then |
393 | key = inputAxis |
394 | end |
395 | |
396 | if self.lastInput[key] and self.lockUntil[key] > g_time then |
397 | return true |
398 | else |
399 | return false |
400 | end |
401 | end |
isLocked
DescriptionCheck if focus input is locked.Definition
isLocked()Code
878 | function FocusManager:isLocked() |
879 | return FocusManager.isFocusLocked |
880 | end |
linkElements
DescriptionLinks an element's focus navigation to another element for a given direction. The link is unidirectional from source to target. If bi-directional links are desired, call this method again with swapped arguments.Definition
linkElements(sourceElement Source, direction Navigation, targetElement Target)Arguments
sourceElement | Source | element which receives the focus link. |
direction | Navigation | direction for the link, is not required to be the actual visual direction. |
targetElement | Target | element |
314 | function FocusManager:linkElements(sourceElement, direction, targetElement) |
315 | if targetElement == nil then |
316 | sourceElement.focusChangeData[direction] = "nil" |
317 | else |
318 | sourceElement.focusChangeData[direction] = targetElement.focusId |
319 | end |
320 | end |
loadElementFromCustomValues
DescriptionAdd an element to the focus system with custom values. The caller should ensure that explicitly set focus IDs are unique. If a duplicate ID is encountered, only the first element with that focus ID is considered for focusing. The method returns a boolean value to indicate any problems with data assignment. Callers can evaluate the value to check if the given parameters were valid. If in doubt or when no elaborate focus navigation is needed, rely on automatic focus ID generation by omitting the ID parameter (or set it to nil).Definition
loadElementFromCustomValues(element Element, focusId Focus, focusChangeData Custom, focusActive If, isAlwaysFocusedOnOpen If)Arguments
element | Element | to add to focus system |
focusId | Focus | ID for element |
focusChangeData | Custom | focus navigation data for the element (map of direction to focus ID) |
focusActive | If | true, the element should be focused right now |
isAlwaysFocusedOnOpen | If | true, the element is supposed to be focused when its parent view is opened. |
True | if | the element and all of its children could be set up with the given values, false otherwise. |
231 | function FocusManager:loadElementFromCustomValues(element, focusId, focusChangeData, focusActive, isAlwaysFocusedOnOpen) |
232 | if focusId and self.currentFocusData.idToElementMapping[focusId] then |
233 | return false -- ignore element, caller is responsible for sensible ID assignment when specified |
234 | end |
235 | |
236 | if not element.focusId then |
237 | if not focusId then |
238 | focusId = FocusManager.serveAutoFocusId() |
239 | end |
240 | |
241 | element.focusId = focusId |
242 | end |
243 | |
244 | element.focusChangeData = element.focusChangeData or focusChangeData or {} |
245 | element.focusActive = focusActive |
246 | element.isAlwaysFocusedOnOpen = isAlwaysFocusedOnOpen |
247 | |
248 | if FocusManager.allElements[element] == nil then |
249 | FocusManager.allElements[element] = {} |
250 | end |
251 | table.insert(FocusManager.allElements[element], self.currentGui) |
252 | self.currentFocusData.idToElementMapping[element.focusId] = element |
253 | |
254 | if isAlwaysFocusedOnOpen then |
255 | self.currentFocusData.initialFocusElement = element |
256 | end |
257 | |
258 | if focusActive then |
259 | self:setFocus(element) |
260 | end |
261 | |
262 | local success = true |
263 | for _, child in pairs(element.elements) do |
264 | success = success and self:loadElementFromCustomValues(child, child.focusId, child.focusChangeData, child.focusActive, child.isAlwaysFocusedOnOpen) |
265 | end |
266 | |
267 | return success |
268 | end |
loadElementFromXML
DescriptionLoad GuiElement focus data from its XML definition. This is called at the end of GuiElement:loadFromXML().Definition
loadElementFromXML()Code
133 | function FocusManager:loadElementFromXML(xmlFile, xmlBaseNode, element) |
134 | local focusId = getXMLString(xmlFile, xmlBaseNode.."#focusId") |
135 | if not focusId then |
136 | focusId = FocusManager.serveAutoFocusId() |
137 | end |
138 | |
139 | element.focusId = focusId |
140 | element.focusChangeData = {} |
141 | -- assign focus change data from configuration if it has not been set by code: |
142 | if not element.focusChangeData[FocusManager.TOP] then |
143 | element.focusChangeData[FocusManager.TOP] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeTop") |
144 | end |
145 | |
146 | if not element.focusChangeData[FocusManager.BOTTOM] then |
147 | element.focusChangeData[FocusManager.BOTTOM] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeBottom") |
148 | end |
149 | |
150 | if not element.focusChangeData[FocusManager.LEFT] then |
151 | element.focusChangeData[FocusManager.LEFT] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeLeft") |
152 | end |
153 | |
154 | if not element.focusChangeData[FocusManager.RIGHT] then |
155 | element.focusChangeData[FocusManager.RIGHT] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeRight") |
156 | end |
157 | |
158 | -- Disabled: it is unused at time of writing but breaks special focus setups for the construction screen |
159 | --[[ |
160 | if GS_IS_CONSOLE_VERSION then |
161 | element.focusChangeData[FocusManager.TOP] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeTop"), element.focusChangeData[FocusManager.TOP]) |
162 | element.focusChangeData[FocusManager.BOTTOM] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeBottom"), element.focusChangeData[FocusManager.BOTTOM]) |
163 | element.focusChangeData[FocusManager.LEFT] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeLeft"), element.focusChangeData[FocusManager.LEFT]) |
164 | element.focusChangeData[FocusManager.RIGHT] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeRight"), element.focusChangeData[FocusManager.RIGHT]) |
165 | |
166 | if element.focusChangeData[FocusManager.TOP] == "nil" then |
167 | element.focusChangeData[FocusManager.TOP] = nil |
168 | end |
169 | |
170 | if element.focusChangeData[FocusManager.BOTTOM] == "nil" then |
171 | element.focusChangeData[FocusManager.BOTTOM] = nil |
172 | end |
173 | |
174 | if element.focusChangeData[FocusManager.LEFT] == "nil" then |
175 | element.focusChangeData[FocusManager.LEFT] = nil |
176 | end |
177 | |
178 | if element.focusChangeData[FocusManager.RIGHT] == "nil" then |
179 | element.focusChangeData[FocusManager.RIGHT] = nil |
180 | end |
181 | end |
182 | ]] |
183 | |
184 | element.focusActive = (getXMLString(xmlFile, xmlBaseNode.."#focusInit") ~= nil) |
185 | local isAlwaysFocusedOnOpen = (getXMLString(xmlFile, xmlBaseNode.."#focusInit") == "onOpen") |
186 | element.isAlwaysFocusedOnOpen = isAlwaysFocusedOnOpen |
187 | |
188 | local focusChangeOverride = getXMLString(xmlFile, xmlBaseNode.."#focusChangeOverride") |
189 | if focusChangeOverride then |
190 | if element.target and element.target.focusChangeOverride then |
191 | element.focusChangeOverride = element.target[focusChangeOverride] |
192 | else |
193 | self.focusChangeOverride = ClassUtil.getFunction(focusChangeOverride) |
194 | end |
195 | end |
196 | |
197 | if FocusManager.allElements[element] == nil then |
198 | FocusManager.allElements[element] = {} |
199 | end |
200 | table.insert(FocusManager.allElements[element], self.currentGui) |
201 | self.currentFocusData.idToElementMapping[focusId] = element |
202 | |
203 | if isAlwaysFocusedOnOpen then |
204 | self.currentFocusData.initialFocusElement = element |
205 | |
206 | -- Force disable any sounds when loading |
207 | local old = element.soundDisabled |
208 | element.soundDisabled = true |
209 | self:setFocus(element) |
210 | element.soundDisabled = old |
211 | else |
212 | if not self.currentFocusData.focusElement then |
213 | self.currentFocusData.focusElement = element |
214 | end |
215 | end |
216 | end |
lockFocusInput
DescriptionLocks a given input axis action's input for a time. Until the delay has passed, the focus manager will not react to that input.Definition
lockFocusInput(inputAxis InputAction, delay Delay, value Axis)Arguments
inputAxis | InputAction | axis or action code |
delay | Delay | in ms |
value | Axis | value [-1, 1], only relevant to identify directional axes |
409 | function FocusManager:lockFocusInput(axisAction, delay, value) |
410 | local key = FocusManager.getDirectionForAxisValue(axisAction, value) |
411 | if not key and axisAction ~= InputAction.MENU_AXIS_UP_DOWN and axisAction ~= InputAction.MENU_AXIS_LEFT_RIGHT then |
412 | key = axisAction |
413 | end |
414 | |
415 | self.lastInput[key] = g_time |
416 | self.lockUntil[key] = g_time + delay |
417 | end |
releaseLock
DescriptionRelease the global focus input lock.Definition
releaseLock()Code
872 | function FocusManager:releaseLock() |
873 | FocusManager.isFocusLocked = false |
874 | end |
releaseMovementFocusInput
DescriptionRelease a focus movement input lock on an action. Called by the UI input handling code. Avoid calling this for anything else.Definition
releaseMovementFocusInput(action Focus)Arguments
action | Focus | movement input action name |
423 | function FocusManager:releaseMovementFocusInput(action) |
424 | -- on input release we do not have a direction input value, need to clear lock for both directions on axes: |
425 | if action == InputAction.MENU_AXIS_LEFT_RIGHT then |
426 | self.lastInput[FocusManager.LEFT] = nil |
427 | self.lockUntil[FocusManager.LEFT] = nil |
428 | self.lastInput[FocusManager.RIGHT] = nil |
429 | self.lockUntil[FocusManager.RIGHT] = nil |
430 | elseif action == InputAction.MENU_AXIS_UP_DOWN then |
431 | self.lastInput[FocusManager.TOP] = nil |
432 | self.lockUntil[FocusManager.TOP] = nil |
433 | self.lastInput[FocusManager.BOTTOM] = nil |
434 | self.lockUntil[FocusManager.BOTTOM] = nil |
435 | end |
436 | end |
removeElement
DescriptionRemove a GuiElement from the current focus context.Definition
removeElement()Code
272 | function FocusManager:removeElement(element) |
273 | if not element.focusId then |
274 | return |
275 | end |
276 | |
277 | for _, child in pairs(element.elements) do |
278 | self:removeElement(child) |
279 | end |
280 | |
281 | if element.focusActive then |
282 | element:onFocusLeave() |
283 | FocusManager:unsetFocus(element) |
284 | end |
285 | |
286 | if FocusManager.allElements[element] ~= nil then |
287 | for _, guiItWasAddedTo in ipairs(FocusManager.allElements[element]) do |
288 | local data = self.guiFocusData[guiItWasAddedTo] |
289 | data.idToElementMapping[element.focusId] = nil |
290 | if data.focusElement == element then |
291 | data.focusElement = nil |
292 | end |
293 | end |
294 | |
295 | FocusManager.allElements[element] = nil -- remove |
296 | end |
297 | |
298 | self.currentFocusData.idToElementMapping[element.focusId] = nil |
299 | element.focusId = nil |
300 | element.focusChangeData = {} |
301 | |
302 | if self.currentFocusData.focusElement == element then |
303 | self.currentFocusData.focusElement = nil |
304 | end |
305 | end |
requireLock
DescriptionGlobally lock focus input.Definition
requireLock()Code
866 | function FocusManager:requireLock() |
867 | FocusManager.isFocusLocked = true |
868 | end |
resetFocusInputLocks
DescriptionReset all locks of focus input.Definition
resetFocusInputLocks()Code
440 | function FocusManager:resetFocusInputLocks() |
441 | for k, _ in pairs(self.lastInput) do |
442 | self.lastInput[k] = nil |
443 | end |
444 | for k, _ in pairs(self.lockUntil) do |
445 | self.lockUntil[k] = 0 |
446 | end |
447 | end |
serveAutoFocusId
DescriptionGet a new automatic focus ID. It's based on a simple integer increment and will be unique unless billions of elements require an ID.Definition
serveAutoFocusId()Code
124 | function FocusManager.serveAutoFocusId() |
125 | local focusId = string.format("focusAuto_%d", FocusManager.autoIDcount) |
126 | FocusManager.autoIDcount = FocusManager.autoIDcount + 1 |
127 | return focusId |
128 | end |
setElementFocusOverlayState
DescriptionSet an elements focus overlay state for displaying.Definition
setElementFocusOverlayState(element Target, isFocus If, handlePreviousState [optional])Arguments
element | Target | element |
isFocus | If | true, the element's state will be set to focused. Otherwise, it's state will be either restored or set to normal. |
handlePreviousState | [optional] | If true or undefined, makes the element store its previous overlay state before modification or restore it when isFocused is false. |
843 | function FocusManager:setElementFocusOverlayState(element, isFocused, handlePreviousState) |
844 | if handlePreviousState == nil then |
845 | handlePreviousState = true |
846 | end |
847 | |
848 | if isFocused then |
849 | if handlePreviousState and element:getOverlayState() ~= GuiOverlay.STATE_NORMAL then |
850 | element:storeOverlayState() |
851 | end |
852 | element:setOverlayState(GuiOverlay.STATE_FOCUSED) |
853 | else |
854 | if handlePreviousState then |
855 | element:restoreOverlayState() |
856 | end |
857 | |
858 | if element:getOverlayState() == GuiOverlay.STATE_FOCUSED then -- could still be focused state after restore, just set to normal |
859 | element:setOverlayState(GuiOverlay.STATE_NORMAL) |
860 | end |
861 | end |
862 | end |
setFocus
DescriptionSet focus on a GuiElement or its focus target. Applies overlay state and triggers onFocusEnter() on the target.Definition
setFocus(element Element, direction Focus, ... Variable)Arguments
element | Element | whose focus target (usually itself) receives focus. |
direction | Focus | navigation direction |
... | Variable | arguments to pass on to the onFocusEnter callback of the target element |
True | if | focus has changed, false otherwise |
774 | function FocusManager:setFocus(element, direction, ...) |
775 | if FocusManager.isFocusLocked or element == nil or not element:canReceiveFocus() then |
776 | return false |
777 | end |
778 | |
779 | -- get the element's focus target (or a descendant's) to return |
780 | local targetElement = FocusManager.getNestedFocusTarget(element, direction) |
781 | if targetElement.targetName ~= self.currentGui then |
782 | return false |
783 | end |
784 | |
785 | if self.currentFocusData.focusElement and |
786 | self.currentFocusData.focusElement == targetElement and |
787 | self.currentFocusData.focusElement.focusActive then |
788 | -- the passed element already has focus |
789 | return false |
790 | end |
791 | |
792 | -- clear focus and highlight on previous elements |
793 | if self.currentFocusData.focusElement ~= nil then |
794 | self:unsetFocus(self.currentFocusData.focusElement) |
795 | self:unsetHighlight(self.currentFocusData.highlightElement) |
796 | end |
797 | |
798 | -- set focus of newly focused element |
799 | targetElement.focusActive = true |
800 | self.currentFocusData.focusElement = targetElement |
801 | targetElement:onFocusEnter(...) |
802 | |
803 | if FocusManager.DEBUG then |
804 | log("focus changed to element", targetElement, "; ID:", targetElement.id, "; profile:", targetElement.profile, "; type:", targetElement.typeName) |
805 | end |
806 | |
807 | self:setElementFocusOverlayState(targetElement, true) |
808 | if not element:getSoundSuppressed() and element:getIsVisible() and element.playHoverSoundOnFocus ~= false and not element.soundDisabled then |
809 | self.soundPlayer:playSample(targetElement.customFocusSample or GuiSoundPlayer.SOUND_SAMPLES.HOVER) |
810 | end |
811 | |
812 | return true |
813 | end |
setGui
DescriptionSet the active GUI for focus input.Definition
setGui(gui Screen)Arguments
gui | Screen | root GuiElement |
73 | function FocusManager:setGui(gui) |
74 | -- reset old gui focus |
75 | if self.currentFocusData then |
76 | local focusElement = self.currentFocusData.focusElement |
77 | if focusElement then |
78 | self:unsetFocus(focusElement) |
79 | end |
80 | end |
81 | |
82 | -- set(up) new gui focus |
83 | self.currentGui = gui |
84 | self.currentFocusData = self.guiFocusData[gui] |
85 | if not self.currentFocusData then |
86 | self.guiFocusData[gui] = {} |
87 | self.guiFocusData[gui].idToElementMapping = {} -- all elements |
88 | self.currentFocusData = self.guiFocusData[gui] |
89 | else |
90 | local focusElement = self.currentFocusData.initialFocusElement or self.currentFocusData.focusElement |
91 | if focusElement ~= nil then |
92 | local oldSound = focusElement.soundDisabled |
93 | focusElement.soundDisabled = true |
94 | self:setFocus(focusElement) |
95 | focusElement.soundDisabled = oldSound |
96 | end |
97 | end |
98 | |
99 | -- reset delay locks |
100 | self:resetFocusInputLocks() |
101 | end |
setHighlight
DescriptionActivate a highlight on an element. Highlighted elements are only visually marked and do not receive focus activation. Only one element will be highlighted at any time, usually corresponding to the current mouse over target.Definition
setHighlight(element Element)Arguments
element | Element | to be highlighted. |
729 | function FocusManager:setHighlight(element) |
730 | -- check if element has highlight already |
731 | if self.currentFocusData.highlightElement and self.currentFocusData.highlightElement == element then |
732 | return |
733 | end |
734 | |
735 | -- unset highlight of currently highlighted element |
736 | self:unsetHighlight(self.currentFocusData.highlightElement) |
737 | |
738 | if not element.disallowFocusedHighlight or not (self.currentFocusData.focusElement and self.currentFocusData.focusElement == element) then |
739 | -- set highlight of new element |
740 | self.currentFocusData.highlightElement = element |
741 | element:storeOverlayState() |
742 | element:setOverlayState(GuiOverlay.STATE_HIGHLIGHTED) |
743 | element:onHighlight() |
744 | |
745 | if not element:getSoundSuppressed() and element:getIsVisible() and element.playHoverSoundOnFocus ~= false and not element.soundDisabled then |
746 | self.soundPlayer:playSample(GuiSoundPlayer.SOUND_SAMPLES.HOVER) |
747 | end |
748 | end |
749 | end |
setSoundPlayer
DescriptionDefinitionsetSoundPlayer()Code
105 | function FocusManager:setSoundPlayer(guiSoundPlayer) |
106 | self.soundPlayer = guiSoundPlayer |
107 | end |
unsetFocus
DescriptionRemoves focus from an element. Applies overlay state and triggers onFocusLeave() on the target.Definition
unsetFocus(element Element, ... Variable)Arguments
element | Element | which should lose focus |
... | Variable | arguments to pass on to the onFocusLeave callback of the target element |
820 | function FocusManager:unsetFocus(element, ...) |
821 | local prevFocusElement = self.currentFocusData.focusElement |
822 | if prevFocusElement ~= element or prevFocusElement == nil then |
823 | -- the element is not focused |
824 | return |
825 | end |
826 | |
827 | if not element.focusActive then |
828 | -- the element has already lost focus |
829 | return |
830 | end |
831 | |
832 | prevFocusElement.focusActive = false |
833 | self:setElementFocusOverlayState(prevFocusElement, false) |
834 | |
835 | prevFocusElement:onFocusLeave(...) -- call focus leave last, can override overlay state if desired |
836 | end |
unsetHighlight
DescriptionRemove highlight status from an element.Definition
unsetHighlight(element Highlighted)Arguments
element | Highlighted | element to revert |
754 | function FocusManager:unsetHighlight(element) |
755 | if self.currentFocusData.highlightElement and self.currentFocusData.highlightElement == element then |
756 | local prevState = self.currentFocusData.highlightElement:getOverlayState() |
757 | if prevState == GuiOverlay.STATE_HIGHLIGHTED then |
758 | self.currentFocusData.highlightElement:setOverlayState(GuiOverlay.STATE_NORMAL) |
759 | self.currentFocusData.highlightElement:restoreOverlayState() -- also try restoring |
760 | end |
761 | |
762 | self.currentFocusData.highlightElement:onHighlightRemove() |
763 | self.currentFocusData.highlightElement = nil |
764 | end |
765 | end |
updateFocus
DescriptionUpdate the current focus target.Definition
updateFocus(element GuiElement, isFocusMoving Only, direction Focus, updateOnly If)Arguments
element | GuiElement | which should be the new focus target |
isFocusMoving | Only | move focus if this is true |
direction | Focus | navigation movement direction, one of FocusManager.[TOP | BOTTOM | LEFT | RIGHT] |
updateOnly | If | true, only updates the lock state of focus movement for the given parameters |
644 | function FocusManager:updateFocus(element, isFocusMoving, direction, updateOnly) |
645 | if element == nil then |
646 | return |
647 | end |
648 | |
649 | if isFocusMoving then |
650 | if self.lastInput[direction] then |
651 | if self.lockUntil[direction] <= g_time then |
652 | self.lastInput[direction] = nil -- release lock |
653 | self.lockJustReleased = true |
654 | end |
655 | -- movement not allowed yet |
656 | return |
657 | end |
658 | |
659 | if updateOnly then |
660 | return |
661 | end |
662 | |
663 | -- delay has passed, focus change is allowed, delay is set up |
664 | self.lastInput[direction] = g_time |
665 | if self.lockJustReleased then |
666 | self.lockUntil[direction] = g_time + self.DELAY_TIME |
667 | else |
668 | self.lockUntil[direction] = g_time + self.FIRST_LOCK |
669 | end |
670 | |
671 | self.lockJustReleased = false |
672 | |
673 | -- used if more than one button was pressed, only the first one is handled -- TODO: is needed?, also: button priority |
674 | if self.currentFocusData.focusElement ~= element then |
675 | return |
676 | end |
677 | |
678 | -- give the element the chance to override the focus change |
679 | if element:shouldFocusChange(direction) then |
680 | -- change focus |
681 | local nextElement, nextElementIsSet |
682 | if element.focusChangeOverride then |
683 | if element.target then |
684 | nextElementIsSet, nextElement = element.focusChangeOverride(element.target, direction) |
685 | else |
686 | nextElementIsSet, nextElement = element:focusChangeOverride(direction) |
687 | end |
688 | end |
689 | |
690 | local actualDirection = direction |
691 | if not nextElementIsSet then |
692 | nextElement, actualDirection = self:getNextFocusElement(element, direction) |
693 | end |
694 | |
695 | if nextElement and nextElement:canReceiveFocus() then |
696 | self:setFocus(nextElement, actualDirection) |
697 | |
698 | return nextElement |
699 | else |
700 | local focusElement = element |
701 | nextElement = element |
702 | if not element.focusChangeOverride or not element:focusChangeOverride(direction) then |
703 | local maxSteps = 30 |
704 | while maxSteps > 0 do |
705 | if nextElement == nil then |
706 | break |
707 | end |
708 | |
709 | nextElement, actualDirection = self:getNextFocusElement(nextElement, direction) |
710 | if nextElement ~= nil and nextElement:canReceiveFocus() then |
711 | focusElement = nextElement |
712 | break |
713 | end |
714 | |
715 | maxSteps = maxSteps - 1 |
716 | end |
717 | end |
718 | |
719 | self:setFocus(focusElement, actualDirection) |
720 | end |
721 | end |
722 | end |
723 | end |