Add reusable UI components for Phase 7 UI Polish
- Introduced `EmojiQuickGrid`, `FontPresetPicker`, and `PresetSizeToggle` classes for emoji selection, font presets, and size toggles respectively. - Implemented configuration methods for each component to handle dynamic content and theming. - Created tests for the new components to ensure functionality and integration with the existing UI. - Added a comprehensive architecture specification for Phase 7 UI Polish detailing scope, implementation, and validation strategies. - Updated existing theme handling to support new UI elements and ensure consistent styling across the application.
This commit is contained in:
+321
-61
@@ -24,6 +24,8 @@ const ASSET_SELECTOR := preload("res://scenes/asset_selector.tscn")
|
||||
const STICKMAN_LIBRARY := preload("res://scripts/stickman_library.gd")
|
||||
const PROP_LIBRARY := preload("res://scripts/prop_library.gd")
|
||||
const THUMBNAIL_CACHE := preload("res://scripts/thumbnails/thumbnail_cache.gd")
|
||||
const SANDBOX_THEME := preload("res://scripts/sandbox_theme.gd")
|
||||
const ASSET_CARD := preload("res://scripts/ui/asset_card.gd")
|
||||
const STICKMAN_THUMBNAIL := preload("res://scripts/thumbnails/stickman_thumbnail.gd")
|
||||
const PROP_THUMBNAIL := preload("res://scripts/thumbnails/prop_thumbnail.gd")
|
||||
|
||||
@@ -69,6 +71,8 @@ const ZOOM_STEP: float = 1.10
|
||||
const MIN_BOX_AREA: float = 16.0
|
||||
const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer"
|
||||
const SELECTOR_DIM_ALPHA := 0.5
|
||||
const TOP_BAR_HEIGHT: float = 64.0
|
||||
const STATUS_BAR_HEIGHT: float = 38.0
|
||||
|
||||
const SETTINGS_PATH := "user://sandbox_settings.json"
|
||||
const THEME_PATH := "res://sandbox_theme.json"
|
||||
@@ -165,9 +169,13 @@ var _selector_dim: ColorRect = null
|
||||
var _browse_dialog: FileDialog = null
|
||||
var _thumbnail_queue: Array[Dictionary] = []
|
||||
var _thumbnail_busy: bool = false
|
||||
var _thumbnail_generation: int = 0
|
||||
|
||||
var _mode_buttons: Dictionary = {} # int (StageMode) -> Button
|
||||
var _palette_buttons: Dictionary = {}
|
||||
var _palette_dock: PanelContainer = null
|
||||
var _palette_drag_payload: Dictionary = {}
|
||||
var _palette_drag_previous_id: String = ""
|
||||
var _status_label: Label
|
||||
var _status_cursor_coords: Label
|
||||
var _direct_hint_label: Label
|
||||
@@ -264,6 +272,7 @@ var _nav_dirty: bool = true
|
||||
|
||||
## Parsed sandbox_theme.json (or {} if missing/malformed).
|
||||
var _theme: Dictionary = {}
|
||||
var _sandbox_theme = null
|
||||
var _theme_grid_default: float = DEFAULT_GRID_SIZE
|
||||
var _accent_edit: Color = Color("#22c6ff")
|
||||
var _accent_direct: Color = Color("#ffb300")
|
||||
@@ -419,6 +428,7 @@ func _ready() -> void:
|
||||
_apply_theme_to_visuals()
|
||||
_rebuild_grid_cells()
|
||||
_apply_grid_settings()
|
||||
_update_stage_chrome_layout()
|
||||
_apply_mode_frame()
|
||||
_sync_mode_buttons()
|
||||
_refresh_mode_badge()
|
||||
@@ -461,6 +471,18 @@ func _physics_process(_delta: float) -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not _palette_drag_payload.is_empty():
|
||||
if event is InputEventMouseMotion:
|
||||
_update_palette_drag((event as InputEventMouseMotion).position)
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
var drag_button := event as InputEventMouseButton
|
||||
if drag_button.button_index == MOUSE_BUTTON_LEFT and not drag_button.pressed:
|
||||
_finish_palette_drag(drag_button.position)
|
||||
return
|
||||
if drag_button.button_index == MOUSE_BUTTON_RIGHT and drag_button.pressed:
|
||||
_cancel_palette_drag()
|
||||
return
|
||||
if event is InputEventMagnifyGesture:
|
||||
# Touchpad pinch zoom.
|
||||
_set_zoom(_camera.zoom.x * (event as InputEventMagnifyGesture).factor)
|
||||
@@ -486,7 +508,9 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if current_mode == StageMode.EDIT:
|
||||
delete_selected()
|
||||
KEY_ESCAPE:
|
||||
if _capture_kind != CaptureKind.NONE:
|
||||
if not _palette_drag_payload.is_empty():
|
||||
_cancel_palette_drag()
|
||||
elif _capture_kind != CaptureKind.NONE:
|
||||
_cancel_capture()
|
||||
elif _rule_step != RuleStep.IDLE:
|
||||
_cancel_rule_build()
|
||||
@@ -527,7 +551,17 @@ func _handle_world_click(mb: InputEventMouseButton) -> void:
|
||||
return
|
||||
if mb.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if mb.pressed:
|
||||
_handle_right_click()
|
||||
if _capture_kind != CaptureKind.NONE:
|
||||
_cancel_capture()
|
||||
elif _rule_step != RuleStep.IDLE:
|
||||
_cancel_rule_build()
|
||||
elif _pending_walk_target:
|
||||
_pending_walk_target = false
|
||||
_context_rig = null
|
||||
_apply_cursor()
|
||||
_refresh_status()
|
||||
else:
|
||||
_handle_right_click()
|
||||
return
|
||||
if mb.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
@@ -614,6 +648,8 @@ func set_mode(mode: StageMode) -> void:
|
||||
if not _save_scene_to_path(AUTOSAVE_PATH, true):
|
||||
push_warning("SandboxStage: autosave before Play failed.")
|
||||
current_mode = mode
|
||||
if not _palette_drag_payload.is_empty():
|
||||
_cancel_palette_drag()
|
||||
match mode:
|
||||
StageMode.EDIT:
|
||||
_enter_edit_mode()
|
||||
@@ -927,6 +963,8 @@ func _block_aabb(cell: Vector2i) -> Rect2:
|
||||
|
||||
|
||||
func _begin_terrain_drag(world_pos: Vector2) -> void:
|
||||
if _snap_enabled:
|
||||
world_pos = _snap_to_grid(world_pos)
|
||||
_free_ghost()
|
||||
_drag_painted.clear()
|
||||
_terrain_dragging = true
|
||||
@@ -1458,7 +1496,7 @@ func _check_autosave_recovery() -> void:
|
||||
dialog.name = "AutosaveRecoveryDialog"; dialog.title = "Recover Sandbox Scene?"; dialog.dialog_text = "A previous Play session was not closed cleanly. Recover it?"
|
||||
dialog.confirmed.connect(func() -> void: _apply_scene_data(read["data"], AUTOSAVE_PATH, true); dialog.queue_free())
|
||||
dialog.canceled.connect(func() -> void: DirAccess.remove_absolute(AUTOSAVE_PATH); dialog.queue_free())
|
||||
get_node(NodePath("UI")).add_child(dialog); dialog.popup_centered()
|
||||
get_node(NodePath("UI")).add_child(dialog); _apply_dialog_theme(dialog); dialog.popup_centered()
|
||||
|
||||
|
||||
func _handle_cli_startup(args: Array) -> void:
|
||||
@@ -1517,7 +1555,7 @@ func _refresh_status() -> void:
|
||||
sel_text = "%d objects" % sel.size()
|
||||
var text := "%s | Objects: %d | Selected: %s" % [_scene_name, count, sel_text]
|
||||
if _pending_walk_target:
|
||||
text += " | Click stage for walk target (Esc to cancel)"
|
||||
text += " | Click stage for walk target (Esc or RMB to cancel)"
|
||||
if _rule_step != RuleStep.IDLE and not _rule_hint.is_empty():
|
||||
text += " | " + _rule_hint
|
||||
if not _toast_text.is_empty():
|
||||
@@ -1554,9 +1592,10 @@ func _sync_mode_buttons() -> void:
|
||||
func _apply_mode_frame() -> void:
|
||||
if _mode_frame == null:
|
||||
return
|
||||
_mode_frame.visible = current_mode == StageMode.DIRECT
|
||||
_update_stage_chrome_layout()
|
||||
_mode_frame.visible = true
|
||||
var sb: StyleBoxFlat = _mode_frame.get_theme_stylebox("panel")
|
||||
sb.border_color = _accent_for_mode(current_mode)
|
||||
if sb != null: sb.border_color = _accent_for_mode(current_mode)
|
||||
|
||||
|
||||
func _refresh_mode_badge() -> void:
|
||||
@@ -1643,15 +1682,96 @@ func _apply_ui_font(control: Control) -> void:
|
||||
control.add_theme_font_override("font", _ui_font)
|
||||
|
||||
|
||||
func _theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i:
|
||||
if _sandbox_theme == null:
|
||||
return fallback
|
||||
var raw: Variant = _sandbox_theme.value("chrome.dialogs." + key, [])
|
||||
if raw is Array and (raw as Array).size() == 2:
|
||||
return Vector2i(maxi(320, int(raw[0])), maxi(180, int(raw[1])))
|
||||
return fallback
|
||||
|
||||
|
||||
## 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:
|
||||
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)
|
||||
|
||||
|
||||
func _apply_dialog_font_to_children(node: Node, size: int) -> void:
|
||||
for child: Node in node.get_children():
|
||||
if child is Control:
|
||||
(child as Control).add_theme_font_size_override("font_size", size)
|
||||
if _ui_font != null: (child as Control).add_theme_font_override("font", _ui_font)
|
||||
_apply_dialog_font_to_children(child, size)
|
||||
|
||||
|
||||
func _update_cursor_coords() -> void:
|
||||
if _status_cursor_coords == null:
|
||||
return
|
||||
if _camera == null or not is_instance_valid(_camera):
|
||||
return
|
||||
var viewport_pos: Vector2 = get_viewport().get_mouse_position()
|
||||
if not _is_stage_frame_position(viewport_pos):
|
||||
_status_cursor_coords.text = "X: — Y: —"
|
||||
return
|
||||
var world_pos := _camera.get_global_mouse_position()
|
||||
_status_cursor_coords.text = "X: %d Y: %d" % [int(round(world_pos.x)), int(round(world_pos.y))]
|
||||
|
||||
|
||||
func _is_stage_frame_position(viewport_pos: Vector2) -> bool:
|
||||
if _mode_frame == null:
|
||||
return false
|
||||
var viewport_size: Vector2 = get_window().size
|
||||
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||||
viewport_size = Vector2(1280.0, 720.0)
|
||||
var left: float = _mode_frame.offset_left
|
||||
var top: float = _mode_frame.offset_top
|
||||
var right: float = maxf(left + 1.0, viewport_size.x)
|
||||
var bottom: float = maxf(top + 1.0, viewport_size.y + _mode_frame.offset_bottom)
|
||||
return is_point_in_stage_frame(viewport_pos, Rect2(Vector2(left, top), Vector2(right - left, bottom - top)))
|
||||
|
||||
|
||||
static func is_point_in_stage_frame(viewport_pos: Vector2, frame_rect: Rect2) -> bool:
|
||||
return frame_rect.abs().has_point(viewport_pos)
|
||||
|
||||
|
||||
func _update_stage_chrome_layout() -> void:
|
||||
if _mode_frame == null:
|
||||
return
|
||||
var palette_width: float = _sandbox_theme.number("palette.dock_width", 116.0, 80.0, 260.0) if _sandbox_theme != null else 116.0
|
||||
var left_inset: float = palette_width if current_mode == StageMode.EDIT else 0.0
|
||||
_mode_frame.offset_left = left_inset
|
||||
_mode_frame.offset_top = TOP_BAR_HEIGHT
|
||||
_mode_frame.offset_right = 0.0
|
||||
_mode_frame.offset_bottom = -STATUS_BAR_HEIGHT
|
||||
if _mode_badge != null:
|
||||
_mode_badge.offset_left = left_inset + 12.0
|
||||
_mode_badge.offset_top = TOP_BAR_HEIGHT + 12.0
|
||||
|
||||
|
||||
func _sync_awaiting_cursor() -> void:
|
||||
if _is_awaiting_click() != _last_awaiting_click:
|
||||
_apply_cursor()
|
||||
@@ -1667,6 +1787,9 @@ func _build_ui() -> void:
|
||||
_mode_frame.name = "ModeFrame"
|
||||
_mode_frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mode_frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_mode_frame.offset_left = _sandbox_theme.number("palette.dock_width", 116.0, 80.0, 260.0) if _sandbox_theme != null else 116.0
|
||||
_mode_frame.offset_top = TOP_BAR_HEIGHT
|
||||
_mode_frame.offset_bottom = -STATUS_BAR_HEIGHT
|
||||
var frame_style := StyleBoxFlat.new()
|
||||
frame_style.bg_color = Color(0.0, 0.0, 0.0, 0.0)
|
||||
frame_style.border_color = _accent_direct
|
||||
@@ -1676,22 +1799,43 @@ func _build_ui() -> void:
|
||||
|
||||
var top_bar := PanelContainer.new()
|
||||
top_bar.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
top_bar.offset_bottom = 40.0
|
||||
top_bar.offset_bottom = TOP_BAR_HEIGHT
|
||||
ui.add_child(top_bar)
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.name = "Content"
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
top_bar.add_child(hbox)
|
||||
var chrome := Control.new()
|
||||
chrome.name = "Content"
|
||||
chrome.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
top_bar.add_child(chrome)
|
||||
var left_group := HBoxContainer.new()
|
||||
left_group.name = "LeftControls"
|
||||
left_group.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
left_group.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||
left_group.offset_left = 8.0
|
||||
left_group.offset_top = 8.0
|
||||
chrome.add_child(left_group)
|
||||
var right_group := HBoxContainer.new()
|
||||
right_group.name = "RightControls"
|
||||
right_group.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
right_group.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||
right_group.offset_right = -8.0
|
||||
right_group.offset_top = 8.0
|
||||
right_group.add_theme_constant_override("separation", 8)
|
||||
chrome.add_child(right_group)
|
||||
|
||||
_file_menu = MenuButton.new()
|
||||
_file_menu.name = "FileMenu"
|
||||
_file_menu.text = "File"
|
||||
_file_menu.custom_minimum_size = Vector2(78.0, 48.0)
|
||||
_file_menu.add_theme_font_size_override("font_size", _sandbox_theme.integer("fonts.sizes.file_menu", 22, 12, 48) if _sandbox_theme != null else 22)
|
||||
var file_popup := _file_menu.get_popup()
|
||||
var file_popup_font_size: int = _sandbox_theme.integer("fonts.sizes.file_menu_popup", 26, 12, 64) if _sandbox_theme != null else 26
|
||||
file_popup.add_theme_font_size_override("font_size", file_popup_font_size)
|
||||
file_popup.add_item("New", 0)
|
||||
file_popup.add_item("Open…", 1)
|
||||
var recent_popup := PopupMenu.new()
|
||||
recent_popup.name = "OpenRecent"
|
||||
recent_popup.add_theme_font_size_override("font_size", file_popup_font_size)
|
||||
recent_popup.id_pressed.connect(_on_recent_scene_selected)
|
||||
file_popup.add_child(recent_popup)
|
||||
file_popup.add_submenu_item("Open Recent", "OpenRecent")
|
||||
@@ -1700,9 +1844,22 @@ func _build_ui() -> void:
|
||||
file_popup.add_item("Rename Scene…", 4)
|
||||
file_popup.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
file_popup.about_to_popup.connect(_update_recent_menu)
|
||||
hbox.add_child(_file_menu)
|
||||
left_group.add_child(_file_menu)
|
||||
|
||||
# 3-segment mode switcher.
|
||||
# This centered group is independent of left/right controls, preserving its
|
||||
# window-center position at every supported stage width.
|
||||
var mode_center := CenterContainer.new()
|
||||
mode_center.name = "ModeCenter"
|
||||
# This full-width layout wrapper must not shield File or utility controls.
|
||||
mode_center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mode_center.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
mode_center.offset_bottom = TOP_BAR_HEIGHT
|
||||
chrome.add_child(mode_center)
|
||||
var mode_switcher := HBoxContainer.new()
|
||||
mode_switcher.name = "ModeSwitcher"
|
||||
mode_switcher.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
mode_switcher.add_theme_constant_override("separation", 4)
|
||||
mode_center.add_child(mode_switcher)
|
||||
var mode_defs: Array = [
|
||||
[StageMode.EDIT, "✏️ Edit"],
|
||||
[StageMode.DIRECT, "🎬 Direct"],
|
||||
@@ -1713,39 +1870,32 @@ func _build_ui() -> void:
|
||||
var btn := Button.new()
|
||||
btn.text = String(def[1])
|
||||
btn.toggle_mode = true
|
||||
btn.custom_minimum_size = Vector2(_sandbox_theme.number("chrome.mode_switcher.segment_min_width", 124.0, 90.0, 220.0) if _sandbox_theme != null else 124.0, 48.0)
|
||||
btn.add_theme_font_size_override("font_size", _sandbox_theme.integer("fonts.sizes.mode_switcher", 24, 12, 48) if _sandbox_theme != null else 24)
|
||||
btn.toggled.connect(_on_mode_segment_toggled.bind(mid))
|
||||
hbox.add_child(btn)
|
||||
mode_switcher.add_child(btn)
|
||||
_mode_buttons[int(mid)] = btn
|
||||
|
||||
for id: String in _spawner.get_spawnable_ids():
|
||||
var btn := Button.new()
|
||||
btn.text = _spawner.get_label(id)
|
||||
btn.toggle_mode = true
|
||||
btn.toggled.connect(_on_palette_toggled.bind(id))
|
||||
hbox.add_child(btn)
|
||||
_palette_buttons[id] = btn
|
||||
|
||||
_direct_hint_label = Label.new()
|
||||
_direct_hint_label.text = "Click a stickman to direct, or ⚡ When… for rules"
|
||||
_direct_hint_label.visible = false
|
||||
_apply_ui_font(_direct_hint_label)
|
||||
hbox.add_child(_direct_hint_label)
|
||||
right_group.add_child(_direct_hint_label)
|
||||
|
||||
_grid_check = CheckBox.new()
|
||||
_grid_check.text = "Grid"
|
||||
_grid_check.button_pressed = _show_grid
|
||||
_grid_check.toggled.connect(_on_grid_toggled)
|
||||
hbox.add_child(_grid_check)
|
||||
right_group.add_child(_grid_check)
|
||||
|
||||
_snap_check = CheckBox.new()
|
||||
_snap_check.text = "Snap"
|
||||
_snap_check.button_pressed = _snap_enabled
|
||||
_snap_check.toggled.connect(_on_snap_toggled)
|
||||
hbox.add_child(_snap_check)
|
||||
right_group.add_child(_snap_check)
|
||||
|
||||
_grid_size_label = Label.new()
|
||||
_grid_size_label.text = "Size"
|
||||
hbox.add_child(_grid_size_label)
|
||||
right_group.add_child(_grid_size_label)
|
||||
|
||||
_grid_size_spin = SpinBox.new()
|
||||
_grid_size_spin.min_value = MIN_GRID_SIZE
|
||||
@@ -1754,12 +1904,32 @@ func _build_ui() -> void:
|
||||
_grid_size_spin.value = _grid_size
|
||||
_grid_size_spin.rounded = true
|
||||
_grid_size_spin.value_changed.connect(_on_grid_size_changed)
|
||||
hbox.add_child(_grid_size_spin)
|
||||
right_group.add_child(_grid_size_spin)
|
||||
|
||||
# Graphical asset palette lives below chrome so stage tools keep their row.
|
||||
_palette_dock = PanelContainer.new()
|
||||
_palette_dock.name = "PaletteDock"
|
||||
_palette_dock.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
|
||||
_palette_dock.offset_top = TOP_BAR_HEIGHT
|
||||
_palette_dock.offset_bottom = -STATUS_BAR_HEIGHT
|
||||
_palette_dock.offset_right = _sandbox_theme.number("palette.dock_width", 116.0, 80.0, 260.0) if _sandbox_theme != null else 116.0
|
||||
if _sandbox_theme != null: _palette_dock.add_theme_stylebox_override("panel", _sandbox_theme.stylebox("palette"))
|
||||
var scroll := ScrollContainer.new(); scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED; _palette_dock.add_child(scroll)
|
||||
var cards := VBoxContainer.new(); cards.name = "PaletteCards"; cards.size_flags_horizontal = Control.SIZE_EXPAND_FILL; scroll.add_child(cards)
|
||||
for entry: Dictionary in _spawner.get_palette_entries():
|
||||
var card: Variant = ASSET_CARD.new()
|
||||
card.configure(entry, null, _sandbox_theme)
|
||||
card.toggle_mode = true
|
||||
card.activated.connect(_on_palette_card_activated)
|
||||
card.drag_started.connect(_begin_palette_drag)
|
||||
cards.add_child(card)
|
||||
_palette_buttons[String(entry["placement_id"])] = card
|
||||
ui.add_child(_palette_dock)
|
||||
|
||||
# Bottom status bar (left = status, right = cursor world coords).
|
||||
var bottom_bar := PanelContainer.new()
|
||||
bottom_bar.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE)
|
||||
bottom_bar.offset_top = -28.0
|
||||
bottom_bar.offset_top = -STATUS_BAR_HEIGHT
|
||||
ui.add_child(bottom_bar)
|
||||
|
||||
var status_hbox := HBoxContainer.new()
|
||||
@@ -1780,8 +1950,8 @@ func _build_ui() -> void:
|
||||
_mode_badge.name = "ModeBadge"
|
||||
_mode_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mode_badge.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||
_mode_badge.offset_top = 48.0
|
||||
_mode_badge.offset_left = 8.0
|
||||
_mode_badge.offset_top = TOP_BAR_HEIGHT + 12.0
|
||||
_mode_badge.offset_left = (_sandbox_theme.number("palette.dock_width", 116.0, 80.0, 260.0) if _sandbox_theme != null else 116.0) + 12.0
|
||||
var badge_style := StyleBoxFlat.new()
|
||||
badge_style.bg_color = _accent_edit
|
||||
badge_style.set_corner_radius_all(12)
|
||||
@@ -1872,6 +2042,7 @@ func _build_ui() -> void:
|
||||
speak_box.add_child(_speak_stop_current_check)
|
||||
_speak_dialog.register_text_enter(_speak_edit)
|
||||
ui.add_child(_speak_dialog)
|
||||
_apply_dialog_theme(_speak_dialog)
|
||||
|
||||
_wait_dialog = AcceptDialog.new()
|
||||
_wait_dialog.title = "Wait"
|
||||
@@ -1883,6 +2054,7 @@ func _build_ui() -> void:
|
||||
_wait_spin.value = 1.0
|
||||
_wait_dialog.add_child(_wait_spin)
|
||||
ui.add_child(_wait_dialog)
|
||||
_apply_dialog_theme(_wait_dialog)
|
||||
|
||||
_apply_popup_theme(_action_popup)
|
||||
_apply_popup_theme(_trigger_popup)
|
||||
@@ -1906,6 +2078,7 @@ func _build_ui() -> void:
|
||||
ui.add_child(_selector)
|
||||
# apply_font AFTER add_child so the selector's @onready nodes resolve first.
|
||||
_selector.apply_font(_ui_font, _emoji_font)
|
||||
_selector.apply_theme(_sandbox_theme)
|
||||
|
||||
_browse_dialog = FileDialog.new()
|
||||
_browse_dialog.title = "Open .stk"
|
||||
@@ -1935,7 +2108,7 @@ func _build_ui() -> void:
|
||||
_rename_scene_dialog.name = "RenameSceneDialog"
|
||||
_rename_scene_dialog.title = "Rename Scene"
|
||||
_rename_scene_dialog.cancel_button_text = "Cancel"
|
||||
_rename_scene_dialog.min_size = Vector2i(540, 0)
|
||||
_rename_scene_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
||||
_scene_name_edit = LineEdit.new()
|
||||
_scene_name_edit.name = "SceneNameEdit"
|
||||
_scene_name_edit.placeholder_text = "Scene name"
|
||||
@@ -1945,24 +2118,29 @@ func _build_ui() -> void:
|
||||
var scene_title := _scene_name_edit.text.strip_edges()
|
||||
if not scene_title.is_empty() and scene_title != _scene_name: _scene_name = scene_title; _mark_scene_dirty())
|
||||
ui.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 = Vector2i(560, 0)
|
||||
_missing_assets_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
||||
ui.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 = Vector2i(560, 0)
|
||||
_cli_error_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
||||
ui.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.dialog_text = "Save changes before continuing?"; _unsaved_changes_dialog.ok_button_text = "Save"
|
||||
_unsaved_changes_dialog.add_button("Discard", false, "discard")
|
||||
_unsaved_changes_dialog.name = "UnsavedChangesDialog"; _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.confirmed.connect(_on_unsaved_save)
|
||||
_unsaved_changes_dialog.custom_action.connect(func(action: StringName) -> void: if action == &"discard": _on_unsaved_discard())
|
||||
_unsaved_changes_dialog.canceled.connect(func() -> void: _pending_file_action = PendingFileAction.NONE)
|
||||
ui.add_child(_unsaved_changes_dialog)
|
||||
_apply_dialog_theme(_unsaved_changes_dialog)
|
||||
|
||||
# ---- Phase 3c editor tools ----
|
||||
|
||||
@@ -1980,8 +2158,10 @@ func _build_ui() -> void:
|
||||
_apply_popup_theme(_waypoint_context)
|
||||
|
||||
_confirm_dialog = ConfirmationDialog.new()
|
||||
_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
||||
_confirm_dialog.confirmed.connect(_on_confirm_confirmed)
|
||||
ui.add_child(_confirm_dialog)
|
||||
_apply_dialog_theme(_confirm_dialog)
|
||||
|
||||
_queue_panel = QUEUE_PANEL.instantiate() as QueuePanel
|
||||
_queue_panel.edit_requested.connect(_on_queue_panel_edit_requested)
|
||||
@@ -1990,6 +2170,7 @@ func _build_ui() -> void:
|
||||
_queue_panel.clear_requested.connect(_on_queue_panel_clear_requested)
|
||||
ui.add_child(_queue_panel)
|
||||
_queue_panel.apply_font(_ui_font, _emoji_font, _font_sizes)
|
||||
_queue_panel.min_size = _theme_dialog_size("queue_size", Vector2i(680, 520))
|
||||
|
||||
_rule_panel = RULE_PANEL.instantiate() as RulePanel
|
||||
_rule_panel.edit_requested.connect(_on_rule_panel_edit_requested)
|
||||
@@ -1999,6 +2180,7 @@ func _build_ui() -> void:
|
||||
_rule_panel.reorder_requested.connect(_on_rule_panel_reorder_requested)
|
||||
ui.add_child(_rule_panel)
|
||||
_rule_panel.apply_font(_ui_font, _emoji_font, _font_sizes)
|
||||
_rule_panel.min_size = _theme_dialog_size("rule_size", Vector2i(720, 560))
|
||||
|
||||
_action_editor = ACTION_EDITOR.instantiate() as ActionEditor
|
||||
_action_editor.committed.connect(_on_action_editor_committed)
|
||||
@@ -2006,6 +2188,7 @@ func _build_ui() -> void:
|
||||
_action_editor.target_requested.connect(_on_action_editor_target_requested)
|
||||
ui.add_child(_action_editor)
|
||||
_action_editor.apply_font(_ui_font, _emoji_font, _font_sizes)
|
||||
_action_editor.min_size = _theme_dialog_size("action_size", Vector2i(560, 390))
|
||||
|
||||
_rule_editor = RULE_EDITOR.instantiate() as RuleEditor
|
||||
_rule_editor.committed.connect(_on_rule_editor_committed)
|
||||
@@ -2015,6 +2198,7 @@ func _build_ui() -> void:
|
||||
_rule_editor.action_edit_requested.connect(_on_rule_editor_action_edit_requested)
|
||||
ui.add_child(_rule_editor)
|
||||
_rule_editor.apply_font(_ui_font, _emoji_font, _font_sizes)
|
||||
_rule_editor.min_size = _theme_dialog_size("rule_size", Vector2i(720, 560))
|
||||
|
||||
|
||||
func _build_gizmo_layer() -> void:
|
||||
@@ -2155,6 +2339,57 @@ func _on_palette_toggled(pressed: bool, id: String) -> void:
|
||||
set_placement_mode("")
|
||||
|
||||
|
||||
func _on_palette_card_activated(payload: Dictionary) -> void:
|
||||
var id: String = String(payload.get("placement_id", ""))
|
||||
if id.is_empty(): return
|
||||
if bool(payload.get("opens_selector", false)):
|
||||
_open_selector(id)
|
||||
else:
|
||||
set_placement_mode(id)
|
||||
|
||||
|
||||
func _begin_palette_drag(payload: Dictionary) -> void:
|
||||
if current_mode != StageMode.EDIT or _selector_open: return
|
||||
var id: String = String(payload.get("placement_id", ""))
|
||||
if id.is_empty(): return
|
||||
_palette_drag_payload = payload.duplicate(true)
|
||||
_palette_drag_previous_id = _placement_id
|
||||
_placement_id = id
|
||||
_spawn_ghost()
|
||||
|
||||
|
||||
func _update_palette_drag(viewport_pos: Vector2) -> void:
|
||||
if _ghost != null and is_instance_valid(_ghost):
|
||||
_ghost.global_position = _viewport_to_world(viewport_pos) + _spawner.get_spawn_offset(_placement_id)
|
||||
|
||||
|
||||
func _finish_palette_drag(viewport_pos: Vector2) -> void:
|
||||
var id: String = _placement_id
|
||||
if _is_stage_drop_position(viewport_pos):
|
||||
_place_at(_viewport_to_world(viewport_pos))
|
||||
_palette_drag_payload.clear()
|
||||
_placement_id = _palette_drag_previous_id
|
||||
_palette_drag_previous_id = ""
|
||||
if _placement_id.is_empty(): _free_ghost()
|
||||
else: _spawn_ghost()
|
||||
for pid: String in _palette_buttons: (_palette_buttons[pid] as Button).set_pressed_no_signal(pid == _placement_id)
|
||||
|
||||
|
||||
func _cancel_palette_drag() -> void:
|
||||
_palette_drag_payload.clear()
|
||||
_placement_id = _palette_drag_previous_id
|
||||
_palette_drag_previous_id = ""
|
||||
if _placement_id.is_empty(): _free_ghost()
|
||||
else: _spawn_ghost()
|
||||
|
||||
|
||||
func _is_stage_drop_position(viewport_pos: Vector2) -> bool:
|
||||
var rect: Rect2 = get_viewport().get_visible_rect()
|
||||
if not rect.has_point(viewport_pos): return false
|
||||
var hovered: Control = get_viewport().gui_get_hovered_control()
|
||||
return hovered == null or hovered.mouse_filter == Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func _open_selector(kind: String) -> void:
|
||||
for pid: String in _palette_buttons:
|
||||
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
|
||||
@@ -2189,6 +2424,7 @@ func _on_asset_selected(entry: Dictionary) -> void:
|
||||
func _close_selector() -> void:
|
||||
_selector_open = false
|
||||
_selector_kind = ""
|
||||
_thumbnail_generation += 1
|
||||
_thumbnail_queue.clear()
|
||||
if _selector_dim != null:
|
||||
_selector_dim.visible = false
|
||||
@@ -2226,26 +2462,28 @@ func _on_refresh_requested() -> void:
|
||||
|
||||
func _load_or_enqueue_thumbnails(kind: String, entries: Array[Dictionary]) -> void:
|
||||
_thumbnail_queue.clear()
|
||||
_thumbnail_generation += 1
|
||||
var generation: int = _thumbnail_generation
|
||||
for entry: Dictionary in entries:
|
||||
if kind == "stickman":
|
||||
var path := String(entry.get("path", ""))
|
||||
var key := _thumbnail_cache.stickman_key(path)
|
||||
var png := _thumbnail_cache.stickman_png(key)
|
||||
if FileAccess.file_exists(png):
|
||||
var tex := _thumbnail_cache.load_png(png)
|
||||
var tex := _thumbnail_cache.get_texture(png)
|
||||
if tex != null:
|
||||
if tex != null and _selector != null:
|
||||
_selector.set_thumbnail(entry, tex)
|
||||
else:
|
||||
_thumbnail_queue.append({ "kind": kind, "entry": entry, "key": key })
|
||||
_thumbnail_queue.append({ "kind": kind, "entry": entry, "key": key, "cache_path": png, "generation": generation })
|
||||
else:
|
||||
var id := String(entry.get("id", ""))
|
||||
var png := _thumbnail_cache.prop_png(id)
|
||||
if FileAccess.file_exists(png):
|
||||
var tex := _thumbnail_cache.load_png(png)
|
||||
var tex := _thumbnail_cache.get_texture(png)
|
||||
if tex != null:
|
||||
if tex != null and _selector != null:
|
||||
_selector.set_thumbnail(entry, tex)
|
||||
else:
|
||||
_thumbnail_queue.append({ "kind": kind, "entry": entry })
|
||||
_thumbnail_queue.append({ "kind": kind, "entry": entry, "cache_path": png, "generation": generation })
|
||||
|
||||
|
||||
func _drain_thumbnail_queue() -> void:
|
||||
@@ -2261,15 +2499,15 @@ func _drain_thumbnail_queue() -> void:
|
||||
var data: Dictionary = entry["data"]
|
||||
tex = await _stickman_thumb.render(data)
|
||||
if tex != null:
|
||||
_thumbnail_cache.save_png(tex, _thumbnail_cache.stickman_png(key))
|
||||
_thumbnail_cache.store_texture(String(job.get("cache_path", _thumbnail_cache.stickman_png(key))), tex)
|
||||
else:
|
||||
var payload: Dictionary = entry["payload"]
|
||||
var preset: int = int(entry["material_preset"])
|
||||
tex = await _prop_thumb.render(payload, preset)
|
||||
if tex != null:
|
||||
_thumbnail_cache.save_png(tex, _thumbnail_cache.prop_png(String(entry["id"])))
|
||||
_thumbnail_cache.store_texture(String(job.get("cache_path", _thumbnail_cache.prop_png(String(entry["id"])))), tex)
|
||||
_thumbnail_busy = false
|
||||
if _selector != null and _selector_open and tex != null:
|
||||
if _selector != null and _selector_open and int(job.get("generation", -1)) == _thumbnail_generation and tex != null:
|
||||
_selector.set_thumbnail(entry, tex)
|
||||
|
||||
|
||||
@@ -2727,11 +2965,11 @@ func _on_trigger_popup_id_pressed(id: int) -> void:
|
||||
_rule_step = RuleStep.TRIGGER_TARGET
|
||||
match id:
|
||||
TRIG_ARRIVED:
|
||||
_rule_hint = "Click the waypoint to trigger on (Esc to cancel)"
|
||||
_rule_hint = "Click the waypoint to trigger on (Esc or RMB to cancel)"
|
||||
TRIG_ENTERED_AREA:
|
||||
_rule_hint = "Click the trigger area to watch (Esc to cancel)"
|
||||
_rule_hint = "Click the trigger area to watch (Esc or RMB to cancel)"
|
||||
TRIG_COLLIDED:
|
||||
_rule_hint = "Click the prop that will be collided with (Esc to cancel)"
|
||||
_rule_hint = "Click the prop that will be collided with (Esc or RMB to cancel)"
|
||||
_refresh_status()
|
||||
else:
|
||||
_rule_step = RuleStep.SELECT_ACTION
|
||||
@@ -2747,7 +2985,7 @@ func _on_rule_action_popup_id_pressed(id: int) -> void:
|
||||
actions.append(action)
|
||||
_rule_builder["actions"] = actions
|
||||
_rule_step = RuleStep.ACTION_TARGET
|
||||
_rule_hint = "Click the stickman who will %s (Esc to cancel)" % _action_verb(id)
|
||||
_rule_hint = "Click the stickman who will %s (Esc or RMB to cancel)" % _action_verb(id)
|
||||
_refresh_status()
|
||||
|
||||
|
||||
@@ -2830,7 +3068,7 @@ func _handle_trigger_target_click(world_pos: Vector2) -> void:
|
||||
_rule_step = RuleStep.SELECT_ACTION
|
||||
_open_rule_action_popup()
|
||||
else:
|
||||
_rule_hint = "Click a waypoint dot (Esc to cancel)"
|
||||
_rule_hint = "Click a waypoint dot (Esc or RMB to cancel)"
|
||||
_refresh_status()
|
||||
"entered_area":
|
||||
var hit := _selection.hit_test(world_pos)
|
||||
@@ -2840,7 +3078,7 @@ func _handle_trigger_target_click(world_pos: Vector2) -> void:
|
||||
_rule_step = RuleStep.SELECT_ACTION
|
||||
_open_rule_action_popup()
|
||||
else:
|
||||
_rule_hint = "Click a trigger area (Esc to cancel)"
|
||||
_rule_hint = "Click a trigger area (Esc or RMB to cancel)"
|
||||
_refresh_status()
|
||||
"collided":
|
||||
var hit2 := _selection.hit_test(world_pos)
|
||||
@@ -2850,7 +3088,7 @@ func _handle_trigger_target_click(world_pos: Vector2) -> void:
|
||||
_rule_step = RuleStep.SELECT_ACTION
|
||||
_open_rule_action_popup()
|
||||
else:
|
||||
_rule_hint = "Click the prop that will be collided with (Esc to cancel)"
|
||||
_rule_hint = "Click the prop that will be collided with (Esc or RMB to cancel)"
|
||||
_refresh_status()
|
||||
|
||||
|
||||
@@ -2869,7 +3107,7 @@ func _handle_action_target_click(world_pos: Vector2) -> void:
|
||||
match String(action.get("type", "")):
|
||||
"walk_to":
|
||||
_rule_step = RuleStep.ACTION_POSITION
|
||||
_rule_hint = "Click where %s should walk (Esc to cancel)" % String(hit.name)
|
||||
_rule_hint = "Click where %s should walk (Esc or RMB to cancel)" % String(hit.name)
|
||||
_refresh_status()
|
||||
"speak":
|
||||
_rule_step = RuleStep.PARAMS
|
||||
@@ -3400,7 +3638,7 @@ func _begin_walk_edit(rig: StickmanRig, index: int, from_panel: bool) -> void:
|
||||
var queue := rig.get_queue()
|
||||
var pos: Vector2 = queue[index].get("target", rig.global_position - STICKMAN_RIG.FOOT_OFFSET)
|
||||
_director_visuals.set_edit_waypoint(pos)
|
||||
_begin_capture(CaptureKind.POSITION, "🚩 Click the new waypoint position (Esc to cancel)", _on_walk_edit_captured, _cb_walk_edit_cancel)
|
||||
_begin_capture(CaptureKind.POSITION, "🚩 Click the new waypoint position (Esc or RMB to cancel)", _on_walk_edit_captured, _cb_walk_edit_cancel)
|
||||
|
||||
|
||||
func _on_walk_edit_captured(value: Vector2) -> void:
|
||||
@@ -3667,11 +3905,11 @@ func _on_rule_editor_trigger_target_requested(type: String) -> void:
|
||||
_rule_editor.hide()
|
||||
match TRIGGER_REGISTRY.target_type(type):
|
||||
"waypoint":
|
||||
_begin_capture(CaptureKind.WAYPOINT, "📍 Click the waypoint (Esc to cancel)", _cb_rule_trigger_waypoint, _cb_rule_trigger_cancel)
|
||||
_begin_capture(CaptureKind.WAYPOINT, "📍 Click the waypoint (Esc or RMB to cancel)", _cb_rule_trigger_waypoint, _cb_rule_trigger_cancel)
|
||||
"area":
|
||||
_begin_capture(CaptureKind.AREA, "🎯 Click the trigger area (Esc to cancel)", _cb_rule_trigger_area, _cb_rule_trigger_cancel)
|
||||
_begin_capture(CaptureKind.AREA, "🎯 Click the trigger area (Esc or RMB to cancel)", _cb_rule_trigger_area, _cb_rule_trigger_cancel)
|
||||
"prop":
|
||||
_begin_capture(CaptureKind.PROP, "💥 Click the prop (Esc to cancel)", _cb_rule_trigger_prop, _cb_rule_trigger_cancel)
|
||||
_begin_capture(CaptureKind.PROP, "💥 Click the prop (Esc or RMB to cancel)", _cb_rule_trigger_prop, _cb_rule_trigger_cancel)
|
||||
|
||||
|
||||
func _on_rule_editor_action_add_requested() -> void:
|
||||
@@ -3763,7 +4001,7 @@ func _on_action_editor_committed(flat: Dictionary) -> void:
|
||||
_show_toast("Action inserted")
|
||||
"rule_add":
|
||||
_pending_rule_action_flat = flat
|
||||
_begin_capture(CaptureKind.STICKMAN, "🎯 Click the stickman who will act (Esc to cancel)", _cb_rule_actor, _cb_rule_actor_cancel)
|
||||
_begin_capture(CaptureKind.STICKMAN, "🎯 Click the stickman who will act (Esc or RMB to cancel)", _cb_rule_actor, _cb_rule_actor_cancel)
|
||||
return
|
||||
"rule_edit":
|
||||
if _rule_editor != null:
|
||||
@@ -3785,7 +4023,7 @@ func _on_action_editor_cancelled() -> void:
|
||||
|
||||
func _on_action_editor_target_requested() -> void:
|
||||
_action_editor.hide()
|
||||
_begin_capture(CaptureKind.POSITION, "🚩 Click where to walk (Esc to cancel)", _cb_editor_walk_target, _cb_editor_walk_target_cancel)
|
||||
_begin_capture(CaptureKind.POSITION, "🚩 Click where to walk (Esc or RMB to cancel)", _cb_editor_walk_target, _cb_editor_walk_target_cancel)
|
||||
|
||||
|
||||
func _cb_editor_walk_target(value: Vector2) -> void:
|
||||
@@ -3865,6 +4103,8 @@ func _apply_grid_settings() -> void:
|
||||
func _set_edit_controls_visible(visible: bool) -> void:
|
||||
for btn: Button in _palette_buttons.values():
|
||||
btn.visible = visible
|
||||
if _palette_dock != null:
|
||||
_palette_dock.visible = visible
|
||||
if _grid_check != null:
|
||||
_grid_check.visible = visible
|
||||
if _snap_check != null:
|
||||
@@ -3899,6 +4139,8 @@ func _world_children_selectable() -> Array[Node2D]:
|
||||
## Loads the theme JSON. `source_path` is overridable for tests (missing /
|
||||
## malformed fallback); defaults to the committed res:// theme.
|
||||
func _load_theme(source_path: String = THEME_PATH) -> void:
|
||||
_sandbox_theme = SANDBOX_THEME.new()
|
||||
_sandbox_theme.load_from(source_path)
|
||||
_theme = {}
|
||||
_build_font_sizes()
|
||||
if not FileAccess.file_exists(source_path):
|
||||
@@ -3912,7 +4154,7 @@ func _load_theme(source_path: String = THEME_PATH) -> void:
|
||||
if not json is Dictionary:
|
||||
push_warning("SandboxStage: theme file '%s' is malformed; using defaults." % source_path)
|
||||
return
|
||||
_theme = json as Dictionary
|
||||
_theme = _sandbox_theme.raw_config()
|
||||
var fonts: Dictionary = _theme.get("fonts", {})
|
||||
_action_popup_font_size = int(fonts.get("action_popup_font_size", 24))
|
||||
_action_popup_emoji_size = int(fonts.get("action_popup_emoji_size", 22))
|
||||
@@ -3951,6 +4193,24 @@ func _load_theme(source_path: String = THEME_PATH) -> void:
|
||||
_accent_direct = _parse_color(String(colors.get("direct_accent", "")), _accent_direct)
|
||||
_accent_play = _parse_color(String(colors.get("play_accent", "")), _accent_play)
|
||||
_guide_line_color = _parse_color(String(colors.get("guide_line", "")), _guide_line_color)
|
||||
# Schema v2 wins only when the source itself opts into v2. Legacy ad-hoc
|
||||
# theme files retain their Phase 3 flat-key precedence for compatibility.
|
||||
if String((json as Dictionary).get("version", "")) == SANDBOX_THEME.SCHEMA_VERSION:
|
||||
_action_popup_font_size = _sandbox_theme.integer("fonts.sizes.action_popup", _action_popup_font_size, 8, 96)
|
||||
_action_popup_emoji_size = _sandbox_theme.integer("fonts.sizes.action_popup_emoji", _action_popup_emoji_size, 8, 96)
|
||||
_tooltip_font_size = _sandbox_theme.integer("fonts.sizes.tooltip", _tooltip_font_size, 8, 96)
|
||||
_status_pill_font_size = _sandbox_theme.integer("fonts.sizes.status_pill", _status_pill_font_size, 8, 96)
|
||||
_queue_panel_font_size = _sandbox_theme.integer("fonts.sizes.queue_panel", _queue_panel_font_size, 8, 96)
|
||||
_rule_panel_font_size = _sandbox_theme.integer("fonts.sizes.rule_panel", _rule_panel_font_size, 8, 96)
|
||||
_action_editor_font_size = _sandbox_theme.integer("fonts.sizes.action_editor", _action_editor_font_size, 8, 96)
|
||||
_rule_editor_font_size = _sandbox_theme.integer("fonts.sizes.rule_editor", _rule_editor_font_size, 8, 96)
|
||||
_panel_row_font_size = _sandbox_theme.integer("fonts.sizes.panel_row", _panel_row_font_size, 8, 96)
|
||||
_panel_title_font_size = _sandbox_theme.integer("fonts.sizes.panel_title", _panel_title_font_size, 8, 96)
|
||||
_accent_edit = _sandbox_theme.color("mode_colors.edit", _accent_edit)
|
||||
_accent_direct = _sandbox_theme.color("mode_colors.direct", _accent_direct)
|
||||
_accent_play = _sandbox_theme.color("mode_colors.play", _accent_play)
|
||||
_guide_line_color = _sandbox_theme.color("mode_colors.guide", _guide_line_color)
|
||||
_theme_grid_default = _sandbox_theme.number("grid.snap_size", _theme_grid_default, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
_build_font_sizes()
|
||||
|
||||
|
||||
@@ -4018,7 +4278,7 @@ func _apply_theme_to_visuals() -> void:
|
||||
_director_visuals.bold_font = _ui_font_bold
|
||||
_director_visuals.italic_font = _ui_font_italic
|
||||
if _placement_overlay != null:
|
||||
_placement_overlay.guide_line_color = _guide_line_color
|
||||
_placement_overlay.apply_style(_sandbox_theme.value("overlays", {}) if _sandbox_theme != null else {})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings persistence
|
||||
|
||||
Reference in New Issue
Block a user