Refresh data management dialogs

This commit is contained in:
Alexander Sellite 2026-09-02 07:19:25 -04:00
parent 3a61056ce5
commit cab313d69d
7 changed files with 720 additions and 103 deletions

View file

@ -30,8 +30,8 @@ const KeyboardMouseMappingManagerType = preload(
const KeyboardMouseMappingPanelType = preload(
"res://ui/keyboard_mouse_mapping_panel.gd"
)
const DialogControllerNavigationType = preload(
"res://ui/file_dialog_controller_navigation.gd"
const DataManagementDialogType = preload(
"res://ui/data_management_dialog.gd"
)
signal applied
signal closed
@ -139,7 +139,7 @@ var _keyboard_mouse_mapping_panel: KeyboardMouseMappingPanelType
var _data_folder_dialog: FileDialog
var _backup_file_dialog: FileDialog
var _export_file_dialog: FileDialog
var _passphrase_dialog: ConfirmationDialog
var _data_management_dialog: DataManagementDialogType
var _passphrase_entry: LineEdit
var _passphrase_confirm: LineEdit
var _pending_identity_operation := ""
@ -150,6 +150,11 @@ var _pending_import_data: Dictionary = {}
func _notification(what: int) -> void:
if what == NOTIFICATION_VISIBILITY_CHANGED and not visible:
if (
_data_management_dialog != null
and _data_management_dialog.is_open()
):
_data_management_dialog.dismiss()
_release_owned_focus()
@ -183,6 +188,22 @@ func _ready() -> void:
_keyboard_mouse_mapping_panel.closed.connect(
_on_keyboard_mouse_mapping_panel_closed
)
_data_management_dialog = DataManagementDialogType.new()
_data_management_dialog.name = "DataManagementDialog"
_data_management_dialog.theme = theme
# Add the dialog beneath this panel first so it becomes ready while the
# scene is assembling. Moving a child directly to a sibling from _ready()
# makes Godot reject the add because the parent is still busy setting up.
add_child(_data_management_dialog)
var dialog_host := get_parent() as Control
# The pause settings panel sits inside a CenterContainer, which would size a
# direct child as a centered dialog instead of a full-screen input blocker.
# Host the overlay on the first non-container parent so it always covers the
# canonical settings presentation.
if dialog_host is Container:
dialog_host = dialog_host.get_parent() as Control
if dialog_host != null:
call_deferred("_move_data_management_dialog_to_host", dialog_host)
resized.connect(_refresh_panel_size)
var parent_control := get_parent() as Control
if parent_control != null:
@ -191,6 +212,27 @@ func _ready() -> void:
call_deferred("_refresh_panel_size")
func _exit_tree() -> void:
if (
_data_management_dialog != null
and is_instance_valid(_data_management_dialog)
and not _data_management_dialog.is_queued_for_deletion()
):
_data_management_dialog.queue_free()
func _move_data_management_dialog_to_host(dialog_host: Control) -> void:
if (
_data_management_dialog == null
or not is_instance_valid(_data_management_dialog)
or dialog_host == null
or not is_instance_valid(dialog_host)
or _data_management_dialog.get_parent() == dialog_host
):
return
_data_management_dialog.reparent(dialog_host)
func _populate_option_buttons() -> void:
for selector: OptionButton in [_world_pixelation, _ui_pixelation]:
selector.clear()
@ -481,6 +523,11 @@ func _finish_animated_close(
func _finish_panel_close(applied_result: bool) -> void:
_cancel_panel_transition()
if (
_data_management_dialog != null
and _data_management_dialog.is_open()
):
_data_management_dialog.dismiss()
if (
_controller_mapping_panel != null
and _controller_mapping_panel.is_open()
@ -512,6 +559,12 @@ func _cancel_panel_transition() -> void:
func handle_back() -> void:
if (
_data_management_dialog != null
and _data_management_dialog.is_open()
):
_data_management_dialog.cancel()
return
if (
_keyboard_mouse_mapping_panel != null
and _keyboard_mouse_mapping_panel.is_open()
@ -606,6 +659,20 @@ func is_input_mapping_capturing() -> bool:
)
func is_data_management_dialog_open() -> bool:
return (
_data_management_dialog != null
and _data_management_dialog.is_open()
)
func get_data_management_text_entry_control() -> Control:
if not is_data_management_dialog_open():
return null
var focused: Control = _data_management_dialog.focused_control()
return focused if focused is LineEdit or focused is TextEdit else null
func _refresh_data_page() -> void:
if not is_node_ready() or _data_root == null:
return
@ -651,7 +718,16 @@ 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):
if (
focus_owner != null
and (
is_ancestor_of(focus_owner)
or (
_data_management_dialog != null
and _data_management_dialog.is_ancestor_of(focus_owner)
)
)
):
focus_owner.release_focus()
@ -716,40 +792,57 @@ func _change_data_folder(path: String) -> void:
func _show_existing_data_folder_choice(path: String) -> void:
var dialog := ConfirmationDialog.new()
dialog.title = "existing straywild data"
dialog.ok_button_text = "use selected data"
dialog.dialog_text = (
"the selected folder contains different straywild data.\n\n"
+ "choose one complete data set. data will not be merged."
_data_management_dialog.present(
"existing straywild data",
(
"the selected folder already contains different straywild data. "
+ "choose one complete data set; data is never merged."
),
[
{
"id": &"use_selected",
"text": "use selected data",
"tone": DataManagementDialogType.ActionTone.PRIMARY,
},
{
"id": &"replace",
"text": "replace selected data",
"tone": DataManagementDialogType.ActionTone.DANGER,
},
{
"id": &"cancel",
"text": "cancel",
"tone": DataManagementDialogType.ActionTone.SECONDARY,
"cancel": true,
},
],
_on_existing_data_folder_choice_action.bind(path),
null,
null,
Vector2(600.0, 390.0),
)
dialog.add_button("replace selected data", false, "replace")
dialog.confirmed.connect(func() -> void:
func _on_existing_data_folder_choice_action(
action: StringName,
path: String,
) -> void:
if action == &"use_selected":
if _data_root.use_existing_root(path):
_complete_data_root_change("data folder changed.")
else:
_feedback.text = _data_root.error_message
dialog.queue_free()
return
if action != &"replace":
return
var result: Dictionary = PortableDataMigration.replace_existing_with_active(
_data_root, path
)
dialog.custom_action.connect(func(action: StringName) -> void:
if action != &"replace":
return
var result: Dictionary = PortableDataMigration.replace_existing_with_active(
_data_root, path
)
_feedback.text = str(
result.get("message", "could not replace selected data.")
)
if bool(result.get("ok", false)):
_complete_data_root_change(_feedback.text.to_lower())
dialog.queue_free()
)
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog)
dialog.popup_centered(Vector2i(620, 340))
_configure_confirmation_dialog.call_deferred(
dialog, dialog.get_cancel_button()
_feedback.text = str(
result.get("message", "could not replace selected data.")
)
if bool(result.get("ok", false)):
_complete_data_root_change(_feedback.text.to_lower())
func _complete_data_root_change(message: String) -> void:
@ -816,51 +909,123 @@ func _identity_import_file_selected(path: String) -> void:
_show_passphrase_dialog(false)
func _show_passphrase_dialog(exporting: bool) -> void:
if _passphrase_dialog == null:
_passphrase_dialog = ConfirmationDialog.new()
_passphrase_dialog.title = "encrypted identity backup"
_passphrase_dialog.confirmed.connect(_submit_identity_passphrase)
var fields := VBoxContainer.new()
func _show_passphrase_dialog(
exporting: bool,
error_message: String = "",
previous_passphrase: String = "",
previous_confirmation: String = "",
) -> void:
var fields := VBoxContainer.new()
fields.name = "PassphraseFields"
fields.add_theme_constant_override("separation", 10)
if not error_message.is_empty():
var error_label := Label.new()
error_label.name = "PassphraseError"
error_label.text = error_message
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
error_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
error_label.add_theme_font_size_override("font_size", 17)
error_label.add_theme_color_override(
"font_color", Color("f08d98")
)
fields.add_child(error_label)
var instruction := Label.new()
instruction.text = (
"set a passphrase for this encrypted backup. you will need it "
+ "to restore the identity later."
if exporting
else "enter the passphrase used to protect this identity backup."
)
instruction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
instruction.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
instruction.add_theme_font_size_override("font_size", 18)
instruction.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
fields.add_child(instruction)
if exporting:
var warning := Label.new()
warning.text = (
"anyone with this backup and passphrase can use your identity."
"keep the backup file and passphrase private. anyone with both "
+ "can use this identity."
)
warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
warning.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
warning.add_theme_font_size_override("font_size", 17)
warning.add_theme_color_override("font_color", Color("f5d78e"))
fields.add_child(warning)
_passphrase_entry = LineEdit.new()
_passphrase_entry.placeholder_text = "passphrase (12 characters minimum)"
_passphrase_entry.secret = true
fields.add_child(_passphrase_entry)
_passphrase_entry = LineEdit.new()
_passphrase_entry.name = "Passphrase"
_passphrase_entry.placeholder_text = (
"new passphrase (12 characters minimum)"
if exporting else "backup passphrase"
)
_passphrase_entry.secret = true
_passphrase_entry.text = previous_passphrase
_passphrase_entry.custom_minimum_size = Vector2(0.0, 46.0)
_passphrase_entry.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_line_edit(_passphrase_entry)
fields.add_child(_passphrase_entry)
_passphrase_confirm = null
if exporting:
_passphrase_confirm = LineEdit.new()
_passphrase_confirm.name = "ConfirmPassphrase"
_passphrase_confirm.placeholder_text = "confirm passphrase"
_passphrase_confirm.secret = true
_passphrase_confirm.text = previous_confirmation
_passphrase_confirm.custom_minimum_size = Vector2(0.0, 46.0)
_passphrase_confirm.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_line_edit(_passphrase_confirm)
fields.add_child(_passphrase_confirm)
_passphrase_dialog.add_child(fields)
_interface_fonts.apply_utility_theme(_passphrase_dialog)
add_child(_passphrase_dialog)
_passphrase_confirm.visible = exporting
_passphrase_entry.clear()
_passphrase_confirm.clear()
_passphrase_dialog.dialog_text = (
"create a passphrase-encrypted backup."
if exporting else "enter the backup passphrase."
)
_passphrase_dialog.popup_centered(Vector2i(560, 300))
_configure_confirmation_dialog.call_deferred(
_passphrase_dialog, _passphrase_entry
_data_management_dialog.present(
("backup %s identity" % _pending_identity_type)
if exporting else ("restore %s identity" % _pending_identity_type),
"",
[
{
"id": &"confirm",
"text": "create backup" if exporting else "restore identity",
"tone": DataManagementDialogType.ActionTone.PRIMARY,
},
{
"id": &"cancel",
"text": "cancel",
"tone": DataManagementDialogType.ActionTone.SECONDARY,
"cancel": true,
},
],
_on_identity_passphrase_action,
fields,
_passphrase_entry,
Vector2(600.0, 385.0 if exporting else 310.0),
)
func _on_identity_passphrase_action(action: StringName) -> void:
if action == &"confirm":
_submit_identity_passphrase()
func _submit_identity_passphrase() -> void:
if _passphrase_entry == null:
return
var passphrase: String = _passphrase_entry.text
if _pending_identity_operation == "export":
_identity_backups.export_backup(
var confirmation: String = (
_passphrase_confirm.text if _passphrase_confirm != null else ""
)
if not _identity_backups.export_backup(
_pending_identity_type,
_pending_identity_path,
passphrase,
_passphrase_confirm.text,
)
confirmation,
):
_show_passphrase_dialog(
true,
_feedback.text,
passphrase,
confirmation,
)
return
var inspected: Dictionary = _identity_backups.import_backup(
_pending_identity_type,
@ -875,46 +1040,74 @@ func _submit_identity_passphrase() -> void:
"incoming": inspected["incoming_fingerprint"],
}
_show_identity_replacement_confirmation()
elif not bool(inspected.get("ok", false)):
_show_passphrase_dialog(false, _feedback.text, passphrase)
func _show_identity_replacement_confirmation() -> void:
var dialog := ConfirmationDialog.new()
dialog.title = "replace active identity?"
dialog.ok_button_text = "review replacement"
dialog.dialog_text = (
"current:\n%s\n\nincoming:\n%s\n\n"
+ "other players will recognize this device as the imported identity."
) % [
NetworkIdentityCrypto.format_fingerprint(_pending_import_data["current"]),
NetworkIdentityCrypto.format_fingerprint(_pending_import_data["incoming"]),
]
dialog.set_meta("confirmation_step", 1)
dialog.confirmed.connect(_advance_identity_replacement.bind(dialog))
dialog.canceled.connect(dialog.queue_free)
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog)
dialog.popup_centered(Vector2i(620, 360))
_configure_confirmation_dialog.call_deferred(
dialog, dialog.get_cancel_button()
_data_management_dialog.present(
"replace active identity?",
(
"current identity\n%s\n\nimported identity\n%s\n\n"
+ "other players will recognize this device as the imported identity."
) % [
NetworkIdentityCrypto.format_fingerprint(_pending_import_data["current"]),
NetworkIdentityCrypto.format_fingerprint(_pending_import_data["incoming"]),
],
[
{
"id": &"review",
"text": "review replacement",
"tone": DataManagementDialogType.ActionTone.PRIMARY,
},
{
"id": &"cancel",
"text": "cancel",
"tone": DataManagementDialogType.ActionTone.SECONDARY,
"cancel": true,
},
],
_on_identity_replacement_review_action,
null,
null,
Vector2(620.0, 400.0),
)
func _advance_identity_replacement(dialog: ConfirmationDialog) -> void:
if int(dialog.get_meta("confirmation_step", 1)) == 1:
dialog.set_meta("confirmation_step", 2)
dialog.ok_button_text = "replace identity"
dialog.dialog_text = (
"replace the active identity and archive the current key locally?"
)
dialog.call_deferred("popup_centered", Vector2i(560, 280))
_configure_confirmation_dialog.call_deferred(
dialog, dialog.get_cancel_button()
)
func _on_identity_replacement_review_action(action: StringName) -> void:
if action != &"review":
_pending_import_data.clear()
return
_confirm_identity_replacement(dialog)
_data_management_dialog.present(
"replace active identity?",
(
"this permanently replaces the active identity. the current key "
+ "will be archived locally so it can be recovered later."
),
[
{
"id": &"replace",
"text": "replace identity",
"tone": DataManagementDialogType.ActionTone.DANGER,
},
{
"id": &"cancel",
"text": "cancel",
"tone": DataManagementDialogType.ActionTone.SECONDARY,
"cancel": true,
},
],
_on_identity_replacement_final_action,
null,
null,
Vector2(580.0, 315.0),
)
func _confirm_identity_replacement(dialog: ConfirmationDialog) -> void:
func _on_identity_replacement_final_action(action: StringName) -> void:
if action != &"replace":
_pending_import_data.clear()
return
_identity_backups.import_backup(
_pending_identity_type,
_pending_identity_path,
@ -922,17 +1115,6 @@ func _confirm_identity_replacement(dialog: ConfirmationDialog) -> void:
true,
)
_pending_import_data.clear()
dialog.queue_free()
func _configure_confirmation_dialog(
dialog: ConfirmationDialog,
preferred_control: Control,
) -> void:
if dialog != null and is_instance_valid(dialog) and dialog.visible:
DialogControllerNavigationType.configure_scope(
dialog, preferred_control
)
func _identity_operation_allowed() -> bool: