483 | function FocusManager.checkElementDistance(curElement, other, dirX, dirY, curElementOffsetY, closestOther, closestDistanceSq) |
484 | local retOther = closestOther |
485 | local retDistSq = closestDistanceSq |
486 | |
487 | local elementBox = curElement:getBorders() |
488 | elementBox[2] = elementBox[2] + curElementOffsetY |
489 | elementBox[4] = elementBox[4] + curElementOffsetY |
490 | local elementCenter = curElement:getCenter() |
491 | elementCenter[2] = elementCenter[2] + curElementOffsetY |
492 | |
493 | if other ~= curElement and not other.disabled and other:getIsVisible() and other:canReceiveFocus() and not (other:isChildOf(curElement) or curElement:isChildOf(other)) then |
494 | local otherBox = other:getBorders() |
495 | local otherCenter = other:getCenter() |
496 | |
497 | -- get vector between bounding box points |
498 | local elementDirX, elementDirY = FocusManager.getShortestBoundingBoxVector(elementBox, otherBox, otherCenter) |
499 | |
500 | -- test direction and distance of bounding box points |
501 | local boxDistanceSq = MathUtil.vector2LengthSq(elementDirX, elementDirY) |
502 | local dot = MathUtil.dotProduct(elementDirX, elementDirY, 0, dirX, dirY, 0) |
503 | if boxDistanceSq < FocusManager.EPSILON then -- boundaries touch, use center points for direction check |
504 | dot = MathUtil.dotProduct(otherCenter[1] - elementCenter[1], otherCenter[2] - elementCenter[2], 0, dirX, dirY, 0) |
505 | end |
506 | |
507 | if dot > 0 then -- other element lies in scanning direction |
508 | local useOther = false |
509 | |
510 | -- when two elements are equally close, choose the one further up (-y) and/or further left (-x) |
511 | if closestOther and math.abs(closestDistanceSq - boxDistanceSq) < FocusManager.EPSILON then |
512 | -- also compare dot products |
513 | local closestBox = closestOther:getBorders() |
514 | local closestCenter = closestOther:getCenter() |
515 | local toClosestX, toClosestY = FocusManager.getShortestBoundingBoxVector(elementBox, closestBox, closestCenter) |
516 | local closestDot = MathUtil.dotProduct(toClosestX, toClosestY, 0, dirX, dirY, 0) |
517 | |
518 | if math.abs(closestDot - dot) < FocusManager.EPSILON then -- same distance and angle as previous best |
519 | -- when going up, go right first, etc. --> ensure symmetric paths in all directions |
520 | if dirY > 0 then |
521 | useOther = other.absPosition[1] > closestOther.absPosition[1] |
522 | elseif dirY < 0 then |
523 | useOther = other.absPosition[1] < closestOther.absPosition[1] |
524 | elseif dirX > 0 then |
525 | useOther = other.absPosition[2] > closestOther.absPosition[2] |
526 | elseif dirX < 0 then |
527 | useOther = other.absPosition[2] < closestOther.absPosition[2] |
528 | end |
529 | elseif dot > closestDot then -- when distance is equal and angles differ, prefer the one closer to the movement direction |
530 | useOther = true |
531 | end |
532 | elseif boxDistanceSq < closestDistanceSq then |
533 | useOther = true |
534 | end |
535 | |
536 | if useOther then |
537 | retOther = other |
538 | retDistSq = boxDistanceSq |
539 | end |
540 | end |
541 | end |
542 | |
543 | return retOther, retDistSq |
544 | end |
881 | function FocusManager:getFocusOverrideFunction(forDirections, substitute, useSubstituteForFocus) |
882 | if forDirections == nil or #forDirections < 1 then |
883 | return function(elementSelf, dir) return false, nil end |
884 | end |
885 | |
886 | local f = function(elementSelf, dir) |
887 | for _, overrideDirection in pairs(forDirections) do |
888 | if dir == overrideDirection then |
889 | if useSubstituteForFocus then |
890 | local next = self:getNextFocusElement(substitute, dir) |
891 | if next then |
892 | return true, next |
893 | end |
894 | else |
895 | return true, substitute |
896 | end |
897 | end |
898 | end |
899 | |
900 | return false, nil |
901 | end |
902 | |
903 | return f |
904 | end |
551 | function FocusManager:getNextFocusElement(element, direction) |
552 | -- if there is a configured next element, return that |
553 | local nextFocusId = element.focusChangeData[direction] |
554 | if nextFocusId then |
555 | return self.currentFocusData.idToElementMapping[nextFocusId], direction |
556 | end |
557 | -- otherwise, find the next one based on proximity: |
558 | local dirX, dirY = unpack(FocusManager.DIRECTION_VECTORS[direction]) |
559 | |
560 | local closestOther = nil |
561 | local closestDistance = math.huge |
562 | |
563 | for _, other in pairs(self.currentFocusData.idToElementMapping) do |
564 | closestOther, closestDistance = FocusManager.checkElementDistance(element, other, dirX, dirY, 0, closestOther, closestDistance) |
565 | end |
566 | |
567 | if closestOther == nil then |
568 | -- wrap around |
569 | if direction == FocusManager.LEFT then |
570 | -- look up instead |
571 | closestOther, direction = self:getNextFocusElement(element, FocusManager.TOP) |
572 | elseif direction == FocusManager.RIGHT then |
573 | -- look down instead |
574 | closestOther, direction = self:getNextFocusElement(element, FocusManager.BOTTOM) |
575 | else |
576 | -- get the right test elements |
577 | local validWrapElements = self.currentFocusData.idToElementMapping -- screen wrap around |
578 | if element.parent and element.parent.wrapAround then -- local box/area wrap around if required |
579 | validWrapElements = element.parent.elements |
580 | end |
581 | |
582 | local wrapOffsetY = 0 |
583 | if direction == FocusManager.TOP then |
584 | 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] |
585 | elseif direction == FocusManager.BOTTOM then |
586 | wrapOffsetY = 1.2 + element.size[2] -- above screen |
587 | end |
588 | |
589 | -- try wrapping around |
590 | for _, other in pairs(validWrapElements) do |
591 | closestOther, closestDistance = FocusManager.checkElementDistance(element, other, dirX, dirY, wrapOffsetY, closestOther, closestDistance) |
592 | end |
593 | end |
594 | end |
595 | |
596 | return closestOther, direction |
597 | end |
457 | function FocusManager.getShortestBoundingBoxVector(elementBox, otherBox, otherCenter) |
458 | local ePointX, ePointY = FocusManager.getClosestPointOnBoundingBox( |
459 | otherCenter[1], otherCenter[2], |
460 | elementBox[1], elementBox[2], elementBox[3], elementBox[4]) |
461 | |
462 | local oPointX, oPointY = FocusManager.getClosestPointOnBoundingBox( |
463 | ePointX, ePointY, -- use the previously calculated bounding box point here to get the closest boundary distance |
464 | otherBox[1], otherBox[2], otherBox[3], otherBox[4]) |
465 | |
466 | -- get vector between bounding box points |
467 | local elementDirX = oPointX - ePointX |
468 | local elementDirY = oPointY - ePointY |
469 | |
470 | return elementDirX, elementDirY |
471 | end |
301 | function FocusManager:inputEvent(action, value, eventUsed) |
302 | local element = self.currentFocusData.focusElement |
303 | |
304 | local pressedUp = false |
305 | local pressedDown = false |
306 | local pressedLeft = false |
307 | local pressedRight = false |
308 | local pressedAccept = false |
309 | |
310 | pressedUp = action == InputAction.MENU_AXIS_UP_DOWN and value > g_analogStickVTolerance |
311 | pressedDown = action == InputAction.MENU_AXIS_UP_DOWN and value < -g_analogStickVTolerance |
312 | pressedLeft = action == InputAction.MENU_AXIS_LEFT_RIGHT and value < -g_analogStickHTolerance |
313 | pressedRight = action == InputAction.MENU_AXIS_LEFT_RIGHT and value > g_analogStickHTolerance |
314 | |
315 | if action == InputAction.MENU_AXIS_UP_DOWN then |
316 | self:updateFocus(element, pressedUp, FocusManager.TOP, eventUsed) |
317 | self:updateFocus(element, pressedDown, FocusManager.BOTTOM, eventUsed) |
318 | elseif action == InputAction.MENU_AXIS_LEFT_RIGHT then |
319 | self:updateFocus(element, pressedLeft, FocusManager.LEFT, eventUsed) |
320 | self:updateFocus(element, pressedRight, FocusManager.RIGHT, eventUsed) |
321 | end |
322 | |
323 | if not eventUsed and element ~= nil and not element.needExternalClick then |
324 | pressedAccept = action == InputAction.MENU_ACCEPT |
325 | if pressedAccept and not self:isFocusInputLocked(action) then |
326 | -- elements can get unfocused, accept is only allowed for currently focused and visible elements |
327 | if element.focusActive and element:getIsVisible() then |
328 | self.focusSystemMadeChanges = true |
329 | element:onFocusActivate() |
330 | self.focusSystemMadeChanges = false |
331 | end |
332 | end |
333 | end |
334 | |
335 | return eventUsed or pressedUp or pressedDown or pressedLeft or pressedRight or pressedAccept |
336 | end |
217 | function FocusManager:loadElementFromCustomValues(element, focusId, focusChangeData, focusActive, isAlwaysFocusedOnOpen) |
218 | if focusId and self.currentFocusData.idToElementMapping[focusId] then |
219 | return false -- ignore element, caller is responsible for sensible ID assignment when specified |
220 | end |
221 | |
222 | if not element.focusId then |
223 | if not focusId then |
224 | focusId = FocusManager.serveAutoFocusId() |
225 | end |
226 | |
227 | element.focusId = focusId |
228 | end |
229 | |
230 | element.focusChangeData = element.focusChangeData or focusChangeData or {} |
231 | element.focusActive = focusActive |
232 | element.isAlwaysFocusedOnOpen = isAlwaysFocusedOnOpen |
233 | |
234 | FocusManager.allElements[element] = self.currentGui |
235 | self.currentFocusData.idToElementMapping[element.focusId] = element |
236 | |
237 | if isAlwaysFocusedOnOpen then |
238 | self.currentFocusData.initialFocusElement = element |
239 | end |
240 | |
241 | if focusActive then |
242 | self:setFocus(element) |
243 | end |
244 | |
245 | local success = true |
246 | for _, child in pairs(element.elements) do |
247 | success = success and self:loadElementFromCustomValues(child, child.focusId, child.focusChangeData, child.focusActive, child.isAlwaysFocusedOnOpen) |
248 | end |
249 | |
250 | return success |
251 | end |
130 | function FocusManager:loadElementFromXML(xmlFile, xmlBaseNode, element) |
131 | local focusId = getXMLString(xmlFile, xmlBaseNode.."#focusId") |
132 | if not focusId then |
133 | focusId = FocusManager.serveAutoFocusId() |
134 | end |
135 | |
136 | element.focusId = focusId |
137 | element.focusChangeData = {} |
138 | -- assign focus change data from configuration if it has not been set by code: |
139 | if not element.focusChangeData[FocusManager.TOP] then |
140 | element.focusChangeData[FocusManager.TOP] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeTop") |
141 | end |
142 | |
143 | if not element.focusChangeData[FocusManager.BOTTOM] then |
144 | element.focusChangeData[FocusManager.BOTTOM] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeBottom") |
145 | end |
146 | |
147 | if not element.focusChangeData[FocusManager.LEFT] then |
148 | element.focusChangeData[FocusManager.LEFT] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeLeft") |
149 | end |
150 | |
151 | if not element.focusChangeData[FocusManager.RIGHT] then |
152 | element.focusChangeData[FocusManager.RIGHT] = getXMLString(xmlFile, xmlBaseNode.."#focusChangeRight") |
153 | end |
154 | |
155 | if GS_IS_CONSOLE_VERSION then |
156 | element.focusChangeData[FocusManager.TOP] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeTop"), element.focusChangeData[FocusManager.TOP]) |
157 | element.focusChangeData[FocusManager.BOTTOM] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeBottom"), element.focusChangeData[FocusManager.BOTTOM]) |
158 | element.focusChangeData[FocusManager.LEFT] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeLeft"), element.focusChangeData[FocusManager.LEFT]) |
159 | element.focusChangeData[FocusManager.RIGHT] = Utils.getNoNil(getXMLString(xmlFile, xmlBaseNode.."#consoleFocusChangeRight"), element.focusChangeData[FocusManager.RIGHT]) |
160 | |
161 | if element.focusChangeData[FocusManager.TOP] == "nil" then |
162 | element.focusChangeData[FocusManager.TOP] = nil |
163 | end |
164 | |
165 | if element.focusChangeData[FocusManager.BOTTOM] == "nil" then |
166 | element.focusChangeData[FocusManager.BOTTOM] = nil |
167 | end |
168 | |
169 | if element.focusChangeData[FocusManager.LEFT] == "nil" then |
170 | element.focusChangeData[FocusManager.LEFT] = nil |
171 | end |
172 | |
173 | if element.focusChangeData[FocusManager.RIGHT] == "nil" then |
174 | element.focusChangeData[FocusManager.RIGHT] = nil |
175 | end |
176 | end |
177 | |
178 | element.focusActive = (getXMLString(xmlFile, xmlBaseNode.."#focusInit") ~= nil) |
179 | local isAlwaysFocusedOnOpen = (getXMLString(xmlFile, xmlBaseNode.."#focusInit") == "onOpen") |
180 | element.isAlwaysFocusedOnOpen = isAlwaysFocusedOnOpen |
181 | |
182 | local focusChangeOverride = getXMLString(xmlFile, xmlBaseNode.."#focusChangeOverride") |
183 | if focusChangeOverride then |
184 | if element.target and element.target.focusChangeOverride then |
185 | element.focusChangeOverride = element.target[focusChangeOverride] |
186 | else |
187 | self.focusChangeOverride = ClassUtil.getFunction(focusChangeOverride) |
188 | end |
189 | end |
190 | |
191 | FocusManager.allElements[element] = self.currentGui |
192 | self.currentFocusData.idToElementMapping[focusId] = element |
193 | |
194 | if isAlwaysFocusedOnOpen then |
195 | self.currentFocusData.initialFocusElement = element |
196 | self:setFocus(element) |
197 | else |
198 | if not self.currentFocusData.focusElement then |
199 | self.currentFocusData.focusElement = element |
200 | end |
201 | end |
202 | end |
401 | function FocusManager:releaseMovementFocusInput(action) |
402 | -- on input release we do not have a direction input value, need to clear lock for both directions on axes: |
403 | if action == InputAction.MENU_AXIS_LEFT_RIGHT then |
404 | self.lastInput[FocusManager.LEFT] = nil |
405 | self.lockUntil[FocusManager.LEFT] = nil |
406 | self.lastInput[FocusManager.RIGHT] = nil |
407 | self.lockUntil[FocusManager.RIGHT] = nil |
408 | elseif action == InputAction.MENU_AXIS_UP_DOWN then |
409 | self.lastInput[FocusManager.TOP] = nil |
410 | self.lockUntil[FocusManager.TOP] = nil |
411 | self.lastInput[FocusManager.BOTTOM] = nil |
412 | self.lockUntil[FocusManager.BOTTOM] = nil |
413 | end |
414 | end |
255 | function FocusManager:removeElement(element) |
256 | if not element.focusId then |
257 | return |
258 | end |
259 | |
260 | for _, child in pairs(element.elements) do |
261 | self:removeElement(child) |
262 | end |
263 | |
264 | if element.focusActive then |
265 | element:onFocusLeave() |
266 | FocusManager:unsetFocus(element) |
267 | end |
268 | |
269 | if FocusManager.allElements[element] ~= nil then |
270 | local guiItWasAddedTo = FocusManager.allElements[element] |
271 | if self.currentGui ~= guiItWasAddedTo then |
272 | -- log("FOCUS LEAK: Found element", element, "in '", guiItWasAddedTo, "' instead of '", self.currentGui,"'") |
273 | self.guiFocusData[guiItWasAddedTo].idToElementMapping[element.focusId] = nil |
274 | end |
275 | |
276 | FocusManager.allElements[element] = nil -- remove |
277 | end |
278 | |
279 | self.currentFocusData.idToElementMapping[element.focusId] = nil |
280 | element.focusId = nil |
281 | element.focusChangeData = {} |
282 | end |
822 | function FocusManager:setElementFocusOverlayState(element, isFocused, handlePreviousState) |
823 | if handlePreviousState == nil then |
824 | handlePreviousState = true |
825 | end |
826 | |
827 | if isFocused then |
828 | if handlePreviousState and element:getOverlayState() ~= GuiOverlay.STATE_NORMAL then |
829 | element:storeOverlayState() |
830 | end |
831 | element:setOverlayState(GuiOverlay.STATE_FOCUSED) |
832 | else |
833 | if handlePreviousState then |
834 | element:restoreOverlayState() |
835 | end |
836 | |
837 | if element:getOverlayState() == GuiOverlay.STATE_FOCUSED then -- could still be focused state after restore, just set to normal |
838 | element:setOverlayState(GuiOverlay.STATE_NORMAL) |
839 | end |
840 | end |
841 | end |
753 | function FocusManager:setFocus(element, direction, ...) |
754 | if FocusManager.isFocusLocked or element == nil or not element:canReceiveFocus() then |
755 | return false |
756 | end |
757 | |
758 | -- get the element's focus target (or a descendant's) to return |
759 | local targetElement = FocusManager.getNestedFocusTarget(element, direction) |
760 | if targetElement.targetName ~= self.currentGui then |
761 | return false |
762 | end |
763 | |
764 | if self.currentFocusData.focusElement and |
765 | self.currentFocusData.focusElement == targetElement and |
766 | self.currentFocusData.focusElement.focusActive then |
767 | -- the passed element already has focus |
768 | return false |
769 | end |
770 | |
771 | -- clear focus and highlight on previous elements |
772 | if self.currentFocusData.focusElement ~= nil then |
773 | self:unsetFocus(self.currentFocusData.focusElement) |
774 | self:unsetHighlight(self.currentFocusData.highlightElement) |
775 | end |
776 | |
777 | -- set focus of newly focused element |
778 | targetElement.focusActive = true |
779 | self.currentFocusData.focusElement = targetElement |
780 | targetElement:onFocusEnter(...) |
781 | |
782 | if FocusManager.DEBUG then |
783 | log("focus changed to element", targetElement, "; ID:", targetElement.id, "; profile:", targetElement.profile, "; type:", targetElement.typeName) |
784 | end |
785 | |
786 | self:setElementFocusOverlayState(targetElement, true) |
787 | if not element:getSoundSuppressed() and element:getIsVisible() and element.playHoverSoundOnFocus ~= false and not element.soundDisabled then |
788 | self.soundPlayer:playSample(GuiSoundPlayer.SOUND_SAMPLES.HOVER) |
789 | end |
790 | |
791 | return true |
792 | end |
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 | self:setFocus(focusElement) |
93 | end |
94 | end |
95 | |
96 | -- reset delay locks |
97 | self:resetFocusInputLocks() |
98 | end |
708 | function FocusManager:setHighlight(element) |
709 | -- check if element has highlight already |
710 | if self.currentFocusData.highlightElement and self.currentFocusData.highlightElement == element then |
711 | return |
712 | end |
713 | |
714 | -- unset highlight of currently highlighted element |
715 | self:unsetHighlight(self.currentFocusData.highlightElement) |
716 | |
717 | if not (self.currentFocusData.focusElement and self.currentFocusData.focusElement == element) then |
718 | -- set highlight of new element |
719 | self.currentFocusData.highlightElement = element |
720 | element:storeOverlayState() |
721 | element:setOverlayState(GuiOverlay.STATE_HIGHLIGHTED) |
722 | element:onHighlight() |
723 | |
724 | if not element:getSoundSuppressed() and element:getIsVisible() and element.playHoverSoundOnFocus ~= false and not element.soundDisabled then |
725 | self.soundPlayer:playSample(GuiSoundPlayer.SOUND_SAMPLES.HOVER) |
726 | end |
727 | end |
728 | end |
622 | function FocusManager:updateFocus(element, isFocusMoving, direction, updateOnly) |
623 | if element == nil then |
624 | return |
625 | end |
626 | |
627 | if isFocusMoving then |
628 | local notFirst = false |
629 | if self.lastInput[direction] then |
630 | if self.lockUntil[direction] <= g_time then |
631 | self.lastInput[direction] = nil -- release lock |
632 | self.lockJustReleased = true |
633 | end |
634 | -- movement not allowed yet |
635 | return |
636 | end |
637 | |
638 | if updateOnly then |
639 | return |
640 | end |
641 | |
642 | -- delay has passed, focus change is allowed, delay is set up |
643 | self.lastInput[direction] = g_time |
644 | if self.lockJustReleased then |
645 | self.lockUntil[direction] = g_time + self.DELAY_TIME |
646 | else |
647 | self.lockUntil[direction] = g_time + self.FIRST_LOCK |
648 | end |
649 | |
650 | self.lockJustReleased = false |
651 | |
652 | -- used if more than one button was pressed, only the first one is handled -- TODO: is needed?, also: button priority |
653 | if self.currentFocusData.focusElement ~= element then |
654 | return |
655 | end |
656 | |
657 | -- give the element the chance to override the focus change |
658 | if element:shouldFocusChange(direction) then |
659 | -- change focus |
660 | local nextElement, nextElementIsSet |
661 | if element.focusChangeOverride then |
662 | if element.target then |
663 | nextElementIsSet, nextElement = element.focusChangeOverride(element.target, direction) |
664 | else |
665 | nextElementIsSet, nextElement = element:focusChangeOverride(direction) |
666 | end |
667 | end |
668 | |
669 | local actualDirection = direction |
670 | if not nextElementIsSet then |
671 | nextElement, actualDirection = self:getNextFocusElement(element, direction) |
672 | end |
673 | |
674 | if nextElement and nextElement:canReceiveFocus() then |
675 | self:setFocus(nextElement, actualDirection) |
676 | |
677 | return nextElement |
678 | else |
679 | local focusElement = element |
680 | nextElement = element |
681 | if not element.focusChangeOverride or not element:focusChangeOverride(direction) then |
682 | local maxSteps = 30 |
683 | while maxSteps > 0 do |
684 | if nextElement == nil then |
685 | break |
686 | end |
687 | |
688 | nextElement, actualDirection = self:getNextFocusElement(nextElement, direction) |
689 | if nextElement ~= nil and nextElement:canReceiveFocus() then |
690 | focusElement = nextElement |
691 | break |
692 | end |
693 | |
694 | maxSteps = maxSteps - 1 |
695 | end |
696 | end |
697 | |
698 | self:setFocus(focusElement, actualDirection) |
699 | end |
700 | end |
701 | end |
702 | end |