From 0a5ef0a61e6379a39f086116f5cff5c3d654cd34 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Fri, 23 Oct 2026 01:16:22 -0400 Subject: [PATCH] feat: Add StageDialogStyler for enhanced dialog management - Introduced StageDialogStyler class to render child-facing dialogs as shared in-stage cards. - Preserved native AcceptDialog/ConfirmationDialog signals for stage workflows. - Implemented methods for applying styles, managing visibility, and handling button actions. - Added support for dynamic theming and responsive design based on viewport size. - Created utility functions for styling buttons and managing dialog content. --- .gitignore | 1 + content/stages/night_street.scene | 857 ++++++++++++++++++ docs/specs/dialog_design_principles.md | 2 + sandbox_theme.json | 29 +- scripts/editors/stage/sandbox_stage.gd | 281 +++--- scripts/editors/stage/speech_composer.gd | 18 +- scripts/editors/stage/stage_modal_views.gd | 98 +- .../shared/stage_ui/stage_dialog_styler.gd | 644 +++++++++++++ .../stage_ui/stage_dialog_styler.gd.uid | 1 + scripts/stage/sandbox_theme.gd | 31 +- 10 files changed, 1724 insertions(+), 238 deletions(-) create mode 100644 content/stages/night_street.scene create mode 100644 scripts/shared/stage_ui/stage_dialog_styler.gd create mode 100644 scripts/shared/stage_ui/stage_dialog_styler.gd.uid diff --git a/.gitignore b/.gitignore index d0e7a0f..77f4676 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ addons/ *.import # Local scratch captures, logs, and temporary browser profiles /tmp/ +/work/ content/review/artist/ content/examples/ /tests/godot_test_user_data/ diff --git a/content/stages/night_street.scene b/content/stages/night_street.scene new file mode 100644 index 0000000..4edc553 --- /dev/null +++ b/content/stages/night_street.scene @@ -0,0 +1,857 @@ +{ + "version": "1.7", + "scene_name": "Plain Background", + "sky_id": "sky_starry_night", + "background_kind": "sky", + "plain_color": "#F5EFE0", + "camera": { + "position": { + "x": -43.8016738891602, + "y": -691.851379394531 + }, + "zoom": 0.605000019073486 + }, + "objects": [ + { + "file_id": "background_ground_1", + "type": "background_ground", + "position": { + "x": 96.0, + "y": 672.0 + }, + "rotation": 0.0, + "size": { + "x": 6592.0, + "y": 192.0 + }, + "finish_id": "finish_background_grass", + "color": "#72a84cff" + }, + { + "file_id": "scenery_artwork_1", + "type": "scenery_artwork", + "position": { + "x": -1472.0, + "y": -320.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_storefront-v1.prop", + "display_name": "Sunny Storefront", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -871.896057128906, + "y": -826.405822753906 + }, + "size": { + "x": 1743.79211425781, + "y": 1652.81164550781 + } + } + }, + { + "file_id": "scenery_artwork_2", + "type": "scenery_artwork", + "position": { + "x": -321.925048828125, + "y": -864.47802734375 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_town_house-v1.prop", + "display_name": "Town House", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -958.074951171875, + "y": -1375.52197265625 + }, + "size": { + "x": 1916.14990234375, + "y": 2751.0439453125 + } + } + }, + { + "file_id": "scenery_artwork_3", + "type": "scenery_artwork", + "position": { + "x": 1408.0, + "y": -320.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_storefront-v1.prop", + "display_name": "Sunny Storefront", + "depth": "near", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -871.896057128906, + "y": -826.405822753906 + }, + "size": { + "x": 1743.79211425781, + "y": 1652.81164550781 + } + } + }, + { + "file_id": "scenery_artwork_4", + "type": "scenery_artwork", + "position": { + "x": -2368.0, + "y": -256.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "far", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -939.5, + "y": -289.301696777344 + }, + "size": { + "x": 1879.0, + "y": 578.603393554688 + } + } + }, + { + "file_id": "scenery_artwork_5", + "type": "scenery_artwork", + "position": { + "x": 704.0, + "y": -256.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "far", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -939.5, + "y": -289.301696777344 + }, + "size": { + "x": 1879.0, + "y": 578.603393554688 + } + } + }, + { + "file_id": "scenery_artwork_6", + "type": "scenery_artwork", + "position": { + "x": 2304.0, + "y": -256.0 + }, + "rotation": 0.0, + "asset_id": "res://content/props/art_city_skyline-v1.prop", + "display_name": "City Skyline", + "depth": "far", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "last_known_local_bounds": { + "position": { + "x": -939.5, + "y": -289.301696777344 + }, + "size": { + "x": 1879.0, + "y": 578.603393554688 + } + } + }, + { + "file_id": "background_ground_2", + "type": "background_ground", + "position": { + "x": 96.0, + "y": 352.0 + }, + "rotation": 0.0, + "size": { + "x": 6848.0, + "y": 832.0 + }, + "finish_id": "finish_background_grass", + "color": "#72a84cff" + }, + { + "file_id": "terrain_1", + "type": "terrain", + "position": { + "x": -3264.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_2", + "type": "terrain", + "position": { + "x": -3072.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_3", + "type": "terrain", + "position": { + "x": -2880.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_4", + "type": "terrain", + "position": { + "x": -2688.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_5", + "type": "terrain", + "position": { + "x": -2496.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_6", + "type": "terrain", + "position": { + "x": -2304.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_7", + "type": "terrain", + "position": { + "x": -2112.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_8", + "type": "terrain", + "position": { + "x": -1920.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_9", + "type": "terrain", + "position": { + "x": -1728.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_10", + "type": "terrain", + "position": { + "x": -1536.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_11", + "type": "terrain", + "position": { + "x": -1344.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_12", + "type": "terrain", + "position": { + "x": -1152.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_13", + "type": "terrain", + "position": { + "x": -960.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_14", + "type": "terrain", + "position": { + "x": -768.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_15", + "type": "terrain", + "position": { + "x": -576.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_16", + "type": "terrain", + "position": { + "x": -384.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_17", + "type": "terrain", + "position": { + "x": -192.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_18", + "type": "terrain", + "position": { + "x": 0.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_19", + "type": "terrain", + "position": { + "x": 192.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_20", + "type": "terrain", + "position": { + "x": 384.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_21", + "type": "terrain", + "position": { + "x": 576.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_22", + "type": "terrain", + "position": { + "x": 768.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_23", + "type": "terrain", + "position": { + "x": 960.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_24", + "type": "terrain", + "position": { + "x": 1152.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_25", + "type": "terrain", + "position": { + "x": 1344.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_26", + "type": "terrain", + "position": { + "x": 1536.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_27", + "type": "terrain", + "position": { + "x": 1728.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_28", + "type": "terrain", + "position": { + "x": 1920.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_29", + "type": "terrain", + "position": { + "x": 2112.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_30", + "type": "terrain", + "position": { + "x": 2304.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_31", + "type": "terrain", + "position": { + "x": 2496.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_32", + "type": "terrain", + "position": { + "x": 2688.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_33", + "type": "terrain", + "position": { + "x": 2880.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_34", + "type": "terrain", + "position": { + "x": 3072.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_35", + "type": "terrain", + "position": { + "x": 3264.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "terrain_36", + "type": "terrain", + "position": { + "x": 3456.0, + "y": 768.0 + }, + "rotation": 0.0, + "asset_id": "ground", + "scale": { + "x": 1.0, + "y": 1.0 + }, + "finish_id": "finish_ground_sidewalk" + }, + { + "file_id": "background_ground_3", + "type": "background_ground", + "position": { + "x": 160.0, + "y": 864.0 + }, + "rotation": 0.0, + "size": { + "x": 6912.0, + "y": 192.0 + }, + "finish_id": "finish_background_concrete", + "color": "#a6aaa3ff" + }, + { + "file_id": "background_ground_4", + "type": "background_ground", + "position": { + "x": -2896.0, + "y": 848.0 + }, + "rotation": 0.0, + "size": { + "x": 800.0, + "y": 160.0 + }, + "finish_id": "finish_background_concrete", + "color": "#a6aaa3ff" + }, + { + "file_id": "background_ground_5", + "type": "background_ground", + "position": { + "x": -3264.0, + "y": 864.0 + }, + "rotation": 0.0, + "size": { + "x": 320.0, + "y": 192.0 + }, + "finish_id": "finish_background_concrete", + "color": "#a6aaa3ff" + }, + { + "file_id": "stickman_1", + "type": "stickman", + "position": { + "x": -2784.0, + "y": 351.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": 2027.27270507813, + "y": 683.355224609375 + }, + "style": "walk" + } + ] + }, + { + "file_id": "stickman_2", + "type": "stickman", + "position": { + "x": -672.0, + "y": 352.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": -3161.15698242188, + "y": 701.537109375 + }, + "style": "walk" + } + ] + }, + { + "file_id": "stickman_3", + "type": "stickman", + "position": { + "x": 3392.0, + "y": 351.0 + }, + "rotation": 0.0, + "stk_path": "res://content/stickmen/break.stk", + "facing_profile": "FORWARD", + "action_queue": [ + { + "type": "move_to", + "target": { + "x": -3333.0576171875, + "y": 726.330444335938 + }, + "style": "run" + } + ] + } + ], + "event_rules": [], + "metadata": { + "created_unix": 1792022400, + "modified_unix": 1792732500.154, + "autosave": false, + "source_path": "" + } +} \ No newline at end of file diff --git a/docs/specs/dialog_design_principles.md b/docs/specs/dialog_design_principles.md index c0f7c56..1f215ba 100644 --- a/docs/specs/dialog_design_principles.md +++ b/docs/specs/dialog_design_principles.md @@ -21,6 +21,8 @@ Use this guide when creating or revising child-facing dialogs, popups, and short ## Theme and implementation - Put reusable colors, sizes, spacing, radii, and border widths in `SandboxTheme` or `PropTheme` tokens. Apply local stylebox overrides instead of mutating a shared theme resource. +- For Sandbox Stage modal windows, use `StageDialogStyler` in `scripts/shared/stage_ui/stage_dialog_styler.gd` for the shared shell, typography, and primary, secondary, or destructive button roles. Custom Control-based dialogs can reuse its `panel_style()` and `style_button()` helpers. +- Render the shared modal surface as a Control in the stage UI CanvasLayer. Keep the native `AcceptDialog` or `ConfirmationDialog` hidden as the signal source; a full-screen native Window can replace the stage with a blank surface on some render paths. - Keep larger text from clipping: use containers that can expand or wrap, and size the dialog after its visible content has been laid out. - Preserve clear hover, pressed, selected, focused, and disabled states. Do not use color as the only indication of a state. - Adapt the arrangement to each workflow while keeping this shared visual language. The Choose Scenery dialog in `scripts/shared/ui/asset_selector.gd` and its `popups.asset_selector` theme tokens are the current implementation reference. diff --git a/sandbox_theme.json b/sandbox_theme.json index 49d64e7..e2e2bf5 100644 --- a/sandbox_theme.json +++ b/sandbox_theme.json @@ -101,9 +101,9 @@ "queue_size": [680, 520], "rule_size": [720, 560], "action_size": [560, 390], - "confirm_size": [620, 300], + "confirm_size": [560, 300], "font_size": 22, - "message_font_size": 26 + "message_font_size": 20 } }, "popups": { @@ -122,24 +122,31 @@ }, "confirmation": { "scenery_erase_size": [520, 208], - "padding": 18, - "body_gap": 10, + "padding": 22, + "body_gap": 12, "footer_gap": 12, - "corner_radius": 16, + "corner_radius": 18, "border_width": 2, - "title_font_size": 26, + "title_font_size": 28, "message_font_size": 20, "button_font_size": 20, - "button_height": 48, + "button_height": 52, + "button_min_width": 112, + "card_width": 560, "button_radius": 12, "button_border_width": 2, "remove_button_width": 170, - "keep_button_width": 132, + "keep_button_width": 160, "surface_color": "#202633FA", - "border_color": "#22C6FF", - "accent_color": "#22C6FF", + "border_color": "#667386", + "secondary_surface": "#293242", + "secondary_border": "#718096", + "focus_color": "#8BD9FF", + "backdrop_color": "#080D15B8", + "accent_color": "#8BD9FF", "action_color": "#087DBA", - "danger_color": "#FF626B", + "danger_color": "#E66B75", + "danger_surface": "#44262D", "danger_hover_surface": "#3B252C", "danger_pressed_surface": "#4A252D" }, diff --git a/scripts/editors/stage/sandbox_stage.gd b/scripts/editors/stage/sandbox_stage.gd index 2d8b991..5dad6bb 100644 --- a/scripts/editors/stage/sandbox_stage.gd +++ b/scripts/editors/stage/sandbox_stage.gd @@ -47,6 +47,7 @@ const STAGE_RUN_SESSION_SCRIPT := preload("res://scripts/editors/stage/stage_run const STAGE_IMPACT_RESOLVER_SCRIPT := preload("res://scripts/stage/stage_impact_resolver.gd") const STAGE_ASSET_BROWSER_SCRIPT := preload("res://scripts/editors/stage/stage_asset_browser.gd") const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_modal_views.gd") +const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd") const STAGE_THEME_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_theme_controller.gd") const STAGE_TARGET_CAPTURE_SCRIPT := preload("res://scripts/editors/stage/stage_target_capture_session.gd") const STAGE_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_scenery_controller.gd") @@ -1543,11 +1544,28 @@ func _place_speech_bubble_at(viewport_position: Vector2) -> void: _speech_session.place_bubble_at(viewport_position) +func _active_styled_dialog() -> Window: + if _chrome == null: + return null + var ui_layer: CanvasLayer = _chrome.get_canvas_layer() + for node: Node in ui_layer.find_children("*", "Window", true, false): + var dialog: Window = node as Window + if dialog != null and STAGE_DIALOG_STYLER.is_styled(dialog) and STAGE_DIALOG_STYLER.is_open(dialog): + return dialog + return null + + func _unhandled_key_input(event: InputEvent) -> void: if _hosted_prop_editor != null: return if not (event is InputEventKey) or not event.pressed or event.echo: return + var active_dialog: Window = _active_styled_dialog() + if active_dialog != null: + if event.keycode == KEY_ESCAPE: + STAGE_DIALOG_STYLER.dismiss_on_escape(active_dialog) + get_viewport().set_input_as_handled() + return if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible: if event.keycode == KEY_ESCAPE: _dismiss_scenery_erase_piece_dialog() @@ -1670,7 +1688,7 @@ func _space_should_pass_to_gui() -> bool: if focus is LineEdit or focus is TextEdit or focus is SpinBox: return true for window: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: - if window != null and window.visible: + if window != null and STAGE_DIALOG_STYLER.is_open(window): return true if _selector_open or (_queue_panel != null and _queue_panel.visible) or (_rule_panel != null and _rule_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible): return true @@ -2074,7 +2092,7 @@ func _pause_toggle_blocked() -> bool: if focus is LineEdit or focus is TextEdit or focus is SpinBox: return true for dialog: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]: - if dialog != null and dialog.visible: + if dialog != null and STAGE_DIALOG_STYLER.is_open(dialog): return true return (_queue_panel != null and _queue_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible) @@ -2630,8 +2648,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool: if is_autosave: return false _pending_group_save_path = path - if _group_preview_save_dialog != null and not _group_preview_save_dialog.visible: - _group_preview_save_dialog.popup_centered() + if _group_preview_save_dialog != null and not STAGE_DIALOG_STYLER.is_open(_group_preview_save_dialog): + STAGE_DIALOG_STYLER.popup(_group_preview_save_dialog) return false if _speech_session != null and _speech_session.is_save_blocked(): if is_autosave: @@ -2639,8 +2657,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool: _speech_pending_scene_save_path = path _speech_pending_scene_save_as = false _speech_scene_save_pending = true - if _speech_save_gate_dialog != null and not _speech_save_gate_dialog.visible: - _speech_save_gate_dialog.popup_centered() + if _speech_save_gate_dialog != null and not STAGE_DIALOG_STYLER.is_open(_speech_save_gate_dialog): + STAGE_DIALOG_STYLER.popup(_speech_save_gate_dialog) return false if _rule_edit_uncommitted: if not is_autosave: @@ -2699,7 +2717,7 @@ func _reset_scene_content() -> void: _scenery_erase_piece_dialog.hide() _clear_erase_scenery_targets() if _scenery_repair_confirm_dialog != null: - _scenery_repair_confirm_dialog.hide() + STAGE_DIALOG_STYLER.hide(_scenery_repair_confirm_dialog) _cancel_scenery_repair_preview() _scenery_repair_target = null _sky_id = &"sky_neutral" @@ -2806,36 +2824,42 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool) func _show_scene_load_warnings(warnings: Array[String]) -> void: var dialog: AcceptDialog = AcceptDialog.new() - dialog.title = "Some saved actions need a look" + dialog.title = "⚠️ Some saved actions need a look" + dialog.ok_button_text = "Got it" var lines: PackedStringArray = PackedStringArray() - for index: int in mini(warnings.size(), 12): + for index: int in mini(warnings.size(), 5): lines.append("• " + warnings[index]) - if warnings.size() > 12: - lines.append("• And %d more warning(s)." % (warnings.size() - 12)) + if warnings.size() > 5: + lines.append("• And %d more warning(s)." % (warnings.size() - 5)) dialog.dialog_text = "These items could not be restored:\n\n%s" % "\n".join(lines) - add_child(dialog) + _chrome.get_canvas_layer().add_child(dialog) + _apply_dialog_theme(dialog) dialog.confirmed.connect(dialog.queue_free) dialog.close_requested.connect(dialog.queue_free) - dialog.popup_centered(Vector2i(620, 420)) + STAGE_DIALOG_STYLER.popup(dialog) func _show_missing_assets_dialog(paths: Array[String]) -> void: if _missing_assets_dialog == null: return var asset_lines: Array[String] = [] - for path: String in paths: - var asset_name := path.get_file() - if asset_name.is_empty(): asset_name = "Unnamed .stk asset" - asset_lines.append("• %s\n %s" % [asset_name, path if not path.is_empty() else "(no asset path was saved)"]) - _missing_assets_dialog.dialog_text = "%d stickman asset(s) could not be loaded:\n\n%s\n\nA red marker was placed where each missing asset belongs." % [paths.size(), "\n\n".join(asset_lines)] - _missing_assets_dialog.popup_centered() + for index: int in mini(paths.size(), 5): + var asset_name: String = paths[index].get_file() + if asset_name.is_empty(): + asset_name = "Unnamed stickman" + asset_lines.append("• %s" % asset_name) + if paths.size() > 5: + asset_lines.append("• And %d more" % (paths.size() - 5)) + _missing_assets_dialog.title = "⚠️ Some scene assets are missing" + _missing_assets_dialog.dialog_text = "Red markers show where the missing stickmen belong.\n\n%s" % "\n".join(asset_lines) + STAGE_DIALOG_STYLER.popup(_missing_assets_dialog) func _show_missing_scenery_dialog(count: int) -> void: if _missing_scenery_dialog == null: return _missing_scenery_dialog.dialog_text = "%d scenery picture%s could not be found. The scene still has a marker in each spot, so nothing was lost." % [count, "" if count == 1 else "s"] - _missing_scenery_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_missing_scenery_dialog) func _add_recent_scene(path: String) -> void: @@ -2884,7 +2908,12 @@ func _check_autosave_recovery(show_starter_chooser_after_discard: bool = false) return var autosave_data: Dictionary = inspection.get("data", {}) as Dictionary var dialog := ConfirmationDialog.new() - dialog.name = "AutosaveRecoveryDialog"; dialog.title = "Recover Sandbox Scene?"; dialog.dialog_text = "A previous Play session was not closed cleanly. Recover it?" + dialog.name = "AutosaveRecoveryDialog" + dialog.title = "🔄 Scene recovery found" + dialog.dialog_text = "A recovery copy of this scene is available. Would you like to restore it?\n\nℹ️ Restoring opens the recovered scene for review before you save it." + dialog.ok_button_text = "Restore scene" + dialog.cancel_button_text = "Discard recovery" + dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300)) dialog.confirmed.connect(func() -> void: _apply_scene_data(autosave_data, AUTOSAVE_PATH, true); dialog.queue_free()) dialog.canceled.connect(func() -> void: _document.remove_autosave() @@ -2892,7 +2921,9 @@ func _check_autosave_recovery(show_starter_chooser_after_discard: bool = false) if show_starter_chooser_after_discard: _queue_startup_starter_chooser() ) - get_node(NodePath("UI")).add_child(dialog); _apply_dialog_theme(dialog); dialog.popup_centered() + get_node(NodePath("UI")).add_child(dialog) + _apply_dialog_theme(dialog, dialog.get_ok_button(), [dialog.get_cancel_button()]) + STAGE_DIALOG_STYLER.popup(dialog) func _queue_startup_starter_chooser() -> void: @@ -2924,7 +2955,7 @@ func _startup_modal_is_visible() -> bool: if ui_layer != null: for child: Node in ui_layer.get_children(): var window := child as Window - if window != null and window.visible: + if (window != null and STAGE_DIALOG_STYLER.is_open(window)) or (child is Control and child.name == "StageDialogOverlay" and (child as Control).visible): return true for dialog: Window in [ _cli_error_dialog, @@ -2940,7 +2971,7 @@ func _startup_modal_is_visible() -> bool: _group_preview_save_dialog, _confirm_dialog, ]: - if dialog != null and is_instance_valid(dialog) and dialog.visible: + if dialog != null and is_instance_valid(dialog) and STAGE_DIALOG_STYLER.is_open(dialog): return true return false @@ -2978,11 +3009,11 @@ func _load_cli_scene(path: String) -> Dictionary: return {"ok": true, "error": ""} -func _show_cli_error_dialog(message: String) -> void: +func _show_cli_error_dialog(_message: String) -> void: if _cli_error_dialog == null: return - _cli_error_dialog.dialog_text = message - _cli_error_dialog.popup_centered() + _cli_error_dialog.dialog_text = "" + STAGE_DIALOG_STYLER.popup(_cli_error_dialog) func _notification(what: int) -> void: @@ -3085,9 +3116,9 @@ func _theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i: ## Dialogs are separate native/embedded windows, so they do not reliably ## inherit the CanvasLayer's visual defaults. Apply only local overrides. -func _apply_dialog_theme(dialog: Window) -> void: +func _apply_dialog_theme(dialog: Window, primary_button: Button = null, destructive_buttons: Array = []) -> void: if _modal_views != null: - _modal_views.apply_dialog_theme(dialog) + _modal_views.apply_dialog_theme(dialog, primary_button, destructive_buttons) func _apply_dialog_font_to_children(node: Node, size: int) -> void: @@ -3446,7 +3477,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void: _missing_scenery_dialog.dialog_text = "This scene has artwork that could not be found. You can still explore the scene and fix it later." _missing_scenery_dialog.ok_button_text = "Show Scenery" _missing_scenery_dialog.cancel_button_text = "Later" - _missing_scenery_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300)) + _missing_scenery_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300)) _missing_scenery_dialog.confirmed.connect(_enter_scenery_edit_mode) ui.add_child(_missing_scenery_dialog) _apply_dialog_theme(_missing_scenery_dialog) @@ -3455,7 +3486,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void: _scenery_repair_confirm_dialog.title = "Use this artwork here?" _scenery_repair_confirm_dialog.ok_button_text = "Use artwork" _scenery_repair_confirm_dialog.cancel_button_text = "Keep for now" - _scenery_repair_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300)) + _scenery_repair_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300)) _scenery_repair_confirm_dialog.confirmed.connect(_confirm_scenery_repair) _scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview) ui.add_child(_scenery_repair_confirm_dialog) @@ -3556,7 +3587,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: var backdrop: ColorRect = ColorRect.new() backdrop.name = "DialogBackdrop" backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#10151D66")) + backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#080D15B8")) backdrop.mouse_filter = Control.MOUSE_FILTER_STOP _scenery_erase_piece_dialog.add_child(backdrop) @@ -3572,7 +3603,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: var margin: MarginContainer = MarginContainer.new() margin.name = "DialogMargin" - var padding: int = _confirmation_dialog_integer("padding", 18, 12, 32) + var padding: int = _confirmation_dialog_integer("padding", 22, 12, 36) margin.add_theme_constant_override("margin_left", padding) margin.add_theme_constant_override("margin_right", padding) margin.add_theme_constant_override("margin_top", padding) @@ -3583,14 +3614,14 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: layout.name = "DialogLayout" layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL layout.size_flags_vertical = Control.SIZE_EXPAND_FILL - layout.add_theme_constant_override("separation", _confirmation_dialog_integer("body_gap", 10, 4, 24)) + layout.add_theme_constant_override("separation", _confirmation_dialog_integer("body_gap", 12, 4, 24)) margin.add_child(layout) _scenery_erase_piece_dialog_title = Label.new() _scenery_erase_piece_dialog_title.name = "DialogTitle" - _scenery_erase_piece_dialog_title.text = "Remove this scenery?" + _scenery_erase_piece_dialog_title.text = "🧱 Remove this scenery piece?" _scenery_erase_piece_dialog_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _scenery_erase_piece_dialog_title.add_theme_font_size_override("font_size", _confirmation_dialog_integer("title_font_size", 26, 20, 40)) + _scenery_erase_piece_dialog_title.add_theme_font_size_override("font_size", _confirmation_dialog_integer("title_font_size", 28, 20, 40)) _scenery_erase_piece_dialog_title.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA"))) _apply_ui_font(_scenery_erase_piece_dialog_title) var title_font: Font = _ui_font @@ -3603,7 +3634,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: _scenery_erase_piece_dialog_message = Label.new() _scenery_erase_piece_dialog_message.name = "DialogMessage" - _scenery_erase_piece_dialog_message.text = "Remove this whole piece? You can’t undo this." + _scenery_erase_piece_dialog_message.text = "This removes the selected scenery piece from the scene." _scenery_erase_piece_dialog_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _scenery_erase_piece_dialog_message.size_flags_horizontal = Control.SIZE_EXPAND_FILL _scenery_erase_piece_dialog_message.add_theme_font_size_override("font_size", _confirmation_dialog_integer("message_font_size", 20, 16, 32)) @@ -3623,14 +3654,14 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: _scenery_erase_piece_keep_button = Button.new() _scenery_erase_piece_keep_button.name = "KeepSceneryButton" - _scenery_erase_piece_keep_button.text = "Keep it" + _scenery_erase_piece_keep_button.text = "Keep this piece" _scenery_erase_piece_keep_button.focus_mode = Control.FOCUS_ALL _scenery_erase_piece_keep_button.pressed.connect(_dismiss_scenery_erase_piece_dialog) _scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_keep_button) _scenery_erase_piece_remove_button = Button.new() _scenery_erase_piece_remove_button.name = "RemoveSceneryButton" - _scenery_erase_piece_remove_button.text = "Remove scenery" + _scenery_erase_piece_remove_button.text = "Remove piece" _scenery_erase_piece_remove_button.focus_mode = Control.FOCUS_ALL _scenery_erase_piece_remove_button.pressed.connect(_confirm_erase_scenery_from_dialog) _scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_remove_button) @@ -3642,66 +3673,18 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void: func _apply_scenery_erase_piece_dialog_style() -> void: if _scenery_erase_piece_dialog == null: return - var shell: StyleBoxFlat = StyleBoxFlat.new() - shell.bg_color = _confirmation_dialog_color("surface_color", Color("#202633FA")) - shell.border_color = _confirmation_dialog_color("border_color", Color("#AAB2C066")) - shell.set_border_width_all(_confirmation_dialog_integer("border_width", 1, 1, 4)) - shell.set_corner_radius_all(_confirmation_dialog_integer("corner_radius", 16, 10, 28)) + var shell: StyleBoxFlat = STAGE_DIALOG_STYLER.panel_style(_sandbox_theme, false) _scenery_erase_piece_dialog_panel.add_theme_stylebox_override("panel", shell) - _apply_scenery_erase_piece_action_style(_scenery_erase_piece_keep_button, true) - _apply_scenery_erase_piece_action_style(_scenery_erase_piece_remove_button, false) - - -func _apply_scenery_erase_piece_action_style(button: Button, primary: bool) -> void: - if button == null: - return - var accent: Color = _confirmation_dialog_color("accent_color", Color("#22C6FF")) - var blue: Color = _confirmation_dialog_color("action_color", Color("#087DBA")) - var danger: Color = _confirmation_dialog_color("danger_color", Color("#FF626B")) - var neutral: Color = _stage_panel_color("action.surface_color", Color("#171B24")) - var hover_neutral: Color = _confirmation_dialog_color("danger_hover_surface", Color("#3B252C")) - var pressed_neutral: Color = _confirmation_dialog_color("danger_pressed_surface", Color("#4A252D")) - var border_width: int = _confirmation_dialog_integer("button_border_width", 2, 1, 5) - var radius: int = _confirmation_dialog_integer("button_radius", 12, 8, 24) - var normal_color: Color = blue if primary else neutral - var hover_color: Color = blue.lightened(0.12) if primary else hover_neutral - var pressed_color: Color = blue.darkened(0.12) if primary else pressed_neutral - var edge_color: Color = accent if primary else danger - button.add_theme_stylebox_override("normal", _make_scenery_erase_button_style(normal_color, edge_color, border_width, radius)) - button.add_theme_stylebox_override("hover", _make_scenery_erase_button_style(hover_color, edge_color.lightened(0.12), border_width + 1, radius)) - button.add_theme_stylebox_override("pressed", _make_scenery_erase_button_style(pressed_color, edge_color, border_width + 1, radius)) - button.add_theme_stylebox_override("hover_pressed", _make_scenery_erase_button_style(pressed_color.lightened(0.05), edge_color, border_width + 1, radius)) - button.add_theme_stylebox_override("focus", _make_scenery_erase_button_style(normal_color, accent, border_width + 1, radius)) - var label_color: Color = Color.WHITE if primary else danger - button.add_theme_color_override("font_color", label_color) - button.add_theme_color_override("font_hover_color", label_color) - button.add_theme_color_override("font_pressed_color", label_color) - button.add_theme_font_size_override("font_size", _confirmation_dialog_integer("button_font_size", 20, 16, 32)) - button.custom_minimum_size = Vector2( - float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)) if not primary else float(_confirmation_dialog_integer("keep_button_width", 132, 112, 220)), - float(_confirmation_dialog_integer("button_height", 48, 44, 80)) + STAGE_DIALOG_STYLER.style_button(_scenery_erase_piece_keep_button, &"primary", _sandbox_theme, _ui_font) + STAGE_DIALOG_STYLER.style_button(_scenery_erase_piece_remove_button, &"danger", _sandbox_theme, _ui_font) + _scenery_erase_piece_keep_button.custom_minimum_size = Vector2( + float(_confirmation_dialog_integer("keep_button_width", 144, 112, 240)), + float(_confirmation_dialog_integer("button_height", 52, 44, 80)) + ) + _scenery_erase_piece_remove_button.custom_minimum_size = Vector2( + float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)), + float(_confirmation_dialog_integer("button_height", 52, 44, 80)) ) - var font: Font = _ui_font - if primary and font != null: - var strong_font: FontVariation = FontVariation.new() - strong_font.base_font = font - strong_font.variation_embolden = 0.55 - button.add_theme_font_override("font", strong_font) - else: - _apply_ui_font(button) - - -func _make_scenery_erase_button_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat: - var style: StyleBoxFlat = StyleBoxFlat.new() - style.bg_color = background - style.border_color = border - style.set_border_width_all(width) - style.set_corner_radius_all(radius) - style.content_margin_left = 12.0 - style.content_margin_right = 12.0 - style.content_margin_top = 8.0 - style.content_margin_bottom = 8.0 - return style func _confirmation_dialog_integer(token: String, fallback: int, minimum: int, maximum: int) -> int: @@ -3961,53 +3944,14 @@ func _build_use_spot_overlay(ui: CanvasLayer) -> void: ui.add_child(_use_choice_popup) _spot_erase_dialog = ConfirmationDialog.new() _spot_erase_dialog.name = "EraseUseSpotDialog" - _spot_erase_dialog.title = "⚠️ Erase Use spot?" + _spot_erase_dialog.title = "📍 Erase this Use spot?" _spot_erase_dialog.ok_button_text = "Erase spot" _spot_erase_dialog.cancel_button_text = "Keep spot" - _spot_erase_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300)) + _spot_erase_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300)) _spot_erase_dialog.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null _spot_erase_dialog.confirmed.connect(_on_spot_erase_confirmed) ui.add_child(_spot_erase_dialog) - _apply_dialog_theme(_spot_erase_dialog) - _style_spot_erase_dialog_buttons() - - -func _style_spot_erase_dialog_buttons() -> void: - if _spot_erase_dialog == null: - return - var danger: Color = _sandbox_theme.color("overlays.use_preview.invalid_color", Color("#F06464")) if _sandbox_theme != null else Color("#F06464") - var safe: Color = _sandbox_theme.color("mode_colors.edit", Color("#22C6FF")) if _sandbox_theme != null else Color("#22C6FF") - _style_spot_erase_titlebar(danger.darkened(0.58)) - _style_spot_erase_action_button(_spot_erase_dialog.get_ok_button(), danger.darkened(0.55), danger) - _style_spot_erase_action_button(_spot_erase_dialog.get_cancel_button(), safe.darkened(0.72), safe) - - -func _style_spot_erase_titlebar(color: Color) -> void: - for style_name: String in ["embedded_border", "embedded_unfocused_border"]: - var original: StyleBoxFlat = _spot_erase_dialog.get_theme_stylebox(style_name) as StyleBoxFlat - if original == null: - continue - var tinted: StyleBoxFlat = original.duplicate() as StyleBoxFlat - tinted.bg_color = color - _spot_erase_dialog.add_theme_stylebox_override(style_name, tinted) - _spot_erase_dialog.add_theme_color_override("title_color", Color("#FFFFFF")) - - -func _style_spot_erase_action_button(button: Button, surface: Color, accent: Color) -> void: - if button == null: - return - button.custom_minimum_size = Vector2(156.0, 56.0) - button.add_theme_color_override("font_color", Color("#FFFFFF")) - button.add_theme_color_override("font_hover_color", Color("#FFFFFF")) - button.add_theme_color_override("font_pressed_color", Color("#FFFFFF")) - for state: String in ["normal", "hover", "pressed", "hover_pressed"]: - var style := StyleBoxFlat.new() - style.bg_color = surface.lightened(0.10) if state == "hover" or state == "hover_pressed" else surface - style.border_color = accent.lightened(0.18) if state == "hover" or state == "pressed" or state == "hover_pressed" else accent - style.set_border_width_all(2 if state == "hover" or state == "pressed" or state == "hover_pressed" else 1) - style.set_corner_radius_all(10) - style.set_content_margin_all(12.0) - button.add_theme_stylebox_override(state, style) + _apply_dialog_theme(_spot_erase_dialog, _spot_erase_dialog.get_cancel_button(), [_spot_erase_dialog.get_ok_button()]) func _stage_panel_integer(key: String, fallback: int, minimum: int, maximum: int) -> int: @@ -4153,8 +4097,9 @@ func _on_spot_erase_requested(spot_id: String) -> void: _spot_erase_prop_id = prop.stage_object_id _spot_erase_id = spot_id var warning: String = "\nThis spot is used in your story. Those steps will need a new spot." if _use_spot_is_referenced(prop.stage_object_id, spot_id) else "" - _spot_erase_dialog.dialog_text = "Erase %s (spot %d)?%s" % [String(spot.get("name", "Spot")), int(spot.get("number", 0)), warning] - _spot_erase_dialog.popup_centered(_spot_erase_dialog.min_size) + _spot_erase_dialog.dialog_text = "The character will no longer be able to use this spot.%s" % warning + STAGE_DIALOG_STYLER.popup(_spot_erase_dialog) + STAGE_DIALOG_STYLER.visual_button(_spot_erase_dialog.get_cancel_button()).grab_focus() func _on_spot_erase_confirmed() -> void: @@ -5600,14 +5545,14 @@ func _start_scenery_repair_preview(entry: Dictionary) -> void: _scenery_repair_candidate = candidate _scenery_repair_path = path if _scenery_repair_confirm_dialog != null: - _scenery_repair_confirm_dialog.dialog_text = "Use %s in this spot? Only this scenery piece will change." % candidate.display_name + _scenery_repair_confirm_dialog.dialog_text = "Use %s in the selected spot?\n\nℹ️ Only this scenery piece will change." % candidate.display_name call_deferred("_popup_scenery_repair_confirm_after_frame") func _popup_scenery_repair_confirm_after_frame() -> void: await get_tree().process_frame if _scenery_repair_confirm_dialog != null and _scenery_repair_candidate != null and is_instance_valid(_scenery_repair_candidate): - _scenery_repair_confirm_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_scenery_repair_confirm_dialog) func _confirm_scenery_repair() -> void: @@ -5667,7 +5612,7 @@ func _exit_scenery_edit_mode() -> void: _scenery_erase_piece_dialog.hide() _clear_erase_scenery_targets() if _scenery_repair_confirm_dialog != null: - _scenery_repair_confirm_dialog.hide() + STAGE_DIALOG_STYLER.hide(_scenery_repair_confirm_dialog) _cancel_scenery_repair_preview() _scenery_repair_target = null if _scenery_inspector != null: @@ -6147,8 +6092,8 @@ func _request_scene_save(path: String = "", force_save_as: bool = false) -> void _speech_pending_scene_save_path = path _speech_pending_scene_save_as = force_save_as _speech_scene_save_pending = true - if _speech_save_gate_dialog != null and not _speech_save_gate_dialog.visible: - _speech_save_gate_dialog.popup_centered() + if _speech_save_gate_dialog != null and not STAGE_DIALOG_STYLER.is_open(_speech_save_gate_dialog): + STAGE_DIALOG_STYLER.popup(_speech_save_gate_dialog) return if _rule_edit_uncommitted: _show_toast("Finish the rule before saving the scene.") @@ -6207,12 +6152,12 @@ func _cancel_deferred_speech_scene_save(reason: String) -> void: func _rename_scene_prompt() -> void: _scene_name_edit.text = _scene_name - _rename_scene_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_rename_scene_dialog) call_deferred("_focus_scene_name_edit") func _focus_scene_name_edit() -> void: - if _scene_name_edit == null or not _scene_name_edit.visible: + if _scene_name_edit == null or not _scene_name_edit.visible or not _scene_name_edit.is_inside_tree(): return _scene_name_edit.grab_focus() _scene_name_edit.select_all() @@ -6227,14 +6172,14 @@ func _request_pending_file_action(action: PendingFileAction, path: String = "") return _pending_file_action = action; _pending_open_path = path if _dirty: - _unsaved_changes_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_unsaved_changes_dialog) else: _continue_pending_file_action() func _continue_pending_file_action() -> void: if _unsaved_changes_dialog != null: - _unsaved_changes_dialog.hide() + STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog) var action := _pending_file_action; var path := _pending_open_path _pending_file_action = PendingFileAction.NONE; _pending_open_path = "" match action: @@ -6248,12 +6193,12 @@ func _continue_pending_file_action() -> void: func _on_unsaved_save() -> void: - _unsaved_changes_dialog.hide() + STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog) _request_scene_save() func _on_unsaved_discard() -> void: - _unsaved_changes_dialog.hide() + STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog) _continue_pending_file_action() @@ -6361,7 +6306,7 @@ func _on_action_popup_id_pressed(id: int) -> void: _open_speech_composer(_context_rig, {"type": "speak", "text": "", "duration": 2.0}, false, "direct") ACT_WAIT: _wait_spin.value = 1.0 - _wait_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_wait_dialog) ACT_RAGDOLL: _context_rig.queue_action({ "type": "ragdoll" }) ACT_RECOVER: @@ -6657,7 +6602,7 @@ func _handle_action_target_click(world_pos: Vector2) -> void: "wait": _rule_step = RuleStep.PARAMS _wait_spin.value = 1.0 - _wait_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_wait_dialog) "pick_up_prop": _begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_rule_action_prop_target, _cb_rule_action_capture_cancel) "throw_prop": @@ -7521,9 +7466,9 @@ func _open_speech_composer(actor: StickmanRig, initial: Dictionary, reactive_opt var queue: Array[Dictionary] = actor.get_queue() if queue_index >= 0 and queue_index < queue.size() and String(queue[queue_index].get("type", "")) == "speak": speech_initial = queue[queue_index].duplicate(true) - if _speech_recovery_matches_loaded_scene() and not _speech_recovery_dialog.visible and not _speech_recovery_in_progress: + if _speech_recovery_matches_loaded_scene() and not STAGE_DIALOG_STYLER.is_open(_speech_recovery_dialog) and not _speech_recovery_in_progress: _speech_pending_open = {"actor_id": actor.get_instance_id(), "initial": speech_initial, "reactive": reactive_options, "context": context} - _speech_recovery_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_speech_recovery_dialog) return _speech_actor = actor _speech_session_kind = context @@ -7547,7 +7492,7 @@ func _update_speech_composition() -> void: func _on_speech_committed(action: Dictionary) -> void: if _speech_session_kind == "recovered_unbound": _speech_pending_commit_action = action.duplicate(true) - _speech_destination_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_speech_destination_dialog) return var speech_updates_uncommitted_rule: bool = _speech_session_kind == "rule_builder" or ( _speech_session_kind == "action_editor" and _action_editor_kind in ["rule_add", "rule_edit"] @@ -7595,9 +7540,9 @@ func _finish_speech_composition(clear_recovery: bool = false) -> void: if _speech_session != null: _speech_session.close(clear_recovery, preserve_draft) if _speech_destination_dialog != null: - _speech_destination_dialog.hide() + STAGE_DIALOG_STYLER.hide(_speech_destination_dialog) if _speech_save_gate_dialog != null: - _speech_save_gate_dialog.hide() + STAGE_DIALOG_STYLER.hide(_speech_save_gate_dialog) _speech_restored_unbound = false @@ -7654,7 +7599,7 @@ func _canonicalize_speech_origin(origin: Dictionary) -> Dictionary: func _check_speech_recovery() -> void: if _speech_recovery_dialog != null and _speech_recovery_matches_loaded_scene(): - _speech_recovery_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_speech_recovery_dialog) func _speech_recovery_matches_loaded_scene() -> bool: @@ -7983,9 +7928,21 @@ func _restore_queue_panel() -> void: func _ask_confirm(title: String, message: String, callback: Callable) -> void: _confirm_callback = callback - _confirm_dialog.title = title + var normalized_title: String = title.to_lower() + var destructive: bool = normalized_title.begins_with("delete") or normalized_title.begins_with("clear") or normalized_title.begins_with("remove") + _confirm_dialog.title = ("🗑️ " if destructive else "❔ ") + title _confirm_dialog.dialog_text = message - _confirm_dialog.popup_centered() + if destructive: + _confirm_dialog.ok_button_text = "Delete" if normalized_title.begins_with("delete") else ("Remove" if normalized_title.begins_with("remove") else "Clear") + _confirm_dialog.cancel_button_text = "Keep" + _apply_dialog_theme(_confirm_dialog, _confirm_dialog.get_cancel_button(), [_confirm_dialog.get_ok_button()]) + else: + _confirm_dialog.ok_button_text = "Continue" + _confirm_dialog.cancel_button_text = "Cancel" + _apply_dialog_theme(_confirm_dialog) + STAGE_DIALOG_STYLER.popup(_confirm_dialog) + if destructive: + STAGE_DIALOG_STYLER.visual_button(_confirm_dialog.get_cancel_button()).grab_focus() func _on_confirm_confirmed() -> void: diff --git a/scripts/editors/stage/speech_composer.gd b/scripts/editors/stage/speech_composer.gd index 4e4c482..28d7118 100644 --- a/scripts/editors/stage/speech_composer.gd +++ b/scripts/editors/stage/speech_composer.gd @@ -4,6 +4,7 @@ extends Control const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd") const SANDBOX_THEME := preload("res://scripts/stage/sandbox_theme.gd") +const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd") const SPEECH_BUBBLE := preload("res://scripts/speech/stickman_speech_bubble.gd") const FIVE_BUBBLE_HINT: String = "Long story? One at a time can be easier to follow." const MAX_BUBBLE_HINT: String = "You can add up to eight bubbles." @@ -510,7 +511,7 @@ func preview_elapsed() -> float: func contains_viewport_point(point: Vector2) -> bool: - if _cancel_confirm_dialog != null and _cancel_confirm_dialog.visible: + if _cancel_confirm_dialog != null and STAGE_DIALOG_STYLER.is_open(_cancel_confirm_dialog): return true if is_message_dialog_open(): return true @@ -608,7 +609,7 @@ func close_without_commit() -> void: if _message_dialog != null: _message_dialog.hide() if _cancel_confirm_dialog != null: - _cancel_confirm_dialog.hide() + STAGE_DIALOG_STYLER.hide(_cancel_confirm_dialog) visible = false _replay_running = false _actor = null @@ -623,7 +624,8 @@ func set_cancel_requires_confirmation(required: bool) -> void: func request_cancel_speech() -> void: var changed: bool = _cancel_requires_confirmation or get_draft() != _initial_action if changed and _cancel_confirm_dialog != null: - _cancel_confirm_dialog.popup_centered() + STAGE_DIALOG_STYLER.popup(_cancel_confirm_dialog) + STAGE_DIALOG_STYLER.visual_button(_cancel_confirm_dialog.get_cancel_button()).grab_focus() return cancelled.emit() @@ -710,13 +712,21 @@ func _build_ui() -> void: _active_editor.text_changed.connect(_on_active_text_changed) _active_editor.caret_changed.connect(_remember_active_editor_state) _cancel_confirm_dialog = ConfirmationDialog.new() - _cancel_confirm_dialog.title = "Discard speech changes?" + _cancel_confirm_dialog.title = "🗣️ Discard speech changes?" _cancel_confirm_dialog.dialog_text = "Your speech edits will be lost. Do you want to discard them?" _cancel_confirm_dialog.get_ok_button().text = "Discard changes" _cancel_confirm_dialog.get_cancel_button().text = "Keep editing" + _cancel_confirm_dialog.min_size = Vector2i(560, 300) _cancel_confirm_dialog.confirmed.connect(func() -> void: cancelled.emit()) _cancel_confirm_dialog.dialog_hide_on_ok = true add_child(_cancel_confirm_dialog) + STAGE_DIALOG_STYLER.apply( + _cancel_confirm_dialog, + _theme, + _theme.font(), + _cancel_confirm_dialog.get_cancel_button(), + [_cancel_confirm_dialog.get_ok_button()] + ) _build_message_dialog() diff --git a/scripts/editors/stage/stage_modal_views.gd b/scripts/editors/stage/stage_modal_views.gd index c326093..04c57ff 100644 --- a/scripts/editors/stage/stage_modal_views.gd +++ b/scripts/editors/stage/stage_modal_views.gd @@ -11,6 +11,7 @@ const SPEECH_SHOW_TOOLBAR_SCRIPT := preload("res://scripts/speech/speech_show_to const WAYPOINT_CONTEXT_SCRIPT := preload("res://scripts/stage/waypoint_context.gd") const PROP_LIBRARY_SCRIPT := preload("res://scripts/props/prop_library.gd") const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd") +const DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd") signal object_selected(node: Node2D) @@ -28,6 +29,7 @@ var scene_name_edit: LineEdit var missing_assets_dialog: AcceptDialog var cli_error_dialog: AcceptDialog var unsaved_changes_dialog: ConfirmationDialog +var unsaved_discard_button: Button var rig_context_popup: PopupMenu var waypoint_context: PopupMenu var confirm_dialog: ConfirmationDialog @@ -51,7 +53,7 @@ func configure(sandbox_theme: SandboxTheme, ui_font: Font) -> void: func build_action_dialogs(parent: Node) -> void: speak_dialog = AcceptDialog.new() - speak_dialog.title = "Speak" + speak_dialog.title = "🗣️ Speak" _speak_box = VBoxContainer.new() speak_dialog.add_child(_speak_box) speak_edit = LineEdit.new() @@ -67,7 +69,7 @@ func build_action_dialogs(parent: Node) -> void: apply_dialog_theme(speak_dialog) wait_dialog = AcceptDialog.new() - wait_dialog.title = "Wait" + wait_dialog.title = "⏱️ Wait" wait_spin = SpinBox.new() wait_spin.min_value = 0.1 wait_spin.max_value = 10.0 @@ -97,7 +99,7 @@ func build_file_dialogs(parent: Node) -> void: speech_save_gate_dialog = ConfirmationDialog.new() speech_save_gate_dialog.name = "FinishSpeechBeforeSaveDialog" - speech_save_gate_dialog.title = "Finish speech before saving" + speech_save_gate_dialog.title = "✏️ Finish speech before saving" speech_save_gate_dialog.dialog_text = "Choose Done speech to finish editing before the scene is saved." speech_save_gate_dialog.ok_button_text = "Done speech" speech_save_gate_dialog.get_cancel_button().text = "Keep editing" @@ -106,50 +108,64 @@ func build_file_dialogs(parent: Node) -> void: group_preview_save_dialog = ConfirmationDialog.new() group_preview_save_dialog.name = "GroupPreviewSaveDialog" - group_preview_save_dialog.title = "Finish scenery preview?" + group_preview_save_dialog.title = "🧱 Finish scenery preview?" group_preview_save_dialog.dialog_text = "Keep the scenery changes before saving, or cancel this save." group_preview_save_dialog.ok_button_text = "Keep changes" group_preview_save_dialog.cancel_button_text = "Cancel save" - group_preview_save_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + group_preview_save_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) parent.add_child(group_preview_save_dialog) apply_dialog_theme(group_preview_save_dialog) rename_scene_dialog = ConfirmationDialog.new() rename_scene_dialog.name = "RenameSceneDialog" - rename_scene_dialog.title = "Rename Scene" + rename_scene_dialog.title = "📝 Rename Scene" + rename_scene_dialog.ok_button_text = "Save name" rename_scene_dialog.cancel_button_text = "Cancel" - rename_scene_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + rename_scene_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) + var scene_name_hint: Label = Label.new() + scene_name_hint.name = "SceneNameHint" + scene_name_hint.text = "Choose a short name that helps you find this scene later." + scene_name_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + scene_name_hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var scene_name_label: Label = Label.new() + scene_name_label.name = "SceneNameLabel" + scene_name_label.text = "Scene name" scene_name_edit = LineEdit.new() scene_name_edit.name = "SceneNameEdit" scene_name_edit.placeholder_text = "Scene name" - scene_name_edit.custom_minimum_size = Vector2(460.0, 0.0) + scene_name_edit.custom_minimum_size = Vector2(460.0, 48.0) + rename_scene_dialog.add_child(scene_name_hint) + rename_scene_dialog.add_child(scene_name_label) rename_scene_dialog.add_child(scene_name_edit) parent.add_child(rename_scene_dialog) apply_dialog_theme(rename_scene_dialog) missing_assets_dialog = AcceptDialog.new() missing_assets_dialog.name = "MissingAssetsDialog" - missing_assets_dialog.title = "Missing Scene Assets" - missing_assets_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + missing_assets_dialog.title = "⚠️ Some scene assets are missing" + missing_assets_dialog.ok_button_text = "Got it" + missing_assets_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) parent.add_child(missing_assets_dialog) apply_dialog_theme(missing_assets_dialog) cli_error_dialog = AcceptDialog.new() cli_error_dialog.name = "CliErrorDialog" - cli_error_dialog.title = "Could Not Load Scene" - cli_error_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + cli_error_dialog.title = "⚠️ Could not load scene" + cli_error_dialog.ok_button_text = "Got it" + cli_error_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) parent.add_child(cli_error_dialog) apply_dialog_theme(cli_error_dialog) unsaved_changes_dialog = ConfirmationDialog.new() unsaved_changes_dialog.name = "UnsavedChangesDialog" - unsaved_changes_dialog.title = "⚠️ Unsaved Changes" + unsaved_changes_dialog.title = "💾 Unsaved Changes" unsaved_changes_dialog.dialog_text = "Would you like to save your scene before continuing?" - unsaved_changes_dialog.ok_button_text = "💾 Save" - unsaved_changes_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) - unsaved_changes_dialog.add_button("🗑️ Discard", false, "discard") + unsaved_changes_dialog.ok_button_text = "Save scene" + unsaved_changes_dialog.cancel_button_text = "Cancel" + unsaved_changes_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) + unsaved_discard_button = unsaved_changes_dialog.add_button("Discard", false, "discard") parent.add_child(unsaved_changes_dialog) - apply_dialog_theme(unsaved_changes_dialog) + apply_dialog_theme(unsaved_changes_dialog, unsaved_changes_dialog.get_ok_button(), [unsaved_discard_button]) func build_context_and_editor_views( @@ -169,7 +185,11 @@ func build_context_and_editor_views( parent.add_child(waypoint_context) confirm_dialog = ConfirmationDialog.new() - confirm_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300)) + confirm_dialog.title = "❔ Confirm this change" + confirm_dialog.dialog_text = "This action updates the current scene. Do you want to continue?" + confirm_dialog.ok_button_text = "Continue" + confirm_dialog.cancel_button_text = "Cancel" + confirm_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300)) parent.add_child(confirm_dialog) apply_dialog_theme(confirm_dialog) @@ -196,18 +216,20 @@ func build_context_and_editor_views( parent.add_child(speech_show_toolbar) speech_recovery_dialog = ConfirmationDialog.new() - speech_recovery_dialog.title = "Speech draft found" - speech_recovery_dialog.dialog_text = "Restore your unsaved speech draft?" - speech_recovery_dialog.get_ok_button().text = "Restore" - speech_recovery_dialog.get_cancel_button().text = "Discard" + speech_recovery_dialog.title = "🗣️ Speech draft found" + speech_recovery_dialog.dialog_text = "There is an unsaved speech draft. Would you like to restore it?\n\nℹ️ Restore brings back the draft so you can continue editing." + speech_recovery_dialog.get_ok_button().text = "Restore draft" + speech_recovery_dialog.get_cancel_button().text = "Discard draft" parent.add_child(speech_recovery_dialog) + apply_dialog_theme(speech_recovery_dialog, speech_recovery_dialog.get_ok_button(), [speech_recovery_dialog.get_cancel_button()]) speech_destination_dialog = ConfirmationDialog.new() - speech_destination_dialog.title = "Save recovered speech" + speech_destination_dialog.title = "🎭 Save recovered speech" speech_destination_dialog.dialog_text = "Add this speech to the chosen character’s action queue?" speech_destination_dialog.get_ok_button().text = "Add to queue" speech_destination_dialog.get_cancel_button().text = "Keep editing" parent.add_child(speech_destination_dialog) + apply_dialog_theme(speech_destination_dialog) func show_objects_here( @@ -310,35 +332,11 @@ func theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i: return fallback -## Dialogs are separate native/embedded windows, so apply local visual overrides. -func apply_dialog_theme(dialog: Window) -> void: +## Keep the native dialog objects as signal sources and draw their shared surface in the stage CanvasLayer. +func apply_dialog_theme(dialog: Window, primary_button: Button = null, destructive_buttons: Array = []) -> void: if dialog == null: return - var size: int = _sandbox_theme.integer("chrome.dialogs.font_size", 22, 12, 48) if _sandbox_theme != null else 22 - var message_size: int = _sandbox_theme.integer("chrome.dialogs.message_font_size", 26, 12, 64) if _sandbox_theme != null else 26 - dialog.add_theme_font_size_override("font_size", size) - if _ui_font != null: - dialog.add_theme_font_override("font", _ui_font) - if dialog is AcceptDialog: - var message: Label = (dialog as AcceptDialog).get_label() - if message != null: - message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - message.add_theme_font_size_override("font_size", message_size) - if _ui_font != null: - message.add_theme_font_override("font", _ui_font) - var ok: Button = (dialog as AcceptDialog).get_ok_button() - if ok != null: - ok.add_theme_font_size_override("font_size", size) - if _ui_font != null: - ok.add_theme_font_override("font", _ui_font) - if dialog is ConfirmationDialog: - var cancel: Button = (dialog as ConfirmationDialog).get_cancel_button() - if cancel != null: - cancel.add_theme_font_size_override("font_size", size) - if _ui_font != null: - cancel.add_theme_font_override("font", _ui_font) - _apply_dialog_font_to_children(dialog, size) + DIALOG_STYLER.apply(dialog, _sandbox_theme, _ui_font, primary_button, destructive_buttons) func apply_dialog_font_to_children(node: Node, size: int) -> void: diff --git a/scripts/shared/stage_ui/stage_dialog_styler.gd b/scripts/shared/stage_ui/stage_dialog_styler.gd new file mode 100644 index 0000000..1cdedf8 --- /dev/null +++ b/scripts/shared/stage_ui/stage_dialog_styler.gd @@ -0,0 +1,644 @@ +class_name StageDialogStyler +extends RefCounted +## Renders child-facing dialogs as a shared in-stage card while preserving the +## native AcceptDialog/ConfirmationDialog signals used by the stage workflows. + +const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd") +const OVERLAY_META := &"stage_dialog_overlay" +const BODY_META := &"stage_dialog_body" +const BODY_SCROLL_META := &"stage_dialog_body_scroll" +const NOTE_META := &"stage_dialog_note" +const DETAIL_META := &"stage_dialog_detail" +const ACTIONS_META := &"stage_dialog_actions" +const EXTRAS_META := &"stage_dialog_extra_controls" +const CARD_META := &"stage_dialog_card" +const TITLE_META := &"stage_dialog_title" +const ICON_META := &"stage_dialog_icon" +const ICON_LABEL_META := &"stage_dialog_icon_label" +const ICON_BADGE_META := &"stage_dialog_icon_badge" +const PRIMARY_META := &"stage_dialog_primary_button" +const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons" +const OPEN_META := &"stage_dialog_open" + + +static func apply( + dialog: Window, + stage_theme: SandboxTheme, + ui_font: Font, + primary_button: Button = null, + destructive_buttons: Array = [] +) -> void: + if dialog == null: + return + var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new() + if not dialog.has_meta(OVERLAY_META): + _install_dialog_surface(dialog, active_theme, ui_font) + var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null + var primary: Button = primary_button if primary_button != null else ok_button + dialog.set_meta(PRIMARY_META, primary) + dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate()) + _refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons) + + +static func popup(dialog: Window) -> void: + if dialog == null: + return + var active_theme: SandboxTheme = _theme_for(dialog) + var font: Font = dialog.get_meta(&"stage_dialog_font") as Font if dialog.has_meta(&"stage_dialog_font") else null + var primary: Button = dialog.get_meta(PRIMARY_META, null) as Button + var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array + _refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons) + var overlay: Control = dialog.get_meta(OVERLAY_META) as Control + if overlay == null: + return + dialog.hide() + dialog.set_meta(OPEN_META, true) + overlay.show() + var host_viewport: Viewport = overlay.get_viewport() + if host_viewport != null: + var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size) + var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer + if card != null: + var card_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720) + card.custom_minimum_size.x = minf(float(card_width), maxf(280.0, float(viewport_size.x - 36))) + _fit_body_scroll(dialog, viewport_size, active_theme) + var tree: SceneTree = dialog.get_tree() + if tree != null: + var fit_after_final_layout := func() -> void: + if not is_instance_valid(dialog) or not dialog.is_inside_tree() or not is_open(dialog): + return + var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size) if host_viewport != null else Vector2i.ZERO + _fit_body_scroll(dialog, viewport_size, active_theme) + var fit_after_layout := func() -> void: + if is_instance_valid(tree): + tree.process_frame.connect(fit_after_final_layout, Object.CONNECT_ONE_SHOT) + var defer_fit_one_frame := func() -> void: + if is_instance_valid(tree): + tree.process_frame.connect(fit_after_layout, Object.CONNECT_ONE_SHOT) + tree.process_frame.connect(defer_fit_one_frame, Object.CONNECT_ONE_SHOT) + var focus_primary: Button = dialog.get_meta(PRIMARY_META, null) as Button + var focus_target: Button = visual_button(focus_primary) + if focus_target != null and focus_target.visible: + focus_target.call_deferred("grab_focus") + + +static func is_open(dialog: Window) -> bool: + if dialog == null or not is_instance_valid(dialog): + return false + if dialog.has_meta(OVERLAY_META): + var overlay: Control = dialog.get_meta(OVERLAY_META) as Control + return overlay != null and is_instance_valid(overlay) and overlay.visible + return dialog.visible + + +static func is_styled(dialog: Window) -> bool: + return dialog != null and is_instance_valid(dialog) and dialog.has_meta(OVERLAY_META) + + +static func hide(dialog: Window) -> void: + if dialog == null or not is_instance_valid(dialog): + return + dialog.set_meta(OPEN_META, false) + var overlay: Control = dialog.get_meta(OVERLAY_META) as Control if dialog.has_meta(OVERLAY_META) else null + if overlay != null and is_instance_valid(overlay): + overlay.hide() + if dialog.visible: + dialog.hide() + + +static func dismiss_on_escape(dialog: Window) -> void: + if dialog == null or not is_instance_valid(dialog): + return + if dialog is ConfirmationDialog: + var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() + var visible_cancel: Button = visual_button(cancel_button) + if visible_cancel != null and visible_cancel.visible: + visible_cancel.pressed.emit() + return + if dialog.has_signal("close_requested"): + dialog.emit_signal("close_requested") + hide(dialog) + + +static func visual_button(original_button: Button) -> Button: + if original_button == null: + return null + var visual: Variant = original_button.get_meta(&"stage_dialog_visual_button", null) + return visual as Button if visual is Button else original_button + + +static func panel_style(stage_theme: SandboxTheme, include_content_padding: bool = true) -> StyleBoxFlat: + var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new() + var style := StyleBoxFlat.new() + style.bg_color = active_theme.color("popups.confirmation.surface_color", Color("#202633FA")) + style.border_color = active_theme.color("popups.confirmation.border_color", Color("#667386")) + style.set_border_width_all(active_theme.integer("popups.confirmation.border_width", 2, 1, 4)) + style.set_corner_radius_all(active_theme.integer("popups.confirmation.corner_radius", 18, 10, 28)) + if include_content_padding: + var padding: int = active_theme.integer("popups.confirmation.padding", 22, 12, 36) + style.content_margin_left = padding + style.content_margin_right = padding + style.content_margin_top = padding + style.content_margin_bottom = padding + return style + + +static func style_button(button: Button, role: StringName, stage_theme: SandboxTheme, ui_font: Font = null) -> void: + if button == null: + return + var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new() + var action: Color = active_theme.color("popups.confirmation.action_color", Color("#087DBA")) + var accent: Color = active_theme.color("popups.confirmation.accent_color", Color("#8BD9FF")) + var danger: Color = active_theme.color("popups.confirmation.danger_color", Color("#D94A56")) + var secondary: Color = active_theme.color("popups.confirmation.secondary_surface", Color("#293242")) + var secondary_border: Color = active_theme.color("popups.confirmation.secondary_border", Color("#718096")) + var normal: Color = action + var hover: Color = action.lightened(0.12) + var pressed: Color = action.darkened(0.12) + var border: Color = accent + var label_color: Color = Color("#F5F7FA") + match role: + &"primary": + normal = action + hover = active_theme.color("stage_panels.action.primary_hover_color", action.lightened(0.12)) + pressed = active_theme.color("stage_panels.action.primary_pressed_color", action.darkened(0.12)) + border = accent + &"danger": + normal = active_theme.color("popups.confirmation.danger_surface", Color("#44262D")) + hover = active_theme.color("popups.confirmation.danger_hover_surface", Color("#3B252C")) + pressed = active_theme.color("popups.confirmation.danger_pressed_surface", Color("#4A252D")) + border = danger + label_color = Color("#FFE6E8") + _: + normal = secondary + hover = active_theme.color("stage_panels.action.hover_surface", Color("#303A4B")) + pressed = active_theme.color("stage_panels.action.selected_surface", Color("#12394A")) + border = secondary_border + var border_width: int = active_theme.integer("popups.confirmation.button_border_width", 2, 1, 5) + var radius: int = active_theme.integer("popups.confirmation.button_radius", 12, 8, 24) + button.add_theme_stylebox_override("normal", _button_box(normal, border, border_width, radius)) + button.add_theme_stylebox_override("hover", _button_box(hover, border.lightened(0.12), border_width + 1, radius)) + button.add_theme_stylebox_override("pressed", _button_box(pressed, border, border_width + 1, radius)) + button.add_theme_stylebox_override("hover_pressed", _button_box(pressed.lightened(0.05), border, border_width + 1, radius)) + button.add_theme_stylebox_override("disabled", _button_box(secondary.darkened(0.18), secondary_border.darkened(0.35), 1, radius)) + button.add_theme_stylebox_override("focus", _button_box(normal, active_theme.color("popups.confirmation.focus_color", Color("#8BD9FF")), border_width + 1, radius)) + button.add_theme_color_override("font_color", label_color) + button.add_theme_color_override("font_hover_color", label_color) + button.add_theme_color_override("font_pressed_color", label_color) + button.add_theme_color_override("font_disabled_color", Color("#AAB2C0")) + button.add_theme_font_size_override("font_size", active_theme.integer("popups.confirmation.button_font_size", 20, 16, 32)) + var min_height: int = active_theme.integer("popups.confirmation.button_height", 52, 44, 80) + var min_width: int = active_theme.integer("popups.confirmation.button_min_width", 112, 88, 240) + button.custom_minimum_size = Vector2(maxf(button.custom_minimum_size.x, float(min_width)), maxf(button.custom_minimum_size.y, float(min_height))) + if ui_font != null: + button.add_theme_font_override("font", _strong_font(ui_font) if role == &"primary" else ui_font) + button.focus_mode = Control.FOCUS_ALL + + +static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, ui_font: Font) -> void: + dialog.set_meta(&"stage_dialog_theme", stage_theme) + if ui_font != null: + dialog.set_meta(&"stage_dialog_font", ui_font) + dialog.theme = stage_theme.control_theme() + dialog.borderless = true + dialog.transparent = true + dialog.transparent_bg = true + dialog.unresizable = true + dialog.wrap_controls = false + dialog.add_theme_color_override("title_color", Color.TRANSPARENT) + dialog.add_theme_font_size_override("title_font_size", 1) + if ui_font != null: + dialog.add_theme_font_override("font", ui_font) + + var overlay_host: Node = _find_overlay_host(dialog) + if overlay_host == null: + return + var overlay := Control.new() + overlay.name = "StageDialogOverlay" + overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + overlay.mouse_filter = Control.MOUSE_FILTER_STOP + overlay.z_index = 100 + overlay.z_as_relative = false + overlay.visible = false + overlay.gui_input.connect(_on_overlay_input.bind(dialog)) + overlay_host.add_child(overlay) + dialog.set_meta(OVERLAY_META, overlay) + dialog.set_meta(OPEN_META, false) + dialog.set_meta(&"stage_dialog_overlay_host", overlay_host) + dialog.tree_exiting.connect(_on_dialog_tree_exiting.bind(dialog, overlay), CONNECT_ONE_SHOT) + if dialog.has_signal("confirmed"): + dialog.connect("confirmed", _on_native_dialog_closed.bind(dialog)) + if dialog.has_signal("canceled"): + dialog.connect("canceled", _on_native_dialog_closed.bind(dialog)) + if dialog.has_signal("custom_action"): + dialog.connect("custom_action", _on_native_dialog_custom_action.bind(dialog)) + if dialog.has_signal("close_requested"): + dialog.connect("close_requested", _on_native_dialog_closed.bind(dialog)) + + var backdrop := ColorRect.new() + backdrop.name = "Backdrop" + backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + backdrop.color = stage_theme.color("popups.confirmation.backdrop_color", Color("#080D15B8")) + backdrop.mouse_filter = Control.MOUSE_FILTER_STOP + overlay.add_child(backdrop) + + var center := CenterContainer.new() + center.name = "CardCenter" + center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + center.mouse_filter = Control.MOUSE_FILTER_IGNORE + overlay.add_child(center) + + var card := PanelContainer.new() + card.name = "DialogCard" + card.custom_minimum_size = Vector2(560.0, 0.0) + card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + card.size_flags_vertical = Control.SIZE_SHRINK_CENTER + card.add_theme_stylebox_override("panel", panel_style(stage_theme)) + center.add_child(card) + dialog.set_meta(CARD_META, card) + + var content := VBoxContainer.new() + content.name = "DialogContent" + content.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24)) + card.add_child(content) + + var header := HBoxContainer.new() + header.name = "DialogHeader" + header.add_theme_constant_override("separation", 12) + content.add_child(header) + + var icon_badge := PanelContainer.new() + icon_badge.name = "IconBadge" + icon_badge.custom_minimum_size = Vector2(40.0, 40.0) + icon_badge.add_theme_stylebox_override("panel", _icon_box(Color("#173A4B"), Color("#4DB9FF"))) + header.add_child(icon_badge) + dialog.set_meta(ICON_BADGE_META, icon_badge) + + var icon_label := Label.new() + icon_label.name = "DialogIcon" + icon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + icon_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + icon_label.add_theme_font_size_override("font_size", 22) + icon_badge.add_child(icon_label) + dialog.set_meta(ICON_LABEL_META, icon_label) + + var title_label := Label.new() + title_label.name = "DialogTitle" + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + title_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40)) + if ui_font != null: + title_label.add_theme_font_override("font", _strong_font(ui_font)) + header.add_child(title_label) + dialog.set_meta(TITLE_META, title_label) + + var body := VBoxContainer.new() + body.name = "DialogBody" + body.add_theme_constant_override("separation", 10) + body.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var body_scroll := ScrollContainer.new() + body_scroll.name = "DialogBodyScroll" + body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + body_scroll.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + content.add_child(body_scroll) + body_scroll.add_child(body) + dialog.set_meta(BODY_META, body) + dialog.set_meta(BODY_SCROLL_META, body_scroll) + + var body_label := Label.new() + body_label.name = "DialogMessage" + body_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + body_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT + body_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + body_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32)) + body.add_child(body_label) + dialog.set_meta(&"stage_dialog_message", body_label) + + var detail_box := PanelContainer.new() + detail_box.name = "DialogDetails" + detail_box.add_theme_stylebox_override("panel", _detail_box(stage_theme)) + var detail_label := Label.new() + detail_label.name = "DetailsText" + detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + detail_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32)) + detail_box.add_child(detail_label) + body.add_child(detail_box) + dialog.set_meta(DETAIL_META, detail_box) + + var note := Label.new() + note.name = "DialogNote" + note.size_flags_horizontal = Control.SIZE_EXPAND_FILL + note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + note.add_theme_color_override("font_color", Color("#B8C7D8")) + note.add_theme_font_size_override("font_size", 17) + body.add_child(note) + dialog.set_meta(NOTE_META, note) + + var action_row := HBoxContainer.new() + action_row.name = "DialogActions" + action_row.alignment = BoxContainer.ALIGNMENT_END + action_row.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.footer_gap", 12, 8, 24)) + content.add_child(action_row) + dialog.set_meta(ACTIONS_META, action_row) + + var extras: Array[Control] = [] + var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null + var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null + var default_label: Label = (dialog as AcceptDialog).get_label() if dialog is AcceptDialog else null + if default_label != null: + default_label.visible = false + if ok_button != null: + ok_button.visible = false + if cancel_button != null: + cancel_button.visible = false + for child: Node in dialog.get_children(): + if child is Control and not child is Button and child != default_label and child != ok_button and child != cancel_button: + var control: Control = child as Control + control.reparent(body) + extras.append(control) + dialog.set_meta(EXTRAS_META, extras) + dialog.hide() + + +static func _refresh_dialog_surface( + dialog: Window, + stage_theme: SandboxTheme, + ui_font: Font, + primary_button: Button = null, + destructive_buttons: Array = [] +) -> void: + if not dialog.has_meta(OVERLAY_META): + return + dialog.set_meta(&"stage_dialog_theme", stage_theme) + if ui_font != null: + dialog.set_meta(&"stage_dialog_font", ui_font) + var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null + var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null + var original_message: String = String((dialog as AcceptDialog).dialog_text) if dialog is AcceptDialog else "" + var body_label: Label = dialog.get_meta(&"stage_dialog_message") as Label + var detail_box: PanelContainer = dialog.get_meta(DETAIL_META) as PanelContainer + var detail_label: Label = detail_box.get_child(0) as Label if detail_box != null and detail_box.get_child_count() > 0 else null + var note_label: Label = dialog.get_meta(NOTE_META) as Label + var body_text: String = original_message + var note_text: String = "" + var note_marker: int = body_text.find("\n\nℹ️") + if note_marker >= 0: + note_text = body_text.substr(note_marker + 2) + body_text = body_text.substr(0, note_marker) + var detail_marker: int = body_text.find("\n\n•") + var detail_text: String = "" + if detail_marker >= 0: + detail_text = body_text.substr(detail_marker + 2) + body_text = body_text.substr(0, detail_marker) + if body_label != null: + body_label.text = body_text + body_label.visible = not body_text.is_empty() + if detail_label != null and detail_box != null: + detail_label.text = detail_text + detail_box.visible = not detail_text.is_empty() + if note_label != null: + note_label.text = note_text + note_label.visible = not note_text.is_empty() + var heading: Dictionary = _split_heading(dialog.title, String(dialog.get_meta(ICON_META, _icon_for_dialog(dialog)))) + var title_label: Label = dialog.get_meta(TITLE_META) as Label + var icon_label: Label = dialog.get_meta(ICON_LABEL_META) as Label + if title_label != null: + title_label.text = String(heading.get("title", dialog.title)) + var font_size: int = stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40) + title_label.add_theme_font_size_override("font_size", font_size) + title_label.add_theme_color_override("font_color", stage_theme.color("global.text_color", Color("#F5F7FA"))) + if ui_font != null: + title_label.add_theme_font_override("font", _strong_font(ui_font)) + if icon_label != null: + icon_label.text = String(heading.get("icon", "❔")) + var icon_badge: PanelContainer = dialog.get_meta(ICON_BADGE_META) as PanelContainer + if icon_badge != null: + var heading_title: String = String(heading.get("title", "")).to_lower() + var badge_background: Color = Color("#173A4B") + var badge_border: Color = Color("#4DB9FF") + if String(heading.get("icon", "")).contains("⚠"): + badge_background = Color("#493A13") + badge_border = Color("#D7AE28") + elif String(heading.get("icon", "")).contains("🗑") or "erase" in heading_title or "discard" in heading_title: + badge_background = Color("#48262D") + badge_border = Color("#E66B75") + icon_badge.add_theme_stylebox_override("panel", _icon_box(badge_background, badge_border)) + var text_color: Color = stage_theme.color("global.text_color", Color("#F5F7FA")) + if body_label != null: + body_label.add_theme_color_override("font_color", text_color) + if detail_label != null: + detail_label.add_theme_color_override("font_color", text_color) + var extras: Array = dialog.get_meta(EXTRAS_META, []) as Array + for extra: Variant in extras: + if extra is Control: + _apply_control_font(extra as Control, ui_font, stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32)) + + var action_row: HBoxContainer = dialog.get_meta(ACTIONS_META) as HBoxContainer + if action_row != null: + for old_button: Node in action_row.get_children(): + action_row.remove_child(old_button) + old_button.queue_free() + var primary: Button = primary_button if primary_button != null else ok_button + if ok_button != null: + ok_button.visible = false + if cancel_button != null: + cancel_button.visible = false + for raw_button: Variant in destructive_buttons: + if raw_button is Button: + (raw_button as Button).visible = false + var ordered: Array[Button] = [] + _append_unique_button(ordered, primary) + for raw_destructive: Variant in destructive_buttons: + if raw_destructive is Button: + _append_unique_button(ordered, raw_destructive as Button) + _append_unique_button(ordered, cancel_button) + _append_unique_button(ordered, ok_button) + for original: Button in ordered: + var visual: Button = Button.new() + visual.text = original.text + visual.name = "Action_%s" % String(original.name) + visual.size_flags_horizontal = Control.SIZE_SHRINK_END + var role: StringName = &"secondary" + if original == primary: + role = &"primary" + elif _contains_button(destructive_buttons, original): + role = &"danger" + style_button(visual, role, stage_theme, ui_font) + visual.pressed.connect(func() -> void: original.pressed.emit()) + visual.set_meta(&"stage_dialog_original_button", original) + original.set_meta(&"stage_dialog_visual_button", visual) + if action_row != null: + action_row.add_child(visual) + + +static func _on_overlay_input(event: InputEvent, dialog: Window) -> void: + if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: + dismiss_on_escape(dialog) + var overlay: Control = dialog.get_meta(OVERLAY_META) as Control + if overlay != null and overlay.get_viewport() != null: + overlay.get_viewport().set_input_as_handled() + + +static func _fit_body_scroll(dialog: Window, viewport_size: Vector2i, stage_theme: SandboxTheme) -> void: + var body_scroll: ScrollContainer = dialog.get_meta(BODY_SCROLL_META) as ScrollContainer + var body: Control = dialog.get_meta(BODY_META) as Control + var overlay: Control = dialog.get_meta(OVERLAY_META) as Control + var header: Control = overlay.get_node_or_null("CardCenter/DialogCard/DialogContent/DialogHeader") as Control if overlay != null else null + var actions: Control = dialog.get_meta(ACTIONS_META) as Control + var card: Control = dialog.get_meta(CARD_META) as Control + if body_scroll == null or body == null or header == null or actions == null or card == null: + return + var card_width: float = card.custom_minimum_size.x + var card_padding: float = float(stage_theme.integer("popups.confirmation.padding", 22, 12, 36)) + var content_gap: float = float(stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24)) + var body_width: float = maxf(0.0, card_width - card_padding * 2.0) + var preferred_body_height: float = body.get_combined_minimum_size().y + var fixed_height: float = header.get_combined_minimum_size().y + actions.get_combined_minimum_size().y + card_padding * 2.0 + content_gap * 2.0 + var max_body_height: float = maxf(48.0, float(viewport_size.y) - 36.0 - fixed_height) + if preferred_body_height > max_body_height: + var vertical_bar: VScrollBar = body_scroll.get_v_scroll_bar() + if vertical_bar != null: + body_width = maxf(0.0, body_width - vertical_bar.get_combined_minimum_size().x) + body.custom_minimum_size.x = body_width + body.size.x = body_width + preferred_body_height = body.get_combined_minimum_size().y + body_scroll.custom_minimum_size = Vector2(0.0, minf(preferred_body_height, max_body_height)) + + +static func _find_overlay_host(dialog: Window) -> Node: + var ancestor: Node = dialog.get_parent() + while ancestor != null: + if ancestor is CanvasLayer: + return ancestor + ancestor = ancestor.get_parent() + var tree: SceneTree = dialog.get_tree() + var scene: Node = tree.current_scene if tree != null else null + if scene != null: + var ui_layer: Node = scene.get_node_or_null("UI") + if ui_layer is CanvasLayer: + return ui_layer + var existing: Node = scene.get_node_or_null("StageDialogLayer") + if existing is CanvasLayer: + return existing + var layer := CanvasLayer.new() + layer.name = "StageDialogLayer" + layer.layer = 100 + scene.add_child(layer) + return layer + return null + + +static func _on_native_dialog_closed(dialog: Window) -> void: + hide(dialog) + + +static func _on_native_dialog_custom_action(_action: String, dialog: Window) -> void: + hide(dialog) + + +static func _on_dialog_tree_exiting(dialog: Window, overlay: Control) -> void: + if dialog != null and is_instance_valid(dialog): + dialog.set_meta(OPEN_META, false) + if overlay != null and is_instance_valid(overlay): + overlay.queue_free() + + +static func _append_unique_button(buttons: Array[Button], button: Button) -> void: + if button != null and not buttons.has(button): + buttons.append(button) + + +static func _contains_button(buttons: Array, button: Button) -> bool: + for item: Variant in buttons: + if item is Button and item == button: + return true + return false + + +static func _split_heading(raw_title: String, fallback_icon: String) -> Dictionary: + var icon: String = fallback_icon + var title: String = raw_title + var space: int = raw_title.find(" ") + if space > 0 and space < 5: + var possible_icon: String = raw_title.substr(0, space) + if possible_icon.length() <= 4 and not possible_icon.is_empty(): + icon = possible_icon + title = raw_title.substr(space + 1).strip_edges() + if icon.is_empty(): + icon = "❔" + return {"icon": icon, "title": title} + + +static func _icon_for_dialog(dialog: Window) -> String: + var dialog_name: String = dialog.name.to_lower() + if "artwork" in dialog_name or "scenery" in dialog_name: + return "🖼️" + if "asset" in dialog_name or "error" in dialog_name or "warning" in dialog_name: + return "⚠️" + if "speech" in dialog_name or "speak" in dialog_name: + return "🗣️" + if "rename" in dialog_name: + return "📝" + return "❔" + + +static func _theme_for(dialog: Window) -> SandboxTheme: + var raw: Variant = dialog.get_meta(&"stage_dialog_theme", null) + return raw as SandboxTheme if raw is SandboxTheme else THEME_DEFAULTS.new() + + +static func _apply_control_font(control: Control, ui_font: Font, size: int) -> void: + control.add_theme_font_size_override("font_size", size) + if ui_font != null: + control.add_theme_font_override("font", ui_font) + for child: Node in control.get_children(): + if child is Control: + _apply_control_font(child as Control, ui_font, size) + + +static func _icon_box(background: Color, border: Color) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + style.set_border_width_all(1) + style.set_corner_radius_all(9) + return style + + +static func _detail_box(stage_theme: SandboxTheme) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = Color("#151C28") + style.border_color = Color("#39465B") + style.set_border_width_all(1) + style.set_corner_radius_all(9) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 8.0 + style.content_margin_bottom = 8.0 + return style + + +static func _button_box(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = background + style.border_color = border + style.set_border_width_all(width) + style.set_corner_radius_all(radius) + style.content_margin_left = 12.0 + style.content_margin_right = 12.0 + style.content_margin_top = 8.0 + style.content_margin_bottom = 8.0 + return style + + +static func _strong_font(base_font: Font) -> Font: + if base_font == null: + return ThemeDB.fallback_font + var strong_font := FontVariation.new() + strong_font.base_font = base_font + strong_font.variation_embolden = 0.55 + return strong_font diff --git a/scripts/shared/stage_ui/stage_dialog_styler.gd.uid b/scripts/shared/stage_ui/stage_dialog_styler.gd.uid new file mode 100644 index 0000000..149b2e5 --- /dev/null +++ b/scripts/shared/stage_ui/stage_dialog_styler.gd.uid @@ -0,0 +1 @@ +uid://6iflwn2v1lkg diff --git a/scripts/stage/sandbox_theme.gd b/scripts/stage/sandbox_theme.gd index a12cd44..1c005a4 100644 --- a/scripts/stage/sandbox_theme.gd +++ b/scripts/stage/sandbox_theme.gd @@ -6,24 +6,31 @@ const SCHEMA_VERSION: String = "2.0" const DEFAULT_PATH: String = "res://sandbox_theme.json" const CONFIRMATION_DEFAULTS: Dictionary = { "scenery_erase_size": [520, 208], - "padding": 18, - "body_gap": 10, + "padding": 22, + "body_gap": 12, "footer_gap": 12, - "corner_radius": 16, + "corner_radius": 18, "border_width": 2, - "title_font_size": 26, + "title_font_size": 28, "message_font_size": 20, "button_font_size": 20, - "button_height": 48, + "button_height": 52, + "button_min_width": 112, + "card_width": 560, "button_radius": 12, "button_border_width": 2, "remove_button_width": 170, - "keep_button_width": 132, + "keep_button_width": 160, "surface_color": "#202633FA", - "border_color": "#22C6FF", - "accent_color": "#22C6FF", + "border_color": "#667386", + "secondary_surface": "#293242", + "secondary_border": "#718096", + "focus_color": "#8BD9FF", + "backdrop_color": "#080D15B8", + "accent_color": "#8BD9FF", "action_color": "#087DBA", - "danger_color": "#FF626B", + "danger_color": "#E66B75", + "danger_surface": "#44262D", "danger_hover_surface": "#3B252C", "danger_pressed_surface": "#4A252D" } @@ -35,7 +42,7 @@ const DEFAULT_CONFIG: Dictionary = { "mode_colors": {"edit": "#22C6FF", "direct": "#FFB300", "play": "#33DD77", "guide": "#22C6FF"}, "effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}}, "grid": {"snap_size": 15.0}, - "chrome": {"style_defaults": {}, "top_bar": {"height": 64}, "status_bar": {"height": 38}, "mode_switcher": {"segment_min_width": 124}, "mode_frame": {"border_width": 3}, "mode_badge": {"corner_radius": 12, "padding": 10}, "timestamp_badge": {"corner_radius": 10, "padding": 8, "bg_color": "#171B24F2"}, "tooltip": {"bg_color": "#101010E6", "border_width": 1}, "dialogs": {"queue_size":[680,520], "rule_size":[720,560], "action_size":[560,390], "confirm_size":[620,300], "font_size":22, "message_font_size":26}}, + "chrome": {"style_defaults": {}, "top_bar": {"height": 64}, "status_bar": {"height": 38}, "mode_switcher": {"segment_min_width": 124}, "mode_frame": {"border_width": 3}, "mode_badge": {"corner_radius": 12, "padding": 10}, "timestamp_badge": {"corner_radius": 10, "padding": 8, "bg_color": "#171B24F2"}, "tooltip": {"bg_color": "#101010E6", "border_width": 1}, "dialogs": {"queue_size":[680,520], "rule_size":[720,560], "action_size":[560,390], "confirm_size":[560,300], "font_size":22, "message_font_size":20}}, "popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":8, "setup_title_size":30, "setup_heading_size":26, "setup_choice_font_size":22, "setup_choice_height":68, "setup_choice_corner_radius":22, "setup_choice_border_width":2, "tab_font_size":22, "tab_height":60, "filter_chip_font_size":20, "filter_chip_height":48, "filter_chip_gap":8, "setup_button_font_size":20, "button_border_width":2, "footer_gap":16, "surface_corner_radius":14, "look_corner_radius":8, "corner_radius":16, "compact_swatch_size":42, "color_group_gap":8, "action_color":"#087DBA", "separator_color":"#AAB2C0A0"}}, "palette": {"style_defaults": {}, "dock_width": 116, "card_size": [96, 94], "icon_size": 30, "drag_threshold": 8, "selected_border_width": 3}, "stage_panels": { @@ -136,7 +143,9 @@ func _apply_phase19_defaults() -> void: func _apply_confirmation_defaults() -> void: var popups: Dictionary = _config.get("popups", {}).duplicate(true) - popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true) + var configured: Variant = popups.get("confirmation", {}) + var overrides: Dictionary = configured as Dictionary if configured is Dictionary else {} + popups["confirmation"] = _deep_merge(CONFIRMATION_DEFAULTS, overrides) _config["popups"] = popups func raw_config() -> Dictionary: