feat: harmonize Prop and Stickman editor UI

This commit is contained in:
2026-10-24 01:02:11 -04:00
parent ddb298f631
commit ce46b5d97e
44 changed files with 1565 additions and 256 deletions
+276 -118
View File
@@ -13,6 +13,8 @@ extends Control
const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
const PropThemeScript := preload("res://scripts/editors/prop/prop_theme.gd")
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
const SandboxThemeScript := preload("res://scripts/stage/sandbox_theme.gd")
const StageDialogStylerScript := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
const PropCanvasScript := preload("res://scripts/editors/prop/prop_canvas.gd")
const PropThumbnailScript := preload("res://scripts/shared/thumbnails/prop_thumbnail.gd")
@@ -20,6 +22,7 @@ const ThumbnailCacheScript := preload("res://scripts/shared/thumbnails/thumbnail
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
const PropAttachPreviewScene := preload("res://scenes/editors/prop/prop_attach_preview.tscn")
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
const PRESET_LABELS: PackedStringArray = ["None", "Wood", "Rubber", "Cardboard", "Metal"]
const PRIMITIVE_IDS: PackedStringArray = ["box", "plank", "triangle", "line", "circle"]
@@ -116,6 +119,7 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
var _theme: PropTheme
var _editor_visual_theme: EditorVisualTheme
var _dialog_theme: SandboxTheme
var _material_preset: int = PropBlockScript.MaterialPreset.NONE
var _current_path: String = ""
var _dirty: bool = false
@@ -128,11 +132,14 @@ var _thumb_cache: ThumbnailCache
var _ui_theme: Theme
# Inspector controls
var _preset_option: OptionButton
var _material_section: VBoxContainer
var _preset_option: PropInspectorDropdown
var _material_section: PanelContainer
var _mass_label: Label
var _mass_value: Label
var _friction_label: Label
var _friction_value: Label
var _bounce_label: Label
var _bounce_value: Label
var _fill_button: ColorPickerButton
var _outline_button: ColorPickerButton
var _width_spin: SpinBox
@@ -143,14 +150,14 @@ var _vertex_label: Label
var _delete_button: Button
var _create_point_button: Button
var _create_point_hint: Label
var _shape_section: VBoxContainer
var _primitive_option: OptionButton
var _shape_section: PanelContainer
var _primitive_option: PropInspectorDropdown
var _params_box: VBoxContainer
var _param_spins: Dictionary = {}
var _add_button: Button
var _updating_inspector: bool = false
var _tag_checks: Dictionary = {}
var _tag_section: VBoxContainer
var _tag_section: PanelContainer
var _stage_authoring: bool = false
var _stage_return_pending: bool = false
var _stage_place_button: Button
@@ -177,7 +184,7 @@ var _points_button: Button
var _attach_button: Button
# Add-shape inspector section (hidden in Reshape mode, Round 3)
var _add_section: VBoxContainer
var _add_section: PanelContainer
var _anchor_section: VBoxContainer
var _anchor_list: ItemList
var _anchor_role: OptionButton
@@ -218,6 +225,8 @@ func _ready() -> void:
get_tree().auto_accept_quit = false
_editor_visual_theme = EditorVisualThemeScript.new()
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
_dialog_theme = SandboxThemeScript.new()
_dialog_theme.load_from(SandboxThemeScript.DEFAULT_PATH)
_theme = PropThemeScript.new()
_theme.load_from(PropThemeScript.DEFAULT_PATH)
_thumb_cache = ThumbnailCacheScript.new()
@@ -254,6 +263,7 @@ func _ready() -> void:
_canvas.set_grid_size(_theme.grid_size())
_canvas.set_snap_enabled(false)
_canvas.set_has_clipboard(false)
_canvas.set_context_menu_visual_theme(_editor_visual_theme, &"prop")
_canvas.set_context_menu_font_size(_theme.font_context_menu())
_canvas.set_anchor_data(_anchors)
var origin_marker_color: Color = _theme.text_muted_color()
@@ -503,26 +513,26 @@ func _setup_menu_bar() -> void:
var file_menu := PopupMenu.new()
file_menu.name = "FileMenu"
file_menu.add_item("🆕 New", MENU_NEW)
file_menu.add_item("💾 Save", MENU_SAVE)
file_menu.add_item("📄 Save As...", MENU_SAVE_AS)
file_menu.add_item("📂 Load...", MENU_LOAD)
file_menu.add_separator()
file_menu.add_item("📂 Load…", MENU_LOAD)
var recent_popup := PopupMenu.new()
recent_popup.name = "RecentProps"
recent_popup.id_pressed.connect(_on_recent_prop_selected)
file_menu.add_child(recent_popup)
file_menu.add_submenu_item("🕑 Recent", "RecentProps")
file_menu.add_submenu_item("🕑 Open Recent", "RecentProps")
file_menu.add_separator()
file_menu.add_item("💾 Save", MENU_SAVE)
file_menu.add_item("📄 Save As…", MENU_SAVE_AS)
file_menu.id_pressed.connect(_on_menu_id_pressed)
file_menu.about_to_popup.connect(_update_recent_menu)
_menu_bar.add_child(file_menu)
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "📁 File")
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
var view_menu := PopupMenu.new()
view_menu.name = "ViewMenu"
view_menu.add_item("🔍 Reset View", MENU_RESET_VIEWS)
view_menu.add_item("🔍 Reset Views", MENU_RESET_VIEWS)
view_menu.id_pressed.connect(_on_menu_id_pressed)
_menu_bar.add_child(view_menu)
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "👁️ View")
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "View")
# ---------------------------------------------------------------------------
@@ -617,25 +627,31 @@ func _setup_dialogs() -> void:
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"prop")
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"prop")
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"prop")
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"prop")
_error_dialog.get_ok_button().text = "Got it"
StageDialogStylerScript.apply(
_error_dialog, _dialog_theme, _dialog_theme.font(),
_error_dialog.get_ok_button()
)
_build_exit_dialog()
_rename_anchor_dialog = ConfirmationDialog.new()
_rename_anchor_dialog.title = "Rename Anchor"
_rename_anchor_dialog.ok_button_text = "Rename"
_rename_anchor_dialog.theme = _ui_theme
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_ok_button(), &"primary", &"prop")
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_cancel_button(), &"secondary", &"prop")
_rename_anchor_dialog.set_meta(StageDialogStylerScript.ICON_META, "📝")
_rename_anchor_edit = LineEdit.new()
_rename_anchor_edit.placeholder_text = "Anchor name"
_rename_anchor_dialog.add_child(_rename_anchor_edit)
add_child(_rename_anchor_dialog)
StageDialogStylerScript.apply(
_rename_anchor_dialog, _dialog_theme, _dialog_theme.font(),
_rename_anchor_dialog.get_ok_button()
)
_rename_anchor_edit.text_changed.connect(_update_rename_anchor_confirmation)
_rename_anchor_edit.text_submitted.connect(_on_rename_anchor_text_submitted)
_rename_anchor_dialog.confirmed.connect(_commit_anchor_rename)
_rename_anchor_dialog.canceled.connect(_clear_pending_anchor_rename)
func _on_rename_anchor_text_submitted(_text: String) -> void:
if _rename_anchor_dialog == null or not _rename_anchor_dialog.visible:
if _rename_anchor_dialog == null or not StageDialogStylerScript.is_open(_rename_anchor_dialog):
return
var ok_button := _rename_anchor_dialog.get_ok_button()
if not ok_button.disabled:
@@ -886,30 +902,31 @@ func _on_whole_prop_rotate_requested(angle_rad: float) -> void:
func _build_exit_dialog() -> void:
_exit_dialog = ConfirmationDialog.new()
_exit_dialog.title = "⚠️ Unsaved Changes"
_exit_dialog.title = "Unsaved Changes"
_exit_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
_exit_dialog.ok_button_text = "💾 Save"
var discard_button: Button = _exit_dialog.add_button("🗑️ Discard", false, "discard")
_exit_dialog.get_cancel_button().text = "❌ Cancel"
_exit_dialog.theme = _ui_theme
_editor_visual_theme.style_button_role(_exit_dialog.get_ok_button(), &"primary", &"prop")
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
_editor_visual_theme.style_button_role(_exit_dialog.get_cancel_button(), &"secondary", &"prop")
_exit_dialog.ok_button_text = "Save"
var discard_button: Button = _exit_dialog.add_button("Discard", false, "discard")
_exit_dialog.get_cancel_button().text = "Cancel"
add_child(_exit_dialog)
StageDialogStylerScript.apply(
_exit_dialog, _dialog_theme, _dialog_theme.font(),
_exit_dialog.get_ok_button(), [discard_button]
)
_exit_dialog.confirmed.connect(_on_exit_save)
_exit_dialog.custom_action.connect(_on_exit_custom_action)
_exit_dialog.canceled.connect(_on_exit_cancel)
_off_origin_save_dialog = ConfirmationDialog.new()
_off_origin_save_dialog.title = "🎯 Center Prop?"
_off_origin_save_dialog.dialog_text = "This prop is far from (0,0). Centering it keeps its shapes and anchors together.\n\nWhat would you like to do?"
_off_origin_save_dialog.ok_button_text = "🎯 Center & Save"
var save_anyway_button: Button = _off_origin_save_dialog.add_button("💾 Save Anyway", false, "save_anyway")
_off_origin_save_dialog.get_cancel_button().text = "❌ Cancel"
_off_origin_save_dialog.theme = _ui_theme
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_ok_button(), &"primary", &"prop")
_editor_visual_theme.style_button_role(save_anyway_button, &"secondary", &"prop")
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_cancel_button(), &"secondary", &"prop")
_off_origin_save_dialog.ok_button_text = "Center & Save"
var save_anyway_button: Button = _off_origin_save_dialog.add_button("Save Anyway", false, "save_anyway")
_off_origin_save_dialog.get_cancel_button().text = "Cancel"
add_child(_off_origin_save_dialog)
StageDialogStylerScript.apply(
_off_origin_save_dialog, _dialog_theme, _dialog_theme.font(),
_off_origin_save_dialog.get_ok_button(), [], [save_anyway_button]
)
_off_origin_save_dialog.confirmed.connect(func() -> void: await _execute_pending_save(true))
_off_origin_save_dialog.custom_action.connect(func(action: StringName) -> void:
if action == &"save_anyway": await _execute_pending_save(false))
@@ -931,7 +948,7 @@ func _request_close() -> void:
_pending_quit = true
_pending_new = false
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
_exit_dialog.popup_centered()
StageDialogStylerScript.popup(_exit_dialog)
func _request_new() -> void:
@@ -942,7 +959,7 @@ func _request_new() -> void:
_pending_new = true
_pending_quit = false
_exit_dialog.dialog_text = "You have unsaved changes. Save them before starting a new prop?"
_exit_dialog.popup_centered()
StageDialogStylerScript.popup(_exit_dialog)
func _request_stage_return() -> void:
@@ -958,11 +975,11 @@ func _request_stage_return() -> void:
_pending_new = false
_pending_quit = false
_exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage."
_exit_dialog.popup_centered()
StageDialogStylerScript.popup(_exit_dialog)
func _on_exit_save() -> void:
_exit_dialog.hide()
StageDialogStylerScript.hide(_exit_dialog)
if _current_path.is_empty():
_save_dialog.popup_centered()
return
@@ -971,7 +988,7 @@ func _on_exit_save() -> void:
func _on_exit_custom_action(action: StringName) -> void:
if action == &"discard":
_exit_dialog.hide()
StageDialogStylerScript.hide(_exit_dialog)
if not _stage_authoring:
_clear_autosave()
if _stage_return_pending:
@@ -1046,47 +1063,60 @@ func _on_menu_id_pressed(id: int) -> void:
# ---------------------------------------------------------------------------
func _build_inspector() -> void:
var header_size: int = _theme.font_header()
var row_height: int = _theme.row_min_height()
_inspector_list.add_theme_constant_override("separation", 10)
_inspector_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Material preset section
_material_section = VBoxContainer.new()
_material_section.add_theme_constant_override("separation", 6)
_material_section = _make_inspector_card()
_inspector_list.add_child(_material_section)
var material_title := Label.new()
material_title.text = "Material"
material_title.add_theme_font_size_override("font_size", header_size)
_material_section.add_child(material_title)
var material_content: VBoxContainer = _inspector_card_content(_material_section)
material_content.add_child(_make_inspector_header("Material", PropInspectorIconsScript.section("material")))
_preset_option = OptionButton.new()
for label: String in PRESET_LABELS:
_preset_option.add_item(label)
_preset_option = PropInspectorDropdown.new()
for index: int in PRESET_LABELS.size():
_preset_option.add_icon_item(
PropInspectorIconsScript.material(index, _theme.material_tint(index, "fill")),
PRESET_LABELS[index]
)
_preset_option.selected = _material_preset
_preset_option.custom_minimum_size.y = float(_theme.button_min_height())
_preset_option.configure(
_ui_theme,
_theme.inspector_background_color(),
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
_theme.border_color(),
_theme.accent_color(),
_editor_visual_theme.chrome_color(&"focus", &"prop"),
_theme.text_color(),
_theme.font_body()
)
_preset_option.item_selected.connect(_on_preset_selected)
_material_section.add_child(_preset_option)
material_content.add_child(_preset_option)
_mass_label = Label.new()
_friction_label = Label.new()
_bounce_label = Label.new()
_material_section.add_child(_mass_label)
_material_section.add_child(_friction_label)
_material_section.add_child(_bounce_label)
var stats := HBoxContainer.new()
stats.add_theme_constant_override("separation", 6)
material_content.add_child(stats)
var mass_tile: Array[Label] = _make_stat_tile(stats, "Mass")
_mass_label = mass_tile[0]
_mass_value = mass_tile[1]
var grip_tile: Array[Label] = _make_stat_tile(stats, "Surface grip")
_friction_label = grip_tile[0]
_friction_value = grip_tile[1]
var bounce_tile: Array[Label] = _make_stat_tile(stats, "Bounce")
_bounce_label = bounce_tile[0]
_bounce_value = bounce_tile[1]
_update_physics_labels()
# Selected-shape section
_shape_section = VBoxContainer.new()
_shape_section.add_theme_constant_override("separation", 6)
_shape_section = _make_inspector_card()
_inspector_list.add_child(_shape_section)
var shape_title := Label.new()
shape_title.text = "Selected Shape"
shape_title.add_theme_font_size_override("font_size", header_size)
_shape_section.add_child(shape_title)
var shape_content: VBoxContainer = _inspector_card_content(_shape_section)
shape_content.add_child(_make_inspector_header("Selected Shape", PropInspectorIconsScript.section("selected_shape")))
var fill_row := HBoxContainer.new()
fill_row.custom_minimum_size.y = float(row_height)
_shape_section.add_child(fill_row)
shape_content.add_child(fill_row)
var fill_label := Label.new()
fill_label.text = "Fill:"
fill_label.custom_minimum_size = Vector2(90.0, 0.0)
@@ -1099,7 +1129,7 @@ func _build_inspector() -> void:
var outline_row := HBoxContainer.new()
outline_row.custom_minimum_size.y = float(row_height)
_shape_section.add_child(outline_row)
shape_content.add_child(outline_row)
var outline_label := Label.new()
outline_label.text = "Outline:"
outline_label.custom_minimum_size = Vector2(90.0, 0.0)
@@ -1112,7 +1142,7 @@ func _build_inspector() -> void:
var width_row := HBoxContainer.new()
width_row.custom_minimum_size.y = float(row_height)
_shape_section.add_child(width_row)
shape_content.add_child(width_row)
var width_label := Label.new()
width_label.text = "Width:"
width_label.custom_minimum_size = Vector2(90.0, 0.0)
@@ -1128,7 +1158,7 @@ func _build_inspector() -> void:
_radius_row = HBoxContainer.new()
_radius_row.custom_minimum_size.y = float(row_height)
_shape_section.add_child(_radius_row)
shape_content.add_child(_radius_row)
var radius_label := Label.new()
radius_label.text = "Radius:"
radius_label.custom_minimum_size = Vector2(90.0, 0.0)
@@ -1145,10 +1175,10 @@ func _build_inspector() -> void:
_reshaped_note = Label.new()
_reshaped_note.text = "✏️ This shape was reshaped"
_reshaped_note.add_theme_color_override("font_color", _theme.text_muted_color())
_shape_section.add_child(_reshaped_note)
shape_content.add_child(_reshaped_note)
_vertex_label = Label.new()
_shape_section.add_child(_vertex_label)
shape_content.add_child(_vertex_label)
_create_point_button = Button.new()
_create_point_button.text = "📍 Create Point"
@@ -1156,46 +1186,55 @@ func _build_inspector() -> void:
_create_point_button.custom_minimum_size.y = float(_theme.button_min_height())
_editor_visual_theme.style_button_role(_create_point_button, &"primary", &"prop")
_create_point_button.pressed.connect(_on_create_point_pressed)
_shape_section.add_child(_create_point_button)
shape_content.add_child(_create_point_button)
_create_point_hint = Label.new()
_create_point_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_create_point_hint.add_theme_color_override("font_color", _theme.text_muted_color())
_shape_section.add_child(_create_point_hint)
shape_content.add_child(_create_point_hint)
_delete_button = Button.new()
_delete_button.text = "Erase Shape"
_delete_button.custom_minimum_size.y = float(_theme.button_min_height())
_editor_visual_theme.style_button_role(_delete_button, &"destructive", &"prop")
_delete_button.pressed.connect(_on_delete_shape_pressed)
_shape_section.add_child(_delete_button)
shape_content.add_child(_delete_button)
# Add-shape section (hidden in Reshape mode, Round 3)
_add_section = VBoxContainer.new()
_add_section.add_theme_constant_override("separation", 8)
_add_section = _make_inspector_card()
_inspector_list.add_child(_add_section)
var add_content: VBoxContainer = _inspector_card_content(_add_section)
add_content.add_child(_make_inspector_header("Add Shape", PropInspectorIconsScript.section("add_shape")))
var add_title := Label.new()
add_title.text = "Add Shape"
add_title.add_theme_font_size_override("font_size", header_size)
_add_section.add_child(add_title)
_primitive_option = OptionButton.new()
for label: String in PRIMITIVE_LABELS:
_primitive_option.add_item(label)
_primitive_option = PropInspectorDropdown.new()
for index: int in PRIMITIVE_LABELS.size():
_primitive_option.add_icon_item(
PropInspectorIconsScript.shape(PRIMITIVE_IDS[index], _primitive_icon_color(PRIMITIVE_IDS[index])),
PRIMITIVE_LABELS[index]
)
_primitive_option.custom_minimum_size.y = float(_theme.button_min_height())
_primitive_option.configure(
_ui_theme,
_theme.inspector_background_color(),
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
_theme.border_color(),
_theme.accent_color(),
_editor_visual_theme.chrome_color(&"focus", &"prop"),
_theme.text_color(),
_theme.font_body()
)
_primitive_option.item_selected.connect(_on_primitive_selected)
_add_section.add_child(_primitive_option)
add_content.add_child(_primitive_option)
_params_box = VBoxContainer.new()
_params_box.add_theme_constant_override("separation", 4)
_add_section.add_child(_params_box)
add_content.add_child(_params_box)
_add_button = Button.new()
_add_button.text = "Add a Shape"
_add_button.custom_minimum_size.y = float(_theme.button_min_height())
_editor_visual_theme.style_button_role(_add_button, &"primary", &"prop")
_add_button.pressed.connect(_on_add_pressed)
_add_section.add_child(_add_button)
add_content.add_child(_add_button)
_apply_control_theme(_inspector_list)
_rebuild_primitive_params()
@@ -1204,6 +1243,117 @@ func _build_inspector() -> void:
_refresh_tag_controls()
func _make_inspector_card() -> PanelContainer:
var card := PanelContainer.new()
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var style := StyleBoxFlat.new()
style.bg_color = _theme.inspector_background_color().darkened(0.07)
style.border_color = _theme.border_color()
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.content_margin_left = 10.0
style.content_margin_top = 10.0
style.content_margin_right = 10.0
style.content_margin_bottom = 10.0
card.add_theme_stylebox_override("panel", style)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 8)
card.add_child(content)
return card
func _inspector_card_content(card: PanelContainer) -> VBoxContainer:
return card.get_child(0) as VBoxContainer
func _make_inspector_header(title: String, icon: Texture2D) -> HBoxContainer:
var header := HBoxContainer.new()
header.add_theme_constant_override("separation", 8)
var icon_view := TextureRect.new()
icon_view.texture = icon
icon_view.custom_minimum_size = Vector2(30.0, 30.0)
icon_view.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon_view.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_child(icon_view)
var label := Label.new()
label.text = title
label.add_theme_font_size_override("font_size", _theme.font_header())
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(label)
return header
func _make_stat_tile(parent: Control, caption_text: String) -> Array[Label]:
var tile := PanelContainer.new()
tile.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var style := StyleBoxFlat.new()
style.bg_color = _editor_visual_theme.chrome_color(&"background", &"prop").darkened(0.02)
style.border_color = _theme.border_color()
style.set_border_width_all(1)
style.set_corner_radius_all(8)
style.content_margin_left = 7.0
style.content_margin_top = 7.0
style.content_margin_right = 7.0
style.content_margin_bottom = 7.0
tile.add_theme_stylebox_override("panel", style)
parent.add_child(tile)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 1)
tile.add_child(content)
var caption := Label.new()
caption.text = caption_text
caption.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
caption.add_theme_font_size_override("font_size", 14)
caption.add_theme_color_override("font_color", _theme.text_muted_color())
content.add_child(caption)
var value := Label.new()
value.add_theme_font_size_override("font_size", 22)
content.add_child(value)
return [caption, value]
func _style_tag_button(button: Button) -> void:
var text_color: Color = _theme.text_color()
var surface: Color = _editor_visual_theme.chrome_color(&"background", &"prop")
var border: Color = _theme.border_color()
var accent: Color = _theme.accent_color()
button.add_theme_stylebox_override("normal", _make_tag_style(surface, border, 1))
button.add_theme_stylebox_override("hover", _make_tag_style(surface.lightened(0.12), accent, 1))
button.add_theme_stylebox_override("pressed", _make_tag_style(_editor_visual_theme.chrome_color(&"selected_surface", &"prop"), accent, 2))
button.add_theme_stylebox_override("focus", _make_tag_style(Color(0.0, 0.0, 0.0, 0.0), _editor_visual_theme.chrome_color(&"focus", &"prop"), 2))
button.add_theme_color_override("font_color", text_color)
button.add_theme_color_override("font_hover_color", text_color)
button.add_theme_color_override("font_pressed_color", text_color)
button.add_theme_color_override("icon_normal_color", Color.WHITE)
button.add_theme_color_override("icon_hover_color", Color.WHITE)
button.add_theme_color_override("icon_pressed_color", Color.WHITE)
func _make_tag_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = fill
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(8)
style.content_margin_left = 7.0
style.content_margin_top = 5.0
style.content_margin_right = 7.0
style.content_margin_bottom = 5.0
return style
func _primitive_icon_color(primitive_id: String) -> Color:
match primitive_id:
"box": return Color("#70C8F0")
"plank": return Color("#F3B65D")
"triangle": return Color("#66C681")
"line": return Color("#DDE7F5")
"circle": return Color("#C9A3FF")
return _theme.accent_color()
func _build_stage_authoring_controls() -> void:
var top_bar: HBoxContainer = get_node_or_null(NodePath("ChromeRoot/TopBar")) as HBoxContainer
if top_bar == null:
@@ -1244,29 +1394,30 @@ func _apply_authoring_context() -> void:
func _build_tag_inspector() -> void:
_tag_section = VBoxContainer.new()
_tag_section.add_theme_constant_override("separation", 4)
_tag_section = _make_inspector_card()
_inspector_list.add_child(_tag_section)
var title := Label.new()
title.text = "Artwork Tags"
title.add_theme_font_size_override("font_size", _theme.font_header())
_tag_section.add_child(title)
var tag_content: VBoxContainer = _inspector_card_content(_tag_section)
tag_content.add_child(_make_inspector_header("Artwork Tags", PropInspectorIconsScript.section("tags")))
var hint := Label.new()
hint.text = "Choose a few so this art is easy to find."
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_tag_section.add_child(hint)
hint.add_theme_color_override("font_color", _theme.text_muted_color())
tag_content.add_child(hint)
var rows := FlowContainer.new()
rows.add_theme_constant_override("h_separation", 6)
rows.add_theme_constant_override("v_separation", 4)
_tag_section.add_child(rows)
rows.add_theme_constant_override("v_separation", 6)
tag_content.add_child(rows)
for index: int in ART_TAG_IDS.size():
var tag_id: String = ART_TAG_IDS[index]
var check := CheckBox.new()
check.text = ART_TAG_LABELS[index]
check.custom_minimum_size.y = float(_theme.button_min_height())
check.toggled.connect(_on_art_tag_toggled.bind(tag_id))
rows.add_child(check)
_tag_checks[tag_id] = check
var tag_button := Button.new()
tag_button.text = ART_TAG_LABELS[index]
tag_button.toggle_mode = true
tag_button.custom_minimum_size = Vector2(122.0, 50.0)
tag_button.icon = PropInspectorIconsScript.tag(tag_id)
tag_button.toggled.connect(_on_art_tag_toggled.bind(tag_id))
_style_tag_button(tag_button)
rows.add_child(tag_button)
_tag_checks[tag_id] = tag_button
func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void:
@@ -1285,9 +1436,9 @@ func _refresh_tag_controls() -> void:
return
_updating_inspector = true
for tag_id: String in ART_TAG_IDS:
var check: CheckBox = _tag_checks.get(tag_id) as CheckBox
if check != null:
check.set_pressed_no_signal(_tags.has(tag_id))
var tag_button: Button = _tag_checks.get(tag_id) as Button
if tag_button != null:
tag_button.set_pressed_no_signal(_tags.has(tag_id))
_updating_inspector = false
func _build_anchor_inspector() -> void:
@@ -1397,13 +1548,17 @@ func _on_rename_anchor_pressed() -> void:
_rename_anchor_source_id = _selected_anchor_id
_rename_anchor_edit.text = _rename_anchor_source_id
_update_rename_anchor_confirmation(_rename_anchor_edit.text)
_rename_anchor_dialog.popup_centered(Vector2i(420, 150))
StageDialogStylerScript.popup(_rename_anchor_dialog)
_rename_anchor_edit.call_deferred("grab_focus")
_rename_anchor_edit.call_deferred("select_all")
func _update_rename_anchor_confirmation(value: String) -> void:
if _rename_anchor_dialog != null:
_rename_anchor_dialog.get_ok_button().disabled = value.strip_edges().is_empty()
var native_button: Button = _rename_anchor_dialog.get_ok_button()
native_button.disabled = value.strip_edges().is_empty()
var visible_button: Button = StageDialogStylerScript.visual_button(native_button)
if visible_button != native_button:
visible_button.disabled = native_button.disabled
func _commit_anchor_rename() -> void:
if _rename_anchor_source_id.is_empty() or not _anchors.has(_rename_anchor_source_id):
@@ -1463,10 +1618,10 @@ func _apply_control_theme(node: Node) -> void:
func _update_physics_labels() -> void:
var mat: PhysicsMaterial = PropBlockScript.physics_material(_material_preset)
_mass_label.text = "Mass: %.1f" % PropBlockScript.mass_for(_material_preset)
_friction_label.text = "Surface grip: %.2f" % mat.friction
_mass_value.text = "%.1f" % PropBlockScript.mass_for(_material_preset)
_friction_value.text = "%.2f" % mat.friction
_friction_label.tooltip_text = "Higher values make sliding harder."
_bounce_label.text = "Bounce: %.2f" % mat.bounce
_bounce_value.text = "%.2f" % mat.bounce
func _on_preset_selected(index: int) -> void:
@@ -1786,14 +1941,16 @@ func _check_autosave_recovery() -> void:
var dialog := ConfirmationDialog.new()
dialog.title = "Restore Prop?"
dialog.dialog_text = "An unsaved prop was recovered. Restore it?"
dialog.ok_button_text = "🔄 Restore"
var discard_button: Button = dialog.add_button("🗑️ Discard", false, "discard")
dialog.theme = _ui_theme
_editor_visual_theme.style_button_role(dialog.get_ok_button(), &"primary", &"prop")
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
_editor_visual_theme.style_button_role(dialog.get_cancel_button(), &"secondary", &"prop")
dialog.set_meta(StageDialogStylerScript.ICON_META, "🔄")
dialog.dialog_text = "An unsaved prop was recovered. Restore this work?"
dialog.ok_button_text = "Restore"
var discard_button: Button = dialog.add_button("Discard", false, "discard")
dialog.get_cancel_button().text = "Cancel"
add_child(dialog)
StageDialogStylerScript.apply(
dialog, _dialog_theme, _dialog_theme.font(),
dialog.get_ok_button(), [discard_button]
)
dialog.confirmed.connect(func() -> void:
_apply_json_data(raw, true)
_current_path = source_path
@@ -1803,7 +1960,8 @@ func _check_autosave_recovery() -> void:
if action == &"discard":
_clear_autosave()
dialog.queue_free())
dialog.popup_centered()
dialog.canceled.connect(func() -> void: dialog.queue_free())
StageDialogStylerScript.popup(dialog)
# ---------------------------------------------------------------------------
@@ -1879,7 +2037,7 @@ func _begin_save_request(path: String, with_thumbnail: bool) -> void:
_pending_save_path = path
_pending_save_with_thumbnail = with_thumbnail
if should_warn_off_origin(_canvas.has_shape_geometry(), _canvas.get_shapes_bounds()):
_off_origin_save_dialog.popup_centered()
StageDialogStylerScript.popup(_off_origin_save_dialog)
return
await _execute_pending_save(false)
@@ -1944,4 +2102,4 @@ func _on_load_file_selected(path: String) -> void:
func _show_error(message: String) -> void:
_error_dialog.dialog_text = message
_error_dialog.popup_centered()
StageDialogStylerScript.popup(_error_dialog)