From 8ca2c10f717b7ad611945f91f46059290aec62ad Mon Sep 17 00:00:00 2001 From: Voyager Date: Tue, 4 Aug 2026 01:09:27 -0400 Subject: [PATCH] Improve handheld data folder setup --- main/main.gd | 107 ++++++++++++++++++++++++++++++-- network/player_data_root.gd | 23 ++++++- ui/interface_font_controller.gd | 65 +++++++++++++++++++ ui/settings_panel.gd | 6 +- 4 files changed, 190 insertions(+), 11 deletions(-) diff --git a/main/main.gd b/main/main.gd index 343435c..7a01295 100644 --- a/main/main.gd +++ b/main/main.gd @@ -200,6 +200,8 @@ var _server_trust_dialog: ConfirmationDialog var _pending_trust_changed: bool = false var _identity_notice_dialog: AcceptDialog var _data_setup_dialog: ConfirmationDialog +var _data_setup_choose_button: Button +var _data_setup_current_button: Button var _data_folder_dialog: FileDialog var _data_folder_picker_generation: int = 0 var _restore_data_setup_after_picker: bool = false @@ -657,10 +659,10 @@ func _show_data_root_setup() -> void: _data_setup_dialog.cancel_button_text = "Quit" _data_setup_dialog.confirmed.connect(_use_default_data_root) _data_setup_dialog.canceled.connect(get_tree().quit) - _data_setup_dialog.add_button( + _data_setup_choose_button = _data_setup_dialog.add_button( "Choose Another Folder", false, "choose" ) - _data_setup_dialog.add_button( + _data_setup_current_button = _data_setup_dialog.add_button( "Keep Current Location", false, "current" ) _data_setup_dialog.custom_action.connect(_on_data_setup_action) @@ -675,20 +677,54 @@ func _show_data_root_setup() -> void: % (default_path if not default_path.is_empty() else "Choose a folder") ) _data_setup_dialog.ok_button_text = ( - "Move to Documents/NETFISHING" if legacy else "Use This Folder" + ( + "Move to Documents/NETFISHING" + if legacy else "Use This Folder" + ) + if not default_path.is_empty() + else "Keep Current Location" ) + _data_setup_current_button.visible = not default_path.is_empty() if not _data_root.error_message.is_empty(): _data_setup_dialog.dialog_text = ( "Your NETfishing data folder is unavailable.\n\n%s" % _data_root.error_message ) _data_setup_dialog.popup_centered(Vector2i(640, 360)) + _focus_data_setup_actions.call_deferred() + + +func _focus_data_setup_actions() -> void: + if _data_setup_dialog == null or not _data_setup_dialog.visible: + return + var actions: Array[Button] = [ + _data_setup_dialog.get_ok_button(), + _data_setup_choose_button, + ] + if _data_setup_current_button.visible: + actions.append(_data_setup_current_button) + actions.append(_data_setup_dialog.get_cancel_button()) + for index: int in actions.size(): + var action: Button = actions[index] + var previous: Button = actions[posmod(index - 1, actions.size())] + var next: Button = actions[(index + 1) % actions.size()] + action.focus_mode = Control.FOCUS_ALL + action.focus_neighbor_left = action.get_path_to(previous) + action.focus_neighbor_top = action.focus_neighbor_left + action.focus_neighbor_right = action.get_path_to(next) + action.focus_neighbor_bottom = action.focus_neighbor_right + _data_setup_dialog.get_ok_button().grab_focus() func _use_default_data_root() -> void: var path: String = _data_root.default_visible_path() if path.is_empty(): - _show_folder_picker() + _activate_selected_data_path( + ProjectSettings.globalize_path( + PlayerDataRoot.APP_DATA_PORTABLE_PATH + ), + true, + ) return _activate_selected_data_path(path) @@ -737,7 +773,7 @@ func _open_folder_picker_after_modal(generation: int) -> void: if not _data_root.default_visible_path().is_empty() else OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) ) - _data_folder_dialog.popup_centered_ratio(0.75) + _interface_fonts.popup_file_dialog(_data_folder_dialog) func _on_data_folder_picker_canceled() -> void: @@ -934,6 +970,9 @@ func _on_peer_identity_observed(_peer_id: int, status: String) -> void: func _input(event: InputEvent) -> void: + if _handle_data_root_controller_input(event): + get_viewport().set_input_as_handled() + return if _game_ui.is_controller_mapping_capturing(): return if ( @@ -972,6 +1011,64 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func _handle_data_root_controller_input(event: InputEvent) -> bool: + var button_event := event as InputEventJoypadButton + if button_event == null or not button_event.pressed: + return false + var setup_visible: bool = ( + _data_setup_dialog != null and _data_setup_dialog.visible + ) + var picker_visible: bool = ( + _data_folder_dialog != null and _data_folder_dialog.visible + ) + if not setup_visible and not picker_visible: + return false + var use_mapping: bool = _controller_mapping_manager.has_custom_mapping() + var accept_pressed: bool = ( + _controller_mapping_manager.event_matches_role( + event, + ControllerMappingManagerType.ROLE_A, + ) + if use_mapping + else ( + button_event.button_index == JOY_BUTTON_A + or event.is_action_pressed("ui_accept") + ) + ) + var cancel_pressed: bool = ( + _controller_mapping_manager.event_matches_role( + event, + ControllerMappingManagerType.ROLE_B, + ) + if use_mapping + else ( + button_event.button_index == JOY_BUTTON_B + or event.is_action_pressed("ui_cancel") + ) + ) + if cancel_pressed: + if picker_visible: + _on_data_folder_picker_canceled() + else: + _data_setup_dialog.get_cancel_button().pressed.emit() + return true + if not accept_pressed: + return false + var focused: Control = ( + _data_folder_dialog.gui_get_focus_owner() + if picker_visible + else _data_setup_dialog.gui_get_focus_owner() + ) + var focused_button := focused as BaseButton + if focused_button != null and not focused_button.disabled: + focused_button.pressed.emit() + return true + if setup_visible: + _data_setup_dialog.get_ok_button().pressed.emit() + return true + return false + + func _is_pause_open_request(event: InputEvent) -> bool: return ( event.is_action_pressed("open_system_menu") diff --git a/network/player_data_root.gd b/network/player_data_root.gd index c0fe26e..ba831f4 100644 --- a/network/player_data_root.gd +++ b/network/player_data_root.gd @@ -72,7 +72,17 @@ func resolve() -> bool: func default_visible_path() -> String: var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) - return documents.path_join("NETFISHING") if not documents.is_empty() else "" + if documents.is_empty(): + return "" + var candidate: String = _normalize(documents.path_join("NETFISHING")) + if not candidate.is_absolute_path(): + return "" + var classification: StringName = classify_candidate_path( + candidate, + ProjectSettings.globalize_path("res://"), + OS.get_executable_path().get_base_dir(), + ) + return candidate if classification == PATH_ALLOWED else "" func select_new_root(path: String, app_data: bool = false) -> bool: @@ -232,14 +242,21 @@ func _validate_candidate(path: String, create: bool) -> bool: var normalized: String = _normalize(path) if normalized.is_empty() or not normalized.is_absolute_path(): return _fail("Choose an absolute filesystem folder.") + var app_data_path: String = _normalize( + ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH) + ) + var exported_app_data: bool = ( + normalized == app_data_path + and not OS.has_feature("editor") + ) var classification: StringName = classify_candidate_path( normalized, ProjectSettings.globalize_path("res://"), OS.get_executable_path().get_base_dir(), ) - if classification == PATH_SOURCE_PROJECT: + if classification == PATH_SOURCE_PROJECT and not exported_app_data: return _fail("The project folder cannot be used as the player data folder.") - if classification == PATH_INSTALLATION: + if classification == PATH_INSTALLATION and not exported_app_data: return _fail( "The application installation folder cannot be used as the player data folder." ) diff --git a/ui/interface_font_controller.gd b/ui/interface_font_controller.gd index 239ebbd..f8db71e 100644 --- a/ui/interface_font_controller.gd +++ b/ui/interface_font_controller.gd @@ -2,9 +2,13 @@ class_name InterfaceFontController extends Node const STANDARD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf") +const COMPACT_FILE_DIALOG_LIMIT := Vector2i(800, 600) +const COMPACT_FILE_DIALOG_MARGIN := Vector2i(12, 12) +const COMPACT_FILE_DIALOG_FONT_SIZE: int = 17 var _game_theme: Theme = preload("res://ui/game_theme.tres") var _utility_theme: Theme +var _compact_file_dialog_theme: Theme func _ready() -> void: @@ -40,5 +44,66 @@ func apply_utility_theme(themed_node: Node) -> void: (themed_node as Window).theme = _utility_theme +func popup_file_dialog(dialog: FileDialog) -> void: + if dialog == null or not dialog.is_inside_tree(): + return + var host_window: Window = dialog.get_parent().get_window() + var host_size: Vector2i = host_window.size + var compact: bool = ( + host_size.x <= COMPACT_FILE_DIALOG_LIMIT.x + or host_size.y <= COMPACT_FILE_DIALOG_LIMIT.y + ) + if not compact: + apply_utility_theme(dialog) + dialog.min_size = Vector2i(200, 70) + dialog.max_size = Vector2i.ZERO + dialog.popup_centered_ratio(0.75) + return + if _compact_file_dialog_theme == null: + _compact_file_dialog_theme = _utility_theme.duplicate(true) + _compact_file_dialog_theme.default_font_size = ( + COMPACT_FILE_DIALOG_FONT_SIZE + ) + dialog.theme = _compact_file_dialog_theme + var available_size := Vector2i( + maxi(1, host_size.x - COMPACT_FILE_DIALOG_MARGIN.x * 2), + maxi(1, host_size.y - COMPACT_FILE_DIALOG_MARGIN.y * 2), + ) + dialog.min_size = Vector2i.ZERO + dialog.max_size = available_size + dialog.popup_centered(available_size) + # FileDialog's desktop-oriented intrinsic minimum can exceed a compact + # 4:3 display. The compact font keeps its controls usable while this final + # clamp guarantees the embedded window cannot escape the physical screen. + dialog.size = available_size + if dialog.get_viewport().gui_embed_subwindows: + dialog.position = (host_size - available_size) / 2 + _finalize_compact_file_dialog.call_deferred( + dialog, host_size, available_size + ) + + +func _finalize_compact_file_dialog( + dialog: FileDialog, + host_size: Vector2i, + requested_size: Vector2i, +) -> void: + await get_tree().process_frame + if dialog == null or not dialog.visible: + return + # Embedded windows report their content origin below the title bar. Account + # for that inset after Godot has laid the window out so its bottom and right + # edges retain the same safe margin as its decorated top and left edges. + var safe_bottom_right: Vector2i = host_size - COMPACT_FILE_DIALOG_MARGIN + var fitted_size: Vector2i = requested_size + fitted_size.x -= maxi( + 0, dialog.position.x + fitted_size.x - safe_bottom_right.x + ) + fitted_size.y -= maxi( + 0, dialog.position.y + fitted_size.y - safe_bottom_right.y + ) + dialog.size = Vector2i(maxi(1, fitted_size.x), maxi(1, fitted_size.y)) + + func readable_font() -> Font: return STANDARD_FONT diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index a29cade..768980a 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -472,7 +472,7 @@ func _choose_data_folder() -> void: _interface_fonts.apply_utility_theme(_data_folder_dialog) add_child(_data_folder_dialog) _data_folder_dialog.current_dir = _data_root.root_path.get_base_dir() - _data_folder_dialog.popup_centered_ratio(0.75) + _interface_fonts.popup_file_dialog(_data_folder_dialog) func _change_data_folder(path: String) -> void: @@ -540,7 +540,7 @@ func _choose_identity_export(identity_type: String) -> void: add_child(_export_file_dialog) _export_file_dialog.current_dir = suggested.get_base_dir() _export_file_dialog.current_file = suggested.get_file() - _export_file_dialog.popup_centered_ratio(0.75) + _interface_fonts.popup_file_dialog(_export_file_dialog) func _identity_export_file_selected(path: String) -> void: @@ -565,7 +565,7 @@ func _choose_identity_import(identity_type: String) -> void: _interface_fonts.apply_utility_theme(_backup_file_dialog) add_child(_backup_file_dialog) _backup_file_dialog.current_dir = _data_root.identity_backup_directory() - _backup_file_dialog.popup_centered_ratio(0.75) + _interface_fonts.popup_file_dialog(_backup_file_dialog) func _identity_import_file_selected(path: String) -> void: