feat: overhaul controller menu accessibility
This commit is contained in:
parent
a7837c3711
commit
4f9c85989b
46 changed files with 4091 additions and 396 deletions
66
main/main.gd
66
main/main.gd
|
|
@ -19,6 +19,9 @@ const PlayerSettingsType = preload("res://settings/player_settings.gd")
|
|||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const TitleScreenType = preload("res://ui/title_screen.gd")
|
||||
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||
|
|
@ -139,6 +142,9 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
|||
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
|
||||
%ControllerMappingManager
|
||||
)
|
||||
@onready var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType = (
|
||||
%KeyboardMouseMappingManager
|
||||
)
|
||||
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
|
||||
@onready var _title_music: AudioStreamPlayer = %TitleMusic
|
||||
@onready var _dusk_music: AudioStreamPlayer = %DuskMusic
|
||||
|
|
@ -279,6 +285,10 @@ func _ready() -> void:
|
|||
_settings_manager.settings_changed.connect(_apply_runtime_settings)
|
||||
_settings_manager.load_settings()
|
||||
_interface_fonts.enforce_standard_font()
|
||||
_interface_fonts.set_controller_text_entry_request(
|
||||
Callable(_game_ui, "request_controller_text_entry_for"),
|
||||
Callable(_game_ui, "is_controller_text_entry_open"),
|
||||
)
|
||||
if _data_root.resolve():
|
||||
_configure_portable_stores()
|
||||
_initialize_after_data_root()
|
||||
|
|
@ -760,6 +770,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_interface_fonts,
|
||||
)
|
||||
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
|
||||
_data_root.conflict_detected.connect(_on_portable_conflict)
|
||||
_data_root.status_changed.connect(_on_data_root_status)
|
||||
if (
|
||||
|
|
@ -919,13 +930,13 @@ func _focus_data_setup_actions() -> void:
|
|||
actions.append(_data_setup_dialog.get_cancel_button())
|
||||
for index: int in actions.size():
|
||||
var action: Button = actions[index]
|
||||
var previous: Button = actions[posmod(index - 1, actions.size())]
|
||||
var next: Button = actions[(index + 1) % actions.size()]
|
||||
var previous: Button = actions[maxi(index - 1, 0)]
|
||||
var next: Button = actions[mini(index + 1, actions.size() - 1)]
|
||||
action.focus_mode = Control.FOCUS_ALL
|
||||
action.focus_neighbor_left = action.get_path_to(previous)
|
||||
action.focus_neighbor_top = action.focus_neighbor_left
|
||||
action.focus_neighbor_right = action.get_path_to(next)
|
||||
action.focus_neighbor_bottom = action.focus_neighbor_right
|
||||
action.focus_neighbor_top = action.get_path_to(action)
|
||||
action.focus_neighbor_bottom = action.get_path_to(action)
|
||||
_data_setup_dialog.get_ok_button().grab_focus()
|
||||
|
||||
|
||||
|
|
@ -1072,12 +1083,16 @@ func _show_existing_root_choice(path: String) -> void:
|
|||
_interface_fonts.apply_utility_theme(dialog)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(680, 360))
|
||||
_configure_popup_dialog.call_deferred(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
|
||||
|
||||
func _show_data_error(message: String) -> void:
|
||||
if _data_setup_dialog != null:
|
||||
_data_setup_dialog.dialog_text = message
|
||||
_data_setup_dialog.popup_centered(Vector2i(640, 300))
|
||||
_focus_data_setup_actions.call_deferred()
|
||||
|
||||
|
||||
func _on_portable_conflict(message: String, _path: String) -> void:
|
||||
|
|
@ -1096,6 +1111,7 @@ func _on_portable_conflict(message: String, _path: String) -> void:
|
|||
dialog.confirmed.connect(dialog.queue_free)
|
||||
_game_ui.add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(560, 300))
|
||||
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
|
||||
|
||||
|
||||
func _on_data_root_status(message: String) -> void:
|
||||
|
|
@ -1113,6 +1129,7 @@ func _on_data_root_status(message: String) -> void:
|
|||
dialog.confirmed.connect(dialog.queue_free)
|
||||
_game_ui.add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(560, 260))
|
||||
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
|
||||
|
||||
|
||||
func _on_server_trust_required(
|
||||
|
|
@ -1153,6 +1170,10 @@ func _on_server_trust_required(
|
|||
)
|
||||
_server_trust_dialog.ok_button_text = "Trust & Connect"
|
||||
_server_trust_dialog.popup_centered(Vector2i(560, 360))
|
||||
_configure_popup_dialog.call_deferred(
|
||||
_server_trust_dialog,
|
||||
_server_trust_dialog.get_cancel_button(),
|
||||
)
|
||||
|
||||
|
||||
func _confirm_server_trust() -> void:
|
||||
|
|
@ -1164,6 +1185,10 @@ func _confirm_server_trust() -> void:
|
|||
)
|
||||
_server_trust_dialog.ok_button_text = "Replace Pin"
|
||||
_server_trust_dialog.popup_centered(Vector2i(520, 260))
|
||||
_configure_popup_dialog.call_deferred(
|
||||
_server_trust_dialog,
|
||||
_server_trust_dialog.get_cancel_button(),
|
||||
)
|
||||
return
|
||||
if _server_trust_dialog != null:
|
||||
_server_trust_dialog.hide()
|
||||
|
|
@ -1182,13 +1207,27 @@ func _on_peer_identity_observed(_peer_id: int, status: String) -> void:
|
|||
+ "This may be a different player using the same display name."
|
||||
)
|
||||
_identity_notice_dialog.popup_centered(Vector2i(480, 240))
|
||||
_configure_popup_dialog.call_deferred(
|
||||
_identity_notice_dialog,
|
||||
_identity_notice_dialog.get_ok_button(),
|
||||
)
|
||||
|
||||
|
||||
func _configure_popup_dialog(
|
||||
dialog: Window,
|
||||
preferred_control: Control = null,
|
||||
) -> void:
|
||||
if dialog != null and is_instance_valid(dialog) and dialog.visible:
|
||||
FileDialogControllerNavigation.configure_scope(
|
||||
dialog, preferred_control
|
||||
)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _handle_data_root_controller_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _game_ui.is_controller_mapping_capturing():
|
||||
if _game_ui.is_input_mapping_capturing():
|
||||
return
|
||||
if (
|
||||
not (
|
||||
|
|
@ -1227,6 +1266,8 @@ func _input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func _handle_data_root_controller_input(event: InputEvent) -> bool:
|
||||
if _game_ui.is_controller_text_entry_open():
|
||||
return false
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null or not button_event.pressed:
|
||||
return false
|
||||
|
|
@ -1238,6 +1279,11 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool:
|
|||
)
|
||||
if not setup_visible and not picker_visible:
|
||||
return false
|
||||
var picker_scope: Window = (
|
||||
FileDialogControllerNavigation.active_scope(_data_folder_dialog)
|
||||
if picker_visible
|
||||
else null
|
||||
)
|
||||
var use_mapping: bool = _controller_mapping_manager != null
|
||||
var accept_pressed: bool = (
|
||||
_controller_mapping_manager.event_matches_role(
|
||||
|
|
@ -1263,6 +1309,9 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool:
|
|||
)
|
||||
if cancel_pressed:
|
||||
if picker_visible:
|
||||
if picker_scope != null and picker_scope != _data_folder_dialog:
|
||||
picker_scope.hide()
|
||||
else:
|
||||
_on_data_folder_picker_canceled()
|
||||
else:
|
||||
_data_setup_dialog.get_cancel_button().pressed.emit()
|
||||
|
|
@ -1270,7 +1319,7 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool:
|
|||
if not accept_pressed:
|
||||
return false
|
||||
var focused: Control = (
|
||||
_data_folder_dialog.gui_get_focus_owner()
|
||||
picker_scope.gui_get_focus_owner()
|
||||
if picker_visible
|
||||
else _data_setup_dialog.gui_get_focus_owner()
|
||||
)
|
||||
|
|
@ -1278,6 +1327,11 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool:
|
|||
if focused_button != null and not focused_button.disabled:
|
||||
focused_button.pressed.emit()
|
||||
return true
|
||||
if (
|
||||
(focused is LineEdit or focused is TextEdit)
|
||||
and _game_ui.request_controller_text_entry_for(focused)
|
||||
):
|
||||
return true
|
||||
if setup_visible:
|
||||
_data_setup_dialog.get_ok_button().pressed.emit()
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
|
||||
[ext_resource type="AudioStream" uid="uid://dck3tf8qkadea" path="res://audio/ambience/waves.wav" id="55_waves"]
|
||||
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
|
||||
[ext_resource type="Script" path="res://settings/keyboard_mouse_mapping_manager.gd" id="keyboard_mouse_mapping"]
|
||||
[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
|
||||
[ext_resource type="AudioStream" uid="uid://c4y8puv0n6xk8" path="res://audio/music/world/craft.mp3" id="dusk_music"]
|
||||
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
|
||||
|
|
@ -391,6 +392,10 @@ script = ExtResource("10_settings")
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("56_controller_mapping")
|
||||
|
||||
[node name="KeyboardMouseMappingManager" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("keyboard_mouse_mapping")
|
||||
|
||||
[node name="TitleMusic" type="AudioStreamPlayer" parent="." unique_id=1292939956]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("13_title_music")
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ jump={
|
|||
sneak={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":8,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
slow_walk={
|
||||
|
|
@ -205,7 +206,8 @@ open_quick_actions={
|
|||
}
|
||||
open_chat={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
focus_gameplay={
|
||||
|
|
|
|||
|
|
@ -10,15 +10,23 @@ readonly RUN_ROOT="$(mktemp -d -t netfishing-validations.XXXXXX)"
|
|||
|
||||
readonly -a QUICK_TESTS=(
|
||||
"tests/android_readiness_validation.gd"
|
||||
"tests/controller_focus_presentation_validation.gd"
|
||||
"tests/controller_focus_recovery_validation.gd"
|
||||
"tests/controller_menu_accessibility_validation.gd"
|
||||
"tests/controller_mapping_validation.gd"
|
||||
"tests/controller_ui_navigation_validation.gd"
|
||||
"tests/dedicated_server_config_validation.gd"
|
||||
"tests/exported_decal_hotfix_validation.gd"
|
||||
"tests/file_dialog_controller_navigation_validation.gd"
|
||||
"tests/fish_catalog_content_validation.gd"
|
||||
"tests/fish_quality_validation.gd"
|
||||
"tests/fishing_audio_validation.gd"
|
||||
"tests/fishing_surface_validation.gd"
|
||||
"tests/fur_pattern_validation.gd"
|
||||
"tests/keyboard_mouse_mapping_validation.gd"
|
||||
"tests/logbook_validation.gd"
|
||||
"tests/network_player_animation_protocol_validation.gd"
|
||||
"tests/on_screen_keyboard_validation.gd"
|
||||
"tests/player_experience_validation.gd"
|
||||
"tests/shoreline_ambience_validation.gd"
|
||||
"tests/surface_drawing_validation.gd"
|
||||
|
|
@ -56,6 +64,7 @@ readonly -a NETWORK_TESTS=(
|
|||
"tests/job_multiplayer_validation.gd"
|
||||
"tests/movement_multiplayer_validation.gd"
|
||||
"tests/operator_multiplayer_validation.gd"
|
||||
"tests/profile_multiplayer_validation.gd"
|
||||
"tests/surface_drawing_multiplayer_validation.gd"
|
||||
"tests/world_time_multiplayer_validation.gd"
|
||||
"tests/world_spawn_multiplayer_validation.gd"
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const ROLE_LABELS: Dictionary = {
|
|||
ROLE_SELECT: "chat",
|
||||
ROLE_START: "pause",
|
||||
ROLE_LEFT_STICK_CLICK: "sprint",
|
||||
ROLE_RIGHT_STICK_CLICK: "unassigned stick click",
|
||||
ROLE_RIGHT_STICK_CLICK: "sneak",
|
||||
ROLE_DPAD_UP: "emote wheel",
|
||||
ROLE_DPAD_DOWN: "quick menu",
|
||||
ROLE_DPAD_LEFT: "previous hotbar slot",
|
||||
|
|
@ -106,8 +106,8 @@ const ROLE_PROMPTS: Dictionary = {
|
|||
ROLE_Y: "press the y button",
|
||||
ROLE_LB: "press the left bumper",
|
||||
ROLE_RB: "press the right bumper",
|
||||
ROLE_POINTER_MODIFIER: "press or squeeze the virtual mouse modifier",
|
||||
ROLE_CAMERA_ZOOM: "press or squeeze the camera zoom control",
|
||||
ROLE_POINTER_MODIFIER: "press or squeeze LT",
|
||||
ROLE_CAMERA_ZOOM: "press or squeeze RT",
|
||||
ROLE_SELECT: "press select / back",
|
||||
ROLE_START: "press start",
|
||||
ROLE_LEFT_STICK_CLICK: "click the left stick",
|
||||
|
|
@ -144,6 +144,7 @@ const BUTTON_ACTION_ROLES: Dictionary = {
|
|||
&"open_chat": ROLE_SELECT,
|
||||
&"open_system_menu": ROLE_START,
|
||||
&"sprint": ROLE_LEFT_STICK_CLICK,
|
||||
&"sneak": ROLE_RIGHT_STICK_CLICK,
|
||||
&"open_emotes": ROLE_DPAD_UP,
|
||||
&"open_quick_actions": ROLE_DPAD_DOWN,
|
||||
&"hotbar_previous": ROLE_DPAD_LEFT,
|
||||
|
|
|
|||
448
settings/keyboard_mouse_mapping_manager.gd
Normal file
448
settings/keyboard_mouse_mapping_manager.gd
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
class_name KeyboardMouseMappingManager
|
||||
extends Node
|
||||
|
||||
signal mapping_changed
|
||||
|
||||
const FORMAT_VERSION: int = 1
|
||||
const MAPPING_PATH: String = "user://keyboard_mouse_bindings.json"
|
||||
const MAPPING_TEMP_PATH: String = "user://keyboard_mouse_bindings.json.tmp"
|
||||
const MAPPING_BACKUP_PATH: String = (
|
||||
"user://keyboard_mouse_bindings.json.backup"
|
||||
)
|
||||
const MAX_MAPPING_BYTES: int = 256 * 1024
|
||||
|
||||
const ROLE_MOVE_FORWARD: StringName = &"move_forward"
|
||||
const ROLE_MOVE_BACKWARD: StringName = &"move_backward"
|
||||
const ROLE_MOVE_LEFT: StringName = &"move_left"
|
||||
const ROLE_MOVE_RIGHT: StringName = &"move_right"
|
||||
const ROLE_SPRINT: StringName = &"sprint"
|
||||
const ROLE_JUMP: StringName = &"jump"
|
||||
const ROLE_SNEAK: StringName = &"sneak"
|
||||
const ROLE_SLOW_WALK: StringName = &"slow_walk"
|
||||
const ROLE_INTERACT: StringName = &"interact"
|
||||
const ROLE_PRIMARY_ACTION: StringName = &"fish_primary"
|
||||
const ROLE_CAMERA_DRAG: StringName = &"camera_drag"
|
||||
const ROLE_CAMERA_ZOOM_IN: StringName = &"camera_zoom_in"
|
||||
const ROLE_CAMERA_ZOOM_OUT: StringName = &"camera_zoom_out"
|
||||
const ROLE_PLAYER_MENU: StringName = &"open_backpack"
|
||||
const ROLE_TACKLE_BOX: StringName = &"open_tacklebox"
|
||||
const ROLE_PROP_BOOK: StringName = &"open_props"
|
||||
const ROLE_EMOTE_WHEEL: StringName = &"open_emotes"
|
||||
const ROLE_CHARACTER_CALL: StringName = &"character_call"
|
||||
const ROLE_CHAT: StringName = &"open_chat"
|
||||
const ROLE_FREE_CAMERA: StringName = &"toggle_free_camera"
|
||||
const ROLE_HIDE_HUD: StringName = &"toggle_hud"
|
||||
const ROLE_MENU_ACCEPT: StringName = &"ui_accept"
|
||||
const ROLE_MENU_BACK: StringName = &"ui_cancel"
|
||||
|
||||
const ROLE_ORDER: Array[StringName] = [
|
||||
ROLE_MOVE_FORWARD,
|
||||
ROLE_MOVE_BACKWARD,
|
||||
ROLE_MOVE_LEFT,
|
||||
ROLE_MOVE_RIGHT,
|
||||
ROLE_SPRINT,
|
||||
ROLE_JUMP,
|
||||
ROLE_SNEAK,
|
||||
ROLE_SLOW_WALK,
|
||||
ROLE_INTERACT,
|
||||
ROLE_PRIMARY_ACTION,
|
||||
ROLE_CAMERA_DRAG,
|
||||
ROLE_CAMERA_ZOOM_IN,
|
||||
ROLE_CAMERA_ZOOM_OUT,
|
||||
ROLE_PLAYER_MENU,
|
||||
ROLE_TACKLE_BOX,
|
||||
ROLE_PROP_BOOK,
|
||||
ROLE_EMOTE_WHEEL,
|
||||
ROLE_CHARACTER_CALL,
|
||||
ROLE_CHAT,
|
||||
ROLE_FREE_CAMERA,
|
||||
ROLE_HIDE_HUD,
|
||||
&"hotbar_1",
|
||||
&"hotbar_2",
|
||||
&"hotbar_3",
|
||||
&"hotbar_4",
|
||||
&"hotbar_5",
|
||||
&"hotbar_6",
|
||||
&"hotbar_7",
|
||||
&"hotbar_8",
|
||||
&"hotbar_9",
|
||||
ROLE_MENU_ACCEPT,
|
||||
ROLE_MENU_BACK,
|
||||
]
|
||||
|
||||
const ROLE_LABELS: Dictionary = {
|
||||
ROLE_MOVE_FORWARD: "move forward",
|
||||
ROLE_MOVE_BACKWARD: "move backward",
|
||||
ROLE_MOVE_LEFT: "move left",
|
||||
ROLE_MOVE_RIGHT: "move right",
|
||||
ROLE_SPRINT: "sprint",
|
||||
ROLE_JUMP: "jump",
|
||||
ROLE_SNEAK: "sneak",
|
||||
ROLE_SLOW_WALK: "slow walk",
|
||||
ROLE_INTERACT: "interact",
|
||||
ROLE_PRIMARY_ACTION: "primary action",
|
||||
ROLE_CAMERA_DRAG: "rotate camera",
|
||||
ROLE_CAMERA_ZOOM_IN: "camera zoom in",
|
||||
ROLE_CAMERA_ZOOM_OUT: "camera zoom out",
|
||||
ROLE_PLAYER_MENU: "player menu",
|
||||
ROLE_TACKLE_BOX: "tackle box",
|
||||
ROLE_PROP_BOOK: "prop book",
|
||||
ROLE_EMOTE_WHEEL: "emote wheel",
|
||||
ROLE_CHARACTER_CALL: "character call",
|
||||
ROLE_CHAT: "chat",
|
||||
ROLE_FREE_CAMERA: "free camera",
|
||||
ROLE_HIDE_HUD: "hide hud",
|
||||
&"hotbar_1": "hotbar 1",
|
||||
&"hotbar_2": "hotbar 2",
|
||||
&"hotbar_3": "hotbar 3",
|
||||
&"hotbar_4": "hotbar 4",
|
||||
&"hotbar_5": "hotbar 5",
|
||||
&"hotbar_6": "hotbar 6",
|
||||
&"hotbar_7": "hotbar 7",
|
||||
&"hotbar_8": "hotbar 8",
|
||||
&"hotbar_9": "hotbar 9",
|
||||
ROLE_MENU_ACCEPT: "menu accept",
|
||||
ROLE_MENU_BACK: "menu back / pause",
|
||||
}
|
||||
|
||||
var _default_events: Dictionary = {}
|
||||
var _default_bindings: Dictionary = {}
|
||||
var _bindings: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_capture_project_defaults()
|
||||
load_mapping()
|
||||
_apply_active_mapping()
|
||||
|
||||
|
||||
func load_mapping() -> bool:
|
||||
_recover_interrupted_write()
|
||||
if not FileAccess.file_exists(MAPPING_PATH):
|
||||
_bindings = {}
|
||||
return true
|
||||
var file := FileAccess.open(MAPPING_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
_bindings = {}
|
||||
return false
|
||||
if file.get_length() > MAX_MAPPING_BYTES:
|
||||
file.close()
|
||||
push_warning("Keyboard bindings are too large; using defaults.")
|
||||
_bindings = {}
|
||||
return false
|
||||
var json := JSON.new()
|
||||
var error: Error = json.parse(file.get_as_text())
|
||||
file.close()
|
||||
if error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||
push_warning("Keyboard bindings are malformed; using defaults.")
|
||||
_bindings = {}
|
||||
return false
|
||||
var data := json.data as Dictionary
|
||||
if int(data.get("format_version", -1)) != FORMAT_VERSION:
|
||||
push_warning("Keyboard binding version is unsupported; using defaults.")
|
||||
_bindings = {}
|
||||
return false
|
||||
var raw_bindings: Variant = data.get("bindings", {})
|
||||
if (
|
||||
typeof(raw_bindings) != TYPE_DICTIONARY
|
||||
or not _validate_complete_bindings(raw_bindings as Dictionary)
|
||||
):
|
||||
push_warning("Keyboard bindings are incomplete; using defaults.")
|
||||
_bindings = {}
|
||||
return false
|
||||
_bindings = (raw_bindings as Dictionary).duplicate(true)
|
||||
return true
|
||||
|
||||
|
||||
func has_custom_mapping() -> bool:
|
||||
return not _bindings.is_empty()
|
||||
|
||||
|
||||
func get_active_bindings() -> Dictionary:
|
||||
if has_custom_mapping():
|
||||
return _bindings.duplicate(true)
|
||||
return _default_bindings.duplicate(true)
|
||||
|
||||
|
||||
func get_role_label(role: StringName) -> String:
|
||||
return str(ROLE_LABELS.get(role, str(role).replace("_", " ")))
|
||||
|
||||
|
||||
func binding_from_event(event: InputEvent) -> Dictionary:
|
||||
var key_event := event as InputEventKey
|
||||
if key_event != null:
|
||||
if not key_event.pressed or key_event.echo:
|
||||
return {}
|
||||
if key_event.keycode in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META]:
|
||||
return {}
|
||||
if key_event.keycode == KEY_NONE and (
|
||||
key_event.physical_keycode == KEY_NONE
|
||||
):
|
||||
return {}
|
||||
return {
|
||||
"kind": "key",
|
||||
"keycode": int(key_event.keycode),
|
||||
"physical_keycode": int(key_event.physical_keycode),
|
||||
"alt": key_event.alt_pressed,
|
||||
"shift": key_event.shift_pressed,
|
||||
"ctrl": key_event.ctrl_pressed,
|
||||
"meta": key_event.meta_pressed,
|
||||
"location": int(key_event.location),
|
||||
}
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event == null or not mouse_event.pressed:
|
||||
return {}
|
||||
return {
|
||||
"kind": "mouse_button",
|
||||
"button": int(mouse_event.button_index),
|
||||
}
|
||||
|
||||
|
||||
func validate_binding(binding: Dictionary) -> bool:
|
||||
var kind: String = str(binding.get("kind", ""))
|
||||
if kind == "mouse_button":
|
||||
var button: int = int(binding.get("button", 0))
|
||||
return (
|
||||
button >= int(MOUSE_BUTTON_LEFT)
|
||||
and button <= int(MOUSE_BUTTON_XBUTTON2)
|
||||
)
|
||||
if kind != "key":
|
||||
return false
|
||||
var keycode: int = int(binding.get("keycode", int(KEY_NONE)))
|
||||
var physical_keycode: int = int(
|
||||
binding.get("physical_keycode", int(KEY_NONE))
|
||||
)
|
||||
if keycode == int(KEY_NONE) and physical_keycode == int(KEY_NONE):
|
||||
return false
|
||||
for modifier: String in ["alt", "shift", "ctrl", "meta"]:
|
||||
if typeof(binding.get(modifier, false)) != TYPE_BOOL:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func set_binding(role: StringName, binding: Dictionary) -> bool:
|
||||
if role not in ROLE_ORDER or not validate_binding(binding):
|
||||
return false
|
||||
var updated: Dictionary = get_active_bindings()
|
||||
updated[str(role)] = binding.duplicate(true)
|
||||
return replace_active_bindings(updated)
|
||||
|
||||
|
||||
func replace_active_bindings(bindings: Dictionary) -> bool:
|
||||
if not _validate_complete_bindings(bindings):
|
||||
return false
|
||||
var previous: Dictionary = _bindings.duplicate(true)
|
||||
_bindings = bindings.duplicate(true)
|
||||
if not _save_mapping():
|
||||
_bindings = previous
|
||||
return false
|
||||
_apply_active_mapping()
|
||||
mapping_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func reset_mapping() -> bool:
|
||||
var previous: Dictionary = _bindings.duplicate(true)
|
||||
_bindings = {}
|
||||
if not _remove_if_present(MAPPING_PATH):
|
||||
_bindings = previous
|
||||
return false
|
||||
_remove_if_present(MAPPING_TEMP_PATH)
|
||||
_remove_if_present(MAPPING_BACKUP_PATH)
|
||||
_restore_project_defaults()
|
||||
mapping_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func binding_label(binding: Dictionary) -> String:
|
||||
if str(binding.get("kind", "")) == "mouse_button":
|
||||
return _mouse_button_label(int(binding.get("button", 0)))
|
||||
if str(binding.get("kind", "")) != "key":
|
||||
return "unmapped"
|
||||
var event := _event_from_binding(binding) as InputEventKey
|
||||
if event == null:
|
||||
return "unmapped"
|
||||
var label: String = event.as_text_physical_keycode()
|
||||
if label.is_empty():
|
||||
label = event.as_text_keycode()
|
||||
return label.to_lower() if not label.is_empty() else "unknown key"
|
||||
|
||||
|
||||
static func bindings_conflict(first: Dictionary, second: Dictionary) -> bool:
|
||||
if str(first.get("kind", "")) != str(second.get("kind", "")):
|
||||
return false
|
||||
if str(first.get("kind", "")) == "mouse_button":
|
||||
return int(first.get("button", 0)) == int(second.get("button", 0))
|
||||
if str(first.get("kind", "")) != "key":
|
||||
return false
|
||||
return (
|
||||
int(first.get("keycode", 0)) == int(second.get("keycode", 0))
|
||||
and int(first.get("physical_keycode", 0))
|
||||
== int(second.get("physical_keycode", 0))
|
||||
and bool(first.get("alt", false)) == bool(second.get("alt", false))
|
||||
and bool(first.get("shift", false)) == bool(second.get("shift", false))
|
||||
and bool(first.get("ctrl", false)) == bool(second.get("ctrl", false))
|
||||
and bool(first.get("meta", false)) == bool(second.get("meta", false))
|
||||
)
|
||||
|
||||
|
||||
func _capture_project_defaults() -> void:
|
||||
_default_events.clear()
|
||||
_default_bindings.clear()
|
||||
for role: StringName in ROLE_ORDER:
|
||||
var action: StringName = role
|
||||
var events: Array[InputEvent] = []
|
||||
if InputMap.has_action(action):
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
if not _is_keyboard_mouse_event(event):
|
||||
continue
|
||||
events.append(event.duplicate())
|
||||
if not _default_bindings.has(str(role)):
|
||||
var binding: Dictionary = binding_from_event(
|
||||
_pressed_copy(event)
|
||||
)
|
||||
if not binding.is_empty():
|
||||
_default_bindings[str(role)] = binding
|
||||
_default_events[action] = events
|
||||
|
||||
|
||||
func _validate_complete_bindings(bindings: Dictionary) -> bool:
|
||||
for role: StringName in ROLE_ORDER:
|
||||
var raw_binding: Variant = bindings.get(str(role), {})
|
||||
if (
|
||||
typeof(raw_binding) != TYPE_DICTIONARY
|
||||
or not validate_binding(raw_binding as Dictionary)
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _apply_active_mapping() -> void:
|
||||
if not has_custom_mapping():
|
||||
_restore_project_defaults()
|
||||
return
|
||||
_remove_managed_keyboard_mouse_events()
|
||||
for role: StringName in ROLE_ORDER:
|
||||
var binding: Variant = _bindings.get(str(role), {})
|
||||
if typeof(binding) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var event: InputEvent = _event_from_binding(binding as Dictionary)
|
||||
if event != null:
|
||||
InputMap.action_add_event(role, event)
|
||||
|
||||
|
||||
func _restore_project_defaults() -> void:
|
||||
_remove_managed_keyboard_mouse_events()
|
||||
for action_value: Variant in _default_events:
|
||||
var action: StringName = StringName(action_value)
|
||||
for event: InputEvent in _default_events[action]:
|
||||
InputMap.action_add_event(action, event.duplicate())
|
||||
|
||||
|
||||
func _remove_managed_keyboard_mouse_events() -> void:
|
||||
for action: StringName in ROLE_ORDER:
|
||||
if not InputMap.has_action(action):
|
||||
continue
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
if _is_keyboard_mouse_event(event):
|
||||
InputMap.action_erase_event(action, event)
|
||||
|
||||
|
||||
func _event_from_binding(binding: Dictionary) -> InputEvent:
|
||||
if not validate_binding(binding):
|
||||
return null
|
||||
if str(binding.get("kind", "")) == "mouse_button":
|
||||
var mouse_event := InputEventMouseButton.new()
|
||||
mouse_event.button_index = int(binding.get("button", 0)) as MouseButton
|
||||
return mouse_event
|
||||
var key_event := InputEventKey.new()
|
||||
key_event.keycode = int(binding.get("keycode", 0)) as Key
|
||||
key_event.physical_keycode = int(
|
||||
binding.get("physical_keycode", 0)
|
||||
) as Key
|
||||
key_event.alt_pressed = bool(binding.get("alt", false))
|
||||
key_event.shift_pressed = bool(binding.get("shift", false))
|
||||
key_event.ctrl_pressed = bool(binding.get("ctrl", false))
|
||||
key_event.meta_pressed = bool(binding.get("meta", false))
|
||||
key_event.location = int(binding.get("location", 0)) as KeyLocation
|
||||
return key_event
|
||||
|
||||
|
||||
static func _pressed_copy(event: InputEvent) -> InputEvent:
|
||||
var result: InputEvent = event.duplicate()
|
||||
if result is InputEventKey:
|
||||
(result as InputEventKey).pressed = true
|
||||
elif result is InputEventMouseButton:
|
||||
(result as InputEventMouseButton).pressed = true
|
||||
return result
|
||||
|
||||
|
||||
static func _is_keyboard_mouse_event(event: InputEvent) -> bool:
|
||||
return event is InputEventKey or event is InputEventMouseButton
|
||||
|
||||
|
||||
static func _mouse_button_label(button: int) -> String:
|
||||
var labels: Dictionary = {
|
||||
int(MOUSE_BUTTON_LEFT): "mouse left",
|
||||
int(MOUSE_BUTTON_RIGHT): "mouse right",
|
||||
int(MOUSE_BUTTON_MIDDLE): "mouse middle",
|
||||
int(MOUSE_BUTTON_WHEEL_UP): "wheel up",
|
||||
int(MOUSE_BUTTON_WHEEL_DOWN): "wheel down",
|
||||
int(MOUSE_BUTTON_WHEEL_LEFT): "wheel left",
|
||||
int(MOUSE_BUTTON_WHEEL_RIGHT): "wheel right",
|
||||
int(MOUSE_BUTTON_XBUTTON1): "mouse 4",
|
||||
int(MOUSE_BUTTON_XBUTTON2): "mouse 5",
|
||||
}
|
||||
return str(labels.get(button, "mouse %d" % button))
|
||||
|
||||
|
||||
func _save_mapping() -> bool:
|
||||
var file := FileAccess.open(MAPPING_TEMP_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_string(JSON.stringify({
|
||||
"format_version": FORMAT_VERSION,
|
||||
"bindings": _bindings,
|
||||
}, "\t"))
|
||||
file.flush()
|
||||
var write_error: Error = file.get_error()
|
||||
file.close()
|
||||
if write_error != OK:
|
||||
_remove_if_present(MAPPING_TEMP_PATH)
|
||||
return false
|
||||
_remove_if_present(MAPPING_BACKUP_PATH)
|
||||
var had_primary: bool = FileAccess.file_exists(MAPPING_PATH)
|
||||
if had_primary and not _rename_file(MAPPING_PATH, MAPPING_BACKUP_PATH):
|
||||
_remove_if_present(MAPPING_TEMP_PATH)
|
||||
return false
|
||||
if not _rename_file(MAPPING_TEMP_PATH, MAPPING_PATH):
|
||||
if had_primary:
|
||||
_rename_file(MAPPING_BACKUP_PATH, MAPPING_PATH)
|
||||
return false
|
||||
_remove_if_present(MAPPING_BACKUP_PATH)
|
||||
return true
|
||||
|
||||
|
||||
func _recover_interrupted_write() -> void:
|
||||
if FileAccess.file_exists(MAPPING_PATH):
|
||||
_remove_if_present(MAPPING_TEMP_PATH)
|
||||
_remove_if_present(MAPPING_BACKUP_PATH)
|
||||
return
|
||||
if FileAccess.file_exists(MAPPING_BACKUP_PATH):
|
||||
_rename_file(MAPPING_BACKUP_PATH, MAPPING_PATH)
|
||||
_remove_if_present(MAPPING_TEMP_PATH)
|
||||
|
||||
|
||||
static func _rename_file(from_path: String, to_path: String) -> bool:
|
||||
return DirAccess.rename_absolute(
|
||||
ProjectSettings.globalize_path(from_path),
|
||||
ProjectSettings.globalize_path(to_path),
|
||||
) == OK
|
||||
|
||||
|
||||
static func _remove_if_present(path: String) -> bool:
|
||||
if not FileAccess.file_exists(path):
|
||||
return true
|
||||
return DirAccess.remove_absolute(
|
||||
ProjectSettings.globalize_path(path)
|
||||
) == OK
|
||||
1
settings/keyboard_mouse_mapping_manager.gd.uid
Normal file
1
settings/keyboard_mouse_mapping_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b7ih6m4nte705
|
||||
|
|
@ -69,6 +69,34 @@ func _run() -> void:
|
|||
"inversion clears when a focused control is defocused",
|
||||
)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.position = Vector2(0.0, 80.0)
|
||||
scroll.size = Vector2(180.0, 90.0)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
stage.add_child(scroll)
|
||||
var list := VBoxContainer.new()
|
||||
list.custom_minimum_size.x = 160.0
|
||||
scroll.add_child(list)
|
||||
var last_scroll_button: Button
|
||||
for index: int in 8:
|
||||
var scroll_button := Button.new()
|
||||
scroll_button.text = "scroll option %d" % index
|
||||
scroll_button.custom_minimum_size = Vector2(160.0, 40.0)
|
||||
list.add_child(scroll_button)
|
||||
last_scroll_button = scroll_button
|
||||
await process_frame
|
||||
last_scroll_button.grab_focus()
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect(
|
||||
scroll.scroll_vertical > 0,
|
||||
"controller focus scrolls an off-screen selection into view",
|
||||
)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == last_scroll_button,
|
||||
"scroll following preserves the selected controller control",
|
||||
)
|
||||
|
||||
stage.queue_free()
|
||||
if _failures.is_empty():
|
||||
print("Controller focus presentation validation: PASS")
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||
str(defaults.keys()),
|
||||
]
|
||||
if ControllerMappingManagerType.ROLE_LABELS.get(
|
||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK, ""
|
||||
) != "sneak":
|
||||
return "right-stick click is not presented as sneak"
|
||||
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
|
||||
&"sneak", &""
|
||||
) != ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK:
|
||||
return "right-stick click is not assigned to the sneak action"
|
||||
if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK):
|
||||
return "sneak does not default to right-stick click"
|
||||
var trigger_button := InputEventJoypadButton.new()
|
||||
trigger_button.device = manager.get_active_device_id()
|
||||
trigger_button.button_index = JOY_BUTTON_MISC1
|
||||
|
|
@ -226,10 +236,41 @@ func _validate_portmaster_launcher() -> String:
|
|||
|
||||
|
||||
func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
||||
if manager.get_role_prompt(ControllerMappingManagerType.ROLE_LT) != (
|
||||
"press or squeeze LT"
|
||||
):
|
||||
return "auto-map does not identify the LT control"
|
||||
if manager.get_role_prompt(ControllerMappingManagerType.ROLE_RT) != (
|
||||
"press or squeeze RT"
|
||||
):
|
||||
return "auto-map does not identify the RT control"
|
||||
var panel := ControllerMappingPanelType.new()
|
||||
root.add_child(panel)
|
||||
panel.setup(manager)
|
||||
panel.open_panel()
|
||||
var mapping_scrolls: Array[Node] = panel.find_children(
|
||||
"", "ScrollContainer", true, false
|
||||
)
|
||||
if mapping_scrolls.is_empty():
|
||||
return "controller mapper does not contain its mapping list"
|
||||
if not (mapping_scrolls[0] as ScrollContainer).follow_focus:
|
||||
return "controller mapper does not scroll to follow controller focus"
|
||||
var first_binding := panel._binding_buttons.get(
|
||||
ControllerMappingManagerType.ROLE_ORDER.front()
|
||||
) as Button
|
||||
var last_binding := panel._binding_buttons.get(
|
||||
ControllerMappingManagerType.ROLE_ORDER.back()
|
||||
) as Button
|
||||
if first_binding.get_node(first_binding.focus_neighbor_top) != first_binding:
|
||||
return "controller mapper loses focus above its first row"
|
||||
if last_binding.get_node(last_binding.focus_neighbor_bottom) != (
|
||||
panel._close_button
|
||||
):
|
||||
return "controller mapper cannot reach Done from its last row"
|
||||
if panel._close_button.get_node(
|
||||
panel._close_button.focus_neighbor_top
|
||||
) != last_binding:
|
||||
return "controller mapper cannot return from Done to its last row"
|
||||
panel._begin_auto_map()
|
||||
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
|
|
@ -391,3 +432,11 @@ func _keyboard_event_count(action: StringName) -> int:
|
|||
if event is InputEventKey or event is InputEventMouseButton:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _has_joy_button(action: StringName, button_index: JoyButton) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var button := event as InputEventJoypadButton
|
||||
if button != null and button.button_index == button_index:
|
||||
return true
|
||||
return false
|
||||
|
|
|
|||
457
tests/controller_menu_accessibility_validation.gd
Normal file
457
tests/controller_menu_accessibility_validation.gd
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
extends SceneTree
|
||||
|
||||
const JoinGamePageScene = preload(
|
||||
"res://ui/network/join_game_page.tscn"
|
||||
)
|
||||
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
|
||||
const BubbleConfirmationScene = preload(
|
||||
"res://ui/components/bubble_menu/bubble_confirmation_page.tscn"
|
||||
)
|
||||
const TitleConfirmationScene = preload(
|
||||
"res://ui/title_confirmation_bubble_page.tscn"
|
||||
)
|
||||
const MailPageType = preload("res://ui/mail_page.gd")
|
||||
const ProfilePageType = preload("res://ui/profile_page.gd")
|
||||
const DialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
await _validate_join_game_navigation()
|
||||
await _validate_data_settings_navigation()
|
||||
await _validate_mail_navigation()
|
||||
await _validate_profile_confirmation_focus()
|
||||
await _validate_confirmation_dialog_navigation()
|
||||
await _validate_bubble_confirmation_navigation()
|
||||
_validate_mapping_capture_contract()
|
||||
if _failures.is_empty():
|
||||
print("Controller menu accessibility validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _validate_join_game_navigation() -> void:
|
||||
var page := JoinGamePageScene.instantiate() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.show()
|
||||
var discover := page.get_node("%DiscoverButton") as Button
|
||||
var direct := page.get_node("%DirectButton") as Button
|
||||
var saved := page.get_node("%SavedButton") as Button
|
||||
var recent := page.get_node("%RecentButton") as Button
|
||||
var address := page.get_node("%Address") as LineEdit
|
||||
var name_edit := page.get_node("%NameEdit") as LineEdit
|
||||
var server_list := page.get_node("%ServerList") as ItemList
|
||||
var refresh := page.get_node("%RefreshButton") as Button
|
||||
var join := page.get_node("%JoinButton") as Button
|
||||
var save := page.get_node("%SaveButton") as Button
|
||||
var edit := page.get_node("%EditButton") as Button
|
||||
var favorite := page.get_node("%FavoriteButton") as Button
|
||||
var delete := page.get_node("%DeleteButton") as Button
|
||||
var cancel := page.get_node("%CancelButton") as Button
|
||||
var back := page.get_node("%BackButton") as Button
|
||||
var modes: Array[Control] = [discover, direct, saved, recent]
|
||||
|
||||
address.hide()
|
||||
name_edit.hide()
|
||||
server_list.show()
|
||||
_set_button_state(refresh, true)
|
||||
_set_button_state(join, true)
|
||||
_set_button_state(save, false)
|
||||
_set_button_state(edit, false)
|
||||
_set_button_state(favorite, false)
|
||||
_set_button_state(delete, false)
|
||||
_set_button_state(cancel, false)
|
||||
_set_button_state(back, true)
|
||||
page.set("_mode", 0)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(discover, &"focus_neighbor_bottom", server_list)
|
||||
_assert_neighbor(server_list, &"focus_neighbor_top", discover)
|
||||
_assert_neighbor(server_list, &"focus_neighbor_bottom", refresh)
|
||||
_assert_neighbor(refresh, &"focus_neighbor_right", join)
|
||||
_assert_neighbor(back, &"focus_neighbor_left", join)
|
||||
var discover_controls: Array[Control] = modes.duplicate()
|
||||
discover_controls.append_array([server_list, refresh, join, back])
|
||||
_assert_directionally_reachable(discover, discover_controls)
|
||||
|
||||
address.show()
|
||||
address.editable = true
|
||||
server_list.hide()
|
||||
_set_button_state(refresh, false)
|
||||
_set_button_state(join, true)
|
||||
_set_button_state(save, true)
|
||||
page.set("_mode", 1)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(direct, &"focus_neighbor_bottom", address)
|
||||
_assert_neighbor(address, &"focus_neighbor_top", direct)
|
||||
_assert_neighbor(address, &"focus_neighbor_bottom", join)
|
||||
var direct_controls: Array[Control] = modes.duplicate()
|
||||
direct_controls.append_array([address, join, save, back])
|
||||
_assert_directionally_reachable(direct, direct_controls)
|
||||
|
||||
name_edit.show()
|
||||
name_edit.editable = true
|
||||
page.set("_name_entry_active", true)
|
||||
page.call("_configure_controller_navigation")
|
||||
await process_frame
|
||||
_assert_neighbor(address, &"focus_neighbor_bottom", name_edit)
|
||||
_assert_neighbor(name_edit, &"focus_neighbor_top", address)
|
||||
page.call("request_back")
|
||||
_expect(
|
||||
not bool(page.get("_name_entry_active")),
|
||||
"Join-game Back should leave the server-name edit substate.",
|
||||
)
|
||||
_expect(
|
||||
page.visible,
|
||||
"Join-game Back should not close the browser from an edit substate.",
|
||||
)
|
||||
await process_frame
|
||||
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_data_settings_navigation() -> void:
|
||||
var panel := SettingsPanelScene.instantiate() as Control
|
||||
root.add_child(panel)
|
||||
await process_frame
|
||||
panel.show()
|
||||
var data_page := panel.get_node("%DataPage") as SettingsBubblePage
|
||||
data_page.show_page(false)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var controls: Array[Control] = [
|
||||
panel.get_node("%OpenDataFolder") as Control,
|
||||
panel.get_node("%ChangeDataFolder") as Control,
|
||||
panel.get_node("%CopyPlayerFingerprint") as Control,
|
||||
panel.get_node("%ExportPlayerIdentity") as Control,
|
||||
panel.get_node("%ImportPlayerIdentity") as Control,
|
||||
panel.get_node("%ExportHostIdentity") as Control,
|
||||
panel.get_node("%ImportHostIdentity") as Control,
|
||||
panel.get_node("%DataBackButton") as Control,
|
||||
]
|
||||
for control: Control in controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_ALL,
|
||||
"Data & Identity control %s is not controller-focusable."
|
||||
% control.name,
|
||||
)
|
||||
_assert_directionally_reachable(controls.front(), controls)
|
||||
panel.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_mail_navigation() -> void:
|
||||
var page := MailPageType.new() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.call("activate")
|
||||
page.call("set_interactive", true)
|
||||
var inbox_list := page.get("_inbox_list") as VBoxContainer
|
||||
var entry := Button.new()
|
||||
entry.text = "test letter"
|
||||
entry.custom_minimum_size = Vector2(1008.0, 54.0)
|
||||
inbox_list.add_child(entry)
|
||||
await process_frame
|
||||
page.call("_refresh_controller_navigation")
|
||||
var archive_view := page.get("_archive_view_button") as Button
|
||||
var send_mail := page.get("_send_mail_button") as Button
|
||||
_assert_neighbor(
|
||||
archive_view, &"focus_neighbor_right", send_mail
|
||||
)
|
||||
_assert_neighbor(archive_view, &"focus_neighbor_bottom", entry)
|
||||
_assert_neighbor(send_mail, &"focus_neighbor_bottom", entry)
|
||||
_assert_neighbor(entry, &"focus_neighbor_top", archive_view)
|
||||
_assert_directionally_reachable(
|
||||
archive_view, [archive_view, send_mail, entry]
|
||||
)
|
||||
|
||||
var inbox := page.get("_inbox") as Control
|
||||
var compose := page.get("_compose") as Control
|
||||
var letter := page.get("_letter") as Control
|
||||
inbox.hide()
|
||||
compose.hide()
|
||||
letter.show()
|
||||
var accept := page.get("_accept") as Button
|
||||
var decline := page.get("_decline") as Button
|
||||
var close := page.get("_letter_close") as Button
|
||||
var archive := page.get("_archive") as Button
|
||||
var delete := page.get("_delete") as Button
|
||||
accept.show()
|
||||
decline.show()
|
||||
delete.disabled = false
|
||||
page.call("_refresh_controller_navigation")
|
||||
_assert_neighbor(accept, &"focus_neighbor_bottom", decline)
|
||||
_assert_neighbor(decline, &"focus_neighbor_bottom", delete)
|
||||
_assert_neighbor(delete, &"focus_neighbor_left", archive)
|
||||
_assert_neighbor(archive, &"focus_neighbor_left", close)
|
||||
_assert_neighbor(close, &"focus_neighbor_top", close)
|
||||
_assert_directionally_reachable(
|
||||
accept, [accept, decline, close, archive, delete]
|
||||
)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_profile_confirmation_focus() -> void:
|
||||
var page := ProfilePageType.new() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.call("activate")
|
||||
page.call("set_interactive", true)
|
||||
page.call("reset_controller_zone")
|
||||
var suggestions := page.get("_suggestions") as HBoxContainer
|
||||
var suggestion := Button.new()
|
||||
suggestion.text = "alternate name"
|
||||
suggestions.add_child(suggestion)
|
||||
page.call("_apply_controller_zone_focus")
|
||||
var account_controls: Array[Control] = []
|
||||
for item: Variant in page.call("_account_controller_controls"):
|
||||
account_controls.append(item as Control)
|
||||
var preview := page.get("_preview") as Control
|
||||
var reset_view := page.get("_reset_view_button") as Button
|
||||
_expect(
|
||||
suggestion in account_controls,
|
||||
"Profile name-conflict choices are outside the account zone.",
|
||||
)
|
||||
_expect(
|
||||
suggestion.focus_mode == Control.FOCUS_ALL,
|
||||
"Profile name-conflict choices are not controller-focusable.",
|
||||
)
|
||||
_expect(
|
||||
preview.focus_mode == Control.FOCUS_NONE,
|
||||
"The profile preview must not take controller focus.",
|
||||
)
|
||||
_expect(
|
||||
reset_view.focus_mode == Control.FOCUS_NONE,
|
||||
"The profile reset-view button must not take controller focus.",
|
||||
)
|
||||
page.call("_show_confirmation", "defaults")
|
||||
var confirmation := page.get("_discard_confirmation") as Control
|
||||
var confirm := page.get("_confirmation_confirm") as Button
|
||||
var keep_editing := page.get("_keep_editing_button") as Button
|
||||
_expect(confirmation.visible, "The profile confirmation did not open.")
|
||||
_expect(
|
||||
confirm.focus_mode == Control.FOCUS_ALL,
|
||||
"The profile confirmation action is not controller-focusable.",
|
||||
)
|
||||
_expect(
|
||||
keep_editing.focus_mode == Control.FOCUS_ALL,
|
||||
"The profile confirmation cancel action is not controller-focusable.",
|
||||
)
|
||||
for control: Control in account_controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_NONE,
|
||||
"Profile confirmation leaked focus to %s." % control.name,
|
||||
)
|
||||
var cancel_event := InputEventAction.new()
|
||||
cancel_event.action = &"ui_cancel"
|
||||
cancel_event.pressed = true
|
||||
_expect(
|
||||
bool(page.call("handle_controller_input", cancel_event)),
|
||||
"Profile confirmation did not consume controller Back.",
|
||||
)
|
||||
_expect(
|
||||
not confirmation.visible,
|
||||
"Controller Back did not close the profile confirmation.",
|
||||
)
|
||||
for control: Control in account_controls:
|
||||
_expect(
|
||||
control.focus_mode == Control.FOCUS_ALL,
|
||||
"Profile account focus was not restored after confirmation.",
|
||||
)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_mapping_capture_contract() -> void:
|
||||
var title_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/title_screen.gd"
|
||||
)
|
||||
var game_ui_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/game_ui.gd"
|
||||
)
|
||||
var chat_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/chat_ui.gd"
|
||||
)
|
||||
var main_source: String = FileAccess.get_file_as_string(
|
||||
"res://main/main.gd"
|
||||
)
|
||||
_expect(
|
||||
title_source.contains(
|
||||
"_settings_panel.is_input_mapping_capturing()"
|
||||
),
|
||||
"Title input does not respect every active binding capture.",
|
||||
)
|
||||
_expect(
|
||||
game_ui_source.contains("or is_input_mapping_capturing()"),
|
||||
"Controller menu scrolling does not respect every binding capture.",
|
||||
)
|
||||
_expect(
|
||||
chat_source.contains("func _configure_controller_focus()"),
|
||||
"Chat does not author controller routes for its exterior controls.",
|
||||
)
|
||||
_expect(
|
||||
chat_source.contains(
|
||||
"Control.FOCUS_ALL if _opened else Control.FOCUS_NONE"
|
||||
),
|
||||
"Passive chat controls can steal world controller focus.",
|
||||
)
|
||||
_expect(
|
||||
main_source.contains("func _configure_popup_dialog("),
|
||||
"Runtime popup dialogs do not receive authored controller routes.",
|
||||
)
|
||||
|
||||
|
||||
func _validate_confirmation_dialog_navigation() -> void:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.ok_button_text = "continue"
|
||||
var fields := VBoxContainer.new()
|
||||
var first := LineEdit.new()
|
||||
first.placeholder_text = "passphrase"
|
||||
first.custom_minimum_size = Vector2(420.0, 42.0)
|
||||
fields.add_child(first)
|
||||
var second := LineEdit.new()
|
||||
second.placeholder_text = "confirm passphrase"
|
||||
second.custom_minimum_size = Vector2(420.0, 42.0)
|
||||
fields.add_child(second)
|
||||
dialog.add_child(fields)
|
||||
root.add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(560, 300))
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
DialogControllerNavigationType.configure_scope(dialog, first)
|
||||
await process_frame
|
||||
var controls: Array[Control] = (
|
||||
DialogControllerNavigationType.interactive_controls(dialog)
|
||||
)
|
||||
_expect(
|
||||
controls.has(first) and controls.has(second),
|
||||
"Confirmation-dialog text fields are not controller reachable.",
|
||||
)
|
||||
_expect(
|
||||
controls.has(dialog.get_ok_button())
|
||||
and controls.has(dialog.get_cancel_button()),
|
||||
"Confirmation-dialog actions are not controller reachable.",
|
||||
)
|
||||
_assert_directionally_reachable(first, controls)
|
||||
_expect(
|
||||
dialog.gui_get_focus_owner() == first,
|
||||
"Confirmation dialog did not focus its requested entry control.",
|
||||
)
|
||||
dialog.hide()
|
||||
await process_frame
|
||||
root.gui_release_focus()
|
||||
dialog.popup_centered(Vector2i(560, 300))
|
||||
await process_frame
|
||||
DialogControllerNavigationType.configure_scope(dialog, first)
|
||||
await process_frame
|
||||
_expect(
|
||||
dialog.gui_get_focus_owner() == first,
|
||||
"A reopened dialog did not restore controller focus.",
|
||||
)
|
||||
dialog.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_bubble_confirmation_navigation() -> void:
|
||||
for scene: PackedScene in [
|
||||
BubbleConfirmationScene,
|
||||
TitleConfirmationScene,
|
||||
]:
|
||||
var page := scene.instantiate() as Control
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.show()
|
||||
page.call("_set_interactive", true)
|
||||
var confirm := page.get_node("BubbleCluster/ConfirmButton") as Button
|
||||
var cancel := page.get_node("BubbleCluster/CancelButton") as Button
|
||||
_assert_neighbor(confirm, &"focus_neighbor_left", cancel)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_right", cancel)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_top", confirm)
|
||||
_assert_neighbor(confirm, &"focus_neighbor_bottom", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_left", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_right", confirm)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_top", cancel)
|
||||
_assert_neighbor(cancel, &"focus_neighbor_bottom", cancel)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
func _set_button_state(button: Button, shown: bool) -> void:
|
||||
button.visible = shown
|
||||
button.disabled = false
|
||||
|
||||
|
||||
func _assert_neighbor(
|
||||
origin: Control,
|
||||
property: StringName,
|
||||
expected: Control,
|
||||
) -> void:
|
||||
var path: NodePath = origin.get(property)
|
||||
_expect(
|
||||
not path.is_empty(),
|
||||
"%s has no %s neighbor." % [origin.name, property],
|
||||
)
|
||||
if path.is_empty():
|
||||
return
|
||||
var actual := origin.get_node_or_null(path) as Control
|
||||
_expect(
|
||||
actual == expected,
|
||||
"%s points %s to %s instead of %s."
|
||||
% [
|
||||
origin.name,
|
||||
property,
|
||||
actual.name if actual != null else "nothing",
|
||||
expected.name,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
func _assert_directionally_reachable(
|
||||
start: Control,
|
||||
controls: Array[Control],
|
||||
) -> void:
|
||||
var expected: Dictionary[int, bool] = {}
|
||||
for control: Control in controls:
|
||||
expected[control.get_instance_id()] = true
|
||||
var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
|
||||
var pending: Array[Control] = [start]
|
||||
while not pending.is_empty():
|
||||
var current: Control = pending.pop_front()
|
||||
for path: NodePath in [
|
||||
current.focus_neighbor_left,
|
||||
current.focus_neighbor_right,
|
||||
current.focus_neighbor_top,
|
||||
current.focus_neighbor_bottom,
|
||||
]:
|
||||
var neighbor := current.get_node_or_null(path) as Control
|
||||
if (
|
||||
neighbor == null
|
||||
or not expected.has(neighbor.get_instance_id())
|
||||
or visited.has(neighbor.get_instance_id())
|
||||
):
|
||||
continue
|
||||
visited[neighbor.get_instance_id()] = true
|
||||
pending.append(neighbor)
|
||||
_expect(
|
||||
visited.size() == expected.size(),
|
||||
"Only %d of %d controls are directionally reachable from %s."
|
||||
% [visited.size(), expected.size(), start.name],
|
||||
)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
1
tests/controller_menu_accessibility_validation.gd.uid
Normal file
1
tests/controller_menu_accessibility_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cdvy3ln6d3b1v
|
||||
|
|
@ -14,7 +14,12 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_spatial_navigation()
|
||||
_validate_strict_directional_navigation()
|
||||
_validate_disabled_control_exclusion()
|
||||
_validate_traversal_cycle()
|
||||
_validate_controller_hierarchy_contract()
|
||||
_validate_player_page_zone_contracts()
|
||||
_validate_shop_navigation_contract()
|
||||
_validate_four_by_three_centering()
|
||||
_validate_low_end_profile_contract()
|
||||
print("Controller UI navigation validation: PASS")
|
||||
|
|
@ -38,16 +43,118 @@ func _validate_spatial_navigation() -> void:
|
|||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_strict_directional_navigation() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var origin := _make_button(host, "origin", Vector2(200.0, 200.0))
|
||||
var mostly_below := _make_button(
|
||||
host, "mostly_below", Vector2(250.0, 400.0)
|
||||
)
|
||||
var controls: Array[Control] = [origin, mostly_below]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
assert(origin.get_node(origin.focus_neighbor_right) == origin)
|
||||
assert(origin.get_node(origin.focus_neighbor_top) == origin)
|
||||
assert(origin.get_node(origin.focus_neighbor_bottom) == mostly_below)
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_disabled_control_exclusion() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var left := _make_button(host, "left", Vector2(50.0, 100.0))
|
||||
var disabled := _make_button(host, "disabled", Vector2(200.0, 100.0))
|
||||
var right := _make_button(host, "right", Vector2(350.0, 100.0))
|
||||
disabled.disabled = true
|
||||
disabled.focus_next = disabled.get_path_to(right)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
[left, disabled, right]
|
||||
)
|
||||
assert(left.get_node(left.focus_neighbor_right) == right)
|
||||
assert(right.get_node(right.focus_neighbor_left) == left)
|
||||
assert(disabled.focus_next.is_empty())
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_traversal_cycle() -> void:
|
||||
var host := Control.new()
|
||||
root.add_child(host)
|
||||
var first := _make_button(host, "first", Vector2(50.0, 50.0))
|
||||
var second := _make_button(host, "second", Vector2(200.0, 50.0))
|
||||
var third := _make_button(host, "third", Vector2(50.0, 150.0))
|
||||
var controls: Array[Control] = [third, first, second]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
assert(first.get_node(first.focus_next) == second)
|
||||
assert(second.get_node(second.focus_next) == third)
|
||||
assert(third.get_node(third.focus_next) == first)
|
||||
assert(first.get_node(first.focus_previous) == third)
|
||||
host.queue_free()
|
||||
|
||||
|
||||
func _validate_controller_hierarchy_contract() -> void:
|
||||
var source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/player_menu.gd"
|
||||
)
|
||||
assert(source.contains("ROLE_POINTER_MODIFIER"))
|
||||
assert(source.contains("ROLE_CAMERA_ZOOM"))
|
||||
assert(source.contains("_handle_controller_secondary_switch"))
|
||||
assert(source.contains("ROLE_LB"))
|
||||
assert(source.contains("ROLE_RB"))
|
||||
assert(not source.contains("_handle_controller_secondary_switch"))
|
||||
assert(source.contains("ControllerOwnership.INVENTORY_TABS"))
|
||||
assert(source.contains("ControllerOwnership.SORT_FILTER"))
|
||||
assert(source.contains("_reset_controller_zone_for_section"))
|
||||
assert(source.contains("preserve_tab_zone"))
|
||||
assert(source.contains("active_tab.call_deferred(\"grab_focus\")"))
|
||||
assert(source.contains("CONTROLLER_PICKUP_HOLD_SECONDS"))
|
||||
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
||||
assert(source.contains("configure_spatial_neighbors(candidates)"))
|
||||
|
||||
|
||||
func _validate_player_page_zone_contracts() -> void:
|
||||
for path: String in [
|
||||
"res://ui/logbook_page.gd",
|
||||
"res://ui/the_net_page.gd",
|
||||
"res://ui/mail_page.gd",
|
||||
"res://ui/profile_page.gd",
|
||||
"res://ui/players_page.gd",
|
||||
]:
|
||||
var source: String = FileAccess.get_file_as_string(path)
|
||||
assert(source.contains("func reset_controller_zone()"))
|
||||
assert(source.contains("func handle_controller_input(event: InputEvent)"))
|
||||
var profile_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/profile_page.gd"
|
||||
)
|
||||
assert(profile_source.contains("ControllerZone.COLOR_PICKER"))
|
||||
assert(profile_source.contains("_adjust_controller_color_gamut"))
|
||||
assert(profile_source.contains("_customize_button.text = \"customize\""))
|
||||
assert(profile_source.contains("_enter_controller_customization"))
|
||||
assert(not profile_source.contains(
|
||||
"if event.is_action_pressed(\"ui_down\"):\n"
|
||||
+ "\t\t\t_controller_zone = ControllerZone.CATEGORIES"
|
||||
))
|
||||
assert(profile_source.contains(
|
||||
"_reset_view_button.focus_mode = Control.FOCUS_NONE"
|
||||
))
|
||||
var preview_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/profile_preview.gd"
|
||||
)
|
||||
assert(preview_source.contains("focus_mode = Control.FOCUS_NONE"))
|
||||
assert(not preview_source.contains("grab_focus()"))
|
||||
var mail_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/mail_page.gd"
|
||||
)
|
||||
assert(mail_source.contains("_configure_compose_controller_navigation"))
|
||||
assert(mail_source.contains("_set_compose_neighbors"))
|
||||
var net_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/the_net_page.gd"
|
||||
)
|
||||
assert(net_source.contains("ROLE_RIGHT_STICK_Y"))
|
||||
|
||||
|
||||
func _validate_shop_navigation_contract() -> void:
|
||||
var source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/fishing_shop.gd"
|
||||
)
|
||||
assert(source.contains("ROLE_LB"))
|
||||
assert(source.contains("ROLE_RB"))
|
||||
assert(source.contains("_configure_controller_focus"))
|
||||
|
||||
|
||||
func _validate_four_by_three_centering() -> void:
|
||||
|
|
|
|||
183
tests/file_dialog_controller_navigation_validation.gd
Normal file
183
tests/file_dialog_controller_navigation_validation.gd
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
extends SceneTree
|
||||
|
||||
const FileDialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
const InterfaceFontControllerType = preload(
|
||||
"res://ui/interface_font_controller.gd"
|
||||
)
|
||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var dialog := FileDialog.new()
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.use_native_dialog = false
|
||||
root.add_child(dialog)
|
||||
dialog.current_dir = "/tmp"
|
||||
dialog.popup_centered(Vector2i(900, 540))
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
FileDialogControllerNavigationType.configure(dialog)
|
||||
await process_frame
|
||||
|
||||
var root_scope: Window = (
|
||||
FileDialogControllerNavigationType.active_scope(dialog)
|
||||
)
|
||||
assert(root_scope == dialog)
|
||||
var root_controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(root_scope)
|
||||
)
|
||||
var directory_list: ItemList
|
||||
var create_folder_button: Button
|
||||
var path_edit: LineEdit
|
||||
var has_menu_button: bool = false
|
||||
var has_cancel: bool = false
|
||||
var has_select: bool = false
|
||||
for control: Control in root_controls:
|
||||
if (
|
||||
control is ItemList
|
||||
and control.accessibility_name == "Directories & Files:"
|
||||
):
|
||||
directory_list = control as ItemList
|
||||
if control is LineEdit and control.accessibility_name == "Path:":
|
||||
path_edit = control as LineEdit
|
||||
var button := control as Button
|
||||
if button != null:
|
||||
create_folder_button = (
|
||||
button
|
||||
if button.tooltip_text == "Create a new folder."
|
||||
else create_folder_button
|
||||
)
|
||||
has_cancel = has_cancel or button.text == "Cancel"
|
||||
has_select = has_select or button.text == "Select Current Folder"
|
||||
has_menu_button = has_menu_button or control is MenuButton
|
||||
assert(directory_list != null)
|
||||
assert(path_edit != null)
|
||||
assert(create_folder_button != null)
|
||||
assert(has_menu_button)
|
||||
assert(has_cancel)
|
||||
assert(has_select)
|
||||
assert(dialog.gui_get_focus_owner() == directory_list)
|
||||
_assert_directionally_reachable(directory_list, root_controls)
|
||||
|
||||
create_folder_button.pressed.emit()
|
||||
for _frame: int in 3:
|
||||
await process_frame
|
||||
FileDialogControllerNavigationType.configure(dialog)
|
||||
await process_frame
|
||||
var folder_scope: Window = (
|
||||
FileDialogControllerNavigationType.active_scope(dialog)
|
||||
)
|
||||
assert(folder_scope != null and folder_scope != dialog)
|
||||
var folder_controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(folder_scope)
|
||||
)
|
||||
var folder_name_edit: LineEdit
|
||||
var folder_cancel: Button
|
||||
var folder_ok: Button
|
||||
for control: Control in folder_controls:
|
||||
if control is LineEdit:
|
||||
folder_name_edit = control as LineEdit
|
||||
var button := control as Button
|
||||
if button != null and button.text == "Cancel":
|
||||
folder_cancel = button
|
||||
if button != null and button.text == "OK":
|
||||
folder_ok = button
|
||||
assert(folder_name_edit != null)
|
||||
assert(folder_cancel != null)
|
||||
assert(folder_ok != null)
|
||||
assert(folder_scope.gui_get_focus_owner() == folder_name_edit)
|
||||
_assert_directionally_reachable(folder_name_edit, folder_controls)
|
||||
|
||||
var keyboard := OnScreenKeyboardType.new()
|
||||
root.add_child(keyboard)
|
||||
keyboard.set_enabled(true)
|
||||
folder_name_edit.grab_focus()
|
||||
assert(keyboard.request_for_control(folder_name_edit))
|
||||
assert(keyboard.is_open())
|
||||
assert(keyboard.get_parent() == folder_scope)
|
||||
keyboard.call("_close_keyboard", true)
|
||||
|
||||
var main_source: String = FileAccess.get_file_as_string(
|
||||
"res://main/main.gd"
|
||||
)
|
||||
assert(main_source.contains("picker_scope != _data_folder_dialog"))
|
||||
assert(main_source.contains("picker_scope.hide()"))
|
||||
assert(main_source.contains("is_controller_text_entry_open()"))
|
||||
|
||||
dialog.queue_free()
|
||||
keyboard.queue_free()
|
||||
await process_frame
|
||||
await _validate_compact_dialog()
|
||||
print("File dialog controller navigation validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_compact_dialog() -> void:
|
||||
root.size = Vector2i(640, 480)
|
||||
var font_controller := InterfaceFontControllerType.new()
|
||||
root.add_child(font_controller)
|
||||
await process_frame
|
||||
var dialog := FileDialog.new()
|
||||
dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
dialog.use_native_dialog = false
|
||||
root.add_child(dialog)
|
||||
dialog.current_dir = "/tmp"
|
||||
font_controller.popup_file_dialog(dialog)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
assert(dialog.size.x <= 616 and dialog.size.y <= 456)
|
||||
var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
|
||||
var controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(scope)
|
||||
)
|
||||
var directory_list: ItemList
|
||||
for control: Control in controls:
|
||||
if (
|
||||
control is ItemList
|
||||
and control.accessibility_name == "Directories & Files:"
|
||||
):
|
||||
directory_list = control as ItemList
|
||||
break
|
||||
assert(directory_list != null)
|
||||
_assert_directionally_reachable(directory_list, controls)
|
||||
dialog.queue_free()
|
||||
font_controller.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _assert_directionally_reachable(
|
||||
start: Control,
|
||||
controls: Array[Control],
|
||||
) -> void:
|
||||
var expected: Dictionary[int, bool] = {}
|
||||
for control: Control in controls:
|
||||
expected[control.get_instance_id()] = true
|
||||
var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
|
||||
var pending: Array[Control] = [start]
|
||||
while not pending.is_empty():
|
||||
var current: Control = pending.pop_front()
|
||||
for path: NodePath in [
|
||||
current.focus_neighbor_left,
|
||||
current.focus_neighbor_right,
|
||||
current.focus_neighbor_top,
|
||||
current.focus_neighbor_bottom,
|
||||
]:
|
||||
var neighbor := current.get_node_or_null(path) as Control
|
||||
if (
|
||||
neighbor == null
|
||||
or not expected.has(neighbor.get_instance_id())
|
||||
or visited.has(neighbor.get_instance_id())
|
||||
):
|
||||
continue
|
||||
visited[neighbor.get_instance_id()] = true
|
||||
pending.append(neighbor)
|
||||
assert(visited.size() == expected.size())
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bgnku8bs7oxx7
|
||||
160
tests/keyboard_mouse_mapping_validation.gd
Normal file
160
tests/keyboard_mouse_mapping_validation.gd
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
extends SceneTree
|
||||
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const KeyboardMouseMappingPanelType = preload(
|
||||
"res://ui/keyboard_mouse_mapping_panel.gd"
|
||||
)
|
||||
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var manager := KeyboardMouseMappingManagerType.new()
|
||||
root.add_child(manager)
|
||||
await process_frame
|
||||
var defaults: Dictionary = manager.get_active_bindings()
|
||||
assert(
|
||||
defaults.size()
|
||||
== KeyboardMouseMappingManagerType.ROLE_ORDER.size()
|
||||
)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
|
||||
].get("kind", "")) == "mouse_button"
|
||||
)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_CHAT)
|
||||
].get("kind", "")) == "key"
|
||||
)
|
||||
|
||||
var joypad_events_before: int = _event_count(&"jump", true)
|
||||
var rebind_key := InputEventKey.new()
|
||||
rebind_key.physical_keycode = KEY_R
|
||||
rebind_key.pressed = true
|
||||
var jump_binding: Dictionary = manager.binding_from_event(rebind_key)
|
||||
assert(manager.validate_binding(jump_binding))
|
||||
assert(manager.set_binding(
|
||||
KeyboardMouseMappingManagerType.ROLE_JUMP,
|
||||
jump_binding,
|
||||
))
|
||||
assert(manager.has_custom_mapping())
|
||||
assert(FileAccess.file_exists(
|
||||
KeyboardMouseMappingManagerType.MAPPING_PATH
|
||||
))
|
||||
assert(_has_physical_key(&"jump", KEY_R))
|
||||
assert(_event_count(&"jump", true) == joypad_events_before)
|
||||
|
||||
var mouse_event := InputEventMouseButton.new()
|
||||
mouse_event.button_index = MOUSE_BUTTON_XBUTTON1
|
||||
mouse_event.pressed = true
|
||||
var mouse_binding: Dictionary = manager.binding_from_event(mouse_event)
|
||||
assert(manager.set_binding(
|
||||
KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION,
|
||||
mouse_binding,
|
||||
))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_XBUTTON1))
|
||||
assert(not _has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
|
||||
var joypad_event := InputEventJoypadButton.new()
|
||||
joypad_event.button_index = JOY_BUTTON_A
|
||||
joypad_event.pressed = true
|
||||
assert(manager.binding_from_event(joypad_event).is_empty())
|
||||
|
||||
var panel := KeyboardMouseMappingPanelType.new()
|
||||
root.add_child(panel)
|
||||
panel.setup(manager)
|
||||
panel.open_panel()
|
||||
await process_frame
|
||||
assert(_has_label_text(panel, "keyboard binds"))
|
||||
var scrolls: Array[Node] = panel.find_children(
|
||||
"", "ScrollContainer", true, false
|
||||
)
|
||||
assert(not scrolls.is_empty())
|
||||
assert((scrolls.front() as ScrollContainer).follow_focus)
|
||||
var first_binding := panel._binding_buttons.get(
|
||||
KeyboardMouseMappingManagerType.ROLE_ORDER.front()
|
||||
) as Button
|
||||
var last_binding := panel._binding_buttons.get(
|
||||
KeyboardMouseMappingManagerType.ROLE_ORDER.back()
|
||||
) as Button
|
||||
assert(first_binding.get_node(
|
||||
first_binding.focus_neighbor_top
|
||||
) == first_binding)
|
||||
assert(last_binding.get_node(
|
||||
last_binding.focus_neighbor_bottom
|
||||
) == panel._close_button)
|
||||
assert(panel._close_button.get_node(
|
||||
panel._close_button.focus_neighbor_top
|
||||
) == last_binding)
|
||||
panel._begin_capture(KeyboardMouseMappingManagerType.ROLE_INTERACT)
|
||||
var interact_key := InputEventKey.new()
|
||||
interact_key.physical_keycode = KEY_F
|
||||
interact_key.pressed = true
|
||||
panel._input(interact_key)
|
||||
assert(_has_physical_key(&"interact", KEY_F))
|
||||
assert(not panel.is_capturing())
|
||||
|
||||
var settings_panel := SettingsPanelScene.instantiate() as SettingsPanel
|
||||
root.add_child(settings_panel)
|
||||
await process_frame
|
||||
var controller_bubble := settings_panel.get_node(
|
||||
"%ControllerMapping"
|
||||
) as Button
|
||||
var keyboard_bubble := settings_panel.get_node("%KeyboardMapping") as Button
|
||||
assert(controller_bubble.text == "controller\nbinds")
|
||||
assert(keyboard_bubble.text == "keyboard\nbinds")
|
||||
|
||||
assert(manager.reset_mapping())
|
||||
assert(not manager.has_custom_mapping())
|
||||
assert(not FileAccess.file_exists(
|
||||
KeyboardMouseMappingManagerType.MAPPING_PATH
|
||||
))
|
||||
assert(_has_physical_key(&"jump", KEY_SPACE))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
assert(_has_physical_key(&"open_chat", KEY_T))
|
||||
assert(_event_count(&"jump", true) == joypad_events_before)
|
||||
|
||||
settings_panel.queue_free()
|
||||
panel.queue_free()
|
||||
manager.queue_free()
|
||||
print("Keyboard and mouse mapping validation: PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _event_count(action: StringName, joypad: bool) -> int:
|
||||
var result: int = 0
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
if joypad == (
|
||||
event is InputEventJoypadButton or event is InputEventJoypadMotion
|
||||
):
|
||||
result += 1
|
||||
return result
|
||||
|
||||
|
||||
func _has_physical_key(action: StringName, key: Key) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var key_event := event as InputEventKey
|
||||
if key_event != null and key_event.physical_keycode == key:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _has_mouse_button(action: StringName, button: MouseButton) -> bool:
|
||||
for event: InputEvent in InputMap.action_get_events(action):
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event != null and mouse_event.button_index == button:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _has_label_text(parent: Node, expected: String) -> bool:
|
||||
for node: Node in parent.find_children("", "Label", true, false):
|
||||
if (node as Label).text == expected:
|
||||
return true
|
||||
return false
|
||||
1
tests/keyboard_mouse_mapping_validation.gd.uid
Normal file
1
tests/keyboard_mouse_mapping_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://br6aeelrpa64k
|
||||
|
|
@ -249,6 +249,36 @@ func _validate_page() -> void:
|
|||
assert(_detail_text(page).contains(LogbookCatalog.facts_for(bluegill)))
|
||||
_validate_detail_field_fonts(page)
|
||||
_validate_handwritten_numeric_scale(page)
|
||||
var detail_buttons: Array = page.get("_detail_buttons") as Array
|
||||
assert(detail_buttons.size() == 4)
|
||||
var portrait_detail := detail_buttons[0] as Button
|
||||
var facts_detail := detail_buttons[1] as Button
|
||||
var quality_detail := detail_buttons[2] as Button
|
||||
var stats_detail := detail_buttons[3] as Button
|
||||
assert(
|
||||
portrait_detail.get_node(portrait_detail.focus_neighbor_right)
|
||||
== facts_detail
|
||||
)
|
||||
assert(
|
||||
portrait_detail.get_node(portrait_detail.focus_neighbor_bottom)
|
||||
== quality_detail
|
||||
)
|
||||
assert(
|
||||
facts_detail.get_node(facts_detail.focus_neighbor_left)
|
||||
== portrait_detail
|
||||
)
|
||||
assert(
|
||||
facts_detail.get_node(facts_detail.focus_neighbor_bottom)
|
||||
== quality_detail
|
||||
)
|
||||
assert(
|
||||
quality_detail.get_node(quality_detail.focus_neighbor_bottom)
|
||||
== stats_detail
|
||||
)
|
||||
assert(
|
||||
stats_detail.get_node(stats_detail.focus_neighbor_top)
|
||||
== quality_detail
|
||||
)
|
||||
var portrait_button := page.get("_detail_portrait_button") as Button
|
||||
assert(portrait_button != null)
|
||||
portrait_button.pressed.emit()
|
||||
|
|
@ -266,6 +296,29 @@ func _validate_page() -> void:
|
|||
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
||||
await process_frame
|
||||
assert(not portrait_overlay.visible)
|
||||
facts_detail.pressed.emit()
|
||||
await process_frame
|
||||
var overlay_text := page.get("_portrait_overlay_text") as Label
|
||||
assert(overlay_text.visible)
|
||||
assert(
|
||||
overlay_text.get_theme_font_size("font_size")
|
||||
== LogbookPage.DETAIL_OVERLAY_TEXT_FONT_SIZE
|
||||
)
|
||||
assert(
|
||||
overlay_text.horizontal_alignment
|
||||
== HORIZONTAL_ALIGNMENT_LEFT
|
||||
)
|
||||
assert(
|
||||
portrait_overlay.size.x
|
||||
- overlay_text.size.x
|
||||
<= float(
|
||||
(LogbookPage.DETAIL_OVERLAY_EDGE_MARGIN
|
||||
+ LogbookPage.DETAIL_OVERLAY_CONTENT_MARGIN) * 2
|
||||
+ 8
|
||||
)
|
||||
)
|
||||
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
||||
await process_frame
|
||||
|
||||
inventory.remove_catch_by_id(fish_catch.catch_id)
|
||||
await process_frame
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ func open_chat() -> void:
|
|||
# prediction would let a remote host continue the last held movement.
|
||||
_session.submit_neutral_local_movement()
|
||||
_entry.show()
|
||||
_configure_controller_focus()
|
||||
_entry.virtual_keyboard_enabled = false
|
||||
_hint.hide()
|
||||
_refresh_input_ownership()
|
||||
|
|
@ -328,6 +329,7 @@ func close_chat() -> void:
|
|||
_entry.virtual_keyboard_enabled = false
|
||||
_entry.release_focus()
|
||||
_entry.hide()
|
||||
_configure_controller_focus()
|
||||
if _input_lock_applied:
|
||||
_player.set_local_input_suppressed(INPUT_OWNER, false)
|
||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||
|
|
@ -421,9 +423,6 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
):
|
||||
open_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.keycode == KEY_T and not _opened and _available:
|
||||
open_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif (
|
||||
event.unicode == 47
|
||||
and not _opened
|
||||
|
|
@ -539,7 +538,7 @@ func _build_ui() -> void:
|
|||
_collapse_button.name = "ChatCollapseButton"
|
||||
_collapse_button.size = HANDLE_SIZE
|
||||
_collapse_button.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_collapse_button.focus_mode = Control.FOCUS_ALL
|
||||
_collapse_button.focus_mode = Control.FOCUS_NONE
|
||||
_collapse_button.z_index = 3
|
||||
_collapse_button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_collapse_button.expand_icon = true
|
||||
|
|
@ -574,7 +573,7 @@ func _build_ui() -> void:
|
|||
_height_button.name = "ChatHeightButton"
|
||||
_height_button.size = HANDLE_SIZE
|
||||
_height_button.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_height_button.focus_mode = Control.FOCUS_ALL
|
||||
_height_button.focus_mode = Control.FOCUS_NONE
|
||||
_height_button.z_index = 3
|
||||
_height_button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_height_button.expand_icon = true
|
||||
|
|
@ -1475,6 +1474,7 @@ func _layout_presentation(animate: bool) -> void:
|
|||
_height_button.position = height_position
|
||||
_unread_indicator.position = unread_position
|
||||
_hint.position = hint_position
|
||||
_configure_controller_focus()
|
||||
return
|
||||
_height_tween = create_tween().set_parallel(true)
|
||||
_height_tween.tween_property(
|
||||
|
|
@ -1521,10 +1521,92 @@ func _layout_presentation(animate: bool) -> void:
|
|||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
_height_tween.finished.connect(func() -> void:
|
||||
_scroll_history_to_bottom()
|
||||
_configure_controller_focus()
|
||||
)
|
||||
_hint.position = hint_position
|
||||
|
||||
|
||||
func _configure_controller_focus() -> void:
|
||||
if _entry == null or _collapse_button == null or _height_button == null:
|
||||
return
|
||||
for button: Button in [_collapse_button, _height_button]:
|
||||
button.focus_mode = (
|
||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||
)
|
||||
if not _opened:
|
||||
return
|
||||
_entry.focus_mode = Control.FOCUS_ALL
|
||||
if _mobile_mode:
|
||||
_set_controller_neighbors(
|
||||
_entry, _entry, _entry, _entry, _height_button
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_height_button,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
_entry,
|
||||
_height_button,
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_collapse_button,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
_entry,
|
||||
_collapse_button,
|
||||
)
|
||||
return
|
||||
var panel_side: Control = _entry
|
||||
if _dock_right:
|
||||
_set_controller_neighbors(
|
||||
_entry, _collapse_button, _entry, _entry, _entry
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_collapse_button,
|
||||
_collapse_button,
|
||||
panel_side,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_height_button,
|
||||
_height_button,
|
||||
panel_side,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
)
|
||||
else:
|
||||
_set_controller_neighbors(
|
||||
_entry, _entry, _collapse_button, _entry, _entry
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_collapse_button,
|
||||
panel_side,
|
||||
_collapse_button,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
)
|
||||
_set_controller_neighbors(
|
||||
_height_button,
|
||||
panel_side,
|
||||
_height_button,
|
||||
_height_button,
|
||||
_collapse_button,
|
||||
)
|
||||
|
||||
|
||||
func _set_controller_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
|
||||
|
||||
func _scroll_history_to_bottom() -> void:
|
||||
if _history == null:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -186,12 +186,16 @@ func _configure_focus() -> void:
|
|||
)
|
||||
_confirm_button.focus_neighbor_left = confirm_to_cancel
|
||||
_confirm_button.focus_neighbor_right = confirm_to_cancel
|
||||
_confirm_button.focus_neighbor_top = confirm_to_cancel
|
||||
_confirm_button.focus_neighbor_bottom = confirm_to_cancel
|
||||
_confirm_button.focus_neighbor_top = _confirm_button.get_path_to(
|
||||
_confirm_button
|
||||
)
|
||||
_confirm_button.focus_neighbor_bottom = _confirm_button.focus_neighbor_top
|
||||
_cancel_button.focus_neighbor_left = cancel_to_confirm
|
||||
_cancel_button.focus_neighbor_right = cancel_to_confirm
|
||||
_cancel_button.focus_neighbor_top = cancel_to_confirm
|
||||
_cancel_button.focus_neighbor_bottom = cancel_to_confirm
|
||||
_cancel_button.focus_neighbor_top = _cancel_button.get_path_to(
|
||||
_cancel_button
|
||||
)
|
||||
_cancel_button.focus_neighbor_bottom = _cancel_button.focus_neighbor_top
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
@ -211,6 +215,8 @@ func _set_interactive(interactive: bool) -> void:
|
|||
if interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
if interactive:
|
||||
_configure_focus()
|
||||
|
||||
|
||||
func _finish_transition_in(
|
||||
|
|
|
|||
|
|
@ -169,10 +169,12 @@ func _on_choice_gui_input(event: InputEvent, index: int) -> void:
|
|||
close_choices()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_up"):
|
||||
_choice_buttons[posmod(index - 1, _choice_buttons.size())].grab_focus()
|
||||
_choice_buttons[maxi(index - 1, 0)].grab_focus()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_down"):
|
||||
_choice_buttons[(index + 1) % _choice_buttons.size()].grab_focus()
|
||||
_choice_buttons[mini(
|
||||
index + 1, _choice_buttons.size() - 1
|
||||
)].grab_focus()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ var _selected: bool = false
|
|||
|
||||
func _ready() -> void:
|
||||
toggle_mode = true
|
||||
set_meta(&"controller_focus_inversion_disabled", true)
|
||||
_selected = button_pressed
|
||||
add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||
add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
|
|
|
|||
|
|
@ -3,16 +3,22 @@ extends RefCounted
|
|||
|
||||
const PERPENDICULAR_WEIGHT: float = 2.5
|
||||
const MINIMUM_FORWARD_DISTANCE: float = 0.5
|
||||
const MAXIMUM_DIRECTION_SLOPE: float = 1.0
|
||||
|
||||
|
||||
static func configure_spatial_neighbors(controls: Array) -> void:
|
||||
var candidates: Array[Control] = []
|
||||
for item: Variant in controls:
|
||||
var control := item as Control
|
||||
if control == null or not control.is_visible_in_tree():
|
||||
if not is_focusable(control):
|
||||
if control != null:
|
||||
_clear_neighbors(control)
|
||||
continue
|
||||
candidates.append(control)
|
||||
var traversal_order: Array[Control] = candidates.duplicate()
|
||||
traversal_order.sort_custom(_sort_by_position)
|
||||
for control: Control in candidates:
|
||||
_enable_ancestor_scroll_follow(control)
|
||||
control.focus_neighbor_left = _neighbor_path(
|
||||
control, candidates, Vector2.LEFT
|
||||
)
|
||||
|
|
@ -25,6 +31,80 @@ static func configure_spatial_neighbors(controls: Array) -> void:
|
|||
control.focus_neighbor_bottom = _neighbor_path(
|
||||
control, candidates, Vector2.DOWN
|
||||
)
|
||||
var traversal_index: int = traversal_order.find(control)
|
||||
control.focus_previous = control.get_path_to(
|
||||
traversal_order[wrapi(
|
||||
traversal_index - 1,
|
||||
0,
|
||||
traversal_order.size(),
|
||||
)]
|
||||
)
|
||||
control.focus_next = control.get_path_to(
|
||||
traversal_order[wrapi(
|
||||
traversal_index + 1,
|
||||
0,
|
||||
traversal_order.size(),
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
static func configure_traversal(controls: Array) -> void:
|
||||
var candidates: Array[Control] = []
|
||||
for item: Variant in controls:
|
||||
var control := item as Control
|
||||
if is_focusable(control):
|
||||
candidates.append(control)
|
||||
elif control != null:
|
||||
control.focus_previous = NodePath()
|
||||
control.focus_next = NodePath()
|
||||
if candidates.is_empty():
|
||||
return
|
||||
for index: int in candidates.size():
|
||||
var control: Control = candidates[index]
|
||||
_enable_ancestor_scroll_follow(control)
|
||||
control.focus_previous = control.get_path_to(
|
||||
candidates[wrapi(index - 1, 0, candidates.size())]
|
||||
)
|
||||
control.focus_next = control.get_path_to(
|
||||
candidates[wrapi(index + 1, 0, candidates.size())]
|
||||
)
|
||||
|
||||
|
||||
static func is_focusable(control: Control) -> bool:
|
||||
if (
|
||||
control == null
|
||||
or not control.is_visible_in_tree()
|
||||
or control.focus_mode not in [Control.FOCUS_CLICK, Control.FOCUS_ALL]
|
||||
):
|
||||
return false
|
||||
var button := control as BaseButton
|
||||
return button == null or not button.disabled
|
||||
|
||||
|
||||
static func _clear_neighbors(control: Control) -> void:
|
||||
control.focus_neighbor_left = NodePath()
|
||||
control.focus_neighbor_right = NodePath()
|
||||
control.focus_neighbor_top = NodePath()
|
||||
control.focus_neighbor_bottom = NodePath()
|
||||
control.focus_previous = NodePath()
|
||||
control.focus_next = NodePath()
|
||||
|
||||
|
||||
static func _enable_ancestor_scroll_follow(control: Control) -> void:
|
||||
var ancestor: Node = control.get_parent()
|
||||
while ancestor != null:
|
||||
var scroll := ancestor as ScrollContainer
|
||||
if scroll != null:
|
||||
scroll.follow_focus = true
|
||||
ancestor = ancestor.get_parent()
|
||||
|
||||
|
||||
static func _sort_by_position(first: Control, second: Control) -> bool:
|
||||
var first_center: Vector2 = first.get_global_rect().get_center()
|
||||
var second_center: Vector2 = second.get_global_rect().get_center()
|
||||
if not is_equal_approx(first_center.y, second_center.y):
|
||||
return first_center.y < second_center.y
|
||||
return first_center.x < second_center.x
|
||||
|
||||
|
||||
static func _neighbor_path(
|
||||
|
|
@ -36,7 +116,7 @@ static func _neighbor_path(
|
|||
var best: Control = null
|
||||
var best_score: float = INF
|
||||
for candidate: Control in candidates:
|
||||
if candidate == origin or candidate.focus_mode == Control.FOCUS_NONE:
|
||||
if candidate == origin or not is_focusable(candidate):
|
||||
continue
|
||||
var delta: Vector2 = (
|
||||
candidate.get_global_rect().get_center() - origin_center
|
||||
|
|
@ -45,6 +125,8 @@ static func _neighbor_path(
|
|||
if forward_distance <= MINIMUM_FORWARD_DISTANCE:
|
||||
continue
|
||||
var perpendicular_distance: float = absf(delta.cross(direction))
|
||||
if perpendicular_distance > forward_distance * MAXIMUM_DIRECTION_SLOPE:
|
||||
continue
|
||||
var score: float = (
|
||||
forward_distance
|
||||
+ perpendicular_distance * PERPENDICULAR_WEIGHT
|
||||
|
|
@ -53,5 +135,5 @@ static func _neighbor_path(
|
|||
best = candidate
|
||||
best_score = score
|
||||
if best == null:
|
||||
return NodePath()
|
||||
return origin.get_path_to(origin)
|
||||
return origin.get_path_to(best)
|
||||
|
|
|
|||
|
|
@ -70,11 +70,33 @@ func _set_controller_active(active: bool) -> void:
|
|||
if _controller_active == active:
|
||||
return
|
||||
_controller_active = active
|
||||
_apply_to_focus(get_viewport().gui_get_focus_owner())
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
_apply_to_focus(focus_owner)
|
||||
if _controller_active:
|
||||
_queue_focus_visibility_update(focus_owner)
|
||||
|
||||
|
||||
func _on_focus_changed(control: Control) -> void:
|
||||
_apply_to_focus(control)
|
||||
if _controller_active:
|
||||
_queue_focus_visibility_update(control)
|
||||
|
||||
|
||||
func _queue_focus_visibility_update(control: Control) -> void:
|
||||
if control == null:
|
||||
return
|
||||
_ensure_focus_visible.call_deferred(control)
|
||||
|
||||
|
||||
func _ensure_focus_visible(control: Control) -> void:
|
||||
if not is_instance_valid(control) or not control.is_visible_in_tree():
|
||||
return
|
||||
var ancestor: Node = control.get_parent()
|
||||
while ancestor != null:
|
||||
var scroll := ancestor as ScrollContainer
|
||||
if scroll != null and scroll.is_visible_in_tree():
|
||||
scroll.ensure_control_visible(control)
|
||||
ancestor = ancestor.get_parent()
|
||||
|
||||
|
||||
func _apply_to_focus(control: Control) -> void:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ var _binding_buttons: Dictionary = {}
|
|||
var _controller_label: Label
|
||||
var _instruction_label: Label
|
||||
var _progress_label: Label
|
||||
var _mapping_scroll: ScrollContainer
|
||||
var _auto_map_button: Button
|
||||
var _reset_button: Button
|
||||
var _close_button: Button
|
||||
|
|
@ -258,17 +259,18 @@ func _build_interface() -> void:
|
|||
)
|
||||
page.add_child(_progress_label)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
page.add_child(scroll)
|
||||
_mapping_scroll = ScrollContainer.new()
|
||||
_mapping_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_mapping_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_mapping_scroll.follow_focus = true
|
||||
page.add_child(_mapping_scroll)
|
||||
|
||||
var role_grid := GridContainer.new()
|
||||
role_grid.columns = 2
|
||||
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
role_grid.add_theme_constant_override("h_separation", 12)
|
||||
role_grid.add_theme_constant_override("v_separation", 7)
|
||||
scroll.add_child(role_grid)
|
||||
_mapping_scroll.add_child(role_grid)
|
||||
|
||||
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||
var role_label := Label.new()
|
||||
|
|
@ -325,6 +327,65 @@ func _build_interface() -> void:
|
|||
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||||
_close_button.pressed.connect(close_panel)
|
||||
footer.add_child(_close_button)
|
||||
_configure_mapping_navigation()
|
||||
|
||||
|
||||
func _configure_mapping_navigation() -> void:
|
||||
var buttons: Array[Button] = []
|
||||
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||
var binding_button := _binding_buttons.get(role) as Button
|
||||
if binding_button != null:
|
||||
buttons.append(binding_button)
|
||||
if buttons.is_empty():
|
||||
return
|
||||
for index: int in buttons.size():
|
||||
var button: Button = buttons[index]
|
||||
var previous: Control = buttons[index - 1] if index > 0 else button
|
||||
var next: Control = (
|
||||
buttons[index + 1]
|
||||
if index < buttons.size() - 1
|
||||
else _close_button
|
||||
)
|
||||
_set_focus_neighbors(button, button, button, previous, next)
|
||||
button.focus_previous = button.get_path_to(previous)
|
||||
button.focus_next = button.get_path_to(next)
|
||||
var last_button: Button = buttons.back()
|
||||
_set_focus_neighbors(
|
||||
_auto_map_button,
|
||||
_auto_map_button,
|
||||
_reset_button,
|
||||
last_button,
|
||||
_auto_map_button,
|
||||
)
|
||||
_set_focus_neighbors(
|
||||
_reset_button,
|
||||
_auto_map_button,
|
||||
_close_button,
|
||||
last_button,
|
||||
_reset_button,
|
||||
)
|
||||
_set_focus_neighbors(
|
||||
_close_button,
|
||||
_reset_button,
|
||||
_close_button,
|
||||
last_button,
|
||||
_close_button,
|
||||
)
|
||||
_close_button.focus_previous = _close_button.get_path_to(last_button)
|
||||
_close_button.focus_next = _close_button.get_path_to(_auto_map_button)
|
||||
|
||||
|
||||
func _set_focus_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
|
||||
|
||||
func _begin_auto_map() -> void:
|
||||
|
|
|
|||
124
ui/file_dialog_controller_navigation.gd
Normal file
124
ui/file_dialog_controller_navigation.gd
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
class_name FileDialogControllerNavigation
|
||||
extends RefCounted
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
|
||||
static func configure(dialog: FileDialog) -> void:
|
||||
if dialog == null or not dialog.visible or not dialog.is_inside_tree():
|
||||
return
|
||||
var scope: Window = active_scope(dialog)
|
||||
if scope == null:
|
||||
return
|
||||
configure_scope(scope)
|
||||
|
||||
|
||||
static func configure_scope(
|
||||
scope: Window,
|
||||
preferred_control: Control = null,
|
||||
) -> void:
|
||||
if scope == null or not scope.visible or not scope.is_inside_tree():
|
||||
return
|
||||
var controls: Array[Control] = []
|
||||
_collect_interactive_controls(scope, scope, controls)
|
||||
if controls.is_empty():
|
||||
return
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
var focus_owner: Control = scope.gui_get_focus_owner()
|
||||
if controls.has(preferred_control):
|
||||
if focus_owner != preferred_control:
|
||||
preferred_control.grab_focus()
|
||||
return
|
||||
if focus_owner != null and controls.has(focus_owner):
|
||||
return
|
||||
var preferred: Control = _preferred_initial_control(scope, controls)
|
||||
if preferred != null:
|
||||
preferred.grab_focus()
|
||||
|
||||
|
||||
static func active_scope(dialog: FileDialog) -> Window:
|
||||
if dialog == null or not dialog.visible:
|
||||
return null
|
||||
return _deepest_exclusive_window(dialog)
|
||||
|
||||
|
||||
static func interactive_controls(scope: Window) -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
if scope != null:
|
||||
_collect_interactive_controls(scope, scope, controls)
|
||||
return controls
|
||||
|
||||
|
||||
static func _deepest_exclusive_window(window: Window) -> Window:
|
||||
for child: Node in window.get_children(true):
|
||||
var child_window := child as Window
|
||||
if (
|
||||
child_window == null
|
||||
or not child_window.visible
|
||||
or not child_window.exclusive
|
||||
):
|
||||
continue
|
||||
return _deepest_exclusive_window(child_window)
|
||||
return window
|
||||
|
||||
|
||||
static func _collect_interactive_controls(
|
||||
node: Node,
|
||||
scope: Window,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in node.get_children(true):
|
||||
var child_window := child as Window
|
||||
if child_window != null and child_window != scope:
|
||||
continue
|
||||
var control := child as Control
|
||||
if control != null and _is_controller_interactive(control):
|
||||
# Godot marks some visible FileDialog toolbar and menu buttons as
|
||||
# accessibility-only. Promote those genuine controls for controller
|
||||
# focus while leaving labels, scrollbars, and splitters untouched.
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
output.append(control)
|
||||
_collect_interactive_controls(child, scope, output)
|
||||
|
||||
|
||||
static func _is_controller_interactive(control: Control) -> bool:
|
||||
if (
|
||||
control == null
|
||||
or not control.is_visible_in_tree()
|
||||
or control.size.x < 8.0
|
||||
or control.size.y < 8.0
|
||||
or control is ScrollBar
|
||||
):
|
||||
return false
|
||||
var button := control as BaseButton
|
||||
if button != null:
|
||||
return not button.disabled
|
||||
return (
|
||||
control is LineEdit
|
||||
or control is TextEdit
|
||||
or control is ItemList
|
||||
or control is Tree
|
||||
)
|
||||
|
||||
|
||||
static func _preferred_initial_control(
|
||||
scope: Window,
|
||||
controls: Array[Control],
|
||||
) -> Control:
|
||||
if scope is FileDialog:
|
||||
for control: Control in controls:
|
||||
if control is ItemList and control.accessibility_name == (
|
||||
"Directories & Files:"
|
||||
):
|
||||
return control
|
||||
for control: Control in controls:
|
||||
if control is LineEdit or control is TextEdit:
|
||||
return control
|
||||
if scope is ConfirmationDialog:
|
||||
var confirmation := scope as ConfirmationDialog
|
||||
var ok_button: Button = confirmation.get_ok_button()
|
||||
if controls.has(ok_button):
|
||||
return ok_button
|
||||
return controls.front()
|
||||
1
ui/file_dialog_controller_navigation.gd.uid
Normal file
1
ui/file_dialog_controller_navigation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://i46n60pwuoqm
|
||||
|
|
@ -28,6 +28,12 @@ const ShopInteractionType = preload(
|
|||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
|
||||
const UIMotionType = preload("res://ui/ui_motion.gd")
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const FALLBACK_SUPPLY_ICON: Texture2D = preload(
|
||||
"res://ui/icons/pictograms/x_light.png"
|
||||
)
|
||||
|
|
@ -168,6 +174,7 @@ var _cooler_page_active: bool = false
|
|||
var _cooler_modal_open: bool = false
|
||||
var _shop_section: ShopSection = ShopSection.UPGRADES
|
||||
var _shop_tabs: Array[OrganizerTab] = []
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -180,6 +187,53 @@ func _ready() -> void:
|
|||
_cooler_purchase.pressed.connect(_purchase_cooler_capacity)
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if (
|
||||
not visible
|
||||
or _closing
|
||||
or _cooler_page_active
|
||||
or _cooler_modal_open
|
||||
):
|
||||
return
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null:
|
||||
return
|
||||
var uses_left_bumper: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_LB,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
||||
)
|
||||
var uses_right_bumper: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_RB,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||
)
|
||||
if not (uses_left_bumper or uses_right_bumper):
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
if not button_event.pressed or _transaction_in_progress:
|
||||
return
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
var next_section: int = wrapi(
|
||||
int(_shop_section) + direction,
|
||||
0,
|
||||
ShopSection.size(),
|
||||
)
|
||||
_select_shop_section(next_section, true)
|
||||
|
||||
|
||||
func _request_shop_cooler() -> bool:
|
||||
if _transaction_in_progress:
|
||||
_set_feedback("Finish the current transaction first.")
|
||||
|
|
@ -193,6 +247,7 @@ func _request_shop_cooler() -> bool:
|
|||
|
||||
func _focus_shop_section() -> void:
|
||||
_set_feedback("")
|
||||
_configure_controller_focus()
|
||||
if _shop_section == ShopSection.UPGRADES:
|
||||
_reel_purchase.grab_focus()
|
||||
return
|
||||
|
|
@ -264,6 +319,31 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
|||
_refresh_supplies()
|
||||
if focus_content and visible:
|
||||
_focus_shop_section()
|
||||
call_deferred("_configure_controller_focus")
|
||||
|
||||
|
||||
func _configure_controller_focus() -> void:
|
||||
if not visible or _cooler_page_active:
|
||||
return
|
||||
var candidates: Array[Control] = []
|
||||
_collect_controller_focusables(self, candidates)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
|
||||
|
||||
|
||||
func _collect_controller_focusables(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var control := child as Control
|
||||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if (
|
||||
ControllerFocusNavigationType.is_focusable(control)
|
||||
and not control is ScrollBar
|
||||
):
|
||||
output.append(control)
|
||||
_collect_controller_focusables(child, output)
|
||||
|
||||
|
||||
func _update_shop_tab_selection() -> void:
|
||||
|
|
@ -410,6 +490,7 @@ func open_shop() -> bool:
|
|||
_shop_tab_bar.show()
|
||||
_select_shop_section(ShopSection.UPGRADES, false)
|
||||
_refresh_all()
|
||||
call_deferred("_configure_controller_focus")
|
||||
_reel_purchase.grab_focus()
|
||||
menu_visibility_changed.emit(true)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ const PlayerSettingsManagerType = preload(
|
|||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
|
|
@ -198,6 +201,7 @@ var _shared_trigger_rest_by_device: Dictionary[int, float] = {}
|
|||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _settings_manager: PlayerSettingsManagerType
|
||||
var _controller_text_entry_request: Callable
|
||||
var _controller_text_entry_is_open: Callable
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -251,8 +255,28 @@ func _ready() -> void:
|
|||
Input.joy_connection_changed.connect(_on_controller_connection_changed)
|
||||
|
||||
|
||||
func set_controller_text_entry_request(request: Callable) -> void:
|
||||
func set_controller_text_entry_request(
|
||||
request: Callable,
|
||||
is_open: Callable = Callable(),
|
||||
) -> void:
|
||||
_controller_text_entry_request = request
|
||||
_controller_text_entry_is_open = is_open
|
||||
|
||||
|
||||
func request_controller_text_entry_for(control: Control = null) -> bool:
|
||||
return (
|
||||
bool(_controller_text_entry_request.call(control))
|
||||
if _controller_text_entry_request.is_valid()
|
||||
else false
|
||||
)
|
||||
|
||||
|
||||
func is_controller_text_entry_open() -> bool:
|
||||
return (
|
||||
bool(_controller_text_entry_is_open.call())
|
||||
if _controller_text_entry_is_open.is_valid()
|
||||
else false
|
||||
)
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -603,10 +627,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
|||
_chat_ui.refocus_gameplay()
|
||||
return true
|
||||
if accept_pressed:
|
||||
if (
|
||||
_controller_text_entry_request.is_valid()
|
||||
and bool(_controller_text_entry_request.call())
|
||||
):
|
||||
if request_controller_text_entry_for():
|
||||
return true
|
||||
return _chat_ui.request_virtual_keyboard()
|
||||
return false
|
||||
|
|
@ -1175,12 +1196,22 @@ func setup_controller_mapping(
|
|||
_emote_radial_menu.setup_controller_mapping(_controller_mapping_manager)
|
||||
_quick_radial_menu.setup_controller_mapping(_controller_mapping_manager)
|
||||
_player_menu.setup_controller_mapping(_controller_mapping_manager)
|
||||
_fishing_shop.setup_controller_mapping(_controller_mapping_manager)
|
||||
for panel: SettingsPanelType in [
|
||||
_title_settings_panel, _pause_settings_panel
|
||||
]:
|
||||
panel.setup_controller_mapping(_controller_mapping_manager)
|
||||
|
||||
|
||||
func setup_keyboard_mouse_mapping(
|
||||
mapping_manager: KeyboardMouseMappingManagerType,
|
||||
) -> void:
|
||||
for panel: SettingsPanelType in [
|
||||
_title_settings_panel, _pause_settings_panel
|
||||
]:
|
||||
panel.setup_keyboard_mouse_mapping(mapping_manager)
|
||||
|
||||
|
||||
func is_controller_mapping_capturing() -> bool:
|
||||
return (
|
||||
_title_settings_panel.is_controller_mapping_capturing()
|
||||
|
|
@ -1188,6 +1219,13 @@ func is_controller_mapping_capturing() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func is_input_mapping_capturing() -> bool:
|
||||
return (
|
||||
_title_settings_panel.is_input_mapping_capturing()
|
||||
or _pause_settings_panel.is_input_mapping_capturing()
|
||||
)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_poll_virtual_mouse_controller_state()
|
||||
_update_virtual_mouse(delta)
|
||||
|
|
@ -1199,7 +1237,7 @@ func _update_controller_menu_scroll(delta: float) -> void:
|
|||
if (
|
||||
_controller_mapping_manager == null
|
||||
or _virtual_mouse_active
|
||||
or is_controller_mapping_capturing()
|
||||
or is_input_mapping_capturing()
|
||||
):
|
||||
return
|
||||
var stick_y: float = _controller_menu_scroll_axis()
|
||||
|
|
|
|||
|
|
@ -123,10 +123,10 @@ func begin_controller_placement(
|
|||
var slot: BubbleHotbarSlotType = _slots[index]
|
||||
slot.focus_mode = Control.FOCUS_ALL
|
||||
slot.focus_neighbor_left = slot.get_path_to(
|
||||
_slots[wrapi(index - 1, 0, slot_count)]
|
||||
_slots[maxi(index - 1, 0)]
|
||||
)
|
||||
slot.focus_neighbor_right = slot.get_path_to(
|
||||
_slots[wrapi(index + 1, 0, slot_count)]
|
||||
_slots[mini(index + 1, slot_count - 1)]
|
||||
)
|
||||
slot.focus_neighbor_top = slot.get_path_to(slot)
|
||||
slot.focus_neighbor_bottom = slot.get_path_to(slot)
|
||||
|
|
@ -158,10 +158,10 @@ func begin_controller_management(initial_slot: int) -> void:
|
|||
var slot: BubbleHotbarSlotType = _slots[index]
|
||||
slot.focus_mode = Control.FOCUS_ALL
|
||||
slot.focus_neighbor_left = slot.get_path_to(
|
||||
_slots[wrapi(index - 1, 0, slot_count)]
|
||||
_slots[maxi(index - 1, 0)]
|
||||
)
|
||||
slot.focus_neighbor_right = slot.get_path_to(
|
||||
_slots[wrapi(index + 1, 0, slot_count)]
|
||||
_slots[mini(index + 1, slot_count - 1)]
|
||||
)
|
||||
slot.focus_neighbor_top = slot.get_path_to(slot)
|
||||
slot.focus_neighbor_bottom = slot.get_path_to(slot)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
class_name InterfaceFontController
|
||||
extends Node
|
||||
|
||||
const FileDialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
const STANDARD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf")
|
||||
const COMPACT_FILE_DIALOG_LIMIT := Vector2i(800, 600)
|
||||
const COMPACT_FILE_DIALOG_MARGIN := Vector2i(12, 12)
|
||||
const COMPACT_FILE_DIALOG_FONT_SIZE: int = 17
|
||||
|
||||
var _controller_text_entry_request: Callable
|
||||
var _controller_text_entry_is_open: Callable
|
||||
var _game_theme: Theme = preload("res://ui/game_theme.tres")
|
||||
var _utility_theme: Theme
|
||||
var _compact_file_dialog_theme: Theme
|
||||
var _tracked_file_dialogs: Dictionary[int, WeakRef] = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -23,6 +29,14 @@ func enforce_standard_font() -> void:
|
|||
_utility_theme.default_font = STANDARD_FONT
|
||||
|
||||
|
||||
func set_controller_text_entry_request(
|
||||
request: Callable,
|
||||
is_open: Callable = Callable(),
|
||||
) -> void:
|
||||
_controller_text_entry_request = request
|
||||
_controller_text_entry_is_open = is_open
|
||||
|
||||
|
||||
func set_readable_font_enabled(_enabled: bool) -> void:
|
||||
# Compatibility seam for callers compiled against the old setting.
|
||||
enforce_standard_font()
|
||||
|
|
@ -47,6 +61,7 @@ func apply_utility_theme(themed_node: Node) -> void:
|
|||
func popup_file_dialog(dialog: FileDialog) -> void:
|
||||
if dialog == null or not dialog.is_inside_tree():
|
||||
return
|
||||
_tracked_file_dialogs[dialog.get_instance_id()] = weakref(dialog)
|
||||
var host_window: Window = dialog.get_parent().get_window()
|
||||
var host_size: Vector2i = host_window.size
|
||||
var compact: bool = (
|
||||
|
|
@ -83,6 +98,55 @@ func popup_file_dialog(dialog: FileDialog) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if (
|
||||
_controller_text_entry_is_open.is_valid()
|
||||
and bool(_controller_text_entry_is_open.call())
|
||||
):
|
||||
return
|
||||
for instance_id: int in _tracked_file_dialogs.keys():
|
||||
var reference: WeakRef = _tracked_file_dialogs[instance_id]
|
||||
var dialog := reference.get_ref() as FileDialog
|
||||
if dialog == null or not is_instance_valid(dialog):
|
||||
_tracked_file_dialogs.erase(instance_id)
|
||||
continue
|
||||
if not dialog.visible:
|
||||
continue
|
||||
FileDialogControllerNavigationType.configure(dialog)
|
||||
_connect_file_dialog_text_controls(dialog)
|
||||
|
||||
|
||||
func _connect_file_dialog_text_controls(dialog: FileDialog) -> void:
|
||||
var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
|
||||
for control: Control in (
|
||||
FileDialogControllerNavigationType.interactive_controls(scope)
|
||||
):
|
||||
if not (control is LineEdit or control is TextEdit):
|
||||
continue
|
||||
var callback := _on_file_dialog_text_gui_input.bind(control)
|
||||
if not control.gui_input.is_connected(callback):
|
||||
control.gui_input.connect(callback)
|
||||
|
||||
|
||||
func _on_file_dialog_text_gui_input(
|
||||
event: InputEvent,
|
||||
control: Control,
|
||||
) -> void:
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if (
|
||||
button_event == null
|
||||
or not button_event.pressed
|
||||
or (
|
||||
button_event.button_index != JOY_BUTTON_A
|
||||
and not event.is_action_pressed("ui_accept")
|
||||
)
|
||||
or not _controller_text_entry_request.is_valid()
|
||||
):
|
||||
return
|
||||
if bool(_controller_text_entry_request.call(control)):
|
||||
control.accept_event()
|
||||
|
||||
|
||||
func _finalize_compact_file_dialog(
|
||||
dialog: FileDialog,
|
||||
host_size: Vector2i,
|
||||
|
|
|
|||
400
ui/keyboard_mouse_mapping_panel.gd
Normal file
400
ui/keyboard_mouse_mapping_panel.gd
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
class_name KeyboardMouseMappingPanel
|
||||
extends Control
|
||||
|
||||
signal closed
|
||||
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
|
||||
var _mapping_manager: KeyboardMouseMappingManagerType
|
||||
var _binding_buttons: Dictionary = {}
|
||||
var _progress_label: Label
|
||||
var _mapping_scroll: ScrollContainer
|
||||
var _reset_button: Button
|
||||
var _close_button: Button
|
||||
var _capturing_role: StringName = &""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
_build_interface()
|
||||
hide()
|
||||
|
||||
|
||||
func setup(mapping_manager: KeyboardMouseMappingManagerType) -> void:
|
||||
_mapping_manager = mapping_manager
|
||||
if not _mapping_manager.mapping_changed.is_connected(_refresh_bindings):
|
||||
_mapping_manager.mapping_changed.connect(_refresh_bindings)
|
||||
_refresh_bindings()
|
||||
|
||||
|
||||
func open_panel() -> void:
|
||||
if _mapping_manager == null:
|
||||
return
|
||||
_cancel_capture()
|
||||
show()
|
||||
_refresh_bindings()
|
||||
var first_button := _binding_buttons.get(
|
||||
KeyboardMouseMappingManagerType.ROLE_ORDER.front()
|
||||
) as Button
|
||||
if first_button != null:
|
||||
first_button.grab_focus()
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func is_capturing() -> bool:
|
||||
return visible and not _capturing_role.is_empty()
|
||||
|
||||
|
||||
func request_back() -> void:
|
||||
if not visible:
|
||||
return
|
||||
if not _capturing_role.is_empty():
|
||||
_cancel_capture()
|
||||
_progress_label.text = "keyboard binding cancelled"
|
||||
return
|
||||
close_panel()
|
||||
|
||||
|
||||
func close_panel() -> void:
|
||||
_cancel_capture()
|
||||
hide()
|
||||
closed.emit()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or _mapping_manager == null or _capturing_role.is_empty():
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
key_event != null
|
||||
and key_event.pressed
|
||||
and not key_event.echo
|
||||
and key_event.keycode == KEY_ESCAPE
|
||||
):
|
||||
get_viewport().set_input_as_handled()
|
||||
_cancel_capture()
|
||||
_progress_label.text = "keyboard binding cancelled"
|
||||
return
|
||||
var binding: Dictionary = _mapping_manager.binding_from_event(event)
|
||||
if binding.is_empty():
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
var role: StringName = _capturing_role
|
||||
var saved: bool = _mapping_manager.set_binding(role, binding)
|
||||
_cancel_capture()
|
||||
_progress_label.text = (
|
||||
"updated " + _mapping_manager.get_role_label(role)
|
||||
if saved else "could not save that keyboard binding"
|
||||
)
|
||||
_refresh_bindings()
|
||||
|
||||
|
||||
func _build_interface() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
z_index = 100
|
||||
UtilityPageStyleType.apply_page(self)
|
||||
|
||||
var frame := PanelContainer.new()
|
||||
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
frame.offset_left = 18.0
|
||||
frame.offset_top = 18.0
|
||||
frame.offset_right = -18.0
|
||||
frame.offset_bottom = -18.0
|
||||
frame.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyleType.rounded_style(
|
||||
UtilityPageStyleType.OCEAN_PANEL_DEEP,
|
||||
20,
|
||||
),
|
||||
)
|
||||
add_child(frame)
|
||||
|
||||
var outer_margin := MarginContainer.new()
|
||||
outer_margin.add_theme_constant_override("margin_left", 24)
|
||||
outer_margin.add_theme_constant_override("margin_top", 20)
|
||||
outer_margin.add_theme_constant_override("margin_right", 24)
|
||||
outer_margin.add_theme_constant_override("margin_bottom", 20)
|
||||
frame.add_child(outer_margin)
|
||||
|
||||
var page := VBoxContainer.new()
|
||||
page.add_theme_constant_override("separation", 12)
|
||||
outer_margin.add_child(page)
|
||||
|
||||
var heading_row := HBoxContainer.new()
|
||||
heading_row.add_theme_constant_override("separation", 12)
|
||||
page.add_child(heading_row)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "keyboard binds"
|
||||
title.add_theme_font_size_override("font_size", 28)
|
||||
title.add_theme_color_override(
|
||||
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
heading_row.add_child(title)
|
||||
|
||||
var device_label := Label.new()
|
||||
device_label.text = "mouse + keyboard"
|
||||
device_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
device_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
device_label.add_theme_font_size_override("font_size", 15)
|
||||
device_label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||||
)
|
||||
heading_row.add_child(device_label)
|
||||
|
||||
var instruction_label := Label.new()
|
||||
instruction_label.text = (
|
||||
"select a row, then press a keyboard key or mouse button. "
|
||||
+ "duplicate bindings are marked in red."
|
||||
)
|
||||
instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
instruction_label.add_theme_font_size_override("font_size", 15)
|
||||
instruction_label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||||
)
|
||||
page.add_child(instruction_label)
|
||||
|
||||
_progress_label = Label.new()
|
||||
_progress_label.custom_minimum_size.y = 34.0
|
||||
_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_progress_label.add_theme_font_size_override("font_size", 18)
|
||||
_progress_label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
page.add_child(_progress_label)
|
||||
|
||||
_mapping_scroll = ScrollContainer.new()
|
||||
_mapping_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_mapping_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_mapping_scroll.follow_focus = true
|
||||
page.add_child(_mapping_scroll)
|
||||
|
||||
var role_grid := GridContainer.new()
|
||||
role_grid.columns = 2
|
||||
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
role_grid.add_theme_constant_override("h_separation", 12)
|
||||
role_grid.add_theme_constant_override("v_separation", 7)
|
||||
_mapping_scroll.add_child(role_grid)
|
||||
|
||||
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||||
var role_label := Label.new()
|
||||
role_label.text = KeyboardMouseMappingManagerType.ROLE_LABELS.get(
|
||||
role,
|
||||
str(role),
|
||||
)
|
||||
role_label.custom_minimum_size = Vector2(330.0, 42.0)
|
||||
role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
role_label.add_theme_font_size_override("font_size", 16)
|
||||
role_label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
role_grid.add_child(role_label)
|
||||
|
||||
var binding_button := Button.new()
|
||||
binding_button.custom_minimum_size = Vector2(210.0, 42.0)
|
||||
binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
binding_button.focus_mode = Control.FOCUS_ALL
|
||||
binding_button.tooltip_text = (
|
||||
"change " + _mapping_manager_label(role)
|
||||
)
|
||||
UtilityPageStyleType.apply_compact_ocean_button(binding_button)
|
||||
binding_button.pressed.connect(_begin_capture.bind(role))
|
||||
role_grid.add_child(binding_button)
|
||||
_binding_buttons[role] = binding_button
|
||||
|
||||
var footer := HBoxContainer.new()
|
||||
footer.alignment = BoxContainer.ALIGNMENT_END
|
||||
footer.add_theme_constant_override("separation", 10)
|
||||
page.add_child(footer)
|
||||
|
||||
_reset_button = Button.new()
|
||||
_reset_button.text = "restore defaults"
|
||||
_reset_button.tooltip_text = "restore the project keyboard defaults"
|
||||
UtilityPageStyleType.apply_ocean_button(_reset_button)
|
||||
_reset_button.pressed.connect(_reset_mapping)
|
||||
footer.add_child(_reset_button)
|
||||
|
||||
_close_button = Button.new()
|
||||
_close_button.text = "done"
|
||||
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||||
_close_button.pressed.connect(close_panel)
|
||||
footer.add_child(_close_button)
|
||||
_configure_mapping_navigation()
|
||||
|
||||
|
||||
func _configure_mapping_navigation() -> void:
|
||||
var buttons: Array[Button] = []
|
||||
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||||
var binding_button := _binding_buttons.get(role) as Button
|
||||
if binding_button != null:
|
||||
buttons.append(binding_button)
|
||||
if buttons.is_empty():
|
||||
return
|
||||
for index: int in buttons.size():
|
||||
var button: Button = buttons[index]
|
||||
var previous: Control = buttons[index - 1] if index > 0 else button
|
||||
var next: Control = (
|
||||
buttons[index + 1]
|
||||
if index < buttons.size() - 1
|
||||
else _close_button
|
||||
)
|
||||
_set_focus_neighbors(button, button, button, previous, next)
|
||||
button.focus_previous = button.get_path_to(previous)
|
||||
button.focus_next = button.get_path_to(next)
|
||||
var last_button: Button = buttons.back()
|
||||
_set_focus_neighbors(
|
||||
_reset_button,
|
||||
_reset_button,
|
||||
_close_button,
|
||||
last_button,
|
||||
_reset_button,
|
||||
)
|
||||
_set_focus_neighbors(
|
||||
_close_button,
|
||||
_reset_button,
|
||||
_close_button,
|
||||
last_button,
|
||||
_close_button,
|
||||
)
|
||||
_close_button.focus_previous = _close_button.get_path_to(last_button)
|
||||
_close_button.focus_next = _close_button.get_path_to(_reset_button)
|
||||
|
||||
|
||||
func _set_focus_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
|
||||
|
||||
func _mapping_manager_label(role: StringName) -> String:
|
||||
if _mapping_manager == null:
|
||||
return str(
|
||||
KeyboardMouseMappingManagerType.ROLE_LABELS.get(role, str(role))
|
||||
)
|
||||
return _mapping_manager.get_role_label(role)
|
||||
|
||||
|
||||
func _begin_capture(role: StringName) -> void:
|
||||
if _mapping_manager == null:
|
||||
return
|
||||
_capturing_role = role
|
||||
_set_action_buttons_disabled(true)
|
||||
_progress_label.text = (
|
||||
"press a key or mouse button for "
|
||||
+ _mapping_manager.get_role_label(role)
|
||||
+ " • escape cancels"
|
||||
)
|
||||
|
||||
|
||||
func _cancel_capture() -> void:
|
||||
_capturing_role = &""
|
||||
_set_action_buttons_disabled(false)
|
||||
if _progress_label != null:
|
||||
_progress_label.text = ""
|
||||
|
||||
|
||||
func _set_action_buttons_disabled(disabled: bool) -> void:
|
||||
if _reset_button != null:
|
||||
_reset_button.disabled = disabled
|
||||
if _close_button != null:
|
||||
_close_button.disabled = disabled
|
||||
for button_value: Variant in _binding_buttons.values():
|
||||
var button := button_value as Button
|
||||
if button != null:
|
||||
button.disabled = disabled
|
||||
|
||||
|
||||
func _reset_mapping() -> void:
|
||||
if _mapping_manager == null:
|
||||
return
|
||||
var reset: bool = _mapping_manager.reset_mapping()
|
||||
_progress_label.text = (
|
||||
"restored the project keyboard defaults"
|
||||
if reset else "could not restore keyboard defaults"
|
||||
)
|
||||
_refresh_bindings()
|
||||
|
||||
|
||||
func _refresh_bindings() -> void:
|
||||
if _mapping_manager == null or _reset_button == null:
|
||||
return
|
||||
var bindings: Dictionary = _mapping_manager.get_active_bindings()
|
||||
for role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||||
var button := _binding_buttons.get(role) as Button
|
||||
if button == null:
|
||||
continue
|
||||
var raw_binding: Variant = bindings.get(str(role), {})
|
||||
button.text = (
|
||||
_mapping_manager.binding_label(raw_binding as Dictionary)
|
||||
if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
|
||||
)
|
||||
var conflict_role: StringName = _find_binding_conflict(
|
||||
role,
|
||||
raw_binding as Dictionary,
|
||||
bindings,
|
||||
)
|
||||
_apply_binding_button_style(button, not conflict_role.is_empty())
|
||||
button.tooltip_text = (
|
||||
"warning: also assigned to "
|
||||
+ _mapping_manager.get_role_label(conflict_role)
|
||||
if not conflict_role.is_empty()
|
||||
else "change " + _mapping_manager.get_role_label(role)
|
||||
)
|
||||
_reset_button.disabled = not _mapping_manager.has_custom_mapping()
|
||||
|
||||
|
||||
func _find_binding_conflict(
|
||||
role: StringName,
|
||||
binding: Dictionary,
|
||||
bindings: Dictionary,
|
||||
) -> StringName:
|
||||
if binding.is_empty():
|
||||
return &""
|
||||
for other_role: StringName in KeyboardMouseMappingManagerType.ROLE_ORDER:
|
||||
if other_role == role:
|
||||
continue
|
||||
var raw_other: Variant = bindings.get(str(other_role), {})
|
||||
if typeof(raw_other) != TYPE_DICTIONARY:
|
||||
continue
|
||||
if KeyboardMouseMappingManagerType.bindings_conflict(
|
||||
binding,
|
||||
raw_other as Dictionary,
|
||||
):
|
||||
return other_role
|
||||
return &""
|
||||
|
||||
|
||||
func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
|
||||
UtilityPageStyleType.apply_compact_ocean_button(button)
|
||||
if not has_conflict:
|
||||
return
|
||||
var colors: Dictionary = {
|
||||
&"normal": UtilityPageStyleType.OCEAN_DANGER,
|
||||
&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||||
&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
|
||||
&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||||
}
|
||||
for state: StringName in colors:
|
||||
var style := UtilityPageStyleType.ocean_button_style(
|
||||
colors[state] as Color
|
||||
)
|
||||
style.content_margin_left = 10.0
|
||||
style.content_margin_right = 10.0
|
||||
style.content_margin_top = 4.0
|
||||
style.content_margin_bottom = 4.0
|
||||
button.add_theme_stylebox_override(state, style)
|
||||
1
ui/keyboard_mouse_mapping_panel.gd.uid
Normal file
1
ui/keyboard_mouse_mapping_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cxgp1c5qok7f6
|
||||
|
|
@ -24,6 +24,10 @@ const DETAIL_FADE_DURATION: float = 0.12
|
|||
const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION
|
||||
const HANDWRITTEN_NUMERIC_SCALE: float = 0.8
|
||||
const PORTRAIT_VIEW_MAX_SIZE := Vector2(860.0, 480.0)
|
||||
const DETAIL_OVERLAY_EDGE_MARGIN: int = 22
|
||||
const DETAIL_OVERLAY_CONTENT_MARGIN: int = 34
|
||||
const DETAIL_OVERLAY_TITLE_FONT_SIZE: int = 40
|
||||
const DETAIL_OVERLAY_TEXT_FONT_SIZE: int = 36
|
||||
const INK := Color("251b10")
|
||||
const MUTED_INK := Color("6d5b45")
|
||||
const LOGBOOK_ARTWORK: Texture2D = preload(
|
||||
|
|
@ -50,6 +54,13 @@ const CATALOG_SNAP_DELAY: float = 0.12
|
|||
const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0)
|
||||
const DETAIL_BOTTOM_INSET: float = 35.0
|
||||
|
||||
enum ControllerZone {
|
||||
TABS,
|
||||
ENTRIES,
|
||||
DETAILS,
|
||||
OVERLAY,
|
||||
}
|
||||
|
||||
var _collection_log: CollectionLogType
|
||||
var _inventory: FishInventoryType
|
||||
var _catalog: FishPoolType
|
||||
|
|
@ -78,9 +89,14 @@ var _catalog_scroll_down_indicator: TextureRect
|
|||
var _empty_state: Label
|
||||
var _detail_body: VBoxContainer
|
||||
var _detail_portrait_button: Button
|
||||
var _detail_buttons: Array[Button] = []
|
||||
var _portrait_overlay: Control
|
||||
var _portrait_overlay_backdrop: Button
|
||||
var _portrait_overlay_artwork: LogbookPortraitType
|
||||
var _portrait_overlay_title: Label
|
||||
var _portrait_overlay_text: Label
|
||||
var _overlay_return_focus: Control
|
||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -136,7 +152,9 @@ func set_interactive(value: bool) -> void:
|
|||
)
|
||||
for tab: Button in _category_tabs:
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL if _interactive else Control.FOCUS_NONE
|
||||
Control.FOCUS_ALL
|
||||
if _interactive and _controller_zone == ControllerZone.TABS
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
tab.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
|
|
@ -145,31 +163,109 @@ func set_interactive(value: bool) -> void:
|
|||
)
|
||||
for entry: Button in _entry_buttons.values():
|
||||
entry.focus_mode = (
|
||||
Control.FOCUS_ALL if _interactive else Control.FOCUS_NONE
|
||||
Control.FOCUS_ALL
|
||||
if _interactive and _controller_zone == ControllerZone.ENTRIES
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
entry.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if _interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
for detail_button: Button in _detail_buttons:
|
||||
if not is_instance_valid(detail_button):
|
||||
continue
|
||||
detail_button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _interactive and _controller_zone == ControllerZone.DETAILS
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
if _portrait_overlay_backdrop != null:
|
||||
_portrait_overlay_backdrop.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _interactive and _controller_zone == ControllerZone.OVERLAY
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
|
||||
|
||||
func focus_initial() -> void:
|
||||
if not _active or not _interactive:
|
||||
return
|
||||
if _portrait_overlay != null and _portrait_overlay.visible:
|
||||
if _controller_zone == ControllerZone.OVERLAY:
|
||||
_portrait_overlay_backdrop.grab_focus()
|
||||
return
|
||||
var selected := _entry_buttons.get(_selected_id) as Button
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
var category_tab_index: int = _category_tab_categories.find(_category)
|
||||
if category_tab_index >= 0:
|
||||
_category_tabs[category_tab_index].grab_focus()
|
||||
return
|
||||
if _controller_zone == ControllerZone.DETAILS:
|
||||
if not _detail_buttons.is_empty():
|
||||
_detail_buttons.front().grab_focus()
|
||||
return
|
||||
var selected := _entry_buttons.get(_selected_entry_key) as Button
|
||||
if selected != null:
|
||||
selected.grab_focus()
|
||||
elif not _entry_buttons.is_empty():
|
||||
var first := _entry_buttons.values().front() as Button
|
||||
first.grab_focus()
|
||||
else:
|
||||
var category_tab_index: int = _category_tab_categories.find(_category)
|
||||
if category_tab_index >= 0:
|
||||
_category_tabs[category_tab_index].grab_focus()
|
||||
|
||||
|
||||
func reset_controller_zone() -> void:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("focus_initial")
|
||||
|
||||
|
||||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _active or not _interactive:
|
||||
return false
|
||||
var accept_pressed: bool = event.is_action_pressed("ui_accept")
|
||||
var cancel_pressed: bool = event.is_action_pressed("ui_cancel")
|
||||
if cancel_pressed:
|
||||
match _controller_zone:
|
||||
ControllerZone.OVERLAY:
|
||||
_hide_portrait_overlay()
|
||||
ControllerZone.DETAILS:
|
||||
_set_controller_zone(ControllerZone.ENTRIES)
|
||||
ControllerZone.ENTRIES:
|
||||
_set_controller_zone(ControllerZone.TABS)
|
||||
ControllerZone.TABS:
|
||||
return false
|
||||
return true
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
if event.is_action_pressed("ui_left"):
|
||||
_select_adjacent_controller_category(-1)
|
||||
return true
|
||||
if event.is_action_pressed("ui_right"):
|
||||
_select_adjacent_controller_category(1)
|
||||
return true
|
||||
if accept_pressed:
|
||||
if not _entry_buttons.is_empty():
|
||||
_set_controller_zone(ControllerZone.ENTRIES)
|
||||
return true
|
||||
if _controller_zone == ControllerZone.ENTRIES and accept_pressed:
|
||||
if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty():
|
||||
_set_controller_zone(ControllerZone.DETAILS)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _set_controller_zone(zone: ControllerZone) -> void:
|
||||
_controller_zone = zone
|
||||
set_interactive(_interactive)
|
||||
call_deferred("focus_initial")
|
||||
|
||||
|
||||
func _select_adjacent_controller_category(direction: int) -> void:
|
||||
var current_index: int = _category_tab_categories.find(_category)
|
||||
var target_index: int = clampi(
|
||||
current_index + direction,
|
||||
0,
|
||||
_category_tab_categories.size() - 1,
|
||||
)
|
||||
if target_index != current_index:
|
||||
_select_category(_category_tab_categories[target_index])
|
||||
|
||||
|
||||
func _build_interface() -> void:
|
||||
|
|
@ -715,6 +811,7 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
_detail_portrait_button.pressed.connect(
|
||||
_show_portrait_overlay.bind(fish.display_texture)
|
||||
)
|
||||
_detail_buttons.append(_detail_portrait_button)
|
||||
portrait_column.add_child(_detail_portrait_button)
|
||||
var artwork_center := CenterContainer.new()
|
||||
artwork_center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
|
@ -729,11 +826,20 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
)
|
||||
artwork_center.add_child(artwork)
|
||||
|
||||
var facts_text: String = LogbookCatalog.facts_for(fish)
|
||||
var facts_button := _make_detail_section_button(
|
||||
"fish facts",
|
||||
facts_text,
|
||||
)
|
||||
facts_button.custom_minimum_size = Vector2(190.0, 132.0)
|
||||
facts_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
facts_button.size_flags_stretch_ratio = 1.0
|
||||
summary_columns.add_child(facts_button)
|
||||
var facts_column := VBoxContainer.new()
|
||||
facts_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
facts_column.size_flags_stretch_ratio = 1.0
|
||||
facts_column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
facts_column.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
facts_column.add_theme_constant_override("separation", 6)
|
||||
summary_columns.add_child(facts_column)
|
||||
facts_button.add_child(facts_column)
|
||||
var facts_heading := _field_label(
|
||||
"shellfish facts"
|
||||
if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH
|
||||
|
|
@ -741,17 +847,37 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
16,
|
||||
)
|
||||
facts_column.add_child(facts_heading)
|
||||
var facts := _label(LogbookCatalog.facts_for(fish), 16)
|
||||
var facts := _label(facts_text, 16)
|
||||
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
facts_column.add_child(facts)
|
||||
_detail_body.add_child(_build_quality_progress(fish.id))
|
||||
var quality_button := _make_detail_section_button(
|
||||
"quality collection",
|
||||
_quality_overlay_text(fish.id),
|
||||
)
|
||||
quality_button.custom_minimum_size.y = 132.0
|
||||
quality_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_detail_body.add_child(quality_button)
|
||||
var quality_progress := _build_quality_progress(fish.id)
|
||||
quality_progress.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
quality_progress.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
quality_button.add_child(quality_progress)
|
||||
|
||||
var stats_button := _make_detail_section_button(
|
||||
"fish stats",
|
||||
_stats_overlay_text(fish, catalog_number),
|
||||
)
|
||||
stats_button.custom_minimum_size.y = 190.0
|
||||
stats_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
stats_button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_detail_body.add_child(stats_button)
|
||||
var stats_anchor := VBoxContainer.new()
|
||||
stats_anchor.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
stats_anchor.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
stats_anchor.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
stats_anchor.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
stats_anchor.alignment = BoxContainer.ALIGNMENT_END
|
||||
_detail_body.add_child(stats_anchor)
|
||||
stats_button.add_child(stats_anchor)
|
||||
var stats_columns := HBoxContainer.new()
|
||||
stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
stats_columns.add_theme_constant_override("separation", 28)
|
||||
|
|
@ -806,6 +932,47 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
bottom_inset.custom_minimum_size.y = DETAIL_BOTTOM_INSET
|
||||
bottom_inset.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
stats_anchor.add_child(bottom_inset)
|
||||
_configure_detail_focus(
|
||||
_detail_portrait_button,
|
||||
facts_button,
|
||||
quality_button,
|
||||
stats_button,
|
||||
)
|
||||
set_interactive(_interactive)
|
||||
|
||||
|
||||
func _configure_detail_focus(
|
||||
portrait: Button,
|
||||
facts: Button,
|
||||
quality: Button,
|
||||
stats: Button,
|
||||
) -> void:
|
||||
var top_left: Button = portrait if portrait != null else facts
|
||||
if portrait != null:
|
||||
portrait.focus_neighbor_left = portrait.get_path_to(portrait)
|
||||
portrait.focus_neighbor_right = portrait.get_path_to(facts)
|
||||
portrait.focus_neighbor_top = portrait.get_path_to(portrait)
|
||||
portrait.focus_neighbor_bottom = portrait.get_path_to(quality)
|
||||
portrait.focus_previous = portrait.get_path_to(stats)
|
||||
portrait.focus_next = portrait.get_path_to(facts)
|
||||
facts.focus_neighbor_left = facts.get_path_to(top_left)
|
||||
facts.focus_neighbor_right = facts.get_path_to(facts)
|
||||
facts.focus_neighbor_top = facts.get_path_to(facts)
|
||||
facts.focus_neighbor_bottom = facts.get_path_to(quality)
|
||||
facts.focus_previous = facts.get_path_to(top_left)
|
||||
facts.focus_next = facts.get_path_to(quality)
|
||||
quality.focus_neighbor_left = quality.get_path_to(quality)
|
||||
quality.focus_neighbor_right = quality.get_path_to(quality)
|
||||
quality.focus_neighbor_top = quality.get_path_to(facts)
|
||||
quality.focus_neighbor_bottom = quality.get_path_to(stats)
|
||||
quality.focus_previous = quality.get_path_to(facts)
|
||||
quality.focus_next = quality.get_path_to(stats)
|
||||
stats.focus_neighbor_left = stats.get_path_to(stats)
|
||||
stats.focus_neighbor_right = stats.get_path_to(stats)
|
||||
stats.focus_neighbor_top = stats.get_path_to(quality)
|
||||
stats.focus_neighbor_bottom = stats.get_path_to(stats)
|
||||
stats.focus_previous = stats.get_path_to(quality)
|
||||
stats.focus_next = stats.get_path_to(top_left)
|
||||
|
||||
|
||||
func _build_quality_progress(fish_id: StringName) -> VBoxContainer:
|
||||
|
|
@ -866,6 +1033,72 @@ func _build_quality_progress(fish_id: StringName) -> VBoxContainer:
|
|||
return quality_section
|
||||
|
||||
|
||||
func _make_detail_section_button(
|
||||
section_title: String,
|
||||
section_text: String,
|
||||
) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = ""
|
||||
button.tooltip_text = "View %s larger" % section_title
|
||||
button.accessibility_name = "View %s larger" % section_title
|
||||
button.add_theme_stylebox_override("normal", _portrait_button_style(false))
|
||||
button.add_theme_stylebox_override("hover", _portrait_button_style(true))
|
||||
button.add_theme_stylebox_override("focus", _portrait_button_style(true))
|
||||
button.add_theme_stylebox_override("pressed", _portrait_button_style(true))
|
||||
button.pressed.connect(
|
||||
_show_detail_text_overlay.bind(section_title, section_text, button)
|
||||
)
|
||||
_detail_buttons.append(button)
|
||||
return button
|
||||
|
||||
|
||||
func _quality_overlay_text(fish_id: StringName) -> String:
|
||||
var lines: Array[String] = []
|
||||
for quality: int in FishQualityType.TIER_COUNT:
|
||||
var collected: bool = _collection_log.has_discovered_quality(
|
||||
fish_id,
|
||||
quality,
|
||||
)
|
||||
lines.append("%s %s" % [
|
||||
"collected" if collected else "not collected",
|
||||
FishQualityType.display_name(quality),
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
func _stats_overlay_text(fish: FishDataType, catalog_number: int) -> String:
|
||||
var habitat_label: String = (
|
||||
"habitat"
|
||||
if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH
|
||||
else "body of water"
|
||||
)
|
||||
return "\n".join([
|
||||
"catalog number: %s" % (
|
||||
"#%03d" % catalog_number if catalog_number > 0 else "unknown"
|
||||
),
|
||||
"%s: %s" % [habitat_label, fish.get_habitat_label()],
|
||||
"weight range: %.2f–%.2f lb" % [
|
||||
fish.get_minimum_weight(),
|
||||
fish.get_maximum_weight(),
|
||||
],
|
||||
"rarity: %s" % fish.get_rarity_name().to_lower(),
|
||||
"time of day: %s" % _availability_text(fish),
|
||||
"value range: %d–%d" % [
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_min,
|
||||
FishQualityType.Tier.BORING,
|
||||
),
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_max,
|
||||
FishQualityType.Tier.SHINY,
|
||||
),
|
||||
],
|
||||
"number owned: %d" % (
|
||||
_inventory.get_count(fish.id) if _inventory != null else 0
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
func _show_no_selection() -> void:
|
||||
_clear_details()
|
||||
var instruction := _label("Select an entry to read its catch record.", 21)
|
||||
|
|
@ -992,6 +1225,7 @@ func _clear_entries() -> void:
|
|||
|
||||
func _clear_details() -> void:
|
||||
_detail_portrait_button = null
|
||||
_detail_buttons.clear()
|
||||
for child: Node in _detail_body.get_children():
|
||||
_detail_body.remove_child(child)
|
||||
child.queue_free()
|
||||
|
|
@ -1110,19 +1344,51 @@ func _build_portrait_overlay() -> void:
|
|||
)
|
||||
_portrait_overlay.add_child(_portrait_overlay_backdrop)
|
||||
|
||||
var center := CenterContainer.new()
|
||||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_portrait_overlay.add_child(center)
|
||||
var overlay_margin := MarginContainer.new()
|
||||
overlay_margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
overlay_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_set_margins(
|
||||
overlay_margin,
|
||||
DETAIL_OVERLAY_EDGE_MARGIN,
|
||||
DETAIL_OVERLAY_EDGE_MARGIN,
|
||||
DETAIL_OVERLAY_EDGE_MARGIN,
|
||||
DETAIL_OVERLAY_EDGE_MARGIN,
|
||||
)
|
||||
_portrait_overlay.add_child(overlay_margin)
|
||||
var card := PanelContainer.new()
|
||||
card.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
card.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
card.add_theme_stylebox_override("panel", _portrait_view_style())
|
||||
center.add_child(card)
|
||||
overlay_margin.add_child(card)
|
||||
var margin := MarginContainer.new()
|
||||
_set_margins(margin, 22, 22, 22, 22)
|
||||
_set_margins(
|
||||
margin,
|
||||
DETAIL_OVERLAY_CONTENT_MARGIN,
|
||||
DETAIL_OVERLAY_CONTENT_MARGIN,
|
||||
DETAIL_OVERLAY_CONTENT_MARGIN,
|
||||
DETAIL_OVERLAY_CONTENT_MARGIN,
|
||||
)
|
||||
card.add_child(margin)
|
||||
var overlay_stack := VBoxContainer.new()
|
||||
overlay_stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
overlay_stack.add_theme_constant_override("separation", 18)
|
||||
margin.add_child(overlay_stack)
|
||||
_portrait_overlay_title = _field_label(
|
||||
"", DETAIL_OVERLAY_TITLE_FONT_SIZE
|
||||
)
|
||||
_portrait_overlay_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
overlay_stack.add_child(_portrait_overlay_title)
|
||||
_portrait_overlay_artwork = LogbookPortraitType.new()
|
||||
margin.add_child(_portrait_overlay_artwork)
|
||||
_portrait_overlay_artwork.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
overlay_stack.add_child(_portrait_overlay_artwork)
|
||||
_portrait_overlay_text = _label("", DETAIL_OVERLAY_TEXT_FONT_SIZE)
|
||||
_portrait_overlay_text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_portrait_overlay_text.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_portrait_overlay_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
_portrait_overlay_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_portrait_overlay_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
overlay_stack.add_child(_portrait_overlay_text)
|
||||
|
||||
|
||||
func _show_portrait_overlay(portrait_texture: Texture2D) -> void:
|
||||
|
|
@ -1132,6 +1398,31 @@ func _show_portrait_overlay(portrait_texture: Texture2D) -> void:
|
|||
portrait_texture,
|
||||
PORTRAIT_VIEW_MAX_SIZE,
|
||||
)
|
||||
_portrait_overlay_title.text = "artwork"
|
||||
_portrait_overlay_artwork.visible = true
|
||||
_portrait_overlay_text.visible = false
|
||||
_overlay_return_focus = _detail_portrait_button
|
||||
_controller_zone = ControllerZone.OVERLAY
|
||||
set_interactive(_interactive)
|
||||
_portrait_overlay.visible = true
|
||||
_portrait_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_portrait_overlay_backdrop.grab_focus()
|
||||
|
||||
|
||||
func _show_detail_text_overlay(
|
||||
section_title: String,
|
||||
section_text: String,
|
||||
return_focus: Control,
|
||||
) -> void:
|
||||
if _portrait_overlay == null:
|
||||
return
|
||||
_portrait_overlay_title.text = section_title
|
||||
_portrait_overlay_artwork.visible = false
|
||||
_portrait_overlay_text.text = section_text
|
||||
_portrait_overlay_text.visible = true
|
||||
_overlay_return_focus = return_focus
|
||||
_controller_zone = ControllerZone.OVERLAY
|
||||
set_interactive(_interactive)
|
||||
_portrait_overlay.visible = true
|
||||
_portrait_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_portrait_overlay_backdrop.grab_focus()
|
||||
|
|
@ -1142,14 +1433,16 @@ func _hide_portrait_overlay(restore_focus: bool = true) -> void:
|
|||
return
|
||||
_portrait_overlay.visible = false
|
||||
_portrait_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_controller_zone = ControllerZone.DETAILS
|
||||
set_interactive(_interactive)
|
||||
if (
|
||||
restore_focus
|
||||
and _detail_portrait_button != null
|
||||
and is_instance_valid(_detail_portrait_button)
|
||||
and _overlay_return_focus != null
|
||||
and is_instance_valid(_overlay_return_focus)
|
||||
and _active
|
||||
and _interactive
|
||||
):
|
||||
_detail_portrait_button.grab_focus()
|
||||
_overlay_return_focus.grab_focus()
|
||||
|
||||
|
||||
func _on_portrait_overlay_backdrop_input(event: InputEvent) -> void:
|
||||
|
|
|
|||
330
ui/mail_page.gd
330
ui/mail_page.gd
|
|
@ -31,6 +31,8 @@ var _compose: Control
|
|||
var _letter: Control
|
||||
var _inbox_list: VBoxContainer
|
||||
var _empty_label: Label
|
||||
var _send_mail_button: Button
|
||||
var _archive_view_button: Button
|
||||
var _recipient: OptionButton
|
||||
var _greeting: OptionButton
|
||||
var _body: TextEdit
|
||||
|
|
@ -45,16 +47,20 @@ var _attachment_amount_currency_icon: TextureRect
|
|||
var _amount_minus: Button
|
||||
var _amount_plus: Button
|
||||
var _attachment_summary: RichTextLabel
|
||||
var _compose_cancel: Button
|
||||
var _send_button: Button
|
||||
var _status: Label
|
||||
var _letter_text: Label
|
||||
var _letter_gift: RichTextLabel
|
||||
var _letter_close: Button
|
||||
var _accept: Button
|
||||
var _decline: Button
|
||||
var _archive: Button
|
||||
var _delete: Button
|
||||
var _current_mail_id := ""
|
||||
var _showing_archive := false
|
||||
var _active: bool = false
|
||||
var _interactive: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -85,13 +91,16 @@ func setup(
|
|||
|
||||
|
||||
func activate() -> void:
|
||||
_active = true
|
||||
_show_inbox()
|
||||
call_deferred("_focus_first")
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func deactivate() -> void:
|
||||
_active = false
|
||||
_current_mail_id = ""
|
||||
_show_inbox()
|
||||
set_interactive(false)
|
||||
|
||||
|
||||
func consume_escape() -> bool:
|
||||
|
|
@ -106,12 +115,263 @@ func is_composing_letter() -> bool:
|
|||
|
||||
|
||||
func set_interactive(value: bool) -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS if value else Control.MOUSE_FILTER_IGNORE
|
||||
for node: Node in find_children("*", "BaseButton", true, false):
|
||||
(node as BaseButton).focus_mode = (
|
||||
Control.FOCUS_ALL if value and node.is_visible_in_tree()
|
||||
_interactive = value and _active
|
||||
mouse_filter = (
|
||||
Control.MOUSE_FILTER_PASS
|
||||
if _interactive else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
_refresh_controller_navigation()
|
||||
|
||||
|
||||
func reset_controller_zone() -> void:
|
||||
_show_inbox()
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _active or not _interactive:
|
||||
return false
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
return consume_escape()
|
||||
return false
|
||||
|
||||
|
||||
func _refresh_controller_navigation() -> void:
|
||||
var controls: Array[Control] = []
|
||||
_collect_visible_controller_controls(self, controls)
|
||||
for control: Control in controls:
|
||||
var button := control as BaseButton
|
||||
control.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _interactive and (button == null or not button.disabled)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
ControllerFocusNavigation.configure_spatial_neighbors(controls)
|
||||
if _compose != null and _compose.visible:
|
||||
_configure_compose_controller_navigation()
|
||||
elif _inbox != null and _inbox.visible:
|
||||
_configure_inbox_controller_navigation()
|
||||
elif _letter != null and _letter.visible:
|
||||
_configure_letter_controller_navigation()
|
||||
if not _interactive or controls.is_empty():
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner == null or not is_ancestor_of(focus_owner):
|
||||
controls.sort_custom(func(first: Control, second: Control) -> bool:
|
||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
||||
return first.global_position.y < second.global_position.y
|
||||
return first.global_position.x < second.global_position.x
|
||||
)
|
||||
controls.front().grab_focus()
|
||||
|
||||
|
||||
func _configure_inbox_controller_navigation() -> void:
|
||||
if not _interactive:
|
||||
return
|
||||
var entries: Array[Control] = []
|
||||
for child: Node in _inbox_list.get_children():
|
||||
var button := child as Button
|
||||
if button != null and _controller_focus_eligible(button):
|
||||
entries.append(button)
|
||||
var first_entry: Control = (
|
||||
entries.front() if not entries.is_empty() else _archive_view_button
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_archive_view_button,
|
||||
_archive_view_button,
|
||||
_send_mail_button,
|
||||
_archive_view_button,
|
||||
first_entry,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_send_mail_button,
|
||||
_archive_view_button,
|
||||
_send_mail_button,
|
||||
_send_mail_button,
|
||||
entries.front() if not entries.is_empty() else _send_mail_button,
|
||||
)
|
||||
for index: int in entries.size():
|
||||
var entry: Control = entries[index]
|
||||
_set_compose_neighbors(
|
||||
entry,
|
||||
entry,
|
||||
entry,
|
||||
entries[index - 1] if index > 0 else _archive_view_button,
|
||||
entries[index + 1] if index < entries.size() - 1 else entry,
|
||||
)
|
||||
var traversal: Array[Control] = [
|
||||
_archive_view_button,
|
||||
_send_mail_button,
|
||||
]
|
||||
traversal.append_array(entries)
|
||||
ControllerFocusNavigation.configure_traversal(traversal)
|
||||
|
||||
|
||||
func _configure_letter_controller_navigation() -> void:
|
||||
if not _interactive:
|
||||
return
|
||||
var pending_gift: bool = (
|
||||
_accept != null
|
||||
and _accept.is_visible_in_tree()
|
||||
and _decline != null
|
||||
and _decline.is_visible_in_tree()
|
||||
)
|
||||
if pending_gift:
|
||||
_set_compose_neighbors(
|
||||
_accept, _accept, _accept, _accept, _decline
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_decline, _decline, _decline, _accept, _delete
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_letter_close,
|
||||
_letter_close,
|
||||
_archive,
|
||||
_letter_close,
|
||||
_letter_close,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_archive,
|
||||
_letter_close,
|
||||
_delete,
|
||||
_archive,
|
||||
_archive,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_delete,
|
||||
_archive,
|
||||
_delete,
|
||||
_decline if pending_gift else _delete,
|
||||
_delete,
|
||||
)
|
||||
var traversal: Array[Control] = [_letter_close, _archive, _delete]
|
||||
if pending_gift:
|
||||
traversal = [_accept, _decline, _letter_close, _archive, _delete]
|
||||
ControllerFocusNavigation.configure_traversal(traversal)
|
||||
|
||||
|
||||
func _configure_compose_controller_navigation() -> void:
|
||||
if not _interactive:
|
||||
return
|
||||
var amount_visible: bool = (
|
||||
_attachment_amount != null
|
||||
and _attachment_amount.is_visible_in_tree()
|
||||
)
|
||||
var amount_left: Control = _amount_minus if amount_visible else _compose_cancel
|
||||
var amount_middle: Control = (
|
||||
_attachment_amount if amount_visible else _compose_cancel
|
||||
)
|
||||
var amount_right: Control = _amount_plus if amount_visible else _send_button
|
||||
_set_compose_neighbors(
|
||||
_greeting, _greeting, _recipient, _greeting, _body
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_recipient, _greeting, _attachment_kind, _recipient, _body
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_body, _body, _attachment_choice, _recipient, _salutation
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_salutation, _salutation, _compose_cancel, _body, _salutation
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_attachment_kind,
|
||||
_recipient,
|
||||
_attachment_kind,
|
||||
_attachment_kind,
|
||||
_attachment_choice,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_attachment_choice,
|
||||
_body,
|
||||
_attachment_choice,
|
||||
_attachment_kind,
|
||||
amount_middle,
|
||||
)
|
||||
if amount_visible:
|
||||
_set_compose_neighbors(
|
||||
_amount_minus,
|
||||
_amount_minus,
|
||||
_attachment_amount,
|
||||
_attachment_choice,
|
||||
_compose_cancel,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_attachment_amount,
|
||||
_amount_minus,
|
||||
_amount_plus,
|
||||
_attachment_choice,
|
||||
_compose_cancel,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_amount_plus,
|
||||
_attachment_amount,
|
||||
_amount_plus,
|
||||
_attachment_choice,
|
||||
_send_button,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_compose_cancel,
|
||||
_salutation,
|
||||
_send_button,
|
||||
amount_left,
|
||||
_compose_cancel,
|
||||
)
|
||||
_set_compose_neighbors(
|
||||
_send_button,
|
||||
_compose_cancel,
|
||||
_send_button,
|
||||
amount_right,
|
||||
_send_button,
|
||||
)
|
||||
|
||||
|
||||
func _set_compose_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
if control == null or not control.is_visible_in_tree():
|
||||
return
|
||||
var safe_left: Control = left if _controller_focus_eligible(left) else control
|
||||
var safe_right: Control = right if _controller_focus_eligible(right) else control
|
||||
var safe_top: Control = top if _controller_focus_eligible(top) else control
|
||||
var safe_bottom: Control = (
|
||||
bottom if _controller_focus_eligible(bottom) else control
|
||||
)
|
||||
control.focus_neighbor_left = control.get_path_to(safe_left)
|
||||
control.focus_neighbor_right = control.get_path_to(safe_right)
|
||||
control.focus_neighbor_top = control.get_path_to(safe_top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(safe_bottom)
|
||||
|
||||
|
||||
func _controller_focus_eligible(control: Control) -> bool:
|
||||
if (
|
||||
control == null
|
||||
or not control.is_visible_in_tree()
|
||||
or control.focus_mode == Control.FOCUS_NONE
|
||||
):
|
||||
return false
|
||||
var button := control as BaseButton
|
||||
return button == null or not button.disabled
|
||||
|
||||
|
||||
func _collect_visible_controller_controls(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var control := child as Control
|
||||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if (
|
||||
control != null
|
||||
and (control is BaseButton or control is LineEdit or control is TextEdit)
|
||||
):
|
||||
output.append(control)
|
||||
_collect_visible_controller_controls(child, output)
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
|
|
@ -144,22 +404,22 @@ func _build_inbox() -> Control:
|
|||
title.size = Vector2(500, 42)
|
||||
title.add_theme_font_size_override("font_size", 30)
|
||||
page.add_child(title)
|
||||
var send := Button.new()
|
||||
send.text = "send mail"
|
||||
send.position = Vector2(820, 0)
|
||||
send.size = Vector2(150, 48)
|
||||
send.pressed.connect(_show_compose)
|
||||
page.add_child(send)
|
||||
var archive_view := Button.new()
|
||||
archive_view.text = "archive"
|
||||
archive_view.position = Vector2(654, 0)
|
||||
archive_view.size = Vector2(154, 48)
|
||||
archive_view.pressed.connect(func() -> void:
|
||||
_send_mail_button = Button.new()
|
||||
_send_mail_button.text = "send mail"
|
||||
_send_mail_button.position = Vector2(820, 0)
|
||||
_send_mail_button.size = Vector2(150, 48)
|
||||
_send_mail_button.pressed.connect(_show_compose)
|
||||
page.add_child(_send_mail_button)
|
||||
_archive_view_button = Button.new()
|
||||
_archive_view_button.text = "archive"
|
||||
_archive_view_button.position = Vector2(654, 0)
|
||||
_archive_view_button.size = Vector2(154, 48)
|
||||
_archive_view_button.pressed.connect(func() -> void:
|
||||
_showing_archive = not _showing_archive
|
||||
archive_view.text = "inbox" if _showing_archive else "archive"
|
||||
_archive_view_button.text = "inbox" if _showing_archive else "archive"
|
||||
_refresh_inbox()
|
||||
)
|
||||
page.add_child(archive_view)
|
||||
page.add_child(_archive_view_button)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.position = Vector2(12, 62)
|
||||
scroll.size = Vector2(1036, 334)
|
||||
|
|
@ -281,12 +541,12 @@ func _build_compose() -> Control:
|
|||
_attachment_summary.scroll_active = false
|
||||
_attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
page.add_child(_attachment_summary)
|
||||
var cancel := Button.new()
|
||||
cancel.text = "cancel"
|
||||
cancel.position = Vector2(688, 342)
|
||||
cancel.size = Vector2(126, 48)
|
||||
cancel.pressed.connect(_show_inbox)
|
||||
page.add_child(cancel)
|
||||
_compose_cancel = Button.new()
|
||||
_compose_cancel.text = "cancel"
|
||||
_compose_cancel.position = Vector2(688, 342)
|
||||
_compose_cancel.size = Vector2(126, 48)
|
||||
_compose_cancel.pressed.connect(_show_inbox)
|
||||
page.add_child(_compose_cancel)
|
||||
_send_button = Button.new()
|
||||
_send_button.text = "send letter"
|
||||
_send_button.position = Vector2(826, 342)
|
||||
|
|
@ -333,12 +593,12 @@ func _build_letter() -> Control:
|
|||
_service.decline_gift(_current_mail_id)
|
||||
)
|
||||
page.add_child(_decline)
|
||||
var close := Button.new()
|
||||
close.text = "close"
|
||||
close.position = Vector2(26, 370)
|
||||
close.size = Vector2(150, 48)
|
||||
close.pressed.connect(_show_inbox)
|
||||
page.add_child(close)
|
||||
_letter_close = Button.new()
|
||||
_letter_close.text = "close"
|
||||
_letter_close.position = Vector2(26, 370)
|
||||
_letter_close.size = Vector2(150, 48)
|
||||
_letter_close.pressed.connect(_show_inbox)
|
||||
page.add_child(_letter_close)
|
||||
_archive = Button.new()
|
||||
_archive.text = "archive"
|
||||
_archive.position = Vector2(550, 370)
|
||||
|
|
@ -360,6 +620,7 @@ func _show_inbox() -> void:
|
|||
_letter.hide()
|
||||
_current_mail_id = ""
|
||||
_refresh_inbox()
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _show_compose() -> void:
|
||||
|
|
@ -370,7 +631,7 @@ func _show_compose() -> void:
|
|||
_signature.text = _service.get_local_display_name()
|
||||
_refresh_recipients()
|
||||
_update_send_state()
|
||||
_greeting.grab_focus()
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _reset_compose() -> void:
|
||||
|
|
@ -427,7 +688,8 @@ func _open_letter(mail_id: String) -> void:
|
|||
"Resolve this gift before deleting the letter." if pending else ""
|
||||
)
|
||||
if pending:
|
||||
_accept.grab_focus()
|
||||
_accept.call_deferred("grab_focus")
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _refresh_inbox() -> void:
|
||||
|
|
@ -444,6 +706,7 @@ func _refresh_inbox() -> void:
|
|||
empty.custom_minimum_size = Vector2(1008, 80)
|
||||
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_inbox_list.add_child(empty)
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
return
|
||||
for letter: Dictionary in letters:
|
||||
var button := Button.new()
|
||||
|
|
@ -473,6 +736,7 @@ func _refresh_inbox() -> void:
|
|||
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
_inbox_list.add_child(button)
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _refresh_recipients() -> void:
|
||||
|
|
@ -556,6 +820,7 @@ func _refresh_attachment_choices(_index: int) -> void:
|
|||
)
|
||||
_update_attachment_amount_limit()
|
||||
_update_attachment_summary()
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _update_attachment_amount_limit() -> void:
|
||||
|
|
@ -616,6 +881,7 @@ func _update_send_state() -> void:
|
|||
or not _reservations.validate_attachment(attachment)
|
||||
or not _attachment_is_available(attachment)
|
||||
)
|
||||
call_deferred("_refresh_controller_navigation")
|
||||
|
||||
|
||||
func _attachment_is_available(attachment: Dictionary) -> bool:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ enum Mode {
|
|||
}
|
||||
|
||||
const DISCOVERY_REFRESH_SECONDS: float = 8.0
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
const ADDRESS_FORMAT_HELP: String = (
|
||||
"Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; "
|
||||
|
|
@ -189,6 +192,8 @@ func open_page(preserved_endpoint: String = "") -> void:
|
|||
if _mode == Mode.DIRECT:
|
||||
_address.grab_focus()
|
||||
_address.select_all()
|
||||
else:
|
||||
_server_list.grab_focus()
|
||||
|
||||
|
||||
func close_page() -> void:
|
||||
|
|
@ -196,6 +201,10 @@ func close_page() -> void:
|
|||
hide_for_join_confirmation()
|
||||
|
||||
|
||||
func request_back() -> void:
|
||||
_on_back_pressed()
|
||||
|
||||
|
||||
func hide_for_join_confirmation() -> void:
|
||||
_clear_edit_state()
|
||||
_discovery_refresh_timer.stop()
|
||||
|
|
@ -649,6 +658,136 @@ func _refresh() -> void:
|
|||
)
|
||||
if not warning.is_empty() and _status.text.is_empty():
|
||||
_status.text = warning
|
||||
_configure_controller_navigation()
|
||||
|
||||
|
||||
func _configure_controller_navigation() -> void:
|
||||
var mode_buttons: Array[Control] = [
|
||||
_discover_button,
|
||||
_direct_button,
|
||||
_saved_button,
|
||||
_recent_button,
|
||||
]
|
||||
var content_controls: Array[Control] = []
|
||||
for control: Control in [_address, _name_edit, _server_list]:
|
||||
if _controller_focus_eligible(control):
|
||||
content_controls.append(control)
|
||||
var action_controls: Array[Control] = []
|
||||
for control: Control in [
|
||||
_refresh_button,
|
||||
_join_button,
|
||||
_save_button,
|
||||
_edit_button,
|
||||
_favorite_button,
|
||||
_delete_button,
|
||||
_cancel_button,
|
||||
_back_button,
|
||||
]:
|
||||
if _controller_focus_eligible(control):
|
||||
action_controls.append(control)
|
||||
var all_controls: Array[Control] = []
|
||||
all_controls.append_array(mode_buttons)
|
||||
all_controls.append_array(content_controls)
|
||||
all_controls.append_array(action_controls)
|
||||
for control: Control in all_controls:
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
for control: Control in [
|
||||
_address,
|
||||
_name_edit,
|
||||
_server_list,
|
||||
_refresh_button,
|
||||
_join_button,
|
||||
_save_button,
|
||||
_edit_button,
|
||||
_favorite_button,
|
||||
_delete_button,
|
||||
_cancel_button,
|
||||
_back_button,
|
||||
]:
|
||||
if control not in all_controls:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
var primary_content: Control = (
|
||||
content_controls.front()
|
||||
if not content_controls.is_empty()
|
||||
else action_controls.front()
|
||||
if not action_controls.is_empty()
|
||||
else _back_button
|
||||
)
|
||||
for index: int in mode_buttons.size():
|
||||
var mode_button: Control = mode_buttons[index]
|
||||
_set_controller_neighbors(
|
||||
mode_button,
|
||||
mode_buttons[maxi(index - 1, 0)],
|
||||
mode_buttons[mini(index + 1, mode_buttons.size() - 1)],
|
||||
mode_button,
|
||||
primary_content,
|
||||
)
|
||||
for index: int in content_controls.size():
|
||||
var content: Control = content_controls[index]
|
||||
var above: Control = (
|
||||
content_controls[index - 1]
|
||||
if index > 0
|
||||
else mode_buttons[int(_mode)]
|
||||
)
|
||||
var below: Control = (
|
||||
content_controls[index + 1]
|
||||
if index < content_controls.size() - 1
|
||||
else action_controls.front()
|
||||
if not action_controls.is_empty()
|
||||
else content
|
||||
)
|
||||
_set_controller_neighbors(content, content, content, above, below)
|
||||
for index: int in action_controls.size():
|
||||
var action: Control = action_controls[index]
|
||||
_set_controller_neighbors(
|
||||
action,
|
||||
action_controls[maxi(index - 1, 0)],
|
||||
action_controls[mini(index + 1, action_controls.size() - 1)],
|
||||
content_controls.back()
|
||||
if not content_controls.is_empty()
|
||||
else mode_buttons[int(_mode)],
|
||||
action,
|
||||
)
|
||||
ControllerFocusNavigationType.configure_traversal(all_controls)
|
||||
_recover_controller_focus(all_controls, primary_content)
|
||||
|
||||
|
||||
func _controller_focus_eligible(control: Control) -> bool:
|
||||
if control == null or not control.is_visible_in_tree():
|
||||
return false
|
||||
var button := control as BaseButton
|
||||
if button != null and button.disabled:
|
||||
return false
|
||||
var line_edit := control as LineEdit
|
||||
return line_edit == null or line_edit.editable
|
||||
|
||||
|
||||
func _set_controller_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
|
||||
|
||||
func _recover_controller_focus(
|
||||
controls: Array[Control],
|
||||
fallback: Control,
|
||||
) -> void:
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
var owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if owner != null and owner in controls:
|
||||
return
|
||||
if owner != null and _delete_confirmation.is_ancestor_of(owner):
|
||||
return
|
||||
if fallback != null:
|
||||
fallback.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _format_entry_details(entry: SavedServerEntry) -> String:
|
||||
|
|
@ -832,9 +971,14 @@ func _on_cancel_pressed() -> void:
|
|||
|
||||
|
||||
func _on_back_pressed() -> void:
|
||||
if _name_entry_active or _delete_armed:
|
||||
if _delete_armed or _delete_confirmation.visible:
|
||||
_cancel_delete()
|
||||
return
|
||||
if _name_entry_active:
|
||||
_clear_edit_state()
|
||||
_refresh()
|
||||
var content: Control = _address if _mode == Mode.DIRECT else _server_list
|
||||
content.call_deferred("grab_focus")
|
||||
return
|
||||
if (
|
||||
_network_session != null
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const CHECK_ICON: Texture2D = preload(
|
|||
)
|
||||
const CHARACTER_KEY_SIZE: Vector2 = Vector2(96.0, 72.0)
|
||||
const CHARACTER_FONT_SIZE: int = 34
|
||||
const CANONICAL_WINDOW_SIZE: Vector2 = Vector2(1280.0, 720.0)
|
||||
const TRIGGER_PRESS_THRESHOLD: float = 0.55
|
||||
const TRIGGER_RELEASE_THRESHOLD: float = 0.25
|
||||
|
||||
|
|
@ -36,6 +37,11 @@ var _space_button: Button
|
|||
var _check_button: Button
|
||||
var _left_trigger_pressed: bool = false
|
||||
var _right_trigger_pressed: bool = false
|
||||
var _portable_host_parent: Node
|
||||
var _portable_host_index: int = -1
|
||||
var _portable_target_window: Window
|
||||
var _portable_target_window_size: Vector2i
|
||||
var _portable_target_window_position: Vector2i
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -63,12 +69,17 @@ func is_open() -> bool:
|
|||
|
||||
|
||||
func request_for_focused_control() -> bool:
|
||||
if not _is_available_for_controller() or visible:
|
||||
return request_for_control(get_viewport().gui_get_focus_owner())
|
||||
|
||||
|
||||
func request_for_control(control: Control = null) -> bool:
|
||||
if (
|
||||
not _is_available_for_controller()
|
||||
or visible
|
||||
or not _can_edit(control)
|
||||
):
|
||||
return false
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if not _can_edit(focus_owner):
|
||||
return false
|
||||
_open_for(focus_owner)
|
||||
_open_for(control)
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -145,6 +156,7 @@ func _open_for(control: Control) -> void:
|
|||
_target.set("virtual_keyboard_enabled", false)
|
||||
_buffer = str(_target.get("text"))
|
||||
_page = Page.LOWER
|
||||
_attach_to_target_window(control)
|
||||
show()
|
||||
_refresh_preview()
|
||||
_rebuild_keys()
|
||||
|
|
@ -159,11 +171,62 @@ func _close_keyboard(restore_focus: bool) -> void:
|
|||
)
|
||||
hide()
|
||||
get_viewport().gui_release_focus()
|
||||
_restore_portable_host()
|
||||
_target = null
|
||||
if restore_focus and is_instance_valid(prior_target):
|
||||
prior_target.grab_focus()
|
||||
|
||||
|
||||
func _attach_to_target_window(control: Control) -> void:
|
||||
var target_window: Window = control.get_window()
|
||||
if target_window == null or target_window == get_window():
|
||||
return
|
||||
_portable_host_parent = get_parent()
|
||||
_portable_host_index = get_index()
|
||||
_portable_target_window = target_window
|
||||
_portable_target_window_size = target_window.size
|
||||
_portable_target_window_position = target_window.position
|
||||
var parent_window := target_window.get_parent() as Window
|
||||
if parent_window != null:
|
||||
target_window.size = parent_window.size
|
||||
target_window.position = parent_window.position
|
||||
reparent(target_window, false)
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
size = CANONICAL_WINDOW_SIZE
|
||||
var available: Vector2 = Vector2(target_window.size)
|
||||
var fit_scale: float = minf(
|
||||
available.x / CANONICAL_WINDOW_SIZE.x,
|
||||
available.y / CANONICAL_WINDOW_SIZE.y,
|
||||
)
|
||||
scale = Vector2.ONE * maxf(fit_scale, 0.01)
|
||||
position = (available - CANONICAL_WINDOW_SIZE * fit_scale) * 0.5
|
||||
|
||||
|
||||
func _restore_portable_host() -> void:
|
||||
if _portable_host_parent == null:
|
||||
return
|
||||
var original_parent: Node = _portable_host_parent
|
||||
var original_index: int = _portable_host_index
|
||||
var target_window: Window = _portable_target_window
|
||||
_portable_host_parent = null
|
||||
_portable_host_index = -1
|
||||
_portable_target_window = null
|
||||
reparent(original_parent, false)
|
||||
if original_index >= 0:
|
||||
original_parent.move_child(
|
||||
self,
|
||||
mini(original_index, original_parent.get_child_count() - 1),
|
||||
)
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
scale = Vector2.ONE
|
||||
if target_window != null and is_instance_valid(target_window):
|
||||
target_window.size = _portable_target_window_size
|
||||
target_window.position = _portable_target_window_position
|
||||
|
||||
|
||||
func _submit() -> void:
|
||||
var submitted_target: Control = _target
|
||||
var submitted_text: String = _buffer
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ func handle_escape() -> bool:
|
|||
if _confirmation_page.visible:
|
||||
_close_confirmation()
|
||||
elif _join_game_page.visible:
|
||||
_close_join_game()
|
||||
_join_game_page.request_back()
|
||||
elif _settings_panel.visible:
|
||||
_settings_panel.handle_back()
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ const PlayerExperienceType = preload(
|
|||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const FishBatchSelectionType = preload(
|
||||
"res://ui/fish_batch_selection.gd"
|
||||
)
|
||||
|
|
@ -131,10 +134,13 @@ enum CloseReason {
|
|||
}
|
||||
|
||||
enum ControllerOwnership {
|
||||
INVENTORY_TABS,
|
||||
ITEM_LIST,
|
||||
NOTEPAD_ACTIONS,
|
||||
SORT_FILTER,
|
||||
HOTBAR_MANAGEMENT,
|
||||
HOTBAR_PLACEMENT,
|
||||
PAGE_CONTENT,
|
||||
}
|
||||
|
||||
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
||||
|
|
@ -149,6 +155,7 @@ const INVENTORY_MAIN_CORNER_RADIUS: int = 58
|
|||
const INVENTORY_INNER_CORNER_RADIUS: int = 45
|
||||
const TACKLE_GRID_COLUMNS: int = 3
|
||||
const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
|
||||
const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42
|
||||
|
||||
@onready var _navigation_cluster: BubbleClusterType = %NavigationCluster
|
||||
@onready var _presentation_scale_root: Control = %PlayerMenuPresentationScaleRoot
|
||||
|
|
@ -308,7 +315,9 @@ var _last_inventory_section: Section = Section.COOLER
|
|||
var _bag_view: BagView = BagView.EQUIPMENT
|
||||
var _selected_tackle_item_id: StringName
|
||||
var _tackle_item_buttons: Dictionary[StringName, Button] = {}
|
||||
var _controller_ownership: ControllerOwnership = ControllerOwnership.ITEM_LIST
|
||||
var _controller_ownership: ControllerOwnership = (
|
||||
ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
var _controller_source_section: Section = Section.COOLER
|
||||
var _controller_source_identity: StringName
|
||||
var _controller_notepad_actions: Array[BaseButton] = []
|
||||
|
|
@ -317,6 +326,8 @@ var _controller_hotbar_assignment_kind: PlayerHotbarType.AssignmentKind = (
|
|||
)
|
||||
var _controller_hotbar_identity: StringName
|
||||
var _controller_previous_hotbar_slot: int = 0
|
||||
var _controller_accept_held: bool = false
|
||||
var _controller_accept_hold_elapsed: float = 0.0
|
||||
var _sort_mode: SortMode = SortMode.CATCH_ORDER
|
||||
var _sort_descending: bool = true
|
||||
var _fish_selection := FishBatchSelectionType.new()
|
||||
|
|
@ -352,8 +363,10 @@ var _page_incoming_root: Control
|
|||
var _page_outgoing_content_root: Control
|
||||
var _inventory_transition_group: Control
|
||||
var _fish_nodes: Dictionary[StringName, CoolerFishSpriteType] = {}
|
||||
var _cooler_slot_nodes: Array[Panel] = []
|
||||
var _sorted_catches: Array[FishCatchType] = []
|
||||
var _bag_item_nodes: Dictionary[StringName, BagItemSpriteType] = {}
|
||||
var _bag_slot_nodes: Array[Panel] = []
|
||||
var _sorted_bag_items: Array[OwnedItemType] = []
|
||||
var _bag_drag_active: bool = false
|
||||
var _motion_elapsed: float = 0.0
|
||||
|
|
@ -594,16 +607,13 @@ func setup_controller_mapping(
|
|||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
_profile_page.setup_controller_mapping(_controller_mapping_manager)
|
||||
_the_net_page.setup_controller_mapping(_controller_mapping_manager)
|
||||
|
||||
|
||||
func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
|
||||
_profile_page.set_world_pixel_size(pixel_size)
|
||||
|
||||
|
||||
var _left_page_trigger_held: bool = false
|
||||
var _right_page_trigger_held: bool = false
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.echo:
|
||||
return
|
||||
|
|
@ -616,9 +626,6 @@ func _input(event: InputEvent) -> void:
|
|||
if _handle_controller_page_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_controller_secondary_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_direct_page_shortcut(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -640,16 +647,21 @@ func _input(event: InputEvent) -> void:
|
|||
func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
if not _is_inventory_section(_current_section):
|
||||
return _handle_active_page_controller_input(event)
|
||||
var button_event := event as InputEventJoypadButton
|
||||
var accept_pressed: bool = (
|
||||
var accept_event: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
)
|
||||
)
|
||||
var accept_pressed: bool = (
|
||||
accept_event
|
||||
and button_event.pressed
|
||||
)
|
||||
var cancel_pressed: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
|
|
@ -659,23 +671,24 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
JOY_BUTTON_B,
|
||||
)
|
||||
)
|
||||
var alternate_pressed: bool = (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_Y,
|
||||
JOY_BUTTON_Y,
|
||||
)
|
||||
)
|
||||
if _controller_ownership == ControllerOwnership.INVENTORY_TABS:
|
||||
if cancel_pressed:
|
||||
close_menu()
|
||||
return true
|
||||
if accept_pressed:
|
||||
_enter_inventory_content_zone()
|
||||
return true
|
||||
if event.is_action_pressed("ui_left"):
|
||||
_switch_inventory_tab_direction(-1)
|
||||
return true
|
||||
if event.is_action_pressed("ui_right"):
|
||||
_switch_inventory_tab_direction(1)
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
if alternate_pressed:
|
||||
if _hotbar != null:
|
||||
_hotbar.clear_slot(_hotbar.get_selected_slot())
|
||||
return true
|
||||
if accept_pressed:
|
||||
return true
|
||||
return false
|
||||
|
|
@ -686,34 +699,90 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
if cancel_pressed:
|
||||
_release_controller_ownership(true, true)
|
||||
return true
|
||||
if alternate_pressed:
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
if alternate_pressed:
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.SORT_FILTER:
|
||||
if cancel_pressed:
|
||||
_enter_inventory_content_zone()
|
||||
return true
|
||||
var direction: int = 0
|
||||
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
|
||||
direction = -1
|
||||
elif (
|
||||
event.is_action_pressed("ui_right")
|
||||
or event.is_action_pressed("ui_down")
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.ITEM_LIST:
|
||||
if cancel_pressed:
|
||||
_cancel_controller_accept_hold()
|
||||
_enter_inventory_tabs_zone()
|
||||
return true
|
||||
if _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_SELECT,
|
||||
JOY_BUTTON_BACK,
|
||||
):
|
||||
direction = 1
|
||||
if direction != 0:
|
||||
_focus_next_notepad_action(direction)
|
||||
if button_event != null and button_event.pressed:
|
||||
if _current_section == Section.COOLER:
|
||||
_enter_inventory_sort_zone()
|
||||
return true
|
||||
if accept_event:
|
||||
if button_event.pressed:
|
||||
_controller_accept_held = true
|
||||
_controller_accept_hold_elapsed = 0.0
|
||||
else:
|
||||
var was_pending: bool = _controller_accept_held
|
||||
_cancel_controller_accept_hold()
|
||||
if was_pending:
|
||||
_activate_inventory_selection()
|
||||
return true
|
||||
return false
|
||||
if alternate_pressed:
|
||||
return _try_begin_controller_hotbar_placement()
|
||||
if accept_pressed:
|
||||
return _try_enter_notepad_controller_ownership()
|
||||
|
||||
|
||||
func _handle_active_page_controller_input(event: InputEvent) -> bool:
|
||||
var handled: bool = false
|
||||
match _current_section:
|
||||
Section.LOGBOOK:
|
||||
handled = _catalog_logbook.handle_controller_input(event)
|
||||
Section.NET:
|
||||
handled = _the_net_page.handle_controller_input(event)
|
||||
Section.MAIL:
|
||||
handled = _mail_page.handle_controller_input(event)
|
||||
Section.PROFILE:
|
||||
handled = _profile_page.handle_controller_input(event)
|
||||
Section.PLAYERS:
|
||||
handled = _players_page.handle_controller_input(event)
|
||||
if handled:
|
||||
return true
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if (
|
||||
button_event != null
|
||||
and button_event.pressed
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
JOY_BUTTON_B,
|
||||
)
|
||||
):
|
||||
close_menu()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _activate_inventory_selection() -> void:
|
||||
if _try_enter_notepad_controller_ownership():
|
||||
return
|
||||
if _current_section != Section.BAG:
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node != null and not item_node.item_id.is_empty():
|
||||
_select_bag_item(item_node.item_id)
|
||||
|
||||
|
||||
func _cancel_controller_accept_hold() -> void:
|
||||
_controller_accept_held = false
|
||||
_controller_accept_hold_elapsed = 0.0
|
||||
|
||||
|
||||
func _event_matches_controller_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
|
|
@ -761,35 +830,11 @@ func _try_enter_notepad_controller_ownership() -> bool:
|
|||
_controller_source_identity = source_identity
|
||||
_controller_notepad_actions = actions
|
||||
_controller_ownership = ControllerOwnership.NOTEPAD_ACTIONS
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(actions)
|
||||
actions.front().call_deferred("grab_focus")
|
||||
return true
|
||||
|
||||
|
||||
func _focus_next_notepad_action(direction: int) -> void:
|
||||
var available: Array[BaseButton] = []
|
||||
for action: BaseButton in _controller_notepad_actions:
|
||||
if (
|
||||
is_instance_valid(action)
|
||||
and action.visible
|
||||
and not action.disabled
|
||||
and action.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
available.append(action)
|
||||
if available.is_empty():
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
var current_index: int = available.find(focused)
|
||||
if current_index < 0:
|
||||
current_index = 0 if direction > 0 else available.size() - 1
|
||||
else:
|
||||
current_index = wrapi(
|
||||
current_index + direction,
|
||||
0,
|
||||
available.size(),
|
||||
)
|
||||
available[current_index].grab_focus()
|
||||
|
||||
|
||||
func _try_begin_controller_hotbar_placement() -> bool:
|
||||
if _hotbar == null:
|
||||
return false
|
||||
|
|
@ -946,88 +991,6 @@ func _restore_controller_item_focus(
|
|||
|
||||
|
||||
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||
var use_mapping: bool = (
|
||||
_controller_mapping_manager != null
|
||||
)
|
||||
var uses_left_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
|
||||
)
|
||||
)
|
||||
var uses_right_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
||||
)
|
||||
)
|
||||
if (
|
||||
not (uses_left_trigger or uses_right_trigger)
|
||||
or not visible
|
||||
):
|
||||
return false
|
||||
var is_pressed: bool = (
|
||||
button_event.pressed
|
||||
if button_event != null
|
||||
else motion_event.axis_value > 0.5
|
||||
)
|
||||
if not is_pressed:
|
||||
if uses_left_trigger:
|
||||
_left_page_trigger_held = false
|
||||
if uses_right_trigger:
|
||||
_right_page_trigger_held = false
|
||||
return true
|
||||
if (
|
||||
(uses_left_trigger and _left_page_trigger_held)
|
||||
or (uses_right_trigger and _right_page_trigger_held)
|
||||
):
|
||||
return true
|
||||
if uses_left_trigger:
|
||||
_left_page_trigger_held = true
|
||||
if uses_right_trigger:
|
||||
_right_page_trigger_held = true
|
||||
# Trigger input belongs to the Player Menu while it is visible, even when a
|
||||
# transition or modal temporarily prevents changing pages.
|
||||
if (
|
||||
_transitioning
|
||||
or _page_transitioning
|
||||
or _sale_confirmation.visible
|
||||
or get_viewport().gui_is_dragging()
|
||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||
):
|
||||
return true
|
||||
var sections: Array[Section] = [
|
||||
_last_inventory_section,
|
||||
Section.LOGBOOK,
|
||||
Section.NET,
|
||||
Section.MAIL,
|
||||
Section.PROFILE,
|
||||
Section.PLAYERS,
|
||||
]
|
||||
var current_index: int = 0
|
||||
if not _is_inventory_section(_current_section):
|
||||
current_index = sections.find(_current_section)
|
||||
if current_index < 0:
|
||||
current_index = 0
|
||||
var direction: int = (
|
||||
-1 if uses_left_trigger else 1
|
||||
)
|
||||
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||
_show_section(sections[next_index])
|
||||
return true
|
||||
|
||||
|
||||
func _handle_controller_secondary_switch(event: InputEvent) -> bool:
|
||||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null or not visible:
|
||||
return false
|
||||
|
|
@ -1050,57 +1013,38 @@ func _handle_controller_secondary_switch(event: InputEvent) -> bool:
|
|||
return false
|
||||
if not button_event.pressed:
|
||||
return true
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
||||
_cycle_from_controller_hotbar_management(direction)
|
||||
return true
|
||||
if (
|
||||
_transitioning
|
||||
or _page_transitioning
|
||||
or _sale_confirmation.visible
|
||||
or get_viewport().gui_is_dragging()
|
||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||
):
|
||||
return true
|
||||
if _is_inventory_section(_current_section):
|
||||
var inventory_index: int = _get_inventory_tab_index()
|
||||
if (
|
||||
(direction > 0 and inventory_index == 3)
|
||||
or (direction < 0 and inventory_index == 0)
|
||||
):
|
||||
_begin_controller_hotbar_management()
|
||||
var sections: Array[Section] = [
|
||||
_last_inventory_section,
|
||||
Section.LOGBOOK,
|
||||
Section.NET,
|
||||
Section.MAIL,
|
||||
Section.PROFILE,
|
||||
Section.PLAYERS,
|
||||
]
|
||||
var current_index: int = 0
|
||||
if not _is_inventory_section(_current_section):
|
||||
current_index = sections.find(_current_section)
|
||||
if current_index < 0:
|
||||
current_index = 0
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
var next_index: int = clampi(
|
||||
current_index + direction,
|
||||
0,
|
||||
sections.size() - 1,
|
||||
)
|
||||
if next_index == current_index:
|
||||
return true
|
||||
_show_inventory_tab(inventory_index + direction)
|
||||
_cancel_controller_accept_hold()
|
||||
_release_controller_ownership(false, true)
|
||||
_show_section(sections[next_index])
|
||||
return true
|
||||
_cycle_visible_secondary_tabs(direction)
|
||||
return true
|
||||
|
||||
|
||||
func _begin_controller_hotbar_management() -> void:
|
||||
if _hotbar == null:
|
||||
return
|
||||
_controller_source_section = _current_section
|
||||
_controller_source_identity = StringName()
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if _current_section == Section.COOLER:
|
||||
var fish_node := focus_owner as CoolerFishSpriteType
|
||||
if fish_node != null:
|
||||
_controller_source_identity = fish_node.catch_id
|
||||
elif _current_section == Section.BAG:
|
||||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node != null:
|
||||
_controller_source_identity = item_node.item_id
|
||||
_controller_ownership = ControllerOwnership.HOTBAR_MANAGEMENT
|
||||
controller_hotbar_management_requested.emit(_hotbar.get_selected_slot())
|
||||
|
||||
|
||||
func _cycle_from_controller_hotbar_management(direction: int) -> void:
|
||||
_release_controller_ownership(false, false)
|
||||
var target_index: int = 0 if direction > 0 else 3
|
||||
if target_index == _get_inventory_tab_index():
|
||||
call_deferred("_focus_current_section")
|
||||
else:
|
||||
_show_inventory_tab(target_index)
|
||||
|
||||
|
||||
func _get_inventory_tab_index() -> int:
|
||||
|
|
@ -1123,41 +1067,69 @@ func _show_inventory_tab(index: int) -> void:
|
|||
_show_section(Section.TACKLE_BOX)
|
||||
|
||||
|
||||
func _cycle_visible_secondary_tabs(direction: int) -> bool:
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var grouped_buttons: Dictionary = {}
|
||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
||||
var selected_group: Variant = null
|
||||
var selected_group_y: float = INF
|
||||
for group_value: Variant in grouped_buttons.keys():
|
||||
var buttons := grouped_buttons[group_value] as Array
|
||||
if buttons.size() < 2:
|
||||
continue
|
||||
var group_y: float = INF
|
||||
for item: Variant in buttons:
|
||||
var button := item as BaseButton
|
||||
group_y = minf(group_y, button.global_position.y)
|
||||
if group_y < selected_group_y:
|
||||
selected_group = group_value
|
||||
selected_group_y = group_y
|
||||
if selected_group == null:
|
||||
return false
|
||||
var tabs := grouped_buttons[selected_group] as Array
|
||||
tabs.sort_custom(func(first: BaseButton, second: BaseButton) -> bool:
|
||||
return first.global_position.x < second.global_position.x
|
||||
func _switch_inventory_tab_direction(direction: int) -> void:
|
||||
var current_index: int = _get_inventory_tab_index()
|
||||
var target_index: int = clampi(current_index + direction, 0, 3)
|
||||
if target_index == current_index:
|
||||
return
|
||||
_show_inventory_tab(target_index)
|
||||
|
||||
|
||||
func _enter_inventory_tabs_zone() -> void:
|
||||
if not _is_inventory_section(_current_section):
|
||||
return
|
||||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.INVENTORY_TABS
|
||||
var active_tab: Button = _inventory_tab_for_section(_current_section)
|
||||
if (
|
||||
active_tab != null
|
||||
and active_tab.is_visible_in_tree()
|
||||
and active_tab.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
active_tab.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _enter_inventory_content_zone() -> void:
|
||||
if not _is_inventory_section(_current_section):
|
||||
return
|
||||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.ITEM_LIST
|
||||
call_deferred("_focus_current_section")
|
||||
|
||||
|
||||
func _enter_inventory_sort_zone() -> void:
|
||||
if _current_section != Section.COOLER:
|
||||
return
|
||||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.SORT_FILTER
|
||||
_controller_source_section = _current_section
|
||||
var sort_controls: Array[Control] = [
|
||||
_cooler_sort_option,
|
||||
_cooler_sort_direction,
|
||||
]
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
sort_controls
|
||||
)
|
||||
var current_index: int = 0
|
||||
for index: int in tabs.size():
|
||||
var tab := tabs[index] as BaseButton
|
||||
if tab.button_pressed:
|
||||
current_index = index
|
||||
break
|
||||
var target := tabs[wrapi(
|
||||
current_index + direction, 0, tabs.size()
|
||||
)] as BaseButton
|
||||
target.set_pressed_no_signal(true)
|
||||
target.pressed.emit()
|
||||
return true
|
||||
_cooler_sort_option.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _reset_controller_zone_for_section() -> void:
|
||||
_cancel_controller_accept_hold()
|
||||
if _is_inventory_section(_current_section):
|
||||
_enter_inventory_tabs_zone()
|
||||
return
|
||||
_controller_ownership = ControllerOwnership.PAGE_CONTENT
|
||||
match _current_section:
|
||||
Section.LOGBOOK:
|
||||
_catalog_logbook.reset_controller_zone()
|
||||
Section.NET:
|
||||
_the_net_page.reset_controller_zone()
|
||||
Section.MAIL:
|
||||
_mail_page.reset_controller_zone()
|
||||
Section.PROFILE:
|
||||
_profile_page.reset_controller_zone()
|
||||
Section.PLAYERS:
|
||||
_players_page.reset_controller_zone()
|
||||
|
||||
|
||||
func _collect_visible_toggle_buttons(
|
||||
|
|
@ -1659,6 +1631,7 @@ func _focus_current_section() -> void:
|
|||
_collect_focusable_content(self, navigation_cluster, candidates)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
|
||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
||||
return first.global_position.y < second.global_position.y
|
||||
|
|
@ -1679,8 +1652,7 @@ func _collect_focusable_content(
|
|||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if (
|
||||
control != null
|
||||
and control.focus_mode != Control.FOCUS_NONE
|
||||
ControllerFocusNavigationType.is_focusable(control)
|
||||
and not control is ScrollBar
|
||||
):
|
||||
output.append(control)
|
||||
|
|
@ -1689,6 +1661,19 @@ func _collect_focusable_content(
|
|||
|
||||
func _process(delta: float) -> void:
|
||||
if visible or _shop_cooler_context_active:
|
||||
if (
|
||||
visible
|
||||
and _controller_accept_held
|
||||
and _controller_ownership == ControllerOwnership.ITEM_LIST
|
||||
and _current_section in [Section.COOLER, Section.BAG]
|
||||
):
|
||||
_controller_accept_hold_elapsed += delta
|
||||
if (
|
||||
_controller_accept_hold_elapsed
|
||||
>= CONTROLLER_PICKUP_HOLD_SECONDS
|
||||
):
|
||||
_cancel_controller_accept_hold()
|
||||
_try_begin_controller_hotbar_placement()
|
||||
_motion_elapsed += delta
|
||||
if visible:
|
||||
_navigation_cluster.advance_motion(delta)
|
||||
|
|
@ -1815,10 +1800,10 @@ func _configure_sale_confirmation_focus() -> void:
|
|||
_confirm_sale_button.focus_neighbor_left
|
||||
)
|
||||
_confirm_sale_button.focus_neighbor_top = (
|
||||
_confirm_sale_button.focus_neighbor_left
|
||||
_confirm_sale_button.get_path_to(_confirm_sale_button)
|
||||
)
|
||||
_confirm_sale_button.focus_neighbor_bottom = (
|
||||
_confirm_sale_button.focus_neighbor_left
|
||||
_confirm_sale_button.focus_neighbor_top
|
||||
)
|
||||
_cancel_sale_button.focus_neighbor_left = (
|
||||
_cancel_sale_button.get_path_to(_confirm_sale_button)
|
||||
|
|
@ -1827,10 +1812,10 @@ func _configure_sale_confirmation_focus() -> void:
|
|||
_cancel_sale_button.focus_neighbor_left
|
||||
)
|
||||
_cancel_sale_button.focus_neighbor_top = (
|
||||
_cancel_sale_button.focus_neighbor_left
|
||||
_cancel_sale_button.get_path_to(_cancel_sale_button)
|
||||
)
|
||||
_cancel_sale_button.focus_neighbor_bottom = (
|
||||
_cancel_sale_button.focus_neighbor_left
|
||||
_cancel_sale_button.focus_neighbor_top
|
||||
)
|
||||
if not _sale_confirmation.visible:
|
||||
for button: Button in [_confirm_sale_button, _cancel_sale_button]:
|
||||
|
|
@ -1906,6 +1891,9 @@ func _show_bag_view(view: BagView) -> void:
|
|||
return
|
||||
if _bag_view == view:
|
||||
return
|
||||
var preserve_tab_zone: bool = (
|
||||
_controller_ownership == ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
_release_controller_ownership(false, true)
|
||||
_bag_view = view
|
||||
_selected_bag_item_id = StringName()
|
||||
|
|
@ -1915,6 +1903,9 @@ func _show_bag_view(view: BagView) -> void:
|
|||
_refresh_inventory_organizer_tabs()
|
||||
_reserve_visible_secondary_navigation()
|
||||
_set_content_interactive(true)
|
||||
if preserve_tab_zone:
|
||||
_enter_inventory_tabs_zone()
|
||||
else:
|
||||
call_deferred("_focus_current_section")
|
||||
|
||||
|
||||
|
|
@ -2058,7 +2049,7 @@ func _configure_tackle_column_focus(
|
|||
button.focus_neighbor_left = button.get_path_to(left_target)
|
||||
button.focus_neighbor_right = button.get_path_to(right_target)
|
||||
button.focus_neighbor_top = (
|
||||
button.get_path_to(_tackle_sub_tab)
|
||||
button.get_path_to(button)
|
||||
if index < TACKLE_GRID_COLUMNS
|
||||
else button.get_path_to(buttons[index - TACKLE_GRID_COLUMNS])
|
||||
)
|
||||
|
|
@ -2756,7 +2747,7 @@ func _finish_menu_entry(generation: int) -> void:
|
|||
_presentation_tween = null
|
||||
_transitioning = false
|
||||
_set_shell_interactive(true)
|
||||
_focus_current_section()
|
||||
_reset_controller_zone_for_section()
|
||||
|
||||
|
||||
func _begin_menu_exit(reason: CloseReason, restore_controls: bool) -> void:
|
||||
|
|
@ -2972,7 +2963,7 @@ func _finish_page_transition(generation: int) -> void:
|
|||
_inventory_sub_tabs.modulate.a = 1.0
|
||||
_reset_page_transition_visuals()
|
||||
_set_content_interactive(true)
|
||||
_focus_current_section()
|
||||
_reset_controller_zone_for_section()
|
||||
|
||||
|
||||
func _set_shell_interactive(interactive: bool) -> void:
|
||||
|
|
@ -3049,6 +3040,9 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
_profile_page.set_interactive(
|
||||
interactive and _current_section == Section.PROFILE
|
||||
)
|
||||
_players_page.set_interactive(
|
||||
interactive and _current_section == Section.PLAYERS
|
||||
)
|
||||
var cooler_interactive: bool = (
|
||||
interactive and _current_section == Section.COOLER
|
||||
)
|
||||
|
|
@ -3303,7 +3297,7 @@ func _sync_bag_item_nodes(owned_items: Array[OwnedItemType]) -> void:
|
|||
|
||||
|
||||
func _layout_bag_items() -> void:
|
||||
if not is_node_ready() or _sorted_bag_items.is_empty():
|
||||
if not is_node_ready():
|
||||
return
|
||||
_bag_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var columns: int = 3
|
||||
|
|
@ -3327,6 +3321,16 @@ func _layout_bag_items() -> void:
|
|||
if _compact_layout
|
||||
else Vector2(820.0, 240.0)
|
||||
)
|
||||
_sync_inventory_slot_visuals(
|
||||
_bag_item_field,
|
||||
_bag_slot_nodes,
|
||||
maxi(9, _sorted_bag_items.size()),
|
||||
columns,
|
||||
cell_size,
|
||||
item_size,
|
||||
origin,
|
||||
18.0,
|
||||
)
|
||||
for index: int in _sorted_bag_items.size():
|
||||
var owned: OwnedItemType = _sorted_bag_items[index]
|
||||
var item_node := _bag_item_nodes.get(
|
||||
|
|
@ -3354,6 +3358,44 @@ func _layout_bag_items() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _sync_inventory_slot_visuals(
|
||||
parent: Control,
|
||||
slots: Array[Panel],
|
||||
slot_count: int,
|
||||
columns: int,
|
||||
cell_size: Vector2,
|
||||
slot_size: Vector2,
|
||||
origin: Vector2,
|
||||
alternate_lane_offset: float,
|
||||
) -> void:
|
||||
for slot: Panel in slots:
|
||||
if is_instance_valid(slot):
|
||||
slot.queue_free()
|
||||
slots.clear()
|
||||
var slot_color: Color = UtilityPageStyle.OCEAN_PANEL_MID
|
||||
slot_color.a = 0.42
|
||||
for index: int in slot_count:
|
||||
var row: int = floori(float(index) / float(columns))
|
||||
var column: int = index % columns
|
||||
var slot := Panel.new()
|
||||
slot.name = "InventorySlot%d" % index
|
||||
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
slot.z_index = -1
|
||||
slot.size = slot_size
|
||||
slot.position = origin + Vector2(
|
||||
float(column) * cell_size.x
|
||||
+ (alternate_lane_offset if row % 2 == 1 else 0.0),
|
||||
float(row) * cell_size.y,
|
||||
)
|
||||
slot.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(slot_color, 14),
|
||||
)
|
||||
parent.add_child(slot)
|
||||
parent.move_child(slot, 0)
|
||||
slots.append(slot)
|
||||
|
||||
|
||||
func _configure_bag_item_focus() -> void:
|
||||
if _current_section != Section.BAG:
|
||||
return
|
||||
|
|
@ -3374,9 +3416,12 @@ func _configure_bag_item_focus() -> void:
|
|||
for index: int in controls.size():
|
||||
var control: BagItemSpriteType = controls[index]
|
||||
var column: int = index % 3
|
||||
var row: int = floori(float(index) / 3.0)
|
||||
var left_index: int = row * 3 + maxi(column - 1, 0)
|
||||
var right_index: int = mini(index + 1, controls.size() - 1)
|
||||
var left_index: int = index - 1 if column > 0 else index
|
||||
var right_index: int = (
|
||||
index + 1
|
||||
if column < 2 and index + 1 < controls.size()
|
||||
else index
|
||||
)
|
||||
control.focus_neighbor_left = control.get_path_to(
|
||||
controls[left_index]
|
||||
)
|
||||
|
|
@ -3384,12 +3429,14 @@ func _configure_bag_item_focus() -> void:
|
|||
controls[right_index]
|
||||
)
|
||||
control.focus_neighbor_top = (
|
||||
control.get_path_to(active_tab)
|
||||
control.get_path_to(control)
|
||||
if index < 3
|
||||
else control.get_path_to(controls[index - 3])
|
||||
)
|
||||
control.focus_neighbor_bottom = control.get_path_to(
|
||||
controls[mini(index + 3, controls.size() - 1)]
|
||||
controls[index + 3]
|
||||
if index + 3 < controls.size()
|
||||
else control
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3654,9 +3701,13 @@ func _layout_cooler_fish(animate: bool = true) -> void:
|
|||
)
|
||||
var side_margin: float = 2.0 if _compact_layout else 8.0
|
||||
var top_margin: float = 4.0 if _compact_layout else 6.0
|
||||
var slot_count: int = maxi(
|
||||
_sorted_catches.size(),
|
||||
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
|
||||
)
|
||||
var required_rows: int = ceili(
|
||||
float(_sorted_catches.size()) / float(columns)
|
||||
) if not _sorted_catches.is_empty() else 0
|
||||
float(slot_count) / float(columns)
|
||||
) if slot_count > 0 else 0
|
||||
var required_height: float = (
|
||||
top_margin * 2.0 + float(required_rows) * cell_size.y
|
||||
)
|
||||
|
|
@ -3667,6 +3718,16 @@ func _layout_cooler_fish(animate: bool = true) -> void:
|
|||
_cooler_host.custom_minimum_size = content_size
|
||||
_fish_field.custom_minimum_size = content_size
|
||||
_cooler_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
_sync_inventory_slot_visuals(
|
||||
_fish_field,
|
||||
_cooler_slot_nodes,
|
||||
slot_count,
|
||||
columns,
|
||||
cell_size,
|
||||
fish_size,
|
||||
Vector2(side_margin, top_margin),
|
||||
5.0,
|
||||
)
|
||||
for index: int in _sorted_catches.size():
|
||||
var fish_catch: FishCatchType = _sorted_catches[index]
|
||||
var fish_node := _fish_nodes.get(
|
||||
|
|
@ -3724,31 +3785,38 @@ func _configure_cooler_fish_focus() -> void:
|
|||
)
|
||||
for index: int in controls.size():
|
||||
var control: CoolerFishSpriteType = controls[index]
|
||||
var column: int = index % columns
|
||||
control.focus_neighbor_left = control.get_path_to(
|
||||
controls[maxi(index - 1, 0)]
|
||||
controls[index - 1] if column > 0 else control
|
||||
)
|
||||
control.focus_neighbor_right = control.get_path_to(
|
||||
controls[mini(index + 1, controls.size() - 1)]
|
||||
controls[index + 1]
|
||||
if column < columns - 1 and index + 1 < controls.size()
|
||||
else control
|
||||
)
|
||||
control.focus_neighbor_top = (
|
||||
control.get_path_to(top_control)
|
||||
if _shop_cooler_context_active and index < columns
|
||||
else control.get_path_to(control)
|
||||
if index < columns
|
||||
else control.get_path_to(controls[index - columns])
|
||||
)
|
||||
control.focus_neighbor_bottom = (
|
||||
control.get_path_to(_favorite_bubble)
|
||||
if (
|
||||
not _favorite_bubble.disabled
|
||||
_shop_cooler_context_active
|
||||
and not _favorite_bubble.disabled
|
||||
and index + columns >= controls.size()
|
||||
)
|
||||
else control.get_path_to(_sell_all_bubble)
|
||||
if (
|
||||
not _sell_all_bubble.disabled
|
||||
_shop_cooler_context_active
|
||||
and not _sell_all_bubble.disabled
|
||||
and index + columns >= controls.size()
|
||||
)
|
||||
else control.get_path_to(
|
||||
controls[mini(index + columns, controls.size() - 1)]
|
||||
)
|
||||
else control.get_path_to(controls[index + columns])
|
||||
if index + columns < controls.size()
|
||||
else control.get_path_to(control)
|
||||
)
|
||||
var focused_node := _fish_nodes.get(
|
||||
_fish_selection.get_focused_id()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ class_name PlayersPage
|
|||
extends Control
|
||||
|
||||
const TOGGLE_STATE_COLOR := Color("c3dfe6")
|
||||
const DialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
|
||||
enum ControllerZone {
|
||||
TABS,
|
||||
BODY,
|
||||
}
|
||||
|
||||
var _service: NetworkPlayerListService
|
||||
var _discovery: DiscoveryClient
|
||||
|
|
@ -17,6 +25,9 @@ var _discoverable_toggle: Button
|
|||
var _discoverable_state_label: Label
|
||||
var _host_discovery_status: Label
|
||||
var _current_tab := 0
|
||||
var _active: bool = false
|
||||
var _interactive: bool = false
|
||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -45,12 +56,107 @@ func setup(
|
|||
|
||||
|
||||
func activate() -> void:
|
||||
_active = true
|
||||
_refresh()
|
||||
_focus_first()
|
||||
reset_controller_zone()
|
||||
|
||||
|
||||
func deactivate() -> void:
|
||||
_active = false
|
||||
_status.text = ""
|
||||
set_interactive(false)
|
||||
|
||||
|
||||
func set_interactive(interactive: bool) -> void:
|
||||
_interactive = interactive and _active
|
||||
var body_controls: Array[Control] = _body_controls()
|
||||
for index: int in _tabs.get_child_count():
|
||||
var tab := _tabs.get_child(index) as Button
|
||||
if tab != null:
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _interactive
|
||||
and _controller_zone == ControllerZone.TABS
|
||||
and tab.visible
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
for control: Control in body_controls:
|
||||
var button := control as BaseButton
|
||||
control.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _interactive
|
||||
and _controller_zone == ControllerZone.BODY
|
||||
and (button == null or not button.disabled)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
ControllerFocusNavigation.configure_spatial_neighbors(body_controls)
|
||||
|
||||
|
||||
func reset_controller_zone() -> void:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _active or not _interactive:
|
||||
return false
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if _controller_zone == ControllerZone.BODY:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("_focus_controller_zone")
|
||||
return true
|
||||
return false
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
var direction: int = 0
|
||||
if event.is_action_pressed("ui_left"):
|
||||
direction = -1
|
||||
elif event.is_action_pressed("ui_right"):
|
||||
direction = 1
|
||||
if direction != 0:
|
||||
_select_adjacent_controller_tab(direction)
|
||||
return true
|
||||
if event.is_action_pressed("ui_accept"):
|
||||
if not _body_controls().is_empty():
|
||||
_controller_zone = ControllerZone.BODY
|
||||
set_interactive(_interactive)
|
||||
call_deferred("_focus_controller_zone")
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _select_adjacent_controller_tab(direction: int) -> void:
|
||||
var available: Array[int] = []
|
||||
for index: int in _tabs.get_child_count():
|
||||
var tab := _tabs.get_child(index) as Button
|
||||
if tab != null and tab.visible:
|
||||
available.append(index)
|
||||
var current_position: int = available.find(_current_tab)
|
||||
var target_position: int = clampi(
|
||||
current_position + direction,
|
||||
0,
|
||||
available.size() - 1,
|
||||
)
|
||||
if target_position != current_position:
|
||||
_select_tab(available[target_position])
|
||||
|
||||
|
||||
func _focus_controller_zone() -> void:
|
||||
if not _interactive:
|
||||
return
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
var tab := _tabs.get_child(_current_tab) as Button
|
||||
if tab != null and tab.visible:
|
||||
tab.grab_focus()
|
||||
return
|
||||
_focus_first()
|
||||
|
||||
|
||||
func _body_controls() -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
_collect_focusable_player_controls(self, controls)
|
||||
return controls
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
|
|
@ -196,7 +302,7 @@ func _select_tab(index: int) -> void:
|
|||
return
|
||||
_current_tab = index
|
||||
_refresh()
|
||||
_focus_first()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
|
|
@ -221,6 +327,9 @@ func _refresh() -> void:
|
|||
_build_relationship_rows()
|
||||
2:
|
||||
_build_ban_rows()
|
||||
if _controller_zone == ControllerZone.BODY and _body_controls().is_empty():
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
|
||||
|
||||
func _refresh_host_settings() -> void:
|
||||
|
|
@ -582,11 +691,22 @@ func _confirm(text: String, action: Callable) -> void:
|
|||
)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(520, 220))
|
||||
_configure_confirmation_dialog.call_deferred(dialog)
|
||||
|
||||
|
||||
func _configure_confirmation_dialog(dialog: ConfirmationDialog) -> void:
|
||||
if dialog != null and is_instance_valid(dialog) and dialog.visible:
|
||||
DialogControllerNavigationType.configure_scope(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
|
||||
|
||||
func _focus_first() -> void:
|
||||
var candidates: Array[Control] = []
|
||||
_collect_focusable_player_controls(self, candidates)
|
||||
candidates = candidates.filter(func(control: Control) -> bool:
|
||||
return control.focus_mode != Control.FOCUS_NONE
|
||||
)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
||||
|
|
@ -609,8 +729,7 @@ func _collect_focusable_player_controls(
|
|||
continue
|
||||
if (
|
||||
control != null
|
||||
and control.focus_mode != Control.FOCUS_NONE
|
||||
and not control is ScrollBar
|
||||
and (control is BaseButton or control is LineEdit)
|
||||
):
|
||||
output.append(control)
|
||||
_collect_focusable_player_controls(child, output)
|
||||
|
|
|
|||
|
|
@ -197,6 +197,22 @@ func _set_interactive(interactive: bool) -> void:
|
|||
if interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
# Most settings pages are configured while their parent panel is hidden.
|
||||
# Rebuild the graph after a page becomes visible so hidden-at-ready controls
|
||||
# do not retain the empty neighbors produced by the initial layout pass.
|
||||
if interactive:
|
||||
call_deferred("_refresh_focus_navigation")
|
||||
else:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
|
||||
|
||||
func _refresh_focus_navigation() -> void:
|
||||
if visible and not _is_transitioning:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
|
||||
|
||||
func _finish_transition_out(generation: int, completed: Callable) -> void:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ const ControllerMappingManagerType = preload(
|
|||
const ControllerMappingPanelType = preload(
|
||||
"res://ui/controller_mapping_panel.gd"
|
||||
)
|
||||
const KeyboardMouseMappingManagerType = preload(
|
||||
"res://settings/keyboard_mouse_mapping_manager.gd"
|
||||
)
|
||||
const KeyboardMouseMappingPanelType = preload(
|
||||
"res://ui/keyboard_mouse_mapping_panel.gd"
|
||||
)
|
||||
const DialogControllerNavigationType = preload(
|
||||
"res://ui/file_dialog_controller_navigation.gd"
|
||||
)
|
||||
|
||||
signal applied
|
||||
signal closed
|
||||
|
|
@ -110,6 +119,8 @@ var _host_identity: HostIdentityStore
|
|||
var _interface_fonts: InterfaceFontController
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _controller_mapping_panel: ControllerMappingPanelType
|
||||
var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType
|
||||
var _keyboard_mouse_mapping_panel: KeyboardMouseMappingPanelType
|
||||
var _data_folder_dialog: FileDialog
|
||||
var _backup_file_dialog: FileDialog
|
||||
var _export_file_dialog: FileDialog
|
||||
|
|
@ -144,6 +155,7 @@ func _ready() -> void:
|
|||
%SoundBackButton.pressed.connect(handle_back)
|
||||
%ControlsBackButton.pressed.connect(handle_back)
|
||||
%ControllerMapping.pressed.connect(_open_controller_mapping)
|
||||
%KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping)
|
||||
%AccessibilityBackButton.pressed.connect(handle_back)
|
||||
%DataBackButton.pressed.connect(handle_back)
|
||||
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
|
|
@ -230,6 +242,11 @@ func _ready() -> void:
|
|||
_controller_mapping_panel.closed.connect(
|
||||
_on_controller_mapping_panel_closed
|
||||
)
|
||||
_keyboard_mouse_mapping_panel = KeyboardMouseMappingPanelType.new()
|
||||
add_child(_keyboard_mouse_mapping_panel)
|
||||
_keyboard_mouse_mapping_panel.closed.connect(
|
||||
_on_keyboard_mouse_mapping_panel_closed
|
||||
)
|
||||
_style_data_page()
|
||||
_style_sound_page()
|
||||
call_deferred("_refresh_panel_size")
|
||||
|
|
@ -250,6 +267,13 @@ func setup_controller_mapping(
|
|||
_controller_mapping_panel.setup(_controller_mapping_manager)
|
||||
|
||||
|
||||
func setup_keyboard_mouse_mapping(
|
||||
mapping_manager: KeyboardMouseMappingManagerType,
|
||||
) -> void:
|
||||
_keyboard_mouse_mapping_manager = mapping_manager
|
||||
_keyboard_mouse_mapping_panel.setup(_keyboard_mouse_mapping_manager)
|
||||
|
||||
|
||||
func setup_data_and_identity(
|
||||
data_root: PlayerDataRoot,
|
||||
identity_backups: IdentityBackupService,
|
||||
|
|
@ -328,6 +352,11 @@ func _finish_panel_close(applied_result: bool) -> void:
|
|||
and _controller_mapping_panel.is_open()
|
||||
):
|
||||
_controller_mapping_panel.close_panel()
|
||||
if (
|
||||
_keyboard_mouse_mapping_panel != null
|
||||
and _keyboard_mouse_mapping_panel.is_open()
|
||||
):
|
||||
_keyboard_mouse_mapping_panel.close_panel()
|
||||
for page: SettingsBubblePage in _pages.values():
|
||||
page.hide_page()
|
||||
_page_stack.clear()
|
||||
|
|
@ -341,6 +370,12 @@ func _finish_panel_close(applied_result: bool) -> void:
|
|||
|
||||
|
||||
func handle_back() -> void:
|
||||
if (
|
||||
_keyboard_mouse_mapping_panel != null
|
||||
and _keyboard_mouse_mapping_panel.is_open()
|
||||
):
|
||||
_keyboard_mouse_mapping_panel.request_back()
|
||||
return
|
||||
if (
|
||||
_controller_mapping_panel != null
|
||||
and _controller_mapping_panel.is_open()
|
||||
|
|
@ -366,6 +401,17 @@ func _on_controller_mapping_panel_closed() -> void:
|
|||
%ControllerMapping.grab_focus()
|
||||
|
||||
|
||||
func _open_keyboard_mouse_mapping() -> void:
|
||||
if _keyboard_mouse_mapping_manager == null:
|
||||
_feedback.text = "no keyboard binding service is available"
|
||||
return
|
||||
_keyboard_mouse_mapping_panel.open_panel()
|
||||
|
||||
|
||||
func _on_keyboard_mouse_mapping_panel_closed() -> void:
|
||||
%KeyboardMapping.grab_focus()
|
||||
|
||||
|
||||
func get_active_page_id() -> StringName:
|
||||
return _page_stack.back() if not _page_stack.is_empty() else StringName()
|
||||
|
||||
|
|
@ -377,6 +423,16 @@ func is_controller_mapping_capturing() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func is_input_mapping_capturing() -> bool:
|
||||
return (
|
||||
is_controller_mapping_capturing()
|
||||
or (
|
||||
_keyboard_mouse_mapping_panel != null
|
||||
and _keyboard_mouse_mapping_panel.is_capturing()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _push_page(page_id: StringName) -> void:
|
||||
if (
|
||||
_page_transition_active
|
||||
|
|
@ -579,6 +635,9 @@ func _show_existing_data_folder_choice(path: String) -> void:
|
|||
_interface_fonts.apply_utility_theme(dialog)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(620, 340))
|
||||
_configure_confirmation_dialog.call_deferred(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
|
||||
|
||||
func _choose_identity_export(identity_type: String) -> void:
|
||||
|
|
@ -662,7 +721,9 @@ func _show_passphrase_dialog(exporting: bool) -> void:
|
|||
if exporting else "Enter the backup passphrase."
|
||||
)
|
||||
_passphrase_dialog.popup_centered(Vector2i(560, 300))
|
||||
_passphrase_entry.grab_focus()
|
||||
_configure_confirmation_dialog.call_deferred(
|
||||
_passphrase_dialog, _passphrase_entry
|
||||
)
|
||||
|
||||
|
||||
func _submit_identity_passphrase() -> void:
|
||||
|
|
@ -707,6 +768,9 @@ func _show_identity_replacement_confirmation() -> void:
|
|||
_interface_fonts.apply_utility_theme(dialog)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(620, 360))
|
||||
_configure_confirmation_dialog.call_deferred(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
|
||||
|
||||
func _advance_identity_replacement(dialog: ConfirmationDialog) -> void:
|
||||
|
|
@ -717,6 +781,9 @@ func _advance_identity_replacement(dialog: ConfirmationDialog) -> void:
|
|||
"Replace the active identity and archive the current key locally?"
|
||||
)
|
||||
dialog.call_deferred("popup_centered", Vector2i(560, 280))
|
||||
_configure_confirmation_dialog.call_deferred(
|
||||
dialog, dialog.get_cancel_button()
|
||||
)
|
||||
return
|
||||
_confirm_identity_replacement(dialog)
|
||||
|
||||
|
|
@ -732,6 +799,16 @@ func _confirm_identity_replacement(dialog: ConfirmationDialog) -> void:
|
|||
dialog.queue_free()
|
||||
|
||||
|
||||
func _configure_confirmation_dialog(
|
||||
dialog: ConfirmationDialog,
|
||||
preferred_control: Control,
|
||||
) -> void:
|
||||
if dialog != null and is_instance_valid(dialog) and dialog.visible:
|
||||
DialogControllerNavigationType.configure_scope(
|
||||
dialog, preferred_control
|
||||
)
|
||||
|
||||
|
||||
func _identity_operation_allowed() -> bool:
|
||||
if _identity_backups == null:
|
||||
_feedback.text = "Identity backup is unavailable."
|
||||
|
|
|
|||
|
|
@ -600,8 +600,8 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
page_id = &"controls"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -713,15 +713,27 @@ motion_phase = 4.7
|
|||
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "map\ncontroller"
|
||||
neutral_size = Vector2(160, 150)
|
||||
desktop_anchor = Vector2(525, 390)
|
||||
compact_anchor = Vector2(485, 355)
|
||||
compact_minimum_size = Vector2(148, 138)
|
||||
text = "controller\nbinds"
|
||||
neutral_size = Vector2(130, 108)
|
||||
desktop_anchor = Vector2(470, 390)
|
||||
compact_anchor = Vector2(425, 350)
|
||||
compact_minimum_size = Vector2(116, 98)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 23
|
||||
maximum_font_size = 20
|
||||
motion_phase = 4.9
|
||||
|
||||
[node name="KeyboardMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "keyboard\nbinds"
|
||||
neutral_size = Vector2(130, 108)
|
||||
desktop_anchor = Vector2(610, 380)
|
||||
compact_anchor = Vector2(545, 345)
|
||||
compact_minimum_size = Vector2(116, 98)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 20
|
||||
motion_phase = 5.2
|
||||
|
||||
[node name="ControlsBackButton" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
|
|
@ -845,8 +857,8 @@ grow_vertical = 2
|
|||
script = ExtResource("3_page")
|
||||
page_id = &"data"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DataBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/DataBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/DataBackButton")
|
||||
focus_paths = Array[NodePath]([NodePath("Paper/Content/OpenDataFolder"), NodePath("Paper/Content/ChangeDataFolder"), NodePath("Paper/Content/CopyPlayerFingerprint"), NodePath("Paper/Content/IdentityGrid/ExportPlayerIdentity"), NodePath("Paper/Content/IdentityGrid/ImportPlayerIdentity"), NodePath("Paper/Content/IdentityGrid/ExportHostIdentity"), NodePath("Paper/Content/IdentityGrid/ImportHostIdentity"), NodePath("BubbleCluster/DataBackButton")])
|
||||
initial_focus_path = NodePath("Paper/Content/OpenDataFolder")
|
||||
back_focus_path = NodePath("BubbleCluster/DataBackButton")
|
||||
|
||||
[node name="BubbleCluster" type="Control" parent="DataPage"]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ enum View {
|
|||
PAYMENTS,
|
||||
}
|
||||
|
||||
enum ControllerZone {
|
||||
TABS,
|
||||
CLAIMS,
|
||||
}
|
||||
|
||||
var _jobs: PlayerJobService
|
||||
var _world_time: WorldTimeService
|
||||
var _header: Label
|
||||
|
|
@ -18,11 +23,15 @@ var _refresh_label: Label
|
|||
var _forecast_list: HBoxContainer
|
||||
var _tabs: HBoxContainer
|
||||
var _list: VBoxContainer
|
||||
var _jobs_scroll: ScrollContainer
|
||||
var _status: Label
|
||||
var _current_view: View = View.DAILY
|
||||
var _forecast_start_index: int = -1
|
||||
var _active: bool = false
|
||||
var _interactive: bool = false
|
||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||
var _claim_buttons: Array[Button] = []
|
||||
var _controller_mapping_manager: ControllerMappingManager
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -40,6 +49,10 @@ func setup(jobs: PlayerJobService, world_time: WorldTimeService) -> void:
|
|||
_refresh()
|
||||
|
||||
|
||||
func setup_controller_mapping(mapping_manager: ControllerMappingManager) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func activate() -> void:
|
||||
_active = true
|
||||
set_process(true)
|
||||
|
|
@ -68,6 +81,7 @@ func set_interactive(interactive: bool) -> void:
|
|||
button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if interactive
|
||||
and _controller_zone == ControllerZone.TABS
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
button.mouse_filter = (
|
||||
|
|
@ -75,20 +89,108 @@ func set_interactive(interactive: bool) -> void:
|
|||
if interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
_refresh()
|
||||
for claim_button: Button in _claim_buttons:
|
||||
if not is_instance_valid(claim_button):
|
||||
continue
|
||||
claim_button.disabled = not interactive
|
||||
claim_button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if interactive
|
||||
and _controller_zone == ControllerZone.CLAIMS
|
||||
and not claim_button.disabled
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
claim_button.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if interactive else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
|
||||
|
||||
func focus_initial() -> void:
|
||||
if _interactive and _tabs != null and _tabs.get_child_count() > 0:
|
||||
if not _interactive:
|
||||
return
|
||||
if _controller_zone == ControllerZone.CLAIMS:
|
||||
if not _claim_buttons.is_empty():
|
||||
_claim_buttons.front().grab_focus()
|
||||
return
|
||||
if _tabs != null and _tabs.get_child_count() > 0:
|
||||
var button := _tabs.get_child(int(_current_view)) as Button
|
||||
if button != null:
|
||||
button.grab_focus()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
func reset_controller_zone() -> void:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("focus_initial")
|
||||
|
||||
|
||||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _active or not _interactive:
|
||||
return false
|
||||
if _handle_controller_scroll(event):
|
||||
return true
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if _controller_zone == ControllerZone.CLAIMS:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("focus_initial")
|
||||
return true
|
||||
return false
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
var direction: int = 0
|
||||
if event.is_action_pressed("ui_left"):
|
||||
direction = -1
|
||||
elif event.is_action_pressed("ui_right"):
|
||||
direction = 1
|
||||
if direction != 0:
|
||||
var target_index: int = clampi(
|
||||
int(_current_view) + direction,
|
||||
0,
|
||||
View.size() - 1,
|
||||
)
|
||||
if target_index != int(_current_view):
|
||||
_select_view(target_index as View)
|
||||
return true
|
||||
if event.is_action_pressed("ui_accept"):
|
||||
if not _claim_buttons.is_empty():
|
||||
_controller_zone = ControllerZone.CLAIMS
|
||||
set_interactive(_interactive)
|
||||
call_deferred("focus_initial")
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _handle_controller_scroll(event: InputEvent) -> bool:
|
||||
var motion := event as InputEventJoypadMotion
|
||||
if motion == null or _jobs_scroll == null:
|
||||
return false
|
||||
var uses_right_y: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
ControllerMappingManager.ROLE_RIGHT_STICK_Y,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else motion.axis == JOY_AXIS_RIGHT_Y
|
||||
)
|
||||
if not uses_right_y:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _active and _jobs != null:
|
||||
_refresh_label.text = _daily_refresh_text()
|
||||
_refresh_forecast(false)
|
||||
var right_y: float = (
|
||||
_controller_mapping_manager.get_role_axis(
|
||||
ControllerMappingManager.ROLE_RIGHT_STICK_Y
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
|
||||
)
|
||||
if _jobs_scroll != null and absf(right_y) >= 0.18:
|
||||
_jobs_scroll.scroll_vertical += roundi(right_y * 320.0 * delta)
|
||||
|
||||
|
||||
func _build_laptop() -> void:
|
||||
|
|
@ -177,15 +279,15 @@ func _build_laptop() -> void:
|
|||
jobs_column.add_theme_constant_override("separation", 8)
|
||||
layout.add_child(jobs_column)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
jobs_column.add_child(scroll)
|
||||
_jobs_scroll = ScrollContainer.new()
|
||||
_jobs_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_jobs_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_jobs_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
jobs_column.add_child(_jobs_scroll)
|
||||
_list = VBoxContainer.new()
|
||||
_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_list.add_theme_constant_override("separation", 7)
|
||||
scroll.add_child(_list)
|
||||
_jobs_scroll.add_child(_list)
|
||||
|
||||
_status = Label.new()
|
||||
_status.add_theme_font_size_override("font_size", 16)
|
||||
|
|
@ -212,6 +314,7 @@ func _refresh() -> void:
|
|||
for child: Node in _list.get_children():
|
||||
_list.remove_child(child)
|
||||
child.queue_free()
|
||||
_claim_buttons.clear()
|
||||
match _current_view:
|
||||
View.DAILY:
|
||||
_build_job_rows(_jobs.get_daily_jobs(), "no daily jobs available")
|
||||
|
|
@ -221,6 +324,10 @@ func _refresh() -> void:
|
|||
)
|
||||
View.PAYMENTS:
|
||||
_build_payment_rows()
|
||||
if _controller_zone == ControllerZone.CLAIMS and _claim_buttons.is_empty():
|
||||
_controller_zone = ControllerZone.TABS
|
||||
set_interactive(_interactive)
|
||||
ControllerFocusNavigation.configure_spatial_neighbors(_claim_buttons)
|
||||
|
||||
|
||||
func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void:
|
||||
|
|
@ -311,6 +418,8 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void:
|
|||
)
|
||||
claim.pressed.connect(_claim.bind(claim_id))
|
||||
UtilityPageStyle.apply_compact_ocean_button(claim)
|
||||
if claimable:
|
||||
_claim_buttons.append(claim)
|
||||
content.add_child(claim)
|
||||
_list.add_child(row)
|
||||
|
||||
|
|
@ -357,6 +466,7 @@ func _build_payment_rows() -> void:
|
|||
_claim.bind(str(reward.get("claim_id", "")))
|
||||
)
|
||||
UtilityPageStyle.apply_ocean_button(claim)
|
||||
_claim_buttons.append(claim)
|
||||
content.add_child(claim)
|
||||
_list.add_child(row)
|
||||
|
||||
|
|
|
|||
|
|
@ -197,14 +197,18 @@ func _configure_focus() -> void:
|
|||
_cancel_button
|
||||
)
|
||||
_confirm_button.focus_neighbor_right = _confirm_button.focus_neighbor_left
|
||||
_confirm_button.focus_neighbor_top = _confirm_button.focus_neighbor_left
|
||||
_confirm_button.focus_neighbor_bottom = _confirm_button.focus_neighbor_left
|
||||
_confirm_button.focus_neighbor_top = _confirm_button.get_path_to(
|
||||
_confirm_button
|
||||
)
|
||||
_confirm_button.focus_neighbor_bottom = _confirm_button.focus_neighbor_top
|
||||
_cancel_button.focus_neighbor_left = _cancel_button.get_path_to(
|
||||
_confirm_button
|
||||
)
|
||||
_cancel_button.focus_neighbor_right = _cancel_button.focus_neighbor_left
|
||||
_cancel_button.focus_neighbor_top = _cancel_button.focus_neighbor_left
|
||||
_cancel_button.focus_neighbor_bottom = _cancel_button.focus_neighbor_left
|
||||
_cancel_button.focus_neighbor_top = _cancel_button.get_path_to(
|
||||
_cancel_button
|
||||
)
|
||||
_cancel_button.focus_neighbor_bottom = _cancel_button.focus_neighbor_top
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
@ -224,6 +228,8 @@ func _set_interactive(interactive: bool) -> void:
|
|||
if interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
if interactive:
|
||||
_configure_focus()
|
||||
|
||||
|
||||
func _finish_transition_in(
|
||||
|
|
|
|||
|
|
@ -556,7 +556,7 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
if (
|
||||
_settings_panel.visible
|
||||
and _settings_panel.is_controller_mapping_capturing()
|
||||
and _settings_panel.is_input_mapping_capturing()
|
||||
):
|
||||
return
|
||||
if _credits_page.visible:
|
||||
|
|
@ -565,7 +565,7 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
if _join_game_page.visible:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_close_join_game()
|
||||
_join_game_page.request_back()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
|
|
@ -624,23 +624,18 @@ func _handle_primary_menu_focus_input(event: InputEvent) -> bool:
|
|||
return false
|
||||
if event is InputEventKey and (event as InputEventKey).echo:
|
||||
return false
|
||||
var moves_forward: bool = (
|
||||
var moves_directionally: bool = (
|
||||
event.is_action_pressed("ui_down")
|
||||
or event.is_action_pressed("ui_right")
|
||||
)
|
||||
var moves_backward: bool = (
|
||||
event.is_action_pressed("ui_up")
|
||||
or event.is_action_pressed("ui_up")
|
||||
or event.is_action_pressed("ui_left")
|
||||
)
|
||||
if not moves_forward and not moves_backward:
|
||||
if not moves_directionally:
|
||||
return false
|
||||
_navigation_focus_active = true
|
||||
if _primary_menu_has_focus():
|
||||
return false
|
||||
if moves_forward:
|
||||
_get_first_available_menu_button().grab_focus()
|
||||
else:
|
||||
_quit_button.grab_focus()
|
||||
return true
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ func _ready() -> void:
|
|||
_on_screen_keyboard = OnScreenKeyboardType.new()
|
||||
_ui_root.add_child(_on_screen_keyboard)
|
||||
_game_ui.set_controller_text_entry_request(
|
||||
Callable(_on_screen_keyboard, "request_for_focused_control")
|
||||
Callable(_on_screen_keyboard, "request_for_control"),
|
||||
Callable(_on_screen_keyboard, "is_open"),
|
||||
)
|
||||
var controller_focus_recovery := ControllerFocusRecoveryType.new()
|
||||
_ui_root.add_child(controller_focus_recovery)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue