Guard destructive settings and radio actions
- Route reset campaign and tutorial replay through one reusable confirmation dialog; repeated presses cannot stack or emit twice. - Keep emergency hold-to-send single-shot and cover the post-submit release. - Add destructive-action regression checks, strings and documentation.
This commit is contained in:
1 parent
f6aa26e6ca
commit
7315166361
10 files changed
+121
-14
No files matched your search
@@ -32,6 +32,7 @@ var _reduce_motion: CheckButton = null
|
||||
var _captions: CheckButton = null
|
||||
var _bind_buttons := {}
|
||||
var _listening_action := ""
|
||||
var _destructive_dialog: ConfirmationDialog = null
|
||||
var settings: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -136,7 +137,7 @@ func _build() -> void:
|
||||
var replay := Button.new()
|
||||
replay.name = "ReplayTutorialButton"
|
||||
replay.text = Strings.lookup("ui.settings_replay")
|
||||
replay.pressed.connect(func(): replay_tutorial_requested.emit())
|
||||
replay.pressed.connect(_confirm_replay_tutorial)
|
||||
UiStyles.apply_button(replay, 16)
|
||||
replay_row.add_child(replay)
|
||||
var reset_campaign := Button.new()
|
||||
@@ -284,15 +285,46 @@ func _on_difficulty_selected(index: int) -> void:
|
||||
settings["ai_difficulty"] = ["easy", "normal", "hard"][clampi(index, 0, 2)]
|
||||
|
||||
func _confirm_reset_campaign() -> void:
|
||||
_open_destructive_dialog(Strings.lookup("ui.settings_reset_title"),
|
||||
Strings.lookup("ui.settings_reset_confirm"),
|
||||
func(): reset_campaign_requested.emit())
|
||||
|
||||
func _confirm_replay_tutorial() -> void:
|
||||
_open_destructive_dialog(Strings.lookup("ui.settings_replay_title"),
|
||||
Strings.lookup("ui.settings_replay_confirm"),
|
||||
func(): replay_tutorial_requested.emit())
|
||||
|
||||
## One modal at a time; a second press while it is open focuses the existing
|
||||
## dialog instead of creating another one (and possibly emitting twice).
|
||||
func _open_destructive_dialog(title: String, body: String, callback: Callable) -> void:
|
||||
if _destructive_dialog != null and is_instance_valid(_destructive_dialog):
|
||||
# Reuse the open confirmation instead of stacking a second one. The
|
||||
# dialog is dismissed on confirm/cancel, so an existing instance is
|
||||
# always the one waiting for the player's decision.
|
||||
if _destructive_dialog.is_inside_tree():
|
||||
_destructive_dialog.grab_focus()
|
||||
return
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "Reset campaign"
|
||||
dialog.dialog_text = Strings.lookup("ui.settings_reset_confirm")
|
||||
dialog.confirmed.connect(func(): reset_campaign_requested.emit())
|
||||
_destructive_dialog = dialog
|
||||
dialog.title = title
|
||||
dialog.dialog_text = body
|
||||
dialog.confirmed.connect(func():
|
||||
if _destructive_dialog != dialog:
|
||||
return
|
||||
_dismiss_destructive_dialog(dialog)
|
||||
callback.call())
|
||||
dialog.canceled.connect(func(): _dismiss_destructive_dialog(dialog))
|
||||
add_child(dialog)
|
||||
UiStyles.apply_button(dialog.get_ok_button(), 16)
|
||||
UiStyles.apply_button(dialog.get_cancel_button(), 16)
|
||||
dialog.popup_centered()
|
||||
dialog.confirmed.connect(dialog.queue_free)
|
||||
|
||||
func _dismiss_destructive_dialog(dialog: ConfirmationDialog) -> void:
|
||||
if _destructive_dialog == dialog:
|
||||
_destructive_dialog = null
|
||||
if is_instance_valid(dialog):
|
||||
dialog.hide()
|
||||
dialog.queue_free()
|
||||
|
||||
func _refresh_bind_buttons() -> void:
|
||||
var remaps: Dictionary = settings.get("input_bindings", {})
|
||||
|
||||
@@ -35,5 +35,8 @@ func test_emergency_requires_hold() -> bool:
|
||||
panel.action_button.button_down.emit()
|
||||
panel._process(RadioPanel.HOLD_NEEDED + 0.1)
|
||||
var c := TestUtils.check_eq(int(calls["count"]), 1, "full hold sends exactly once")
|
||||
# Releasing after the hold has already submitted must not send again.
|
||||
panel.action_button.button_up.emit()
|
||||
var d := TestUtils.check_eq(int(calls["count"]), 1, "release after a completed hold does not double-send")
|
||||
panel.free()
|
||||
return a and b and c
|
||||
return a and b and c and d
|
||||
@@ -96,6 +96,49 @@ func test_settings_panel_cancel_discards() -> bool:
|
||||
panel.free()
|
||||
return a and b
|
||||
|
||||
func test_destructive_settings_require_confirmation() -> bool:
|
||||
var panel: Node = SettingsPanel.new()
|
||||
var tree := Engine.get_main_loop() as SceneTree
|
||||
if tree:
|
||||
tree.root.add_child(panel)
|
||||
panel._build()
|
||||
var resets := {"count": 0}
|
||||
var replays := {"count": 0}
|
||||
panel.reset_campaign_requested.connect(func(): resets["count"] += 1)
|
||||
panel.replay_tutorial_requested.connect(func(): replays["count"] += 1)
|
||||
var reset_button: Button = panel.find_child("ResetCampaignButton", true, false) as Button
|
||||
if reset_button:
|
||||
reset_button.pressed.emit()
|
||||
var first: ConfirmationDialog = panel._destructive_dialog
|
||||
panel._confirm_reset_campaign()
|
||||
var a := TestUtils.check(first != null and panel._destructive_dialog == first,
|
||||
"a second reset press reuses the open confirmation")
|
||||
if first:
|
||||
first.confirmed.emit()
|
||||
var b := TestUtils.check_eq(resets["count"], 1, "reset emits once after confirmation")
|
||||
var c := TestUtils.check(panel._destructive_dialog == null,
|
||||
"the reset confirmation is dismissed after use")
|
||||
var replay_button: Button = panel.find_child("ReplayTutorialButton", true, false) as Button
|
||||
if replay_button:
|
||||
replay_button.pressed.emit()
|
||||
var replay_dialog: ConfirmationDialog = panel._destructive_dialog
|
||||
var d := TestUtils.check_eq(replays["count"], 0,
|
||||
"tutorial replay waits for confirmation")
|
||||
if replay_dialog:
|
||||
replay_dialog.confirmed.emit()
|
||||
var e := TestUtils.check_eq(replays["count"], 1,
|
||||
"tutorial replay emits once from its confirmation")
|
||||
panel._confirm_replay_tutorial()
|
||||
var cancel_dialog: ConfirmationDialog = panel._destructive_dialog
|
||||
if cancel_dialog:
|
||||
cancel_dialog.canceled.emit()
|
||||
var f := TestUtils.check_eq(replays["count"], 1,
|
||||
"canceling tutorial replay does not emit")
|
||||
if tree:
|
||||
tree.root.remove_child(panel)
|
||||
panel.free()
|
||||
return a and b and c and d and e and f
|
||||
|
||||
func test_routine_traffic_is_ephemeral() -> bool:
|
||||
var avatar: Node = NpcAvatar.new()
|
||||
var a := TestUtils.check(avatar.ephemeral, "avatars default to ephemeral")
|
||||
|
||||
Reference in new issue
Block a user