Improve handheld controller mapping
This commit is contained in:
parent
8ca2c10f71
commit
1e56781e23
4 changed files with 347 additions and 20 deletions
|
|
@ -3,6 +3,7 @@ extends Node
|
|||
|
||||
signal active_profile_changed
|
||||
signal active_controller_changed(controller_name: String)
|
||||
signal controller_input_observed(event: InputEvent)
|
||||
|
||||
const FORMAT_VERSION: int = 1
|
||||
const PROFILE_PATH: String = "user://controller_mappings.json"
|
||||
|
|
@ -10,6 +11,21 @@ const PROFILE_TEMP_PATH: String = "user://controller_mappings.json.tmp"
|
|||
const PROFILE_BACKUP_PATH: String = "user://controller_mappings.json.backup"
|
||||
const MAX_PROFILE_BYTES: int = 1024 * 1024
|
||||
const CAPTURE_AXIS_THRESHOLD: float = 0.55
|
||||
const CAPTURE_AXIS_RELEASE_THRESHOLD: float = 0.30
|
||||
const MUOS_MAPPING_REVISION: String = "muos-v1"
|
||||
const MUOS_CONTROLLER_NAMES: Array[String] = [
|
||||
"muOS-Keys",
|
||||
"Deeplay-keys",
|
||||
]
|
||||
const MUOS_MAPPING_BINDINGS: String = (
|
||||
"a:b3,b:b4,x:b6,y:b5,"
|
||||
+ "leftshoulder:b7,rightshoulder:b8,"
|
||||
+ "lefttrigger:b13,righttrigger:b14,"
|
||||
+ "guide:b11,start:b10,back:b9,"
|
||||
+ "dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,"
|
||||
+ "leftx:a0,lefty:a1,leftstick:b12,"
|
||||
+ "rightx:a2,righty:a3,rightstick:b15,platform:Linux,"
|
||||
)
|
||||
const ROLE_A: StringName = &"a"
|
||||
const ROLE_B: StringName = &"b"
|
||||
const ROLE_X: StringName = &"x"
|
||||
|
|
@ -149,9 +165,15 @@ var _active_profile_key: String = "default"
|
|||
var _active_controller_name: String = "controller"
|
||||
var _default_joy_events: Dictionary = {}
|
||||
var _axis_rest_by_device: Dictionary = {}
|
||||
var _installed_compatibility_guids: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not Input.joy_connection_changed.is_connected(
|
||||
_on_joy_connection_changed
|
||||
):
|
||||
Input.joy_connection_changed.connect(_on_joy_connection_changed)
|
||||
_install_known_controller_mappings()
|
||||
_capture_project_defaults()
|
||||
load_profiles()
|
||||
_refresh_active_controller()
|
||||
|
|
@ -160,16 +182,15 @@ func _ready() -> void:
|
|||
func _input(event: InputEvent) -> void:
|
||||
var device_id: int = -1
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event != null and button_event.pressed:
|
||||
if button_event != null:
|
||||
device_id = button_event.device
|
||||
var motion_event := event as InputEventJoypadMotion
|
||||
if (
|
||||
motion_event != null
|
||||
and absf(motion_event.axis_value) >= CAPTURE_AXIS_THRESHOLD
|
||||
):
|
||||
if motion_event != null:
|
||||
device_id = motion_event.device
|
||||
if device_id >= 0 and device_id != _active_device_id:
|
||||
_set_active_controller(device_id)
|
||||
if button_event != null or motion_event != null:
|
||||
controller_input_observed.emit(event)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
|
|
@ -278,6 +299,31 @@ func role_accepts_axis(role: StringName) -> bool:
|
|||
return role in STICK_AXIS_ROLES or role in TRIGGER_ROLES
|
||||
|
||||
|
||||
func are_capture_inputs_neutral(device_id: int = -1) -> bool:
|
||||
var target_device: int = (
|
||||
_active_device_id if device_id < 0 else device_id
|
||||
)
|
||||
for button_index: int in JOY_BUTTON_MAX:
|
||||
if Input.is_joy_button_pressed(target_device, button_index):
|
||||
return false
|
||||
for axis_index: int in JOY_AXIS_MAX:
|
||||
var rest: float = _axis_rest_value(target_device, axis_index)
|
||||
var current: float = Input.get_joy_axis(target_device, axis_index)
|
||||
if absf(current - rest) > CAPTURE_AXIS_RELEASE_THRESHOLD:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func get_pressed_capture_button(device_id: int = -1) -> int:
|
||||
var target_device: int = (
|
||||
_active_device_id if device_id < 0 else device_id
|
||||
)
|
||||
for button_index: int in JOY_BUTTON_MAX:
|
||||
if Input.is_joy_button_pressed(target_device, button_index):
|
||||
return button_index
|
||||
return -1
|
||||
|
||||
|
||||
func get_active_bindings() -> Dictionary:
|
||||
var raw_profile: Variant = _profiles.get(_active_profile_key, {})
|
||||
var profile: Dictionary = (
|
||||
|
|
@ -546,6 +592,49 @@ func _refresh_active_controller() -> void:
|
|||
_set_active_controller(connected[0] if not connected.is_empty() else 0)
|
||||
|
||||
|
||||
func _on_joy_connection_changed(device_id: int, connected: bool) -> void:
|
||||
if connected:
|
||||
_install_known_controller_mapping(device_id)
|
||||
_refresh_active_controller()
|
||||
|
||||
|
||||
func _install_known_controller_mappings() -> void:
|
||||
for device_id: int in Input.get_connected_joypads():
|
||||
_install_known_controller_mapping(device_id)
|
||||
|
||||
|
||||
func _install_known_controller_mapping(device_id: int) -> bool:
|
||||
var controller_name: String = Input.get_joy_name(device_id).strip_edges()
|
||||
if controller_name not in MUOS_CONTROLLER_NAMES:
|
||||
return false
|
||||
var guid: String = Input.get_joy_guid(device_id).strip_edges()
|
||||
if guid.is_empty():
|
||||
return false
|
||||
if _installed_compatibility_guids.has(guid):
|
||||
return true
|
||||
_installed_compatibility_guids[guid] = true
|
||||
Input.add_joy_mapping(
|
||||
build_muos_controller_mapping(guid, controller_name),
|
||||
true,
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
static func build_muos_controller_mapping(
|
||||
guid: String,
|
||||
controller_name: String,
|
||||
) -> String:
|
||||
return "%s,%s,%s" % [
|
||||
guid.strip_edges(),
|
||||
controller_name.strip_edges(),
|
||||
MUOS_MAPPING_BINDINGS,
|
||||
]
|
||||
|
||||
|
||||
static func is_muos_controller_name(controller_name: String) -> bool:
|
||||
return controller_name.strip_edges() in MUOS_CONTROLLER_NAMES
|
||||
|
||||
|
||||
func _set_active_controller(device_id: int) -> void:
|
||||
var previous_profile_key: String = _active_profile_key
|
||||
_active_device_id = maxi(device_id, 0)
|
||||
|
|
@ -563,11 +652,12 @@ func _set_active_controller(device_id: int) -> void:
|
|||
if controller_name.is_empty():
|
||||
controller_name = "controller"
|
||||
_active_controller_name = controller_name
|
||||
_active_profile_key = (
|
||||
guid
|
||||
if not guid.is_empty()
|
||||
else "name:" + controller_name
|
||||
)
|
||||
if guid.is_empty():
|
||||
_active_profile_key = "name:" + controller_name
|
||||
elif is_muos_controller_name(controller_name):
|
||||
_active_profile_key = guid + ":" + MUOS_MAPPING_REVISION
|
||||
else:
|
||||
_active_profile_key = guid
|
||||
if (
|
||||
previous_profile_key == "name:controller"
|
||||
and _active_profile_key != previous_profile_key
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ func _run() -> void:
|
|||
failure = _validate_auto_map(manager)
|
||||
if failure.is_empty():
|
||||
failure = _validate_virtual_mouse_bounds()
|
||||
if failure.is_empty():
|
||||
failure = _validate_controller_scroll_target()
|
||||
if failure.is_empty():
|
||||
print("controller mapping validation passed")
|
||||
quit(0)
|
||||
|
|
@ -31,6 +33,34 @@ func _run() -> void:
|
|||
|
||||
|
||||
func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
||||
var muos_mapping: String = (
|
||||
ControllerMappingManagerType.build_muos_controller_mapping(
|
||||
"19004ca6010000000100000000010000",
|
||||
"muOS-Keys",
|
||||
)
|
||||
)
|
||||
for expected_binding: String in [
|
||||
"a:b3",
|
||||
"b:b4",
|
||||
"x:b6",
|
||||
"y:b5",
|
||||
"leftx:a0",
|
||||
"righty:a3",
|
||||
]:
|
||||
if expected_binding not in muos_mapping:
|
||||
return "muOS compatibility mapping omitted " + expected_binding
|
||||
if not muos_mapping.begins_with(
|
||||
"19004ca6010000000100000000010000,muOS-Keys,"
|
||||
):
|
||||
return "muOS compatibility mapping does not use the runtime guid"
|
||||
if not ControllerMappingManagerType.is_muos_controller_name(
|
||||
"muOS-Keys"
|
||||
):
|
||||
return "muOS controller name was not recognized"
|
||||
if ControllerMappingManagerType.is_muos_controller_name(
|
||||
"ordinary controller"
|
||||
):
|
||||
return "muOS mapping would affect unrelated controllers"
|
||||
var defaults: Dictionary = manager.get_active_bindings()
|
||||
if defaults.size() != ControllerMappingManagerType.ROLE_ORDER.size():
|
||||
return "default mapping covers %d of %d controller roles: %s" % [
|
||||
|
|
@ -89,6 +119,7 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
|||
panel.open_panel()
|
||||
panel._begin_auto_map()
|
||||
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
var binding := (
|
||||
ControllerMappingManagerType.default_bindings()[str(role)]
|
||||
as Dictionary
|
||||
|
|
@ -103,12 +134,12 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
|||
var button := InputEventJoypadButton.new()
|
||||
button.button_index = int(binding.get("button", -1))
|
||||
button.pressed = true
|
||||
panel._input(button)
|
||||
manager.controller_input_observed.emit(button)
|
||||
else:
|
||||
var motion := InputEventJoypadMotion.new()
|
||||
motion.axis = int(binding.get("axis", -1))
|
||||
motion.axis_value = float(binding.get("direction", -1.0))
|
||||
panel._input(motion)
|
||||
manager.controller_input_observed.emit(motion)
|
||||
if panel._auto_map_active:
|
||||
return "auto-map did not complete after every requested input"
|
||||
if not manager.has_custom_mapping():
|
||||
|
|
@ -130,11 +161,31 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
|||
):
|
||||
return "duplicate controller bindings were not marked in red"
|
||||
panel._begin_manual_capture(ControllerMappingManagerType.ROLE_LT)
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
if "left trigger" in panel._progress_label.text.to_lower():
|
||||
return "manual remapping still dictates a specific physical input"
|
||||
if "any button" not in panel._progress_label.text.to_lower():
|
||||
return "manual remapping does not request a generic controller input"
|
||||
panel._cancel_capture()
|
||||
panel._begin_auto_map()
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
var held_left := InputEventJoypadMotion.new()
|
||||
held_left.axis = JOY_AXIS_LEFT_X
|
||||
held_left.axis_value = -1.0
|
||||
for _index: int in 4:
|
||||
manager.controller_input_observed.emit(held_left)
|
||||
if panel._auto_map_index != 0:
|
||||
return "auto-map accepted repeated analog motion without a neutral gate"
|
||||
var first_button := InputEventJoypadButton.new()
|
||||
first_button.button_index = JOY_BUTTON_A
|
||||
first_button.pressed = true
|
||||
manager.controller_input_observed.emit(first_button)
|
||||
if panel._auto_map_index != 1 or not panel._waiting_for_neutral:
|
||||
return "auto-map did not gate the next step after a captured input"
|
||||
manager.controller_input_observed.emit(first_button)
|
||||
if panel._auto_map_index != 1:
|
||||
return "held input advanced more than one auto-map step"
|
||||
panel._cancel_capture()
|
||||
return ""
|
||||
|
||||
|
||||
|
|
@ -149,6 +200,38 @@ func _validate_virtual_mouse_bounds() -> String:
|
|||
return ""
|
||||
|
||||
|
||||
func _validate_controller_scroll_target() -> String:
|
||||
var game_ui := GameUIType.new()
|
||||
var page := VBoxContainer.new()
|
||||
var scroll := ScrollContainer.new()
|
||||
var content := VBoxContainer.new()
|
||||
var button := Button.new()
|
||||
var footer_button := Button.new()
|
||||
root.add_child(page)
|
||||
page.add_child(scroll)
|
||||
scroll.add_child(content)
|
||||
content.add_child(button)
|
||||
page.add_child(footer_button)
|
||||
var target: ScrollContainer = game_ui._focused_scroll_container(button)
|
||||
if target != scroll:
|
||||
game_ui.free()
|
||||
page.free()
|
||||
return "focused menu content did not resolve its scroll container"
|
||||
target = game_ui._focused_scroll_container(footer_button)
|
||||
if target != scroll:
|
||||
game_ui.free()
|
||||
page.free()
|
||||
return "menu footer focus did not resolve the page's only scroll container"
|
||||
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
if game_ui._focused_scroll_container(button) != null:
|
||||
game_ui.free()
|
||||
page.free()
|
||||
return "controller scrolling ignored a disabled scroll container"
|
||||
game_ui.free()
|
||||
page.free()
|
||||
return ""
|
||||
|
||||
|
||||
func _keyboard_event_count(action: StringName) -> int:
|
||||
var count: int = 0
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const ControllerMappingManagerType = preload(
|
|||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
|
||||
|
||||
var _mapping_manager: ControllerMappingManagerType
|
||||
var _binding_buttons: Dictionary = {}
|
||||
|
|
@ -20,10 +21,14 @@ var _capturing_role: StringName = &""
|
|||
var _auto_map_active: bool = false
|
||||
var _auto_map_index: int = -1
|
||||
var _auto_map_draft: Dictionary = {}
|
||||
var _capture_device_id: int = 0
|
||||
var _waiting_for_neutral: bool = false
|
||||
var _neutral_elapsed: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
_build_interface()
|
||||
hide()
|
||||
|
||||
|
|
@ -40,6 +45,12 @@ func setup(mapping_manager: ControllerMappingManagerType) -> void:
|
|||
_refresh_bindings
|
||||
):
|
||||
_mapping_manager.active_profile_changed.connect(_refresh_bindings)
|
||||
if not _mapping_manager.controller_input_observed.is_connected(
|
||||
_on_controller_input_observed
|
||||
):
|
||||
_mapping_manager.controller_input_observed.connect(
|
||||
_on_controller_input_observed
|
||||
)
|
||||
_refresh_bindings()
|
||||
|
||||
|
||||
|
|
@ -91,11 +102,61 @@ func _input(event: InputEvent) -> void:
|
|||
_cancel_capture()
|
||||
_progress_label.text = "controller mapping cancelled"
|
||||
return
|
||||
if not (
|
||||
event is InputEventJoypadButton
|
||||
or event is InputEventJoypadMotion
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if (
|
||||
not visible
|
||||
or _mapping_manager == null
|
||||
or _capturing_role.is_empty()
|
||||
):
|
||||
return
|
||||
if _waiting_for_neutral:
|
||||
if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
|
||||
_neutral_elapsed = 0.0
|
||||
return
|
||||
_neutral_elapsed += delta
|
||||
if _neutral_elapsed < CAPTURE_NEUTRAL_SECONDS:
|
||||
return
|
||||
_waiting_for_neutral = false
|
||||
_neutral_elapsed = 0.0
|
||||
_refresh_capture_prompt()
|
||||
return
|
||||
var pressed_button: int = _mapping_manager.get_pressed_capture_button(
|
||||
_capture_device_id
|
||||
)
|
||||
if pressed_button < 0:
|
||||
return
|
||||
var button_event := InputEventJoypadButton.new()
|
||||
button_event.device = _capture_device_id
|
||||
button_event.button_index = pressed_button as JoyButton
|
||||
button_event.pressed = true
|
||||
_try_capture_event(button_event)
|
||||
|
||||
|
||||
func _on_controller_input_observed(event: InputEvent) -> void:
|
||||
if not visible or _mapping_manager == null:
|
||||
return
|
||||
if _capturing_role.is_empty():
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
if _waiting_for_neutral:
|
||||
return
|
||||
var button_event := event as InputEventJoypadButton
|
||||
var motion_event := event as InputEventJoypadMotion
|
||||
var event_device: int = -1
|
||||
if button_event != null:
|
||||
event_device = button_event.device
|
||||
elif motion_event != null:
|
||||
event_device = motion_event.device
|
||||
else:
|
||||
return
|
||||
if event_device != _capture_device_id:
|
||||
return
|
||||
_try_capture_event(event)
|
||||
|
||||
|
||||
func _try_capture_event(event: InputEvent) -> void:
|
||||
var binding: Dictionary = _mapping_manager.binding_from_event(
|
||||
_capturing_role,
|
||||
event,
|
||||
|
|
@ -104,7 +165,6 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
if not _mapping_manager.validate_binding(_capturing_role, binding):
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
_accept_captured_binding(binding)
|
||||
|
||||
|
||||
|
|
@ -258,6 +318,7 @@ func _begin_auto_map() -> void:
|
|||
_auto_map_active = true
|
||||
_auto_map_index = 0
|
||||
_auto_map_draft = {}
|
||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
|
||||
|
||||
|
||||
|
|
@ -265,27 +326,36 @@ func _begin_manual_capture(role: StringName) -> void:
|
|||
_auto_map_active = false
|
||||
_auto_map_index = -1
|
||||
_auto_map_draft.clear()
|
||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||
_set_capture_role(role)
|
||||
|
||||
|
||||
func _set_capture_role(role: StringName) -> void:
|
||||
_capturing_role = role
|
||||
_waiting_for_neutral = true
|
||||
_neutral_elapsed = 0.0
|
||||
_set_action_buttons_disabled(true)
|
||||
_progress_label.text = "release all controller inputs"
|
||||
|
||||
|
||||
func _refresh_capture_prompt() -> void:
|
||||
if _capturing_role.is_empty():
|
||||
return
|
||||
if _auto_map_active:
|
||||
_progress_label.text = "step %d of %d: %s" % [
|
||||
_auto_map_index + 1,
|
||||
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||
_mapping_manager.get_role_prompt(role),
|
||||
_mapping_manager.get_role_prompt(_capturing_role),
|
||||
]
|
||||
return
|
||||
var input_instruction: String = "press any controller button"
|
||||
if _mapping_manager.role_expects_axis(role):
|
||||
if _mapping_manager.role_expects_axis(_capturing_role):
|
||||
input_instruction = "move any controller axis"
|
||||
elif _mapping_manager.role_accepts_axis(role):
|
||||
elif _mapping_manager.role_accepts_axis(_capturing_role):
|
||||
input_instruction = "press any button or move any controller axis"
|
||||
_progress_label.text = "%s for %s" % [
|
||||
input_instruction,
|
||||
_mapping_manager.get_role_label(role),
|
||||
_mapping_manager.get_role_label(_capturing_role),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -322,6 +392,8 @@ func _cancel_capture() -> void:
|
|||
_auto_map_active = false
|
||||
_auto_map_index = -1
|
||||
_auto_map_draft.clear()
|
||||
_waiting_for_neutral = false
|
||||
_neutral_elapsed = 0.0
|
||||
_set_action_buttons_disabled(false)
|
||||
if _progress_label != null:
|
||||
_progress_label.text = ""
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55
|
|||
const VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD: float = 0.35
|
||||
const VIRTUAL_MOUSE_STICK_DEADZONE: float = 0.18
|
||||
const VIRTUAL_MOUSE_SPEED: float = 720.0
|
||||
const CONTROLLER_MENU_SCROLL_DEADZONE: float = 0.25
|
||||
const CONTROLLER_MENU_SCROLL_SPEED: float = 660.0
|
||||
# Android controller mappings may expose LT on its own axis or as the negative
|
||||
# half of the same signed axis used by RT. Support both without letting the RT
|
||||
# zoom direction enter virtual-pointer mode.
|
||||
|
|
@ -998,6 +1000,7 @@ func is_controller_mapping_capturing() -> bool:
|
|||
func _process(delta: float) -> void:
|
||||
_poll_virtual_mouse_controller_state()
|
||||
_update_virtual_mouse(delta)
|
||||
_update_controller_menu_scroll(delta)
|
||||
_update_experience_bubble_position()
|
||||
if _item_effects == null or not _gameplay_ui_enabled:
|
||||
_effect_status.hide()
|
||||
|
|
@ -1026,6 +1029,85 @@ func _process(delta: float) -> void:
|
|||
_effect_status.visible = not parts.is_empty()
|
||||
|
||||
|
||||
func _update_controller_menu_scroll(delta: float) -> void:
|
||||
if (
|
||||
_controller_mapping_manager == null
|
||||
or _virtual_mouse_active
|
||||
or is_controller_mapping_capturing()
|
||||
):
|
||||
return
|
||||
var stick_y: float = _controller_menu_scroll_axis()
|
||||
var magnitude: float = absf(stick_y)
|
||||
if magnitude <= CONTROLLER_MENU_SCROLL_DEADZONE:
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
var scroll: ScrollContainer = _focused_scroll_container(focus_owner)
|
||||
if scroll == null:
|
||||
return
|
||||
var adjusted_axis: float = (
|
||||
signf(stick_y)
|
||||
* (magnitude - CONTROLLER_MENU_SCROLL_DEADZONE)
|
||||
/ (1.0 - CONTROLLER_MENU_SCROLL_DEADZONE)
|
||||
)
|
||||
scroll.scroll_vertical += roundi(
|
||||
adjusted_axis * CONTROLLER_MENU_SCROLL_SPEED * delta
|
||||
)
|
||||
|
||||
|
||||
func _focused_scroll_container(focus_owner: Control) -> ScrollContainer:
|
||||
if focus_owner == null or not focus_owner.is_visible_in_tree():
|
||||
return null
|
||||
var current: Node = focus_owner
|
||||
while current != null:
|
||||
var scroll := current as ScrollContainer
|
||||
if scroll != null:
|
||||
if _controller_scroll_container_is_available(scroll):
|
||||
return scroll
|
||||
return null
|
||||
var candidates: Array[ScrollContainer] = []
|
||||
_collect_controller_scroll_containers(current, candidates)
|
||||
if candidates.size() == 1:
|
||||
return candidates[0]
|
||||
if candidates.size() > 1:
|
||||
return null
|
||||
current = current.get_parent()
|
||||
return null
|
||||
|
||||
|
||||
func _collect_controller_scroll_containers(
|
||||
root_node: Node,
|
||||
result: Array[ScrollContainer],
|
||||
) -> void:
|
||||
for child: Node in root_node.get_children():
|
||||
var canvas_item := child as CanvasItem
|
||||
if canvas_item != null and not canvas_item.is_visible_in_tree():
|
||||
continue
|
||||
var scroll := child as ScrollContainer
|
||||
if scroll != null and _controller_scroll_container_is_available(scroll):
|
||||
result.append(scroll)
|
||||
_collect_controller_scroll_containers(child, result)
|
||||
|
||||
|
||||
func _controller_scroll_container_is_available(
|
||||
scroll: ScrollContainer,
|
||||
) -> bool:
|
||||
return (
|
||||
scroll.is_visible_in_tree()
|
||||
and scroll.vertical_scroll_mode != ScrollContainer.SCROLL_MODE_DISABLED
|
||||
)
|
||||
|
||||
|
||||
func _controller_menu_scroll_axis() -> float:
|
||||
if _controller_mapping_manager.has_custom_mapping():
|
||||
return _controller_mapping_manager.get_role_axis(
|
||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||
)
|
||||
return Input.get_joy_axis(
|
||||
_controller_mapping_manager.get_active_device_id(),
|
||||
JOY_AXIS_RIGHT_Y,
|
||||
)
|
||||
|
||||
|
||||
func close_player_menu() -> void:
|
||||
_player_menu.close_menu()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue