Refresh data management dialogs
This commit is contained in:
parent
3a61056ce5
commit
cab313d69d
7 changed files with 720 additions and 103 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue