diff --git a/main/main.gd b/main/main.gd index 6177e17..3a85307 100644 --- a/main/main.gd +++ b/main/main.gd @@ -1328,20 +1328,13 @@ func _handle_data_root_controller_input(event: InputEvent) -> bool: if picker_visible else _data_setup_dialog.gui_get_focus_owner() ) - var focused_button := focused as BaseButton - if ( - focused_button != null - and FileDialogControllerNavigation.activate_control(focused_button) - ): - 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 + # Buttons are activated once through Godot's native ui_accept path. This + # handler only owns modal back behavior and controller text entry. return false diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 72aeffe..912a59c 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -13,7 +13,6 @@ readonly -a QUICK_TESTS=( "tests/android_readiness_validation.gd" "tests/camera_drag_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_world_interaction_validation.gd" diff --git a/tests/controller_focus_recovery_validation.gd b/tests/controller_focus_recovery_validation.gd deleted file mode 100644 index f598511..0000000 --- a/tests/controller_focus_recovery_validation.gd +++ /dev/null @@ -1,184 +0,0 @@ -extends SceneTree - -const ControllerFocusRecoveryType = preload( - "res://ui/controller_focus_recovery.gd" -) - -var _failures: Array[String] = [] - - -func _initialize() -> void: - _run.call_deferred() - - -func _run() -> void: - var stage := Control.new() - stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - root.add_child(stage) - var recovery := ControllerFocusRecoveryType.new() - stage.add_child(recovery) - var option_list := VBoxContainer.new() - option_list.position = Vector2(100.0, 100.0) - stage.add_child(option_list) - var original := _make_button("cute", "cute (show variants)") - option_list.add_child(original) - await process_frame - original.grab_focus() - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "programmatic focus stays pending outside navigation mode", - ) - var keyboard_navigation := InputEventKey.new() - keyboard_navigation.keycode = KEY_DOWN - keyboard_navigation.pressed = true - recovery._input(keyboard_navigation) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == original, - "keyboard navigation restores pending initial focus", - ) - var menu_shortcut := InputEventKey.new() - menu_shortcut.physical_keycode = KEY_TAB - menu_shortcut.pressed = true - recovery._input(menu_shortcut) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "the menu shortcut does not leave a pseudo-focused option", - ) - - var controller_event := InputEventJoypadButton.new() - controller_event.button_index = JOY_BUTTON_A - controller_event.pressed = true - recovery._input(controller_event) - await process_frame - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == original, - "controller navigation restores pending initial focus", - ) - - option_list.remove_child(original) - original.queue_free() - var replacement := _make_button("cute", "cute (hide variants)") - option_list.add_child(replacement) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == replacement, - "focus follows a rebuilt semantic option", - ) - - var explicit_target := _make_button("explicit", "explicit") - option_list.add_child(explicit_target) - option_list.remove_child(replacement) - replacement.queue_free() - explicit_target.grab_focus() - await process_frame - _expect( - root.gui_get_focus_owner() == explicit_target, - "explicit focus changes take precedence over recovery", - ) - var pointer_motion := InputEventMouseMotion.new() - pointer_motion.position = Vector2(4.0, 4.0) - recovery._input(pointer_motion) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "mouse motion releases controller focus", - ) - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "mouse motion prevents automatic focus recovery", - ) - recovery._input(controller_event) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == explicit_target, - "controller navigation can resume from pointer mode", - ) - var pointer_press := InputEventMouseButton.new() - pointer_press.button_index = MOUSE_BUTTON_LEFT - pointer_press.pressed = true - recovery._input(pointer_press) - await process_frame - _expect( - root.gui_get_focus_owner() == explicit_target, - "mouse-down preserves focus through button activation", - ) - var pointer_release := InputEventMouseButton.new() - pointer_release.button_index = MOUSE_BUTTON_LEFT - pointer_release.pressed = false - recovery._input(pointer_release) - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "mouse-up clears the completed button focus", - ) - recovery._input(controller_event) - await process_frame - await process_frame - - var outside_button := _make_button("outside", "outside") - stage.add_child(outside_button) - option_list.hide() - root.gui_release_focus() - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "focus recovery never escapes a hidden menu scope", - ) - option_list.show() - explicit_target.grab_focus() - await process_frame - var leave_world_ui_event := InputEventJoypadButton.new() - leave_world_ui_event.button_index = JOY_BUTTON_LEFT_SHOULDER - leave_world_ui_event.pressed = true - recovery._input(leave_world_ui_event) - root.gui_release_focus() - await process_frame - await process_frame - _expect( - root.gui_get_focus_owner() == null, - "LB intentionally leaving world UI is never recovered", - ) - var accessibility_only := Control.new() - accessibility_only.focus_mode = Control.FOCUS_ACCESSIBILITY - stage.add_child(accessibility_only) - await process_frame - _expect( - not bool(recovery.call("_is_focusable", accessibility_only)), - "accessibility-only controls are not recovery targets", - ) - - stage.queue_free() - if _failures.is_empty(): - print("Controller focus recovery validation: PASS") - quit(0) - return - for failure: String in _failures: - push_error(failure) - quit(1) - - -func _make_button(text: String, tooltip: String) -> Button: - var button := Button.new() - button.text = text - button.tooltip_text = tooltip - button.custom_minimum_size = Vector2(120.0, 48.0) - return button - - -func _expect(condition: bool, message: String) -> void: - if not condition: - _failures.append(message) diff --git a/tests/controller_menu_accessibility_validation.gd b/tests/controller_menu_accessibility_validation.gd index 8eeaf0f..10e3624 100644 --- a/tests/controller_menu_accessibility_validation.gd +++ b/tests/controller_menu_accessibility_validation.gd @@ -44,6 +44,7 @@ func _run() -> void: await _validate_join_game_navigation() await _validate_data_settings_navigation() await _validate_settings_adjustment_navigation() + await _validate_settings_presentation_requires_apply() await _validate_mail_navigation() await _validate_profile_confirmation_focus() await _validate_profile_voice_navigation() @@ -163,6 +164,47 @@ func _validate_join_game_navigation() -> void: ) await process_frame + var saved_entries: Array[SavedServerEntry] = [] + server_list.clear() + for index: int in 3: + var entry := SavedServerEntry.new() + entry.entry_id = "controller-saved-%d" % index + entry.display_name = "Saved server %d" % index + saved_entries.append(entry) + server_list.add_item(entry.display_name) + page.set("_mode", 2) + page.set("_visible_entries", saved_entries) + server_list.show() + page.call("_configure_controller_navigation") + page.call("_restore_entry_selection_and_focus", 1) + await process_frame + _expect( + server_list.is_selected(1) + and root.gui_get_focus_owner() == server_list, + "Saved-server refresh did not restore the same list position.", + ) + saved_entries.remove_at(1) + server_list.remove_item(1) + page.set("_visible_entries", saved_entries) + page.call("_restore_entry_selection_and_focus", 1) + await process_frame + _expect( + server_list.is_selected(1) + and ( + page.get("_selected_entry") as SavedServerEntry + ).entry_id == "controller-saved-2", + "Deleting a saved server did not select the row that replaced it.", + ) + saved_entries.clear() + server_list.clear() + page.set("_visible_entries", saved_entries) + page.call("_restore_entry_selection_and_focus", 0) + await process_frame + _expect( + root.gui_get_focus_owner() == saved, + "An empty saved-server list did not return focus to its mode tab.", + ) + page.queue_free() await process_frame @@ -190,13 +232,13 @@ func _validate_data_settings_navigation() -> void: open_activation["count"] = int(open_activation["count"]) + 1 ) data_tab.grab_focus() - var accept_press := InputEventAction.new() - accept_press.action = &"ui_accept" + var accept_press := InputEventJoypadButton.new() + accept_press.button_index = JOY_BUTTON_A accept_press.pressed = true Input.parse_input_event(accept_press) await process_frame - var accept_release := InputEventAction.new() - accept_release.action = &"ui_accept" + var accept_release := InputEventJoypadButton.new() + accept_release.button_index = JOY_BUTTON_A accept_release.pressed = false Input.parse_input_event(accept_release) for _frame: int in 2: @@ -213,10 +255,6 @@ func _validate_data_settings_navigation() -> void: root.gui_get_focus_owner() == data_tab, "Selecting the Data tab transferred focus into its actions.", ) - var test_data_root := PlayerDataRoot.new() - root.add_child(test_data_root) - test_data_root.root_path = "/tmp/netfishing-controller-data" - panel.set("_data_root", test_data_root) open_data_folder.grab_focus() _expect( root.gui_get_focus_owner() == open_data_folder, @@ -226,13 +264,13 @@ func _validate_data_settings_navigation() -> void: not open_data_folder.disabled, "Open Data Folder is unexpectedly disabled.", ) - var action_press := InputEventAction.new() - action_press.action = &"ui_accept" + var action_press := InputEventJoypadButton.new() + action_press.button_index = JOY_BUTTON_A action_press.pressed = true Input.parse_input_event(action_press) await process_frame - var action_release := InputEventAction.new() - action_release.action = &"ui_accept" + var action_release := InputEventJoypadButton.new() + action_release.button_index = JOY_BUTTON_A action_release.pressed = false Input.parse_input_event(action_release) for _frame: int in 2: @@ -244,31 +282,27 @@ func _validate_data_settings_navigation() -> void: % int(open_activation["count"]) ), ) - var confirmations: Array[Node] = panel.find_children( - "*", "ConfirmationDialog", true, false - ) _expect( - confirmations.size() == 1 - and (confirmations.front() as ConfirmationDialog).visible, - "Explicitly accepting Open Data Folder did not request confirmation.", + panel.find_children( + "*", "ConfirmationDialog", true, false + ).is_empty(), + "Open Data Folder left a controller-activatable dialog behind.", ) - if not confirmations.is_empty(): - var confirmation := confirmations.front() as ConfirmationDialog - _expect( - confirmation.gui_get_focus_owner() - == confirmation.get_cancel_button(), - "Open Data Folder confirmation did not default to Cancel.", - ) - confirmation.queue_free() + var change_data_folder := panel.get_node("%ChangeDataFolder") as Button + var copy_fingerprint := panel.get_node("%CopyPlayerFingerprint") as Button + var export_player := panel.get_node("%ExportPlayerIdentity") as Button + var import_player := panel.get_node("%ImportPlayerIdentity") as Button + var export_host := panel.get_node("%ExportHostIdentity") as Button + var import_host := panel.get_node("%ImportHostIdentity") as Button var controls: Array[Control] = [ data_tab, open_data_folder, - 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, + change_data_folder, + copy_fingerprint, + export_player, + import_player, + export_host, + import_host, panel.get_node("%ApplySettingsButton") as Control, panel.get_node("%SettingsBackButton") as Control, ] @@ -278,13 +312,113 @@ func _validate_data_settings_navigation() -> void: "Data & Identity control %s is not controller-focusable." % control.name, ) + _assert_neighbor(data_tab, &"focus_neighbor_bottom", open_data_folder) + _assert_neighbor( + open_data_folder, + &"focus_neighbor_right", + change_data_folder, + ) + _assert_neighbor( + open_data_folder, + &"focus_neighbor_bottom", + copy_fingerprint, + ) + _assert_neighbor( + change_data_folder, + &"focus_neighbor_left", + open_data_folder, + ) + _assert_neighbor( + change_data_folder, + &"focus_neighbor_bottom", + copy_fingerprint, + ) + _assert_neighbor( + copy_fingerprint, + &"focus_neighbor_top", + open_data_folder, + ) + _assert_neighbor( + copy_fingerprint, + &"focus_neighbor_bottom", + export_player, + ) + _assert_neighbor(export_player, &"focus_neighbor_right", import_player) + _assert_neighbor(export_player, &"focus_neighbor_bottom", export_host) + _assert_neighbor(import_player, &"focus_neighbor_left", export_player) + _assert_neighbor(import_player, &"focus_neighbor_bottom", import_host) + _assert_neighbor(export_host, &"focus_neighbor_top", export_player) + _assert_neighbor(export_host, &"focus_neighbor_right", import_host) + _assert_neighbor(import_host, &"focus_neighbor_top", import_player) + _assert_neighbor(import_host, &"focus_neighbor_left", export_host) _assert_directionally_reachable(controls.front(), controls) + await process_frame + open_data_folder.grab_focus() + panel.call("_select_page", &"sound", true) + _expect( + panel.get_active_page_id() == &"sound", + "Settings did not leave the Data page.", + ) + _expect( + root.gui_get_focus_owner() == panel.get_node("%SoundTab"), + "Leaving Data retained focus on a hidden Data action.", + ) + var sound_down: Control = ( + (panel.get_node("%SoundTab") as Control).find_valid_focus_neighbor( + SIDE_BOTTOM + ) + ) + _expect( + sound_down != null and not panel.get_node("%DataPage").is_ancestor_of( + sound_down + ), + "Sound navigation still pointed into the hidden Data page.", + ) + var hidden_data_activation_count: int = int(open_activation["count"]) + Input.parse_input_event(action_press) + await process_frame + Input.parse_input_event(action_release) + for _frame: int in 2: + await process_frame + _expect( + int(open_activation["count"]) == hidden_data_activation_count, + "Controller A activated Open Data Folder from another settings page.", + ) + open_data_folder.pressed.emit() + await process_frame + _expect( + panel.find_children( + "*", "ConfirmationDialog", true, false + ).is_empty(), + "A hidden Data action created a folder dialog.", + ) + panel.call("_select_page", &"data", true) + await process_frame + copy_fingerprint.grab_focus() + var feedback := panel.get_node("%SettingsFeedback") as Label + feedback.text = "activation boundary intact" + open_data_folder.pressed.emit() + await process_frame + _expect( + feedback.text == "activation boundary intact", + ( + "Open Data Folder crossed its activation boundary without owning " + + "controller focus." + ), + ) + panel.hide() + await process_frame + var hidden_focus_owner: Control = root.gui_get_focus_owner() + _expect( + hidden_focus_owner == null + or not panel.is_ancestor_of(hidden_focus_owner), + "Hiding Settings retained focus on one of its controls.", + ) _expect( panel.find_children("*", "BubbleButton", true, false).is_empty(), "Settings children still contain bubble controls.", ) panel.queue_free() - test_data_root.queue_free() await process_frame @@ -313,6 +447,11 @@ func _validate_settings_adjustment_navigation() -> void: var controller_slider := panel.get_node( "%ControllerSensitivitySlider" ) as HSlider + var on_screen_keyboard := panel.get_node( + "%OnScreenKeyboardToggle" + ) as Button + var controller_binds := panel.get_node("%ControllerMapping") as Button + var keyboard_binds := panel.get_node("%KeyboardMapping") as Button _expect( mouse_slider.focus_mode == Control.FOCUS_ALL and controller_slider.focus_mode == Control.FOCUS_ALL, @@ -323,14 +462,39 @@ func _validate_settings_adjustment_navigation() -> void: and is_equal_approx(controller_slider.step, 0.1), "Sensitivity sliders do not retain their authored increments.", ) + _assert_neighbor( + on_screen_keyboard, + &"focus_neighbor_bottom", + controller_binds, + ) + _assert_neighbor( + controller_binds, + &"focus_neighbor_top", + on_screen_keyboard, + ) + _assert_neighbor( + controller_binds, + &"focus_neighbor_right", + keyboard_binds, + ) + _assert_neighbor( + keyboard_binds, + &"focus_neighbor_top", + on_screen_keyboard, + ) + _assert_neighbor( + keyboard_binds, + &"focus_neighbor_left", + controller_binds, + ) var control_inputs: Array[Control] = [ panel.get_node("%ControlsTab") as Control, mouse_slider, controller_slider, panel.get_node("%InvertYToggle") as Control, - panel.get_node("%OnScreenKeyboardToggle") as Control, - panel.get_node("%ControllerMapping") as Control, - panel.get_node("%KeyboardMapping") as Control, + on_screen_keyboard, + controller_binds, + keyboard_binds, panel.get_node("%ApplySettingsButton") as Control, panel.get_node("%SettingsBackButton") as Control, ] @@ -339,6 +503,38 @@ func _validate_settings_adjustment_navigation() -> void: await process_frame +func _validate_settings_presentation_requires_apply() -> void: + var panel := SettingsPanelScene.instantiate() as SettingsPanel + var settings_manager := PlayerSettingsManager.new() + root.add_child(settings_manager) + root.add_child(panel) + await process_frame + panel.open_panel(settings_manager) + await process_frame + var chat_dock := panel.get_node("%ChatDockSelector") as OptionButton + var chat_mode := panel.get_node("%ChatModeSelector") as OptionButton + var paint_dock := panel.get_node("%PaintDockSelector") as OptionButton + chat_dock.select(1) + chat_dock.item_selected.emit(1) + chat_mode.select(1) + chat_mode.item_selected.emit(1) + paint_dock.select(0) + paint_dock.item_selected.emit(0) + _expect( + not settings_manager.current_settings.chat_dock_right + and not settings_manager.current_settings.chat_mobile_mode + and settings_manager.current_settings.paint_dock_right, + ( + "Navigating presentation choices changed saved docking settings " + + "before Apply." + ), + ) + panel.close_panel(true) + panel.queue_free() + settings_manager.queue_free() + await process_frame + + func _validate_mail_navigation() -> void: var page := MailPageType.new() as Control root.add_child(page) @@ -1334,13 +1530,8 @@ func _assert_directionally_reachable( 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 + for side: Side in [SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, SIDE_BOTTOM]: + var neighbor: Control = current.find_valid_focus_neighbor(side) if ( neighbor == null or not expected.has(neighbor.get_instance_id()) diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index adc0ec4..8355ef1 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -151,7 +151,11 @@ func _validate_player_page_zone_contracts() -> void: var net_source: String = FileAccess.get_file_as_string( "res://ui/the_net_page.gd" ) - assert(net_source.contains("ROLE_RIGHT_STICK_Y")) + var game_ui_source: String = FileAccess.get_file_as_string( + "res://ui/game_ui.gd" + ) + assert(not net_source.contains("ROLE_RIGHT_STICK_Y")) + assert(game_ui_source.contains("_update_controller_menu_scroll")) func _validate_shop_navigation_contract() -> void: diff --git a/tests/file_dialog_controller_navigation_validation.gd b/tests/file_dialog_controller_navigation_validation.gd index 838196a..7ac8df9 100644 --- a/tests/file_dialog_controller_navigation_validation.gd +++ b/tests/file_dialog_controller_navigation_validation.gd @@ -172,7 +172,15 @@ func _run() -> void: ) ) assert(dialog.gui_get_focus_owner() == select_button) - assert(FileDialogControllerNavigationType.activate_control(sort_button)) + sort_button.grab_focus() + var accept_press := InputEventAction.new() + accept_press.action = &"ui_accept" + accept_press.pressed = true + Input.parse_input_event(accept_press) + var accept_release := InputEventAction.new() + accept_release.action = &"ui_accept" + accept_release.pressed = false + Input.parse_input_event(accept_release) await process_frame assert(sort_button.get_popup().visible) sort_button.get_popup().hide() @@ -283,13 +291,16 @@ func _validate_compact_dialog() -> void: var down_event := InputEventJoypadButton.new() down_event.button_index = JOY_BUTTON_DPAD_DOWN down_event.pressed = true - font_controller.call("_input", down_event) + var dialog_controller := font_controller.get( + "_file_dialog_controller" + ) as FileDialogController + dialog_controller.call("_input", down_event) assert(scope.gui_get_focus_owner() == select_button) path_edit.grab_focus() var accept_event := InputEventJoypadButton.new() accept_event.button_index = JOY_BUTTON_A accept_event.pressed = true - font_controller.call("_input", accept_event) + dialog_controller.call("_input", accept_event) assert(keyboard.is_open()) keyboard.call("_close_keyboard", true) dialog.queue_free() diff --git a/tests/fishing_shop_controller_validation.gd b/tests/fishing_shop_controller_validation.gd index 97263c3..488a01e 100644 --- a/tests/fishing_shop_controller_validation.gd +++ b/tests/fishing_shop_controller_validation.gd @@ -32,6 +32,9 @@ func _run() -> void: var accept := InputEventJoypadButton.new() accept.button_index = JOY_BUTTON_A accept.pressed = true + var accept_release := InputEventJoypadButton.new() + accept_release.button_index = JOY_BUTTON_A + accept_release.pressed = false var cancel := InputEventJoypadButton.new() cancel.button_index = JOY_BUTTON_B cancel.pressed = true @@ -72,7 +75,10 @@ func _run() -> void: )) if section_index == FishingShop.ShopSection.BAIT: assert(root.gui_get_focus_owner() == dummy_stock) - shop.call("_input", accept) + Input.parse_input_event(accept) + await process_frame + Input.parse_input_event(accept_release) + await process_frame assert(activations[0] == 1) shop.call("_input", cancel) for _frame: int in 2: diff --git a/ui/components/bubble_menu/bubble_confirmation_page.gd b/ui/components/bubble_menu/bubble_confirmation_page.gd index 80f17bb..0565406 100644 --- a/ui/components/bubble_menu/bubble_confirmation_page.gd +++ b/ui/components/bubble_menu/bubble_confirmation_page.gd @@ -23,9 +23,6 @@ enum InitialFocus { ^"BubbleCluster/CancelButton/CancelLabel" ) @export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0) -@export var compact_maximum_layout_size: Vector2 = Vector2(544.0, 400.0) -@export var compact_width_threshold: float = 680.0 -@export var compact_height_threshold: float = 500.0 @export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 var _cluster: BubbleCluster @@ -158,15 +155,9 @@ func _process(delta: float) -> void: func _update_layout() -> void: if not is_node_ready() or _cluster == null: return - var compact: bool = false - var layout_maximum: Vector2 = ( - compact_maximum_layout_size - if compact - else maximum_layout_size - ) var field_size := Vector2( - minf(size.x, layout_maximum.x), - minf(size.y, layout_maximum.y) + minf(size.x, maximum_layout_size.x), + minf(size.y, maximum_layout_size.y) ) field_size.x = maxf(1.0, field_size.x) field_size.y = maxf(1.0, field_size.y) @@ -174,7 +165,7 @@ func _update_layout() -> void: if not _is_transitioning: _cluster.position = _resting_cluster_position _cluster.size = field_size - _cluster.apply_layout(field_size, compact) + _cluster.apply_layout(field_size, false) func _configure_focus() -> void: diff --git a/ui/controller_focus_recovery.gd b/ui/controller_focus_recovery.gd deleted file mode 100644 index bcaf6c8..0000000 --- a/ui/controller_focus_recovery.gd +++ /dev/null @@ -1,277 +0,0 @@ -class_name ControllerFocusRecovery -extends Node - -const CONTROLLER_AXIS_THRESHOLD: float = 0.35 -const SEMANTIC_MATCH_BONUS: float = 1000000.0 -const KEYBOARD_NAVIGATION_ACTIONS: Array[StringName] = [ - &"ui_accept", - &"ui_cancel", - &"ui_up", - &"ui_down", - &"ui_left", - &"ui_right", - &"ui_focus_next", - &"ui_focus_prev", - &"ui_page_up", - &"ui_page_down", - &"ui_home", - &"ui_end", -] - -var _controller_active: bool = false -var _focus_navigation_active: bool = false -var _last_focus_center: Vector2 = Vector2.ZERO -var _last_focus_key: String = "" -var _scope_chain: Array[WeakRef] = [] -var _pending_focus: WeakRef -var _pointer_button_down: bool = false -var _recovery_generation: int = 0 - - -func _ready() -> void: - set_process_input(true) - set_process(true) - get_viewport().gui_focus_changed.connect(_on_gui_focus_changed) - - -func _exit_tree() -> void: - var viewport := get_viewport() - if viewport.gui_focus_changed.is_connected(_on_gui_focus_changed): - viewport.gui_focus_changed.disconnect(_on_gui_focus_changed) - - -func _input(event: InputEvent) -> void: - if event is InputEventJoypadButton: - var button_event := event as InputEventJoypadButton - if button_event.pressed: - _pointer_button_down = false - _controller_active = true - _focus_navigation_active = true - if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER: - _recovery_generation += 1 - _scope_chain.clear() - _pending_focus = null - return - _request_pending_focus() - return - if event is InputEventJoypadMotion: - if absf((event as InputEventJoypadMotion).axis_value) >= ( - CONTROLLER_AXIS_THRESHOLD - ): - _pointer_button_down = false - _controller_active = true - _focus_navigation_active = true - _request_pending_focus() - return - if event is InputEventMouseMotion: - _pointer_button_down = ( - (event as InputEventMouseMotion).button_mask != 0 - ) - _leave_focus_navigation(not _pointer_button_down) - return - if event is InputEventMouseButton: - var mouse_button := event as InputEventMouseButton - if mouse_button.button_index in [ - MOUSE_BUTTON_LEFT, - MOUSE_BUTTON_RIGHT, - MOUSE_BUTTON_MIDDLE, - ]: - _pointer_button_down = mouse_button.pressed - _leave_focus_navigation(false) - if not mouse_button.pressed: - _release_current_pointer_focus.call_deferred() - return - if event is InputEventKey and (event as InputEventKey).pressed: - _pointer_button_down = false - _controller_active = false - if _is_keyboard_navigation_event(event): - _focus_navigation_active = true - _request_pending_focus() - else: - _leave_focus_navigation() - - -func _process(_delta: float) -> void: - if ( - _controller_active - and not _scope_chain.is_empty() - and get_viewport().gui_get_focus_owner() == null - ): - _recover_focus(_recovery_generation) - - -func _on_gui_focus_changed(control: Control) -> void: - _recovery_generation += 1 - if control != null: - if _focus_navigation_active: - _pending_focus = null - if _controller_active and control is BaseButton: - _remember_focus(control) - return - if _keeps_pointer_focus(control): - return - if _is_focusable(control): - _pending_focus = weakref(control) - _release_focus_if_inactive.call_deferred(control) - return - if not _controller_active or _scope_chain.is_empty(): - return - var generation: int = _recovery_generation - _recover_focus.call_deferred(generation) - - -func _remember_focus(control: Control) -> void: - _last_focus_center = control.get_global_rect().get_center() - _last_focus_key = _semantic_key(control) - _scope_chain.clear() - var recovery_root: Node = get_parent() - var ancestor: Node = control.get_parent() - while ancestor != null: - if ancestor is Control: - _scope_chain.append(weakref(ancestor)) - if ancestor == recovery_root: - break - ancestor = ancestor.get_parent() - - -func _recover_focus(generation: int) -> void: - if generation != _recovery_generation: - return - if not _controller_active or get_viewport().gui_get_focus_owner() != null: - return - for scope_reference: WeakRef in _scope_chain: - var scope := scope_reference.get_ref() as Control - if ( - scope == null - or not is_instance_valid(scope) - or not scope.is_inside_tree() - ): - continue - if not scope.is_visible_in_tree(): - _scope_chain.clear() - return - var replacement := _best_replacement_in(scope) - if replacement != null: - replacement.grab_focus() - else: - _scope_chain.clear() - return - _scope_chain.clear() - - -func _best_replacement_in(scope: Control) -> Control: - var best: Control = null - var best_score: float = INF - for node: Node in scope.find_children("*", "Control", true, false): - var candidate := node as Control - if not _is_focusable(candidate): - continue - var distance: float = candidate.get_global_rect().get_center().distance_squared_to( - _last_focus_center - ) - if not _last_focus_key.is_empty() and _semantic_key(candidate) == _last_focus_key: - distance -= SEMANTIC_MATCH_BONUS - if distance < best_score: - best = candidate - best_score = distance - return best - - -func _is_focusable(control: Control) -> bool: - if ( - control == null - or not control.is_inside_tree() - 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 - - -func _semantic_key(control: Control) -> String: - if control.has_meta(&"controller_focus_key"): - return str(control.get_meta(&"controller_focus_key")) - var button := control as Button - var label: String = button.text if button != null else "" - var tooltip: String = control.tooltip_text - tooltip = tooltip.trim_suffix(" (show variants)") - tooltip = tooltip.trim_suffix(" (hide variants)") - return "%s|%s|%s" % [control.get_class(), label, tooltip] - - -func _leave_focus_navigation(release_focus: bool = true) -> void: - _controller_active = false - _focus_navigation_active = false - _recovery_generation += 1 - _scope_chain.clear() - var focus_owner: Control = get_viewport().gui_get_focus_owner() - if focus_owner == null or _keeps_pointer_focus(focus_owner): - return - if _is_focusable(focus_owner): - _pending_focus = weakref(focus_owner) - if release_focus: - _release_focus_if_inactive.call_deferred(focus_owner) - - -func _release_focus_if_inactive(control: Control) -> void: - if ( - _focus_navigation_active - or _pointer_button_down - or control == null - or not is_instance_valid(control) - or get_viewport().gui_get_focus_owner() != control - or _keeps_pointer_focus(control) - ): - return - get_viewport().gui_release_focus() - - -func _release_current_pointer_focus() -> void: - var focus_owner: Control = get_viewport().gui_get_focus_owner() - if focus_owner != null: - _release_focus_if_inactive(focus_owner) - - -func _request_pending_focus() -> void: - if ( - not _focus_navigation_active - or _pending_focus == null - or get_viewport().gui_get_focus_owner() != null - ): - return - _restore_pending_focus.call_deferred(_recovery_generation) - - -func _restore_pending_focus(generation: int) -> void: - if ( - generation != _recovery_generation - or not _focus_navigation_active - or _pending_focus == null - or get_viewport().gui_get_focus_owner() != null - ): - return - var target := _pending_focus.get_ref() as Control - if not _is_focusable(target): - _pending_focus = null - return - _pending_focus = null - target.grab_focus() - - -func _is_keyboard_navigation_event(event: InputEvent) -> bool: - if event.is_action_pressed(&"open_backpack"): - return false - if ( - event.is_action_pressed(&"ui_cancel") - and get_viewport().gui_get_focus_owner() == null - ): - return false - for action: StringName in KEYBOARD_NAVIGATION_ACTIONS: - if event.is_action_pressed(action): - return true - return false - - -func _keeps_pointer_focus(control: Control) -> bool: - return control is LineEdit or control is TextEdit diff --git a/ui/emote_radial_menu.gd b/ui/emote_radial_menu.gd index c081e27..1d6c245 100644 --- a/ui/emote_radial_menu.gd +++ b/ui/emote_radial_menu.gd @@ -1,153 +1,28 @@ class_name EmoteRadialMenu -extends Control +extends RadialActionMenu signal emote_selected(emote_id: StringName) -const BubbleButtonScene: PackedScene = preload( - "res://ui/components/bubble_menu/bubble_button.tscn" -) -const BubbleProfile: BubbleMenuProfile = preload( - "res://ui/components/bubble_menu/bubble_menu_profile.tres" -) const SECTOR_COUNT: int = 8 const SIT_SECTOR: int = 0 -const RING_RADIUS: float = 172.0 -const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) -const CONTROLLER_SELECTION_DEADZONE: float = 0.35 -const ControllerMappingManagerType = preload( - "res://settings/controller_mapping_manager.gd" -) - -var _buttons: Array[BubbleButton] = [] -var _is_open: bool = false -var _selected_sector: int = SIT_SECTOR -var _controller_selection_mode: bool = false -var _controller_mapping_manager: ControllerMappingManagerType -func setup_controller_mapping( - mapping_manager: ControllerMappingManagerType, -) -> void: - _controller_mapping_manager = mapping_manager +func _input_action_name() -> StringName: + return &"open_emotes" -func _ready() -> void: - visible = false - mouse_filter = Control.MOUSE_FILTER_IGNORE - for sector: int in SECTOR_COUNT: - var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton - bubble.profile = BubbleProfile - bubble.focus_mode = Control.FOCUS_NONE - bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE - bubble.text = "Sit" if sector == SIT_SECTOR else "" - add_child(bubble) - _buttons.append(bubble) - _apply_selection_styles() +func _sector_count() -> int: + return SECTOR_COUNT -func handle_input(event: InputEvent, can_open: bool) -> bool: - if not event.is_action("open_emotes"): - return false - var key_event: InputEventKey = event as InputEventKey - if key_event != null and key_event.echo: - return false - if event.is_pressed(): - if _is_open or not can_open: - return _is_open - open_menu(event is InputEventJoypadButton) - return true - if not _is_open: - return false - var selected: int = _selected_sector - close_menu() - if selected == SIT_SECTOR: +func _initial_sector() -> int: + return SIT_SECTOR + + +func _label_for_sector(sector: int) -> String: + return "Sit" if sector == SIT_SECTOR else "" + + +func _emit_selected_sector(sector: int) -> void: + if sector == SIT_SECTOR: emote_selected.emit(&"sit") - return true - - -func open_menu(controller_selection: bool = false) -> void: - _is_open = true - _selected_sector = SIT_SECTOR - _controller_selection_mode = controller_selection - visible = true - _layout_bubbles() - _apply_selection_styles() - - -func close_menu() -> void: - _is_open = false - visible = false - - -func is_open() -> bool: - return _is_open - - -func _process(_delta: float) -> void: - if not _is_open: - return - _layout_bubbles() - _update_selection() - - -func _layout_bubbles() -> void: - var center: Vector2 = size * 0.5 - for sector: int in SECTOR_COUNT: - var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT) - var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS - var bubble: BubbleButton = _buttons[sector] - bubble.position = bubble_center - BUBBLE_SIZE * 0.5 - bubble.size = BUBBLE_SIZE - bubble.pivot_offset = BUBBLE_SIZE * 0.5 - - -func _update_selection() -> void: - var stick: Vector2 = _get_selection_stick() - if stick.length() >= CONTROLLER_SELECTION_DEADZONE: - _select_sector_from_offset(stick) - return - if _controller_selection_mode: - return - _update_mouse_selection() - - -func _get_selection_stick() -> Vector2: - if _controller_mapping_manager != null: - return Vector2( - _controller_mapping_manager.get_role_axis( - ControllerMappingManagerType.ROLE_RIGHT_STICK_X - ), - _controller_mapping_manager.get_role_axis( - ControllerMappingManagerType.ROLE_RIGHT_STICK_Y - ), - ) - return Vector2( - Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), - Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), - ) - - -func _update_mouse_selection() -> void: - var offset: Vector2 = get_local_mouse_position() - size * 0.5 - if offset.length() < 24.0: - return - _select_sector_from_offset(offset) - - -func _select_sector_from_offset(offset: Vector2) -> void: - var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU) - var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT)))) - if sector == _selected_sector: - return - _selected_sector = sector - _apply_selection_styles() - - -func _apply_selection_styles() -> void: - for sector: int in _buttons.size(): - var bubble: BubbleButton = _buttons[sector] - bubble.apply_profile() - if sector == _selected_sector: - bubble.add_theme_stylebox_override( - "normal", BubbleProfile.make_hover_style() - ) diff --git a/ui/file_dialog_controller.gd b/ui/file_dialog_controller.gd new file mode 100644 index 0000000..ea20494 --- /dev/null +++ b/ui/file_dialog_controller.gd @@ -0,0 +1,128 @@ +class_name FileDialogController +extends Node + +const FileDialogControllerNavigationType = preload( + "res://ui/file_dialog_controller_navigation.gd" +) + +var _text_entry_request: Callable +var _text_entry_is_open: Callable +var _tracked_dialogs: Dictionary[int, WeakRef] = {} + + +func setup_text_entry( + request: Callable, + is_open: Callable = Callable(), +) -> void: + _text_entry_request = request + _text_entry_is_open = is_open + + +func track_dialog(dialog: FileDialog) -> void: + if dialog != null: + _tracked_dialogs[dialog.get_instance_id()] = weakref(dialog) + + +func _process(_delta: float) -> void: + if _text_entry_is_active(): + return + for instance_id: int in _tracked_dialogs.keys(): + var reference: WeakRef = _tracked_dialogs[instance_id] + var dialog := reference.get_ref() as FileDialog + if dialog == null or not is_instance_valid(dialog): + _tracked_dialogs.erase(instance_id) + continue + if not dialog.visible: + continue + FileDialogControllerNavigationType.configure(dialog) + _connect_text_controls(dialog) + + +func _input(event: InputEvent) -> void: + if _text_entry_is_active(): + return + if not (event is InputEventJoypadButton or event is InputEventJoypadMotion): + return + var scope: Window = _active_scope() + if scope == null: + return + var focused: Control = scope.gui_get_focus_owner() + if focused == null: + return + var direction: Vector2 = _controller_direction(event) + var item_list := focused as ItemList + if ( + item_list != null + and direction != Vector2.ZERO + and FileDialogControllerNavigationType.move_from_item_list( + item_list, direction + ) + ): + get_viewport().set_input_as_handled() + return + var button_event := event as InputEventJoypadButton + if ( + button_event != null + and button_event.pressed + and event.is_action_pressed(&"ui_accept") + and (focused is LineEdit or focused is TextEdit) + and _text_entry_request.is_valid() + and bool(_text_entry_request.call(focused)) + ): + get_viewport().set_input_as_handled() + + +func _active_scope() -> Window: + for reference_value: WeakRef in _tracked_dialogs.values(): + var dialog := reference_value.get_ref() as FileDialog + if dialog == null or not is_instance_valid(dialog) or not dialog.visible: + continue + return FileDialogControllerNavigationType.active_scope(dialog) + return null + + +func _controller_direction(event: InputEvent) -> Vector2: + if event.is_action_pressed(&"ui_up"): + return Vector2.UP + if event.is_action_pressed(&"ui_down"): + return Vector2.DOWN + if event.is_action_pressed(&"ui_left"): + return Vector2.LEFT + if event.is_action_pressed(&"ui_right"): + return Vector2.RIGHT + return Vector2.ZERO + + +func _connect_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_text_gui_input.bind(control) + if not control.gui_input.is_connected(callback): + control.gui_input.connect(callback) + + +func _on_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 _text_entry_request.is_valid() + ): + return + if bool(_text_entry_request.call(control)): + control.accept_event() + + +func _text_entry_is_active() -> bool: + return ( + _text_entry_is_open.is_valid() + and bool(_text_entry_is_open.call()) + ) diff --git a/ui/file_dialog_controller.gd.uid b/ui/file_dialog_controller.gd.uid new file mode 100644 index 0000000..7ef6dbe --- /dev/null +++ b/ui/file_dialog_controller.gd.uid @@ -0,0 +1 @@ +uid://dtqkbg7flp1pl diff --git a/ui/file_dialog_controller_navigation.gd b/ui/file_dialog_controller_navigation.gd index e475ec1..a368ac8 100644 --- a/ui/file_dialog_controller_navigation.gd +++ b/ui/file_dialog_controller_navigation.gd @@ -72,20 +72,6 @@ static func interactive_controls(scope: Window) -> Array[Control]: return controls -static func activate_control(control: Control) -> bool: - if control == null or not control.is_visible_in_tree(): - return false - var menu_button := control as MenuButton - if menu_button != null and not menu_button.disabled: - menu_button.show_popup() - return true - var button := control as BaseButton - if button != null and not button.disabled: - button.pressed.emit() - return true - return false - - static func move_from_item_list( item_list: ItemList, direction: Vector2, diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 4cebb6d..68827e9 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -236,13 +236,15 @@ func _input(event: InputEvent) -> void: close_shop() return if uses_accept: + if _controller_zone != ControllerZone.TABS: + # Content buttons use Godot's native ui_accept activation. Keeping a + # second manual pressed.emit() path here allowed one controller press + # to be interpreted by two different owners. + return get_viewport().set_input_as_handled() if not button_event.pressed or _transaction_in_progress: return - if _controller_zone == ControllerZone.TABS: - _enter_shop_content_zone() - else: - _activate_focused_shop_control() + _enter_shop_content_zone() return var uses_left_bumper: bool = ( _controller_mapping_manager.event_uses_role( @@ -295,19 +297,6 @@ func _enter_shop_content_zone() -> void: _select_shop_section(section_index, true) -func _activate_focused_shop_control() -> bool: - var focused := get_viewport().gui_get_focus_owner() as BaseButton - if ( - focused == null - or focused.disabled - or _is_shop_tab(focused) - or not focused.is_visible_in_tree() - ): - return false - focused.pressed.emit() - return true - - func _focused_shop_tab_index() -> int: var focused: Control = get_viewport().gui_get_focus_owner() for tab_index: int in _shop_tabs.size(): @@ -430,11 +419,34 @@ func _configure_controller_focus() -> void: if not visible or _cooler_page_active: return _apply_shop_controller_zone_focus_modes() - var candidates: Array[Control] = [] - _collect_controller_focusables(self, candidates) + var candidates: Array[Control] = _active_shop_controller_controls() ControllerFocusNavigationType.configure_spatial_neighbors(candidates) +func _active_shop_controller_controls() -> Array[Control]: + var controls: Array[Control] = [] + if _controller_zone == ControllerZone.TABS: + for tab: OrganizerTab in _shop_tabs: + if ControllerFocusNavigationType.is_focusable(tab): + controls.append(tab) + return controls + var active_root: Control = ( + _upgrades_content + if _shop_section == ShopSection.UPGRADES + else _supplies_content + ) + for node: Node in active_root.find_children( + "*", "BaseButton", true, false + ): + var button := node as BaseButton + if ControllerFocusNavigationType.is_focusable(button): + controls.append(button) + var close_button := %CloseButton as Button + if ControllerFocusNavigationType.is_focusable(close_button): + controls.append(close_button) + return controls + + func _apply_shop_controller_zone_focus_modes() -> void: var tabs_active: bool = _controller_zone == ControllerZone.TABS for tab: OrganizerTab in _shop_tabs: @@ -459,22 +471,6 @@ func _set_descendant_button_focus_mode( button.focus_mode = focus_mode -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: for tab_index: int in range(_shop_tabs.size()): _shop_tabs[tab_index].set_selected( diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 766dd11..42fb025 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -1268,6 +1268,10 @@ func _update_controller_menu_scroll(delta: float) -> void: _controller_mapping_manager == null or _virtual_mouse_active or is_input_mapping_capturing() + or ( + _player_menu.visible + and not _player_menu.allows_global_controller_scroll() + ) ): return var stick_y: float = _controller_menu_scroll_axis() diff --git a/ui/interface_font_controller.gd b/ui/interface_font_controller.gd index f504236..a27e56a 100644 --- a/ui/interface_font_controller.gd +++ b/ui/interface_font_controller.gd @@ -1,8 +1,8 @@ class_name InterfaceFontController extends Node -const FileDialogControllerNavigationType = preload( - "res://ui/file_dialog_controller_navigation.gd" +const FileDialogControllerType = preload( + "res://ui/file_dialog_controller.gd" ) const STANDARD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf") const COMPACT_FILE_DIALOG_LIMIT := Vector2i(800, 600) @@ -14,13 +14,19 @@ 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] = {} +var _file_dialog_controller: FileDialogControllerType func _ready() -> void: enforce_standard_font() _utility_theme = _game_theme.duplicate(true) _utility_theme.default_font = STANDARD_FONT + _file_dialog_controller = FileDialogControllerType.new() + add_child(_file_dialog_controller) + _file_dialog_controller.setup_text_entry( + _controller_text_entry_request, + _controller_text_entry_is_open, + ) func enforce_standard_font() -> void: @@ -35,6 +41,8 @@ func set_controller_text_entry_request( ) -> void: _controller_text_entry_request = request _controller_text_entry_is_open = is_open + if _file_dialog_controller != null: + _file_dialog_controller.setup_text_entry(request, is_open) func set_readable_font_enabled(_enabled: bool) -> void: @@ -61,7 +69,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) + _file_dialog_controller.track_dialog(dialog) var host_window: Window = dialog.get_parent().get_window() var host_size: Vector2i = host_window.size var compact: bool = ( @@ -98,113 +106,6 @@ 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 _input(event: InputEvent) -> void: - if ( - _controller_text_entry_is_open.is_valid() - and bool(_controller_text_entry_is_open.call()) - ): - return - if not (event is InputEventJoypadButton or event is InputEventJoypadMotion): - return - var scope: Window = _active_file_dialog_scope() - if scope == null: - return - var focused: Control = scope.gui_get_focus_owner() - if focused == null: - return - var direction: Vector2 = _controller_direction(event) - var item_list := focused as ItemList - if ( - item_list != null - and direction != Vector2.ZERO - and FileDialogControllerNavigationType.move_from_item_list( - item_list, direction - ) - ): - get_viewport().set_input_as_handled() - return - var button_event := event as InputEventJoypadButton - if ( - button_event != null - and button_event.pressed - and event.is_action_pressed(&"ui_accept") - and (focused is LineEdit or focused is TextEdit) - and _controller_text_entry_request.is_valid() - and bool(_controller_text_entry_request.call(focused)) - ): - get_viewport().set_input_as_handled() - - -func _active_file_dialog_scope() -> Window: - for reference_value: WeakRef in _tracked_file_dialogs.values(): - var dialog := reference_value.get_ref() as FileDialog - if dialog == null or not is_instance_valid(dialog) or not dialog.visible: - continue - return FileDialogControllerNavigationType.active_scope(dialog) - return null - - -func _controller_direction(event: InputEvent) -> Vector2: - if event.is_action_pressed(&"ui_up"): - return Vector2.UP - if event.is_action_pressed(&"ui_down"): - return Vector2.DOWN - if event.is_action_pressed(&"ui_left"): - return Vector2.LEFT - if event.is_action_pressed(&"ui_right"): - return Vector2.RIGHT - return Vector2.ZERO - - -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, diff --git a/ui/mail_page.gd b/ui/mail_page.gd index d2a063b..792aa81 100644 --- a/ui/mail_page.gd +++ b/ui/mail_page.gd @@ -137,32 +137,96 @@ func handle_controller_input(event: InputEvent) -> bool: func _refresh_controller_navigation() -> void: - var controls: Array[Control] = [] - _collect_visible_controller_controls(self, controls) - for control: Control in controls: + var active_controls: Array[Control] = _active_controller_controls() + for control: Control in _all_controller_controls(): var button := control as BaseButton control.focus_mode = ( Control.FOCUS_ALL - if _interactive and (button == null or not button.disabled) + if ( + _interactive + and control in active_controls + and control.is_visible_in_tree() + and (button == null or not button.disabled) + ) else Control.FOCUS_NONE ) - ControllerFocusNavigation.configure_spatial_neighbors(controls) + ControllerFocusNavigation.configure_spatial_neighbors(active_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(): + if not _interactive or active_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() + active_controls.front().grab_focus() + + +func _active_controller_controls() -> Array[Control]: + if _compose != null and _compose.visible: + return _compose_controller_controls() + if _letter != null and _letter.visible: + return _letter_controller_controls() + return _inbox_controller_controls() + + +func _all_controller_controls() -> Array[Control]: + var controls: Array[Control] = _inbox_controller_controls() + controls.append_array(_compose_controller_controls()) + controls.append_array(_letter_controller_controls()) + return controls + + +func _inbox_controller_controls() -> Array[Control]: + var controls: Array[Control] = [] + _append_controller_control(controls, _archive_view_button) + _append_controller_control(controls, _send_mail_button) + if _inbox_list != null: + for child: Node in _inbox_list.get_children(): + _append_controller_control(controls, child as Button) + return controls + + +func _compose_controller_controls() -> Array[Control]: + var controls: Array[Control] = [] + for control: Control in [ + _greeting, + _recipient, + _attachment_kind, + _body, + _attachment_choice, + _salutation, + _amount_minus, + _attachment_amount, + _amount_plus, + _compose_cancel, + _send_button, + ]: + _append_controller_control(controls, control) + return controls + + +func _letter_controller_controls() -> Array[Control]: + var controls: Array[Control] = [] + for control: Control in [ + _accept, + _decline, + _letter_close, + _archive, + _delete, + ]: + _append_controller_control(controls, control) + return controls + + +func _append_controller_control( + controls: Array[Control], + control: Control, +) -> void: + if control != null and control.is_visible_in_tree(): + controls.append(control) func _configure_inbox_controller_navigation() -> void: @@ -358,22 +422,6 @@ func _controller_focus_eligible(control: Control) -> bool: 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: var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) var root := Control.new() @@ -1041,12 +1089,6 @@ func _attachment_text(attachment: Dictionary) -> String: return "Invalid gift." -func _focus_first() -> void: - var buttons := _inbox_list.find_children("*", "Button", true, false) - if not buttons.is_empty(): - (buttons.front() as Button).grab_focus() - - func _style_controls(root: Node) -> void: for node: Node in root.find_children("*", "", true, false): if node is BaseButton: diff --git a/ui/network/join_game_page.gd b/ui/network/join_game_page.gd index 94ddbfa..a6e5c11 100644 --- a/ui/network/join_game_page.gd +++ b/ui/network/join_game_page.gd @@ -347,7 +347,7 @@ func _on_save_pressed() -> void: true, ) _set_mode(Mode.SAVED) - _select_entry_id(existing.entry_id) + _restore_entry_selection_and_focus(0, existing.entry_id) return _address.text = endpoint.normalized_display _name_edit.text = ( @@ -389,8 +389,7 @@ func _commit_name_entry() -> void: _clear_edit_state() _mode = Mode.SAVED _refresh_entries() - _select_entry_id(saved.entry_id) - _refresh() + _restore_entry_selection_and_focus(0, saved.entry_id) func _on_edit_pressed() -> void: @@ -412,8 +411,7 @@ func _on_favorite_pressed() -> void: var entry_id: String = _selected_entry.entry_id _saved_servers.set_favorite(entry_id, not _selected_entry.favorite) _refresh_entries() - _select_entry_id(entry_id) - _refresh() + _restore_entry_selection_and_focus(0, entry_id) func _on_delete_pressed() -> void: @@ -437,10 +435,12 @@ func _confirm_delete() -> void: if _selected_entry == null or not _delete_armed: _cancel_delete() return + var removed_index: int = _visible_entries.find(_selected_entry) + var selected_id: String = _selected_entry.entry_id var removed: bool = ( - _saved_servers.remove_recent_entry(_selected_entry.entry_id) + _saved_servers.remove_recent_entry(selected_id) if _mode == Mode.RECENT - else _saved_servers.remove_saved_entry(_selected_entry.entry_id) + else _saved_servers.remove_saved_entry(selected_id) ) _set_status( ( @@ -452,11 +452,13 @@ func _confirm_delete() -> void: else "Could not remove the local entry.", not removed, ) - _selected_entry = null _delete_armed = false - _refresh_entries() - _refresh() _delete_confirmation.hide_page() + _refresh_entries() + _restore_entry_selection_and_focus( + maxi(removed_index, 0), + "" if removed else selected_id, + ) func _cancel_delete() -> void: @@ -478,12 +480,51 @@ func _on_list_item_selected(index: int) -> void: _refresh() -func _select_entry_id(entry_id: String) -> void: +func _select_entry_id(entry_id: String) -> bool: for index: int in range(_visible_entries.size()): if _visible_entries[index].entry_id == entry_id: - _server_list.select(index) - _selected_entry = _visible_entries[index] - return + return _select_entry_index(index) + return false + + +func _select_entry_index(index: int) -> bool: + if index < 0 or index >= _visible_entries.size(): + return false + _server_list.select(index) + _server_list.ensure_current_is_visible() + _selected_entry = _visible_entries[index] + return true + + +func _restore_entry_selection_and_focus( + preferred_index: int, + preferred_id: String = "", +) -> void: + var selected: bool = ( + not preferred_id.is_empty() and _select_entry_id(preferred_id) + ) + if not selected and not _visible_entries.is_empty(): + selected = _select_entry_index( + clampi(preferred_index, 0, _visible_entries.size() - 1) + ) + if not selected: + _selected_entry = null + _server_list.deselect_all() + _refresh() + _defer_focus_control( + _server_list if selected else _current_mode_button() + ) + + +func _current_mode_button() -> Button: + match _mode: + Mode.DIRECT: + return _direct_button + Mode.SAVED: + return _saved_button + Mode.RECENT: + return _recent_button + return _discover_button func _select_discovery_index(index: int) -> bool: @@ -1041,10 +1082,18 @@ func _on_peer_count_changed(player_count: int, max_players: int) -> void: func _on_store_changed() -> void: + var selected_index: int = _visible_entries.find(_selected_entry) var selected_id: String = ( _selected_entry.entry_id if _selected_entry != null else "" ) _refresh_entries() - if not selected_id.is_empty(): - _select_entry_id(selected_id) + _selected_entry = null + if ( + not selected_id.is_empty() + and not _select_entry_id(selected_id) + and not _visible_entries.is_empty() + ): + _select_entry_index( + clampi(maxi(selected_index, 0), 0, _visible_entries.size() - 1) + ) _refresh() diff --git a/ui/on_screen_keyboard.gd b/ui/on_screen_keyboard.gd index 80f936b..8c2122d 100644 --- a/ui/on_screen_keyboard.gd +++ b/ui/on_screen_keyboard.gd @@ -341,6 +341,13 @@ func _preserve_native_text_focus(control: Control) -> void: func _can_edit(control: Control) -> bool: + if ( + control == null + or not is_instance_valid(control) + or not control.is_inside_tree() + or not control.is_visible_in_tree() + ): + return false if control is LineEdit: return (control as LineEdit).editable if control is TextEdit: diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index 6326ae5..edd3065 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -3,8 +3,6 @@ extends Control const INPUT_OWNER: StringName = &"game_menu" const PAUSE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) -const PAUSE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0) -const COMPACT_HEIGHT_THRESHOLD: float = 560.0 const VISIBILITY_FADE_DURATION: float = 0.55 const PlayerType = preload("res://player/player.gd") const SaveManagerType = preload("res://save/player_save_manager.gd") diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index dbea731..e1aa4ad 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -76,9 +76,6 @@ focus_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath( initial_focus_path = NodePath("BubbleCluster/ResumeButton") back_focus_path = NodePath("BubbleCluster/ResumeButton") maximum_layout_size = Vector2(720, 520) -compact_maximum_layout_size = Vector2(544, 400) -compact_width_threshold = 680.0 -compact_height_threshold = 500.0 [node name="BubbleCluster" type="Control" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage"] layout_mode = 0 diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 5d46f07..5016464 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -51,12 +51,6 @@ const BubbleButtonType = preload( const BubbleClusterType = preload( "res://ui/components/bubble_menu/bubble_cluster.gd" ) -const BubbleContentShellType = preload( - "res://ui/components/bubble_menu/bubble_content_shell.gd" -) -const BubbleStatusBubbleType = preload( - "res://ui/components/bubble_menu/bubble_status_bubble.gd" -) const NotepadInkActionType = preload( "res://ui/components/bubble_menu/notepad_ink_action.gd" ) @@ -78,17 +72,7 @@ const BagItemSpriteScene = preload( const BagStorageSlotType = preload( "res://ui/components/bubble_menu/bag_storage_slot.gd" ) -const LogbookEntryType = preload( - "res://ui/components/bubble_menu/logbook_entry.gd" -) -const LogbookEntryScene = preload( - "res://ui/components/bubble_menu/logbook_entry.tscn" -) - -const LOGBOOK_PAGE_DURATION: float = 0.18 const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0) -const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0) -const COMPACT_HEIGHT_THRESHOLD: float = 560.0 const NAVIGATION_PRESENTATION_SCALE: float = 0.60 const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0) const NAVIGATION_SELECTED_SCALE: float = 1.02 @@ -236,18 +220,6 @@ const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42 @onready var _mail_page: MailPage = %MailPage @onready var _profile_page: ProfilePage = %ProfilePage @onready var _players_page: PlayersPage = %PlayersPage -@onready var _book_backing: PanelContainer = %BookBacking -@onready var _book_spread: BoxContainer = %BookSpread -@onready var _left_page: PanelContainer = %LeftPage -@onready var _right_page: PanelContainer = %RightPage -@onready var _left_heading: Label = %LeftHeading -@onready var _right_heading: Label = %RightHeading -@onready var _left_entry_field: BoxContainer = %LeftEntryField -@onready var _right_entry_field: BoxContainer = %RightEntryField -@onready var _logbook_empty_state: Label = %LogbookEmptyState -@onready var _logbook_previous: BubbleButtonType = %LogbookPrevious -@onready var _logbook_next: BubbleButtonType = %LogbookNext -@onready var _logbook_page_status: BubbleStatusBubbleType = %LogbookPageStatus @onready var _inventory_tab: BubbleButtonType = %InventoryTab @onready var _logbook_tab: BubbleButtonType = %LogbookTab @onready var _the_net_tab: BubbleButtonType = %TheNetTab @@ -256,42 +228,12 @@ const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42 @onready var _players_tab: BubbleButtonType = %PlayersTab @onready var _mail_unread_badge: Label = %MailUnreadBadge @onready var _close_button: BubbleButtonType = %CloseButton -@onready var _content_shell: BubbleContentShellType = %MenuPanel -@onready var _content_stage: Control = %Content @onready var _cooler_scroll: ScrollContainer = %CoolerScroll @onready var _cooler_host: Control = %CoolerHost -@onready var _wallet_balance: CurrencyAmount = %WalletBalance -@onready var _header: Control = %Header -@onready var _separator: Control = %Separator -@onready var _wallet_status: BubbleStatusBubbleType = %WalletStatus -@onready var _capacity_status: BubbleStatusBubbleType = %CapacityStatus -@onready var _held_value_status: BubbleStatusBubbleType = %HeldValueStatus -@onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus -@onready var _offer_status: BubbleStatusBubbleType = %OfferStatus -@onready var _inventory_section: Control = %InventorySection -@onready var _bag_section: Control = %BagSection -@onready var _bag_empty: Label = %BagEmpty -@onready var _bag_grid: GridContainer = %BagGrid -@onready var _bag_list: Control = %BagList -@onready var _bag_detail: Control = %BagDetail -@onready var _bag_detail_texture: TextureRect = %BagDetailTexture -@onready var _bag_detail_name: Label = %BagDetailName -@onready var _bag_detail_data: Label = %BagDetailData -@onready var _sort_option: OptionButton = %SortOption -@onready var _sort_direction: Button = %SortDirection -@onready var _held_value: CurrencyAmount = %HeldValue -@onready var _cooler_count: Label = %CoolerCount -@onready var _inventory_empty: Label = %InventoryEmpty -@onready var _selection_summary: Label = %SelectionSummary -@onready var _favorite_button: Button = %FavoriteButton -@onready var _sell_button: Button = %SellButton -@onready var _sale_unavailable: Label = %SaleUnavailable -@onready var _transaction_feedback: RichTextLabel = %TransactionFeedback @onready var _sale_confirmation: PanelContainer = %SaleConfirmation @onready var _confirmation_message: RichTextLabel = %ConfirmationMessage @onready var _confirm_sale_button: Button = %ConfirmSaleButton @onready var _cancel_sale_button: Button = %CancelSaleButton -@onready var _logbook_empty: Label = %LogbookEmpty var _compact_layout: bool = false var _player: PlayerType @@ -361,8 +303,6 @@ var _transition_generation: int = 0 var _page_transition_generation: int = 0 var _transitioning: bool = false var _page_transitioning: bool = false -var _presentation_rest_position: Vector2 = Vector2.ZERO -var _content_rest_position: Vector2 = Vector2.ZERO var _cooler_rest_position: Vector2 = Vector2.ZERO var _bag_rest_position: Vector2 = Vector2.ZERO var _tackle_rest_position: Vector2 = Vector2.ZERO @@ -382,12 +322,6 @@ var _bag_slot_nodes: Array[BagStorageSlotType] = [] var _sorted_bag_items: Array[OwnedItemType] = [] var _bag_drag_active: bool = false var _motion_elapsed: float = 0.0 -var _logbook_species: Array[FishDataType] = [] -var _logbook_current_page: int = 0 -var _logbook_page_count: int = 1 -var _logbook_page_transitioning: bool = false -var _logbook_page_generation: int = 0 -var _logbook_page_tween: Tween func _ready() -> void: @@ -410,22 +344,14 @@ func _ready() -> void: _profile_tab.pressed.connect(_show_section.bind(Section.PROFILE)) _players_tab.pressed.connect(_show_section.bind(Section.PLAYERS)) _close_button.pressed.connect(close_menu) - _sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option)) - _sort_direction.pressed.connect(_on_sort_direction_pressed) _cooler_sort_option.item_selected.connect(_on_cooler_sort_selected) _cooler_sort_direction.pressed.connect(_on_sort_direction_pressed) - _favorite_button.pressed.connect(_on_favorite_pressed) - _sell_button.pressed.connect(_on_sell_pressed) _favorite_bubble.pressed.connect(_on_favorite_pressed) _sell_bubble.pressed.connect(_on_sell_pressed) _sell_all_bubble.pressed.connect(_on_sell_all_pressed) _confirm_sale_button.pressed.connect(_on_confirm_sale_pressed) _cancel_sale_button.pressed.connect(_close_sale_confirmation) _configure_sale_confirmation_focus() - _sort_option.add_item("catch order", SortMode.CATCH_ORDER) - _sort_option.add_item("name", SortMode.NAME) - _sort_option.add_item("rarity", SortMode.RARITY) - _sort_option.select(SortMode.CATCH_ORDER) _cooler_sort_option.add_item("catch order", SortMode.CATCH_ORDER) _cooler_sort_option.add_item("name", SortMode.NAME) _cooler_sort_option.add_item("rarity", SortMode.RARITY) @@ -446,7 +372,6 @@ func _ready() -> void: # Keep the exposed tab hitboxes above the full-page roots. The individual # main panels retain a higher z-index and cover the tab bodies below y=166. _inventory_sub_tabs.move_to_front() - _apply_cooler_control_styles() _apply_cooler_wall_styles() _apply_cooler_notepad_style() _cooler_water_surface.resized.connect(_update_cooler_water_mask) @@ -626,6 +551,12 @@ func set_profile_preview_world_pixel_size(pixel_size: int) -> void: _profile_page.set_world_pixel_size(pixel_size) +func allows_global_controller_scroll() -> bool: + # The profile page owns the right stick for preview orbit, zoom, and its + # color picker. The shared menu scroller must not interpret that axis too. + return _current_section != Section.PROFILE + + func _input(event: InputEvent) -> void: if event is InputEventKey and event.echo: return @@ -1768,12 +1699,6 @@ func _show_section_immediate(section: Section) -> void: _mail_page.visible = section == Section.MAIL _profile_page.visible = section == Section.PROFILE _players_page.visible = section == Section.PLAYERS - _content_shell.visible = false - _inventory_section.visible = false - _bag_section.visible = false - _content_shell.set_background_visible(true) - _header.visible = section != Section.COOLER - _separator.visible = section != Section.COOLER if section == Section.COOLER: _refresh_inventory() else: @@ -1787,7 +1712,6 @@ func _show_section_immediate(section: Section) -> void: _refresh_bag() _refresh_tackle_box() if section != Section.LOGBOOK: - _cancel_logbook_page_transition(true) _catalog_logbook.deactivate() else: _catalog_logbook.activate() @@ -1874,12 +1798,9 @@ func _focus_current_section() -> void: if _shop_cooler_context_active: _focus_shop_cooler() return - _reserve_main_navigation_for_page_switching() - _reserve_visible_secondary_navigation() - var navigation_cluster := get_node_or_null("%NavigationCluster") as Control - var candidates: Array[Control] = [] - _collect_focusable_content(self, navigation_cluster, candidates) + var candidates: Array[Control] = _inventory_content_focus_candidates() if candidates.is_empty(): + _enter_inventory_tabs_zone() return ControllerFocusNavigationType.configure_spatial_neighbors(candidates) candidates.sort_custom(func(first: Control, second: Control) -> bool: @@ -1890,23 +1811,22 @@ func _focus_current_section() -> void: candidates[0].grab_focus() -func _collect_focusable_content( - root: Node, - navigation_cluster: Control, - output: Array[Control], -) -> void: - for child: Node in root.get_children(): - if child == navigation_cluster: - continue - 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_focusable_content(child, navigation_cluster, output) +func _inventory_content_focus_candidates() -> Array[Control]: + var candidates: Array[Control] = [] + match _current_section: + Section.COOLER: + for fish_node: CoolerFishSpriteType in _fish_nodes.values(): + if ControllerFocusNavigationType.is_focusable(fish_node): + candidates.append(fish_node) + Section.BAG: + for item_node: BagItemSpriteType in _bag_item_nodes.values(): + if ControllerFocusNavigationType.is_focusable(item_node): + candidates.append(item_node) + Section.TACKLE_BOX: + for tackle_button: Button in _tackle_item_buttons.values(): + if ControllerFocusNavigationType.is_focusable(tackle_button): + candidates.append(tackle_button) + return candidates func _process(delta: float) -> void: @@ -2573,35 +2493,6 @@ func _on_active_lure_changed(_item_id: StringName) -> void: _update_tackle_detail() -func _apply_cooler_control_styles() -> void: - var profile: BubbleMenuProfile = _inventory_tab.profile - if profile == null: - return - _sort_option.add_theme_stylebox_override( - "normal", - profile.make_normal_style(), - ) - _sort_option.add_theme_stylebox_override( - "hover", - profile.make_hover_style(), - ) - _sort_option.add_theme_stylebox_override( - "focus", - profile.make_hover_style(), - ) - _sort_option.add_theme_stylebox_override( - "pressed", - profile.make_pressed_style(), - ) - _sort_option.add_theme_color_override("font_color", profile.text_color) - _sort_option.add_theme_color_override( - "font_hover_color", - profile.text_hover_color, - ) - _sort_option.add_theme_color_override( - "font_focus_color", - profile.text_hover_color, - ) func _apply_cooler_wall_styles() -> void: var outer := StyleBoxFlat.new() outer.bg_color = Color(0.76, 0.9, 0.96, 1.0) @@ -2708,59 +2599,6 @@ func _apply_bag_styles() -> void: inner.set_border_width_all(0) inner.set_corner_radius_all(INVENTORY_INNER_CORNER_RADIUS) _bag_inner_liner.add_theme_stylebox_override("panel", inner) -func _apply_logbook_styles() -> void: - var backing := StyleBoxFlat.new() - backing.bg_color = Color(0.54, 0.42, 0.27, 1.0) - backing.border_color = Color(0.72, 0.59, 0.39, 1.0) - backing.set_border_width_all(6) - backing.corner_radius_top_left = 42 - backing.corner_radius_top_right = 34 - backing.corner_radius_bottom_right = 46 - backing.corner_radius_bottom_left = 36 - _book_backing.add_theme_stylebox_override("panel", backing) - var left_paper := StyleBoxFlat.new() - left_paper.bg_color = Color(0.95, 0.91, 0.79, 1.0) - left_paper.border_color = Color(0.80, 0.70, 0.52, 1.0) - left_paper.set_border_width_all(3) - left_paper.corner_radius_top_left = 30 - left_paper.corner_radius_bottom_left = 38 - left_paper.corner_radius_top_right = 8 - left_paper.corner_radius_bottom_right = 6 - _left_page.add_theme_stylebox_override("panel", left_paper) - var right_paper := StyleBoxFlat.new() - right_paper.bg_color = Color(0.925, 0.875, 0.74, 1.0) - right_paper.border_color = Color(0.77, 0.66, 0.48, 1.0) - right_paper.set_border_width_all(3) - right_paper.corner_radius_top_left = 8 - right_paper.corner_radius_bottom_left = 6 - right_paper.corner_radius_top_right = 34 - right_paper.corner_radius_bottom_right = 40 - _right_page.add_theme_stylebox_override("panel", right_paper) - for label: Label in [_left_heading, _right_heading, _logbook_empty_state]: - label.add_theme_color_override( - "font_color", - Color(0.22, 0.16, 0.09, 1.0), - ) - var page_profile: BubbleMenuProfile = _logbook_previous.profile - if page_profile != null: - var disabled_style: StyleBoxFlat = page_profile.make_normal_style() - disabled_style.bg_color.a = 0.82 - disabled_style.border_color.a = 0.72 - for control: BubbleButtonType in [ - _logbook_previous, - _logbook_next, - ]: - control.add_theme_stylebox_override( - "disabled", - disabled_style.duplicate(), - ) - control.add_theme_color_override( - "font_disabled_color", - Color(0.035, 0.145, 0.22, 0.68), - ) - _logbook_page_status.set_content("pages", "1 / 1") - - func _update_navigation_selection() -> void: _set_navigation_target(_current_section) @@ -2790,9 +2628,6 @@ func _update_shell_layout() -> void: _presentation_scale_root.scale = Vector2.ONE _presentation_scale_root.position = Vector2.ZERO _presentation_scale_root.pivot_offset = reference_size * 0.5 - _content_shell.position = Vector2(14.0, 92.0) if compact else Vector2(42.0, 104.0) - _content_shell.size = Vector2(612.0, 286.0) if compact else Vector2(1196.0, 478.0) - _presentation_rest_position = _content_shell.position var navigation_size := ( Vector2(840.0, 75.0) if compact else Vector2(840.0, 100.0) ) @@ -2982,13 +2817,7 @@ func _update_shell_layout() -> void: 520.0 if compact else 800.0, 512.0 if compact else 420.0, ) - _bag_grid.columns = 2 if compact else 3 - _bag_list.custom_minimum_size.x = 300.0 if compact else 360.0 - _bag_detail.custom_minimum_size.x = 176.0 if compact else 220.0 - _content_stage.custom_minimum_size.y = 220.0 if compact else 260.0 - _content_rest_position = _content_stage.position if not _transitioning: - _content_shell.position = _presentation_rest_position _cooler_page.position = _cooler_rest_position _bag_page.position = _bag_rest_position _tackle_box_page.position = _tackle_rest_position @@ -3005,8 +2834,6 @@ func _update_shell_layout() -> void: _mail_page.modulate.a = 1.0 _profile_page.modulate.a = 1.0 _players_page.modulate.a = 1.0 - if not _page_transitioning: - _content_stage.position = _content_rest_position _layout_cooler_fish(false) _layout_bag_items() @@ -3177,7 +3004,6 @@ func _finish_close( ) -> void: _cancel_presentation_tween() _cancel_page_tween() - _cancel_logbook_page_transition(true) _the_net_page.deactivate() _transitioning = false _page_transitioning = false @@ -3243,7 +3069,6 @@ func _get_section_rest_position(section: Section) -> Vector2: func _begin_page_transition(section: Section) -> void: - _cancel_logbook_page_transition(true) _page_transition_generation += 1 _cancel_page_tween() _page_transitioning = true @@ -3354,11 +3179,6 @@ func _set_shell_interactive(interactive: bool) -> void: func _set_content_interactive(interactive: bool) -> void: _content_interactive_enabled = interactive - _content_stage.mouse_filter = ( - Control.MOUSE_FILTER_PASS - if interactive - else Control.MOUSE_FILTER_IGNORE - ) _cooler_page.mouse_filter = ( Control.MOUSE_FILTER_PASS if interactive and _current_section == Section.COOLER @@ -3499,19 +3319,8 @@ func _reset_page_transition_visuals() -> void: _page_outgoing_content_root = null -func _on_sort_selected(index: int, source: OptionButton) -> void: - var selected_id: int = source.get_item_id(index) - if selected_id < SortMode.CATCH_ORDER or selected_id > SortMode.RARITY: - return - _sort_mode = selected_id as SortMode - _sort_option.select(_sort_mode) - _cooler_sort_option.select(_sort_mode) - _refresh_inventory() - - func _on_cooler_sort_selected(sort_id: int) -> void: _sort_mode = sort_id as SortMode - _sort_option.select(_sort_mode) _cooler_sort_option.select(_sort_mode) _refresh_inventory() @@ -3541,7 +3350,6 @@ func _update_sort_direction_text() -> void: direction_text = ( "high to low" if _sort_descending else "low to high" ) - _sort_direction.text = direction_text _cooler_sort_direction.text = direction_text @@ -3592,7 +3400,6 @@ func _refresh_bag() -> void: owned_items = filtered_items owned_items.sort_custom(_sort_bag_storage_items) _sorted_bag_items = owned_items - _bag_empty.visible = false _bag_empty_state.visible = false _bag_empty_state.text = ( "No equipment in your Bag." @@ -3932,23 +3739,8 @@ func _update_bag_detail() -> void: item_state = "equippable" elif item.usable: item_state = "usable" - _bag_detail_texture.texture = null - _bag_detail_name.text = item.display_name if item != null else "" - _bag_detail_data.text = ( - "%s\nquantity: %d\n%s\n%s\n%s" - % [ - _item_category_display_name(item), - quantity, - item_state, - hotbar_assignment, - item.description, - ] - if item != null - else "select a bag item for details." - ) _bag_sprite_detail_texture.texture = null if item != null: - _bag_detail_texture.texture = item.icon _bag_sprite_detail_texture.texture = item.icon _bag_sprite_detail_name.text = ( item.display_name if item != null else "" @@ -4065,23 +3857,11 @@ func _refresh_economy_summary() -> void: if _inventory != null else 0 ) - _wallet_balance.set_amount(balance) - _wallet_status.set_currency_amount("wallet", balance) _notepad_wallet_value.set_amount(balance) - _capacity_status.set_content("cooler", "%d / %d" % [ - _inventory.get_all_catches().size() if _inventory != null else 0, - _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, - ]) _notepad_capacity_value.text = "%d / %d" % [ _inventory.get_all_catches().size() if _inventory != null else 0, _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, ] - _held_value_status.set_currency_amount("held value", held_total) - _held_value.set_amount(held_total) - _cooler_count.text = "%d / %d" % [ - _inventory.get_all_catches().size() if _inventory != null else 0, - _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, - ] func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: @@ -4098,7 +3878,6 @@ func _refresh_inventory() -> void: catches.append(fish_catch) catches.sort_custom(_compare_catches) _sorted_catches = catches - _inventory_empty.visible = catches.is_empty() var visible_ids: Array[StringName] = [] for fish_catch: FishCatchType in catches: visible_ids.append(fish_catch.catch_id) @@ -4358,7 +4137,6 @@ func _on_catch_card_pressed(catch_id: StringName) -> void: Input.is_key_pressed(KEY_CTRL), Input.is_key_pressed(KEY_SHIFT) ) - _set_transaction_feedback("") _refresh_inventory() @@ -4388,7 +4166,6 @@ func _on_fish_field_gui_input(event: InputEvent) -> void: and not get_viewport().gui_is_dragging() ): _fish_selection.clear() - _set_transaction_feedback("") _refresh_inventory() @@ -4465,8 +4242,6 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: _cooler_detail_texture.visible = false _cooler_detail_name.text = "select a fish" _clear_cooler_detail_stats() - _favorite_button.disabled = true - _favorite_button.text = "favorite" _favorite_bubble.disabled = true _favorite_bubble.text = "favorite" _favorite_bubble.persistent_mark = false @@ -4511,12 +4286,10 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: else: _cooler_offer_label.text = "buyer unavailable" _cooler_offer_value.visible = false - _favorite_button.disabled = false - _favorite_button.text = ( + _favorite_bubble.disabled = false + _favorite_bubble.text = ( "unfavorite" if fish_catch.is_favorited else "favorite" ) - _favorite_bubble.disabled = false - _favorite_bubble.text = _favorite_button.text _favorite_bubble.persistent_mark = fish_catch.is_favorited _favorite_bubble.refresh_ink_state() _detail_constellation.visible = _current_section == Section.COOLER @@ -4555,28 +4328,19 @@ func _update_sale_summary() -> void: active_buyer.id if active_buyer != null else StringName() ) _update_sell_all_action(active_buyer, buyer_id) - var offer_label: String = _get_offer_label(active_buyer) var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() var selected_count: int = selected_ids.size() - _sell_button.text = ( + _sell_bubble.text = ( "sell fish" if selected_count == 1 else "sell %d fish" % selected_count ) - _sell_bubble.text = _sell_button.text if selected_count == 0: - _selection_summary.text = "no fish selected" - _selection_status.set_content("selected", "none") - _offer_status.set_content(offer_label, "โ€”") - _sell_button.text = "sell fish" - _sell_button.disabled = true _sell_bubble.disabled = true _sell_bubble.text = "sell fish" _sell_bubble.persistent_mark = false _sell_bubble.refresh_ink_state() _set_cooler_selection_summary_empty() - _sale_unavailable.text = "" - _sale_unavailable.visible = false return if ( _sale_in_progress @@ -4585,51 +4349,24 @@ func _update_sale_summary() -> void: and _network_sale_service.is_local_sale_pending() ) ): - _selection_summary.text = ( - "1 fish selected" - if selected_count == 1 - else "%d fish selected" % selected_count - ) - _selection_status.set_content("selected", str(selected_count)) - _offer_status.set_content(offer_label, "pending") - _sell_button.disabled = true _sell_bubble.disabled = true - _sale_unavailable.text = "Sellingโ€ฆ" - _sale_unavailable.visible = true + _set_cooler_selection_summary(selected_count, -1) return if ( _network_sale_service == null or buyer_id.is_empty() or not _network_sale_service.can_request_sale(buyer_id) ): - _selection_summary.text = ( - "1 fish selected" - if selected_count == 1 - else "%d fish selected" % selected_count - ) - _selection_status.set_content("selected", str(selected_count)) - _offer_status.set_content(offer_label, "unavailable") - _sell_button.disabled = true _sell_bubble.disabled = true _sell_bubble.persistent_mark = false _sell_bubble.refresh_ink_state() - _sale_unavailable.text = ( - "Selling is not supported by this server." - ) - _sale_unavailable.visible = true + _set_cooler_selection_summary(selected_count, -1) return var preview: FishSaleResultType = ( _sale_service.preview_batch(selected_ids, active_buyer) if _sale_service != null and active_buyer != null else null ) - var count_text: String = ( - "1 fish selected" - if selected_count == 1 - else "%d fish selected" % selected_count - ) - _selection_summary.text = count_text - _selection_status.set_content("selected", str(selected_count)) if ( preview != null and preview.payout >= 0 @@ -4640,24 +4377,11 @@ func _update_sale_summary() -> void: ) ): _set_cooler_selection_summary(selected_count, preview.payout) - _offer_status.set_currency_amount(offer_label, preview.payout) else: _set_cooler_selection_summary(selected_count, -1) - _offer_status.set_content(offer_label, "unavailable") - _sell_button.disabled = preview == null or not preview.is_success() - _sell_bubble.disabled = _sell_button.disabled + _sell_bubble.disabled = preview == null or not preview.is_success() _sell_bubble.persistent_mark = false _sell_bubble.refresh_ink_state() - if preview != null and preview.status == FishSaleResultType.Status.FAVORITED: - _sale_unavailable.text = ( - "favorited fish cannot be sold. " - + "remove them from the selection first." - ) - elif preview != null and not preview.is_success(): - _sale_unavailable.text = preview.get_message() - else: - _sale_unavailable.text = "" - _sale_unavailable.visible = not _sale_unavailable.text.is_empty() func _update_sell_all_action( @@ -4723,13 +4447,7 @@ func _on_favorite_pressed() -> void: fish_catch.catch_id, not fish_catch.is_favorited ): - _set_transaction_feedback( - "%s %s." - % [ - fish_catch.fish.display_name, - "favorited" if fish_catch.is_favorited else "unfavorited", - ] - ) + _refresh_inventory() func _on_sell_pressed() -> void: @@ -4739,7 +4457,6 @@ func _on_sell_pressed() -> void: func _on_sell_all_pressed() -> void: var catch_ids: Array[StringName] = _get_sell_all_catch_ids() if catch_ids.is_empty(): - _set_transaction_feedback("No sellable fish in the cooler.") return _begin_sale_confirmation(catch_ids) @@ -4756,16 +4473,12 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void: and _network_sale_service.is_local_sale_pending() ) ): - _set_transaction_feedback("Sellingโ€ฆ") return if ( _network_sale_service == null or buyer_id.is_empty() or not _network_sale_service.can_request_sale(buyer_id) ): - _set_transaction_feedback( - "Selling is not supported by this server." - ) return if ( _inventory == null @@ -4779,12 +4492,6 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void: active_buyer ) if not preview.is_success(): - _set_transaction_feedback( - "favorited fish cannot be sold. " - + "remove them from the selection first." - if preview.status == FishSaleResultType.Status.FAVORITED - else preview.get_message() - ) _refresh_inventory() return _confirmation_catch_ids = catch_ids.duplicate() @@ -4847,7 +4554,7 @@ func _on_confirm_sale_pressed() -> void: and transaction_generation == _menu_generation and (visible or _shop_cooler_context_active) ): - _set_transaction_feedback("Sellingโ€ฆ") + _update_sale_summary() func _can_use_shared_world_actions() -> bool: @@ -4860,16 +4567,15 @@ func _can_use_shared_world_actions() -> bool: func _on_network_sale_pending(_request_id: String) -> void: _sale_in_progress = true if visible or _shop_cooler_context_active: - _set_transaction_feedback("Sellingโ€ฆ") _update_sale_summary() func _on_network_sale_finished( _request_id: String, accepted: bool, - message: String, + _message: String, catch_ids: Array[StringName], - payout: int, + _payout: int, ) -> void: _sale_in_progress = false if accepted: @@ -4877,16 +4583,6 @@ func _on_network_sale_finished( if not visible and not _shop_cooler_context_active: return _refresh_all() - var feedback_message: String = ( - "Sale complete โ€ข %s received." - % CurrencyPresentationType.bbcode_amount(payout, 20) - if accepted - else message - ) - _set_transaction_feedback(feedback_message) - if not accepted: - _sale_unavailable.text = feedback_message - _sale_unavailable.visible = true if _current_section == Section.COOLER: call_deferred("_restore_inventory_tab_focus") @@ -4939,7 +4635,6 @@ func _revalidate_confirmation() -> void: or _confirmation_buyer.id != _confirmation_buyer_id ): _close_sale_confirmation() - _set_transaction_feedback("sale selection is no longer available.") return var preview: FishSaleResultType = _sale_service.preview_batch( _confirmation_catch_ids, @@ -4954,12 +4649,6 @@ func _revalidate_confirmation() -> void: ) ): _close_sale_confirmation() - _set_transaction_feedback( - "favorited fish cannot be sold. " - + "remove them from the selection first." - if preview.status == FishSaleResultType.Status.FAVORITED - else preview.get_message() - ) func _get_buyer_display_group( @@ -4974,10 +4663,6 @@ func _get_buyer_display_group( return "buyer" -func _set_transaction_feedback(message: String) -> void: - _transaction_feedback.text = "[center]%s[/center]" % message - - func _get_active_sale_buyer() -> FishBuyerProfileType: if _sale_buyer_override != null and _sale_buyer_override.is_valid(): return _sale_buyer_override @@ -4994,108 +4679,6 @@ func _get_offer_label(buyer: FishBuyerProfileType) -> String: return "buyer offer" -func _refresh_logbook() -> void: - if not is_node_ready(): - return - var valid_species: Array[FishDataType] = [] - if _catalog != null: - for fish: FishDataType in _catalog.candidates: - if fish != null and fish.active and not fish.id.is_empty(): - valid_species.append(fish) - _logbook_species = valid_species - _logbook_empty.visible = valid_species.is_empty() - _logbook_empty.text = ( - "no fish catalog configured." - if valid_species.is_empty() - else "no species discovered yet." - ) - _logbook_empty_state.visible = valid_species.is_empty() - _logbook_empty_state.text = ( - "no fish catalog configured" - if valid_species.is_empty() - else "" - ) - _refresh_logbook_page(false) - - -func _refresh_logbook_page(keep_transition_state: bool = true) -> void: - if not is_node_ready(): - return - var entries_per_spread: int = 2 if _compact_layout else 4 - _logbook_page_count = maxi( - 1, - ceili(float(_logbook_species.size()) / float(entries_per_spread)), - ) - _logbook_current_page = clampi( - _logbook_current_page, - 0, - _logbook_page_count - 1, - ) - _clear_logbook_entries() - var start_index: int = _logbook_current_page * entries_per_spread - var end_index: int = mini( - start_index + entries_per_spread, - _logbook_species.size(), - ) - var visible_species: Array[FishDataType] = [] - for index: int in range(start_index, end_index): - visible_species.append(_logbook_species[index]) - if _compact_layout: - for fish: FishDataType in visible_species: - _left_entry_field.add_child(_create_logbook_entry(fish)) - else: - var left_count: int = mini(2, visible_species.size()) - for index: int in visible_species.size(): - var destination: BoxContainer = ( - _left_entry_field - if index < left_count - else _right_entry_field - ) - destination.add_child(_create_logbook_entry( - visible_species[index] - )) - _logbook_page_status.set_content( - "pages", - "%d / %d" % [_logbook_current_page + 1, _logbook_page_count], - ) - _logbook_page_status.visible = _logbook_page_count > 1 - _update_logbook_page_control_state( - keep_transition_state - and visible - and _current_section == Section.LOGBOOK - and not _transitioning - and not _page_transitioning - and not _logbook_page_transitioning - ) - _configure_logbook_focus() - - -func _create_logbook_entry(fish: FishDataType) -> LogbookEntryType: - var discovered: bool = ( - _collection_log != null - and _collection_log.has_discovered(fish.id) - ) - var entry := LogbookEntryScene.instantiate() as LogbookEntryType - entry.set_meta("fish_id", fish.id) - entry.configure( - fish.display_texture, - fish.display_name, - fish.get_rarity_name(), - _inventory.get_count(fish.id) if _inventory != null else 0, - discovered, - ) - entry.apply_compact_layout(_compact_layout) - return entry - - -func _clear_logbook_entries() -> void: - for field: BoxContainer in [_left_entry_field, _right_entry_field]: - for child: Node in field.get_children(): - _release_focus_from(child, _logbook_tab) - field.remove_child(child) - child.queue_free() - - func _release_focus_from(node: Node, fallback: Control) -> void: var focus_owner: Control = get_viewport().gui_get_focus_owner() if ( @@ -5119,164 +4702,6 @@ func _release_focus_from(node: Node, fallback: Control) -> void: fallback.call_deferred("grab_focus") -func _request_logbook_page(direction: int) -> void: - if ( - _current_section != Section.LOGBOOK - or _logbook_page_transitioning - or _transitioning - or _page_transitioning - ): - return - var target_page: int = clampi( - _logbook_current_page + direction, - 0, - _logbook_page_count - 1, - ) - if target_page == _logbook_current_page: - return - _logbook_page_generation += 1 - _logbook_page_transitioning = true - _update_logbook_page_control_state(false) - var generation: int = _logbook_page_generation - var travel: float = -12.0 if direction > 0 else 12.0 - _logbook_page_tween = create_tween() - _logbook_page_tween.tween_property( - _book_spread, - "modulate:a", - 0.0, - LOGBOOK_PAGE_DURATION * 0.5, - ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - _logbook_page_tween.parallel().tween_property( - _book_spread, - "position:x", - travel, - LOGBOOK_PAGE_DURATION * 0.5, - ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - _logbook_page_tween.tween_callback( - _swap_logbook_page.bind(target_page, travel, generation) - ) - _logbook_page_tween.tween_property( - _book_spread, - "modulate:a", - 1.0, - LOGBOOK_PAGE_DURATION * 0.5, - ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - _logbook_page_tween.parallel().tween_property( - _book_spread, - "position:x", - 0.0, - LOGBOOK_PAGE_DURATION * 0.5, - ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - _logbook_page_tween.finished.connect( - _finish_logbook_page_transition.bind(generation), - CONNECT_ONE_SHOT, - ) - - -func _swap_logbook_page( - target_page: int, - travel: float, - generation: int, -) -> void: - if ( - generation != _logbook_page_generation - or not visible - or _current_section != Section.LOGBOOK - ): - return - _logbook_current_page = target_page - _refresh_logbook_page(false) - _book_spread.position.x = -travel - _book_spread.modulate.a = 0.0 - - -func _finish_logbook_page_transition(generation: int) -> void: - if generation != _logbook_page_generation: - return - _logbook_page_tween = null - _logbook_page_transitioning = false - _book_spread.position.x = 0.0 - _book_spread.modulate.a = 1.0 - _update_logbook_page_control_state( - visible and _current_section == Section.LOGBOOK - ) - _configure_logbook_focus() - if _logbook_next.visible and not _logbook_next.disabled: - _logbook_next.grab_focus() - elif _logbook_previous.visible and not _logbook_previous.disabled: - _logbook_previous.grab_focus() - - -func _cancel_logbook_page_transition(reset_visuals: bool) -> void: - _logbook_page_generation += 1 - if _logbook_page_tween != null: - _logbook_page_tween.kill() - _logbook_page_tween = null - _logbook_page_transitioning = false - if reset_visuals and is_instance_valid(_book_spread): - _book_spread.position.x = 0.0 - _book_spread.modulate.a = 1.0 - - -func _update_logbook_page_control_state(interactive: bool) -> void: - var has_multiple_pages: bool = _logbook_page_count > 1 - _logbook_previous.visible = has_multiple_pages - _logbook_next.visible = has_multiple_pages - _logbook_previous.disabled = ( - not interactive or _logbook_current_page <= 0 - ) - _logbook_next.disabled = ( - not interactive - or _logbook_current_page >= _logbook_page_count - 1 - ) - for control: BubbleButtonType in [_logbook_previous, _logbook_next]: - control.focus_mode = ( - Control.FOCUS_ALL - if control.visible and not control.disabled - else Control.FOCUS_NONE - ) - control.mouse_filter = ( - Control.MOUSE_FILTER_STOP - if control.visible and not control.disabled - else Control.MOUSE_FILTER_IGNORE - ) - - -func _configure_logbook_focus() -> void: - if _current_section != Section.LOGBOOK: - return - _logbook_tab.focus_neighbor_bottom = _logbook_tab.focus_neighbor_right - if _logbook_page_count <= 1: - return - var first_control: BubbleButtonType = ( - _logbook_previous - if not _logbook_previous.disabled - else _logbook_next - ) - _logbook_tab.focus_neighbor_bottom = _logbook_tab.get_path_to( - first_control - ) - for control: BubbleButtonType in [_logbook_previous, _logbook_next]: - control.focus_neighbor_top = control.get_path_to(_logbook_tab) - control.focus_neighbor_bottom = control.focus_neighbor_top - control.focus_neighbor_left = control.get_path_to( - _logbook_previous - ) - control.focus_neighbor_right = control.get_path_to(_logbook_next) - - -func _advance_logbook_page_controls(delta: float) -> void: - for control: BubbleButtonType in [_logbook_previous, _logbook_next]: - if not control.visible: - continue - control.advance_emphasis(delta) - control.apply_presentation( - control.calculate_target(_motion_elapsed, 1.0, 1.0), - control.calculate_visual_scale(_motion_elapsed, 1.0), - minf(1.0, delta * 10.0), - ) - - func _create_texture_frame( texture: Texture2D, minimum_size: Vector2, diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index 049107b..93fcf92 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -1,13 +1,10 @@ -[gd_scene load_steps=34 format=3] +[gd_scene load_steps=30 format=3] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] -[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_content_shell.gd" id="3_shell"] [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="4_profile"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="5_cluster"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="6_bubble"] -[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_status_bubble.tscn" id="7_status"] -[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_information_panel.gd" id="8_info"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_information_panel.tscn" id="9_info_scene"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_status_bubble.tscn" id="10_status_scene"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="11_bubble"] @@ -33,8 +30,6 @@ [ext_resource type="Texture2D" path="res://ui/icons/tab_menu/close.png" id="31_close"] [ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="32_currency"] -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"] - [sub_resource type="ShaderMaterial" id="ShaderMaterial_cooler_water"] shader = ExtResource("14_water") shader_parameter/decorative_bubble_1 = ExtResource("15_bubble1") @@ -814,597 +809,6 @@ offset_bottom = 720.0 mouse_filter = 1 script = ExtResource("20_players_page") -[node name="BookBacking" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"] -unique_name_in_owner = true -visible = false -layout_mode = 0 -offset_left = 58.0 -offset_top = 128.0 -offset_right = 1222.0 -offset_bottom = 588.0 -mouse_filter = 2 - -[node name="BookOuterMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/margin_left = 12 -theme_override_constants/margin_top = 12 -theme_override_constants/margin_right = 12 -theme_override_constants/margin_bottom = 12 - -[node name="BookSpread" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin"] -unique_name_in_owner = true -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/separation = 0 - -[node name="LeftPage" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -mouse_filter = 2 - -[node name="LeftPageMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/margin_left = 22 -theme_override_constants/margin_top = 18 -theme_override_constants/margin_right = 24 -theme_override_constants/margin_bottom = 18 - -[node name="LeftPageLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/separation = 8 - -[node name="LeftHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin/LeftPageLayout"] -unique_name_in_owner = true -layout_mode = 2 -mouse_filter = 2 -theme_override_colors/font_color = Color(0.22, 0.16, 0.09, 1) -theme_override_font_sizes/font_size = 20 -text = "field notes" -horizontal_alignment = 1 - -[node name="LeftEntryField" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/LeftPage/LeftPageMargin/LeftPageLayout"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_vertical = 3 -mouse_filter = 2 -theme_override_constants/separation = 10 -vertical = true - -[node name="BookGutter" type="ColorRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"] -unique_name_in_owner = true -custom_minimum_size = Vector2(18, 0) -layout_mode = 2 -mouse_filter = 2 -color = Color(0.44, 0.34, 0.21, 0.52) - -[node name="RightPage" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -mouse_filter = 2 - -[node name="RightPageMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/margin_left = 24 -theme_override_constants/margin_top = 18 -theme_override_constants/margin_right = 22 -theme_override_constants/margin_bottom = 18 - -[node name="RightPageLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin"] -layout_mode = 2 -mouse_filter = 2 -theme_override_constants/separation = 8 - -[node name="RightHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin/RightPageLayout"] -unique_name_in_owner = true -layout_mode = 2 -mouse_filter = 2 -theme_override_colors/font_color = Color(0.22, 0.16, 0.09, 1) -theme_override_font_sizes/font_size = 20 -text = "catch record" -horizontal_alignment = 1 - -[node name="RightEntryField" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage/BookBacking/BookOuterMargin/BookSpread/RightPage/RightPageMargin/RightPageLayout"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_vertical = 3 -mouse_filter = 2 -theme_override_constants/separation = 10 -vertical = true - -[node name="LogbookEmptyState" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"] -unique_name_in_owner = true -visible = false -layout_mode = 0 -offset_left = 402.0 -offset_top = 314.0 -offset_right = 878.0 -offset_bottom = 366.0 -mouse_filter = 2 -theme_override_colors/font_color = Color(0.28, 0.21, 0.13, 0.82) -theme_override_font_sizes/font_size = 20 -text = "no fish catalog configured" -horizontal_alignment = 1 -vertical_alignment = 1 - -[node name="LogbookPrevious" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(76, 72) -layout_mode = 0 -offset_left = 78.0 -offset_top = 494.0 -offset_right = 154.0 -offset_bottom = 566.0 -text = "previous" -script = ExtResource("11_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(76, 72) -desktop_anchor = Vector2(116, 530) -compact_anchor = Vector2(58, 332) -compact_minimum_size = Vector2(66, 62) -minimum_font_size = 11 -maximum_font_size = 15 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.5 -deformation_amplitude = 0.006 - -[node name="LogbookNext" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(76, 72) -layout_mode = 0 -offset_left = 1126.0 -offset_top = 494.0 -offset_right = 1202.0 -offset_bottom = 566.0 -text = "next" -script = ExtResource("11_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(76, 72) -desktop_anchor = Vector2(1164, 530) -compact_anchor = Vector2(582, 332) -compact_minimum_size = Vector2(66, 62) -minimum_font_size = 11 -maximum_font_size = 15 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.5 -motion_phase = 2.2 -deformation_amplitude = 0.006 - -[node name="LogbookPageStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage" instance=ExtResource("10_status_scene")] -unique_name_in_owner = true -visible = false -layout_mode = 0 -offset_left = 555.0 -offset_top = 524.0 -offset_right = 725.0 -offset_bottom = 580.0 -mouse_filter = 2 - -[node name="MenuPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] -unique_name_in_owner = true -layout_mode = 0 -offset_left = 42.0 -offset_top = 104.0 -offset_right = 1238.0 -offset_bottom = 582.0 -script = ExtResource("3_shell") -profile = ExtResource("4_profile") -content_margin = 22.0 -corner_radius = 74 - -[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 14 -theme_override_constants/margin_top = 12 -theme_override_constants/margin_right = 14 -theme_override_constants/margin_bottom = 12 - -[node name="Layout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin"] -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="Header" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="WalletHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header"] -layout_mode = 2 -text = "wallet" -theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) - -[node name="WalletBalance" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header" instance=ExtResource("32_currency")] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -alignment = 0 - -[node name="Separator" type="HSeparator" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"] -unique_name_in_owner = true -layout_mode = 2 - -[node name="TransactionFeedback" type="RichTextLabel" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"] -unique_name_in_owner = true -custom_minimum_size = Vector2(0, 30) -layout_mode = 2 -text = "" -bbcode_enabled = true -fit_content = true -scroll_active = false -theme_override_colors/default_color = Color(1, 0.82, 0.4, 1) -theme_override_font_sizes/normal_font_size = 21 - -[node name="Content" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"] -unique_name_in_owner = true -custom_minimum_size = Vector2(0, 260) -layout_mode = 2 -size_flags_vertical = 3 - -[node name="InventorySection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"] -unique_name_in_owner = true -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/separation = 7 - -[node name="StatusShoal" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_constants/separation = 8 -alignment = 1 - -[node name="WalletStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")] -unique_name_in_owner = true -layout_mode = 2 - -[node name="CapacityStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")] -unique_name_in_owner = true -layout_mode = 2 - -[node name="HeldValueStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/StatusShoal" instance=ExtResource("7_status")] -unique_name_in_owner = true -layout_mode = 2 - -[node name="SortBar" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="SortLabel" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -layout_mode = 2 -text = "sort:" - -[node name="SortOption" type="OptionButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -unique_name_in_owner = true -layout_mode = 2 -custom_minimum_size = Vector2(136, 104) - -[node name="SortDirection" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -unique_name_in_owner = true -layout_mode = 2 -custom_minimum_size = Vector2(136, 104) -text = "newest first" -script = ExtResource("6_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(136, 104) -minimum_font_size = 15 -maximum_font_size = 20 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="SortSpacer" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="CoolerCount" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -unique_name_in_owner = true -visible = false -layout_mode = 2 -text = "0 / 12" -theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) - -[node name="HeldValueHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] -visible = false -layout_mode = 2 -text = "held fish base value" -theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) - -[node name="HeldValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar" instance=ExtResource("32_currency")] -unique_name_in_owner = true -visible = false -layout_mode = 2 - -[node name="InventoryBody" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_vertical = 3 -theme_override_constants/separation = 8 - -[node name="InventoryList" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] -unique_name_in_owner = true -custom_minimum_size = Vector2(350, 0) -layout_mode = 2 -theme_override_styles/panel = SubResource("StyleBoxEmpty_collection") - -[node name="InventoryListMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"] -layout_mode = 2 -theme_override_constants/margin_left = 8 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 8 -theme_override_constants/margin_bottom = 8 - -[node name="InventoryStack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="InventoryEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] -unique_name_in_owner = true -layout_mode = 2 -text = "your cooler is empty." -horizontal_alignment = 1 - -[node name="InventoryScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="InventoryGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack/InventoryScroll"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/h_separation = 7 -theme_override_constants/v_separation = 7 -columns = 3 - -[node name="DetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] -unique_name_in_owner = true -custom_minimum_size = Vector2(210, 0) -layout_mode = 2 -script = ExtResource("8_info") -profile = ExtResource("4_profile") -content_margin = 12.0 -corner_radius = 96 - -[node name="DetailMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 9 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 9 - -[node name="DetailStack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="DetailTexture" type="TextureRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -unique_name_in_owner = true -custom_minimum_size = Vector2(178, 100) -layout_mode = 2 -expand_mode = 1 -stretch_mode = 5 -texture_filter = 1 - -[node name="DetailName" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) -theme_override_font_sizes/font_size = 29 -horizontal_alignment = 1 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) - -[node name="DetailData" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -autowrap_mode = 2 -horizontal_alignment = 1 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) - -[node name="SelectionSummary" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -unique_name_in_owner = true -visible = false -layout_mode = 2 -text = "no fish selected" -autowrap_mode = 2 -horizontal_alignment = 1 -theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) - -[node name="ActionRow" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -layout_mode = 2 -theme_override_constants/separation = 8 -alignment = 1 - -[node name="FavoriteButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ActionRow"] -unique_name_in_owner = true -layout_mode = 2 -disabled = true -text = "favorite" -custom_minimum_size = Vector2(112, 104) -size_flags_horizontal = 4 -script = ExtResource("6_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(112, 104) -minimum_font_size = 15 -maximum_font_size = 20 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="SellButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ActionRow"] -unique_name_in_owner = true -layout_mode = 2 -disabled = true -text = "sell" -custom_minimum_size = Vector2(112, 104) -size_flags_horizontal = 4 -script = ExtResource("6_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(112, 104) -minimum_font_size = 15 -maximum_font_size = 20 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="ContextStatus" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -layout_mode = 2 -theme_override_constants/separation = 6 -alignment = 1 - -[node name="SelectionStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ContextStatus" instance=ExtResource("7_status")] -unique_name_in_owner = true -custom_minimum_size = Vector2(108, 78) -layout_mode = 2 - -[node name="OfferStatus" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack/ContextStatus" instance=ExtResource("7_status")] -unique_name_in_owner = true -custom_minimum_size = Vector2(128, 78) -layout_mode = 2 - -[node name="SaleUnavailable" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] -unique_name_in_owner = true -visible = false -layout_mode = 2 -autowrap_mode = 2 -horizontal_alignment = 1 -theme_override_colors/font_color = Color(1, 0.702, 0.278, 1) - -[node name="BagSection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"] -unique_name_in_owner = true -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/separation = 7 - -[node name="BagHint" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"] -layout_mode = 2 -text = "drag fish or hotbar-compatible items onto a slot below. right-click a slot to clear it." -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="BagBody" type="HSplitContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"] -layout_mode = 2 -size_flags_vertical = 3 -split_offset = 540 -theme_override_constants/separation = 8 - -[node name="BagList" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody"] -unique_name_in_owner = true -custom_minimum_size = Vector2(360, 0) -layout_mode = 2 - -[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList"] -layout_mode = 2 -theme_override_constants/margin_left = 8 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 8 -theme_override_constants/margin_bottom = 8 - -[node name="Stack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="BagEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"] -unique_name_in_owner = true -layout_mode = 2 -text = "your bag is empty." -horizontal_alignment = 1 - -[node name="BagScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"] -layout_mode = 2 -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="BagGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack/BagScroll"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/h_separation = 7 -theme_override_constants/v_separation = 7 -columns = 3 - -[node name="BagDetail" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody"] -unique_name_in_owner = true -custom_minimum_size = Vector2(220, 0) -layout_mode = 2 - -[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail"] -layout_mode = 2 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 9 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 9 - -[node name="Stack" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="BagDetailTexture" type="TextureRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"] -unique_name_in_owner = true -custom_minimum_size = Vector2(180, 96) -layout_mode = 2 -expand_mode = 1 -stretch_mode = 5 -texture_filter = 1 - -[node name="BagDetailName" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_font_sizes/font_size = 29 -horizontal_alignment = 1 - -[node name="BagDetailData" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"] -unique_name_in_owner = true -layout_mode = 2 -text = "select a bag item for details." -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="LogbookSection" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content"] -unique_name_in_owner = true -visible = false -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/separation = 7 - -[node name="LogbookEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection"] -unique_name_in_owner = true -layout_mode = 2 -text = "no species discovered yet." -horizontal_alignment = 1 - -[node name="LogbookScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection"] -layout_mode = 2 -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="LogbookGrid" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/LogbookSection/LogbookScroll"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/h_separation = 7 -theme_override_constants/v_separation = 7 -columns = 4 - [node name="SaleConfirmation" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true visible = false diff --git a/ui/profile_page.gd b/ui/profile_page.gd index fcc8e3b..fd70fe6 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -219,7 +219,12 @@ func consume_escape() -> bool: func handle_controller_input(event: InputEvent) -> bool: if not _profile_active or not _profile_interactive: return false - if event is InputEventJoypadButton or event is InputEventJoypadMotion: + if ( + event.is_action_pressed(&"ui_up") + or event.is_action_pressed(&"ui_down") + or event.is_action_pressed(&"ui_left") + or event.is_action_pressed(&"ui_right") + ): _ensure_controller_zone_focus() var cancel_pressed := _event_matches_controller_press( event, diff --git a/ui/quick_radial_menu.gd b/ui/quick_radial_menu.gd index f04b295..05ce0a3 100644 --- a/ui/quick_radial_menu.gd +++ b/ui/quick_radial_menu.gd @@ -1,20 +1,8 @@ class_name QuickRadialMenu -extends Control +extends RadialActionMenu signal action_selected(action_id: StringName) -const BubbleButtonScene: PackedScene = preload( - "res://ui/components/bubble_menu/bubble_button.tscn" -) -const BubbleProfile: BubbleMenuProfile = preload( - "res://ui/components/bubble_menu/bubble_menu_profile.tres" -) -const RING_RADIUS: float = 172.0 -const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) -const CONTROLLER_SELECTION_DEADZONE: float = 0.35 -const ControllerMappingManagerType = preload( - "res://settings/controller_mapping_manager.gd" -) const ACTIONS: Array[StringName] = [ &"chat", &"freecam", @@ -27,80 +15,20 @@ const LABELS: Array[String] = [ ] const SECTOR_COUNT: int = 3 -var _buttons: Array[BubbleButton] = [] -var _is_open: bool = false -var _selected_sector: int = 0 -var _controller_selection_mode: bool = false -var _controller_mapping_manager: ControllerMappingManagerType var _hud_hidden: bool = false -func setup_controller_mapping( - mapping_manager: ControllerMappingManagerType, -) -> void: - _controller_mapping_manager = mapping_manager - - -func _ready() -> void: - visible = false - mouse_filter = Control.MOUSE_FILTER_IGNORE - for sector: int in SECTOR_COUNT: - var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton - bubble.profile = BubbleProfile - bubble.focus_mode = Control.FOCUS_NONE - bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE - bubble.text = _label_for_sector(sector) - add_child(bubble) - _buttons.append(bubble) - _apply_selection_styles() - - -func handle_input(event: InputEvent, can_open: bool) -> bool: - if not event.is_action("open_quick_actions"): - return false - var key_event: InputEventKey = event as InputEventKey - if key_event != null and key_event.echo: - return false - if event.is_pressed(): - if _is_open or not can_open: - return _is_open - open_menu(event is InputEventJoypadButton) - return true - if not _is_open: - return false - var selected: int = _selected_sector - close_menu() - call_deferred("_emit_selected_action", ACTIONS[selected]) - return true - - -func open_menu(controller_selection: bool = false) -> void: - _is_open = true - _selected_sector = 0 - _controller_selection_mode = controller_selection - visible = true - _refresh_labels() - _layout_bubbles() - _apply_selection_styles() - - -func close_menu() -> void: - _is_open = false - visible = false - - -func is_open() -> bool: - return _is_open - - func set_hud_hidden(hidden: bool) -> void: _hud_hidden = hidden _refresh_labels() -func _refresh_labels() -> void: - for sector: int in _buttons.size(): - _buttons[sector].text = _label_for_sector(sector) +func _input_action_name() -> StringName: + return &"open_quick_actions" + + +func _sector_count() -> int: + return SECTOR_COUNT func _label_for_sector(sector: int) -> String: @@ -109,76 +37,9 @@ func _label_for_sector(sector: int) -> String: return LABELS[sector] +func _emit_selected_sector(sector: int) -> void: + call_deferred("_emit_selected_action", ACTIONS[sector]) + + func _emit_selected_action(action_id: StringName) -> void: action_selected.emit(action_id) - - -func _process(_delta: float) -> void: - if not _is_open: - return - _layout_bubbles() - _update_selection() - - -func _layout_bubbles() -> void: - var center: Vector2 = size * 0.5 - for sector: int in SECTOR_COUNT: - var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT) - var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS - var bubble: BubbleButton = _buttons[sector] - bubble.position = bubble_center - BUBBLE_SIZE * 0.5 - bubble.size = BUBBLE_SIZE - bubble.pivot_offset = BUBBLE_SIZE * 0.5 - - -func _update_selection() -> void: - var stick: Vector2 = _get_selection_stick() - if stick.length() >= CONTROLLER_SELECTION_DEADZONE: - _select_sector_from_offset(stick) - return - if _controller_selection_mode: - return - _update_mouse_selection() - - -func _get_selection_stick() -> Vector2: - if _controller_mapping_manager != null: - return Vector2( - _controller_mapping_manager.get_role_axis( - ControllerMappingManagerType.ROLE_RIGHT_STICK_X - ), - _controller_mapping_manager.get_role_axis( - ControllerMappingManagerType.ROLE_RIGHT_STICK_Y - ), - ) - return Vector2( - Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), - Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), - ) - - -func _update_mouse_selection() -> void: - var offset: Vector2 = get_local_mouse_position() - size * 0.5 - if offset.length() < 24.0: - return - _select_sector_from_offset(offset) - - -func _select_sector_from_offset(offset: Vector2) -> void: - var half_sector: float = TAU / (float(SECTOR_COUNT) * 2.0) - var angle: float = fposmod(offset.angle() + PI * 0.5 + half_sector, TAU) - var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT)))) - if sector == _selected_sector: - return - _selected_sector = sector - _apply_selection_styles() - - -func _apply_selection_styles() -> void: - for sector: int in _buttons.size(): - var bubble: BubbleButton = _buttons[sector] - bubble.apply_profile() - if sector == _selected_sector: - bubble.add_theme_stylebox_override( - "normal", BubbleProfile.make_hover_style() - ) diff --git a/ui/radial_action_menu.gd b/ui/radial_action_menu.gd new file mode 100644 index 0000000..76a06e3 --- /dev/null +++ b/ui/radial_action_menu.gd @@ -0,0 +1,176 @@ +class_name RadialActionMenu +extends Control + +const BubbleButtonScene: PackedScene = preload( + "res://ui/components/bubble_menu/bubble_button.tscn" +) +const BubbleProfile: BubbleMenuProfile = preload( + "res://ui/components/bubble_menu/bubble_menu_profile.tres" +) +const RING_RADIUS: float = 172.0 +const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) +const CONTROLLER_SELECTION_DEADZONE: float = 0.35 +const ControllerMappingManagerType = preload( + "res://settings/controller_mapping_manager.gd" +) + +var _buttons: Array[BubbleButton] = [] +var _is_open: bool = false +var _selected_sector: int = 0 +var _controller_selection_mode: bool = false +var _controller_mapping_manager: ControllerMappingManagerType + + +func setup_controller_mapping( + mapping_manager: ControllerMappingManagerType, +) -> void: + _controller_mapping_manager = mapping_manager + + +func _ready() -> void: + visible = false + mouse_filter = Control.MOUSE_FILTER_IGNORE + for sector: int in _sector_count(): + var bubble := BubbleButtonScene.instantiate() as BubbleButton + bubble.profile = BubbleProfile + bubble.focus_mode = Control.FOCUS_NONE + bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + bubble.text = _label_for_sector(sector) + add_child(bubble) + _buttons.append(bubble) + _apply_selection_styles() + + +func handle_input(event: InputEvent, can_open: bool) -> bool: + if not event.is_action(_input_action_name()): + return false + var key_event := event as InputEventKey + if key_event != null and key_event.echo: + return false + if event.is_pressed(): + if _is_open or not can_open: + return _is_open + open_menu(event is InputEventJoypadButton) + return true + if not _is_open: + return false + var selected: int = _selected_sector + close_menu() + _emit_selected_sector(selected) + return true + + +func open_menu(controller_selection: bool = false) -> void: + _is_open = true + _selected_sector = _initial_sector() + _controller_selection_mode = controller_selection + visible = true + _refresh_labels() + _layout_bubbles() + _apply_selection_styles() + + +func close_menu() -> void: + _is_open = false + visible = false + + +func is_open() -> bool: + return _is_open + + +func _refresh_labels() -> void: + for sector: int in _buttons.size(): + _buttons[sector].text = _label_for_sector(sector) + + +func _process(_delta: float) -> void: + if not _is_open: + return + _layout_bubbles() + _update_selection() + + +func _layout_bubbles() -> void: + var center: Vector2 = size * 0.5 + var sector_count: int = _sector_count() + for sector: int in sector_count: + var angle: float = ( + -PI * 0.5 + TAU * float(sector) / float(sector_count) + ) + var bubble_center: Vector2 = ( + center + Vector2.from_angle(angle) * RING_RADIUS + ) + var bubble: BubbleButton = _buttons[sector] + bubble.position = bubble_center - BUBBLE_SIZE * 0.5 + bubble.size = BUBBLE_SIZE + bubble.pivot_offset = BUBBLE_SIZE * 0.5 + + +func _update_selection() -> void: + var stick: Vector2 = _get_selection_stick() + if stick.length() >= CONTROLLER_SELECTION_DEADZONE: + _select_sector_from_offset(stick) + return + if _controller_selection_mode: + return + var offset: Vector2 = get_local_mouse_position() - size * 0.5 + if offset.length() >= 24.0: + _select_sector_from_offset(offset) + + +func _get_selection_stick() -> Vector2: + if _controller_mapping_manager != null: + return Vector2( + _controller_mapping_manager.get_role_axis( + ControllerMappingManagerType.ROLE_RIGHT_STICK_X + ), + _controller_mapping_manager.get_role_axis( + ControllerMappingManagerType.ROLE_RIGHT_STICK_Y + ), + ) + return Vector2( + Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), + Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), + ) + + +func _select_sector_from_offset(offset: Vector2) -> void: + var sector_count: int = _sector_count() + var half_sector: float = TAU / (float(sector_count) * 2.0) + var angle: float = fposmod(offset.angle() + PI * 0.5 + half_sector, TAU) + var sector: int = int(floor(angle / (TAU / float(sector_count)))) + if sector == _selected_sector: + return + _selected_sector = sector + _apply_selection_styles() + + +func _apply_selection_styles() -> void: + for sector: int in _buttons.size(): + var bubble: BubbleButton = _buttons[sector] + bubble.apply_profile() + if sector == _selected_sector: + bubble.add_theme_stylebox_override( + "normal", BubbleProfile.make_hover_style() + ) + + +func _input_action_name() -> StringName: + return StringName() + + +func _sector_count() -> int: + return 1 + + +func _initial_sector() -> int: + return 0 + + +func _label_for_sector(_sector: int) -> String: + return "" + + +func _emit_selected_sector(_sector: int) -> void: + pass diff --git a/ui/radial_action_menu.gd.uid b/ui/radial_action_menu.gd.uid new file mode 100644 index 0000000..a0818f4 --- /dev/null +++ b/ui/radial_action_menu.gd.uid @@ -0,0 +1 @@ +uid://clthphpughvu2 diff --git a/ui/settings/settings_bubble_page.gd b/ui/settings/settings_bubble_page.gd index 8fc6fca..2a8dc6d 100644 --- a/ui/settings/settings_bubble_page.gd +++ b/ui/settings/settings_bubble_page.gd @@ -13,9 +13,6 @@ const ControllerFocusNavigationType = preload( @export var back_focus_path: NodePath @export var back_entry_path: NodePath @export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0) -@export var compact_maximum_layout_size: Vector2 = Vector2.ZERO -@export var compact_width_threshold: float = 680.0 -@export var compact_height_threshold: float = 500.0 @export_range(0.0, 128.0, 1.0) var transition_safe_margin: float = 24.0 var _cluster: BubbleCluster @@ -176,13 +173,9 @@ func _process(delta: float) -> void: func _update_layout() -> void: if not is_node_ready() or _cluster == null: return - var compact: bool = false - var layout_maximum: Vector2 = maximum_layout_size - if compact and compact_maximum_layout_size != Vector2.ZERO: - layout_maximum = compact_maximum_layout_size var field_size := Vector2( - minf(size.x, layout_maximum.x), - minf(size.y, layout_maximum.y) + minf(size.x, maximum_layout_size.x), + minf(size.y, maximum_layout_size.y) ) field_size.x = maxf(1.0, field_size.x) field_size.y = maxf(1.0, field_size.y) @@ -190,7 +183,7 @@ func _update_layout() -> void: if not _is_transitioning: _cluster.position = _resting_cluster_position _cluster.size = field_size - _cluster.apply_layout(field_size, compact) + _cluster.apply_layout(field_size, false) _configure_focus_navigation() diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index ee8c049..f1c4707 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -33,10 +33,6 @@ const KeyboardMouseMappingPanelType = preload( const DialogControllerNavigationType = preload( "res://ui/file_dialog_controller_navigation.gd" ) -const ControllerFocusNavigationType = preload( - "res://ui/controller_focus_navigation.gd" -) - signal applied signal closed signal closing @@ -117,6 +113,10 @@ var _mouse_sensitivity: float = 0.005 var _controller_sensitivity: float = 2.5 var _invert_camera_y: bool = false var _on_screen_keyboard_enabled: bool = false +var _chat_dock_right: bool = false +var _chat_mobile_mode: bool = false +var _paint_dock_right: bool = true +var _fullscreen_enabled: bool = false var _master_volume: float = 1.0 var _music_volume: float = 1.0 var _effects_volume: float = 1.0 @@ -144,6 +144,11 @@ var _pending_identity_path := "" var _pending_import_data: Dictionary = {} +func _notification(what: int) -> void: + if what == NOTIFICATION_VISIBILITY_CHANGED and not visible: + _release_owned_focus() + + func _ready() -> void: _pages = { PAGE_DISPLAY: _display_page, @@ -235,7 +240,7 @@ func _connect_controls() -> void: ) %ControllerMapping.pressed.connect(_open_controller_mapping) %KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping) - %OpenDataFolder.pressed.connect(_confirm_open_data_folder) + %OpenDataFolder.pressed.connect(_open_data_folder) %ChangeDataFolder.pressed.connect(_choose_data_folder) %ExportPlayerIdentity.pressed.connect( _choose_identity_export.bind("player") @@ -348,6 +353,7 @@ func open_panel( animate_gameplay_host_transitions: bool = false, ) -> void: _cancel_panel_transition() + _release_owned_focus() _presentation_mode = presentation_mode _animate_gameplay_host_transitions = animate_gameplay_host_transitions _settings_manager = settings_manager @@ -359,7 +365,6 @@ func open_panel( panel_visibility_changed.emit(true) _main_panel.modulate.a = 1.0 _main_panel.scale = Vector2.ONE - call_deferred("_configure_controller_navigation") call_deferred("_focus_active_tab") if ( presentation_mode == PresentationMode.TITLE_EMBEDDED @@ -472,6 +477,7 @@ func _finish_panel_close(applied_result: bool) -> void: _keyboard_mouse_mapping_panel.close_panel() _main_panel.modulate.a = 1.0 _main_panel.scale = Vector2.ONE + _release_owned_focus() hide() panel_visibility_changed.emit(false) if applied_result: @@ -521,6 +527,7 @@ func handle_back() -> void: func _select_page(page_id: StringName, focus_tab: bool = true) -> void: if not _pages.has(page_id): return + var target_tab: OrganizerTab = _tabs.get(page_id) _active_page_id = page_id for candidate_id: StringName in _pages: _pages[candidate_id].visible = candidate_id == page_id @@ -530,9 +537,8 @@ func _select_page(page_id: StringName, focus_tab: bool = true) -> void: ) if page_id == PAGE_DATA: _refresh_data_page() - call_deferred("_configure_controller_navigation") - if focus_tab: - _tabs[page_id].call_deferred("grab_focus") + if focus_tab and target_tab != null: + target_tab.grab_focus() func _focus_active_tab() -> void: @@ -541,30 +547,6 @@ func _focus_active_tab() -> void: tab.grab_focus() -func _configure_controller_navigation() -> void: - if not is_node_ready() or not visible: - return - var controls: Array[Control] = [] - _collect_focusable_controls(_main_panel, controls) - ControllerFocusNavigationType.configure_spatial_neighbors(controls) - - -func _collect_focusable_controls( - node: Node, - controls: Array[Control], -) -> void: - for child: Node in node.get_children(): - var control := child as Control - if control != null and not control.is_visible_in_tree(): - continue - if ( - control != null - and ControllerFocusNavigationType.is_focusable(control) - ): - controls.append(control) - _collect_focusable_controls(child, controls) - - func _open_controller_mapping() -> void: if _controller_mapping_manager == null: _feedback.text = "no controller mapping service is available" @@ -627,38 +609,30 @@ func _refresh_data_page() -> void: "copy player fingerprint ยท %s" % NetworkIdentityCrypto.compact_suffix(fingerprint) ) - call_deferred("_configure_controller_navigation") func _open_data_folder() -> void: - if _data_root == null or not _data_root.open_folder(): - _feedback.text = "could not open the data folder." - - -func _confirm_open_data_folder() -> void: + if ( + not is_visible_in_tree() + or _active_page_id != PAGE_DATA + or not _data_page.is_visible_in_tree() + or not %OpenDataFolder.is_visible_in_tree() + or get_viewport().gui_get_focus_owner() != %OpenDataFolder + ): + return if _data_root == null: _feedback.text = "the data folder is unavailable." return - var dialog := ConfirmationDialog.new() - dialog.title = "open data folder?" - dialog.ok_button_text = "open folder" - dialog.cancel_button_text = "cancel" - dialog.dialog_text = ( - "open this folder outside NETfishing?\n\n" + _data_root.root_path - ) - dialog.confirmed.connect(func() -> void: - _open_data_folder() - dialog.queue_free() - ) - dialog.canceled.connect(dialog.queue_free) - if _interface_fonts != null: - _interface_fonts.apply_utility_theme(dialog) - add_child(dialog) - dialog.popup_centered(Vector2i(620, 300)) - _configure_confirmation_dialog.call_deferred( - dialog, - dialog.get_cancel_button(), - ) + if not _data_root.open_folder(): + _feedback.text = "could not open the data folder." + + +func _release_owned_focus() -> void: + if not is_inside_tree(): + return + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and is_ancestor_of(focus_owner): + focus_owner.release_focus() func _copy_player_fingerprint() -> void: @@ -950,6 +924,10 @@ func _apply_settings() -> void: edited.controller_camera_sensitivity = _controller_sensitivity edited.invert_camera_y = _invert_camera_y edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled + edited.chat_dock_right = _chat_dock_right + edited.chat_mobile_mode = _chat_mobile_mode + edited.paint_dock_right = _paint_dock_right + edited.fullscreen_enabled = _fullscreen_enabled edited.master_volume = _master_volume edited.music_volume = _music_volume edited.effects_volume = _effects_volume @@ -976,16 +954,20 @@ func _load_controls() -> void: _controller_sensitivity = settings.controller_camera_sensitivity _invert_camera_y = settings.invert_camera_y _on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled + _chat_dock_right = settings.chat_dock_right + _chat_mobile_mode = settings.chat_mobile_mode + _paint_dock_right = settings.paint_dock_right + _fullscreen_enabled = settings.fullscreen_enabled _master_volume = settings.master_volume _music_volume = settings.music_volume _effects_volume = settings.effects_volume _environment_volume = settings.environment_volume _world_pixelation.select(settings.world_pixel_size - 1) _ui_pixelation.select(settings.ui_pixel_size - 1) - _chat_dock.select(1 if settings.chat_dock_right else 0) - _chat_mode.select(1 if settings.chat_mobile_mode else 0) - _paint_dock.select(1 if settings.paint_dock_right else 0) - _fullscreen_toggle.set_pressed_no_signal(settings.fullscreen_enabled) + _chat_dock.select(1 if _chat_dock_right else 0) + _chat_mode.select(1 if _chat_mobile_mode else 0) + _paint_dock.select(1 if _paint_dock_right else 0) + _fullscreen_toggle.set_pressed_no_signal(_fullscreen_enabled) _master_volume_slider.set_value_no_signal(_master_volume * 100.0) _music_volume_slider.set_value_no_signal(_music_volume * 100.0) _effects_volume_slider.set_value_no_signal(_effects_volume * 100.0) @@ -1084,43 +1066,22 @@ func _set_ui_pixelation(pixel_size: int) -> void: func _on_chat_dock_selected(index: int) -> void: - _set_presentation_toggle( - &"chat_dock_right", - _chat_dock.get_item_id(index) == 1, - ) + _chat_dock_right = _chat_dock.get_item_id(index) == 1 func _on_chat_mode_selected(index: int) -> void: - _set_presentation_toggle( - &"chat_mobile_mode", - _chat_mode.get_item_id(index) == 1, - ) + _chat_mobile_mode = _chat_mode.get_item_id(index) == 1 func _on_paint_dock_selected(index: int) -> void: - _set_presentation_toggle( - &"paint_dock_right", - _paint_dock.get_item_id(index) == 1, - ) + _paint_dock_right = _paint_dock.get_item_id(index) == 1 func _set_fullscreen(enabled: bool) -> void: - _set_presentation_toggle(&"fullscreen_enabled", enabled) + _fullscreen_enabled = enabled _refresh_value_labels() -func _set_presentation_toggle( - property_name: StringName, - enabled: bool, -) -> void: - if _settings_manager == null: - return - var edited: PlayerSettings = _settings_manager.current_settings.copy() - edited.set(property_name, enabled) - if not _settings_manager.apply_settings(edited): - _feedback.text = "failed to save display setting." - - func _on_mouse_sensitivity_changed(value: float) -> void: _mouse_sensitivity = clampf( value, diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index 01dcccd..23642f9 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -85,6 +85,7 @@ unique_name_in_owner = true custom_minimum_size = Vector2(115, 52) layout_mode = 2 focus_mode = 2 +focus_neighbor_bottom = NodePath("../../ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow/OpenDataFolder") text = "data" script = ExtResource("3_tab") palette_index = 1 @@ -417,6 +418,7 @@ unique_name_in_owner = true custom_minimum_size = Vector2(330, 48) layout_mode = 2 focus_mode = 2 +focus_neighbor_bottom = NodePath("../../BindingButtons/ControllerMapping") toggle_mode = true text = "off" @@ -433,6 +435,8 @@ unique_name_in_owner = true custom_minimum_size = Vector2(300, 52) layout_mode = 2 focus_mode = 2 +focus_neighbor_right = NodePath("../KeyboardMapping") +focus_neighbor_top = NodePath("../../Grid/OnScreenKeyboardToggle") text = "controller binds" [node name="KeyboardMapping" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/ControlsPage/BindingButtons"] @@ -440,6 +444,8 @@ unique_name_in_owner = true custom_minimum_size = Vector2(300, 52) layout_mode = 2 focus_mode = 2 +focus_neighbor_left = NodePath("../ControllerMapping") +focus_neighbor_top = NodePath("../../Grid/OnScreenKeyboardToggle") text = "keyboard binds" [node name="AccessibilityPage" type="VBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack"] @@ -554,6 +560,9 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_right = NodePath("../ChangeDataFolder") +focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab") +focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint") text = "open data folder" [node name="ChangeDataFolder" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/FolderRow"] @@ -562,6 +571,9 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_left = NodePath("../OpenDataFolder") +focus_neighbor_top = NodePath("../../../../../../../../TabBar/DataTab") +focus_neighbor_bottom = NodePath("../../CopyPlayerFingerprint") text = "change data folder" [node name="CopyPlayerFingerprint" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] @@ -569,6 +581,8 @@ unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 focus_mode = 2 +focus_neighbor_top = NodePath("../FolderRow/OpenDataFolder") +focus_neighbor_bottom = NodePath("../PlayerIdentityRow/ExportPlayerIdentity") text = "copy player fingerprint" [node name="PlayerIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] @@ -581,6 +595,9 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_right = NodePath("../ImportPlayerIdentity") +focus_neighbor_top = NodePath("../../CopyPlayerFingerprint") +focus_neighbor_bottom = NodePath("../../HostIdentityRow/ExportHostIdentity") text = "export player identity" [node name="ImportPlayerIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/PlayerIdentityRow"] @@ -589,6 +606,9 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_left = NodePath("../ExportPlayerIdentity") +focus_neighbor_top = NodePath("../../CopyPlayerFingerprint") +focus_neighbor_bottom = NodePath("../../HostIdentityRow/ImportHostIdentity") text = "import player identity" [node name="HostIdentityRow" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons"] @@ -601,6 +621,8 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_right = NodePath("../ImportHostIdentity") +focus_neighbor_top = NodePath("../../PlayerIdentityRow/ExportPlayerIdentity") text = "export host identity" [node name="ImportHostIdentity" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/PageStack/DataPage/DataScroll/DataButtons/HostIdentityRow"] @@ -609,6 +631,8 @@ custom_minimum_size = Vector2(0, 44) layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 +focus_neighbor_left = NodePath("../ExportHostIdentity") +focus_neighbor_top = NodePath("../../PlayerIdentityRow/ImportPlayerIdentity") text = "import host identity" [node name="FooterGroup" type="MarginContainer" parent="MainPanel/OuterMargin/Layout"] diff --git a/ui/the_net_page.gd b/ui/the_net_page.gd index 047debf..555eeda 100644 --- a/ui/the_net_page.gd +++ b/ui/the_net_page.gd @@ -128,8 +128,6 @@ func reset_controller_zone() -> void: 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 @@ -160,37 +158,10 @@ func handle_controller_input(event: InputEvent) -> bool: 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: +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: diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index 4edacbb..d609894 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -8,9 +8,6 @@ const UIReferencePresentationType = preload( const ControllerFocusPresentationType = preload( "res://ui/controller_focus_presentation.gd" ) -const ControllerFocusRecoveryType = preload( - "res://ui/controller_focus_recovery.gd" -) const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) @@ -50,8 +47,6 @@ func _ready() -> void: 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) var controller_focus_presentation := ControllerFocusPresentationType.new() _ui_root.add_child(controller_focus_presentation) var root_viewport: Viewport = get_viewport()