feat: harmonize Prop and Stickman editor UI
This commit is contained in:
@@ -76,6 +76,7 @@ const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PropAnchorOverlayScript := preload("res://scripts/props/prop_anchor_overlay.gd")
|
||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const WholeStickmanPreviewScript := preload("res://scripts/editors/stickman/whole_stickman_preview.gd")
|
||||
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||
|
||||
const SELECTION_CORE_COLOR := Color.WHITE
|
||||
const SELECTION_HALO_COLOR := Color(0.0, 0.0, 0.0, 0.85)
|
||||
@@ -122,7 +123,7 @@ var _grid_size: int = 15
|
||||
var _snap_enabled: bool = false
|
||||
var _has_clipboard: bool = false
|
||||
var _create_point_armed: bool = false
|
||||
var _context_menu: PopupMenu
|
||||
var _context_menu: EditorContextMenu
|
||||
var _edit_mode: EditMode = EditMode.TRANSFORM
|
||||
var _anchor_overlay: RefCounted
|
||||
var _origin_marker_color: Color = ORIGIN_MARKER_DEFAULT_COLOR
|
||||
@@ -189,7 +190,7 @@ func _ready() -> void:
|
||||
_anchor_overlay = PropAnchorOverlayScript.new()
|
||||
_anchor_overlay.anchors_changed.connect(func(data: Dictionary) -> void: anchors_changed.emit(data); queue_redraw())
|
||||
_anchor_overlay.anchor_selected.connect(func(anchor_id: String) -> void: anchor_selected.emit(anchor_id); queue_redraw())
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||
_context_menu.name = "ContextMenu"
|
||||
add_child(_context_menu)
|
||||
if not _context_menu.id_pressed.is_connected(_on_context_menu_id_pressed):
|
||||
@@ -983,7 +984,12 @@ func set_has_clipboard(state: bool) -> void:
|
||||
## Sets the font size of the right-click context menu (Round 4, theme-driven).
|
||||
func set_context_menu_font_size(size: int) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.add_theme_font_size_override("font_size", size)
|
||||
_context_menu.set_font_size(size)
|
||||
|
||||
|
||||
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.configure(visual_theme, editor)
|
||||
|
||||
|
||||
func is_cursor_over_drawing(global_pos: Vector2) -> bool:
|
||||
@@ -1335,8 +1341,8 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
||||
if context_shape >= 0 and context_vertex >= 0:
|
||||
_context_menu.set_meta("context_shape", context_shape)
|
||||
_context_menu.set_meta("context_vertex", context_vertex)
|
||||
_context_menu.add_item("Remove Point", MENU_REMOVE_POINT)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_destructive_item("Remove Point", MENU_REMOVE_POINT)
|
||||
_context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
# Segment proximity -> Create Point (selects the shape first).
|
||||
@@ -1348,7 +1354,7 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
||||
queue_redraw()
|
||||
shape_selected.emit(seg_shape)
|
||||
_context_menu.add_item("Create Point", MENU_CREATE_POINT)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(seg_shape))
|
||||
return
|
||||
|
||||
# Interior body: select only, no menu.
|
||||
@@ -1381,17 +1387,18 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
||||
_context_menu.set_meta("click_position", world_pos)
|
||||
queue_redraw()
|
||||
shape_selected.emit(hit_shape)
|
||||
_context_menu.add_item("Copy", MENU_COPY)
|
||||
_context_menu.add_item("Copy Shape", MENU_COPY)
|
||||
if _has_clipboard:
|
||||
_context_menu.add_item("Paste", MENU_PASTE)
|
||||
_context_menu.add_item("Erase", MENU_DELETE)
|
||||
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Flip Left-Right", MENU_MIRROR_X)
|
||||
_context_menu.add_item("Flip Up-Down", MENU_MIRROR_Y)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Bring Forward", MENU_BRING_FORWARD)
|
||||
_context_menu.add_item("Send Back", MENU_SEND_BACK)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_destructive_item("Erase Shape", MENU_DELETE)
|
||||
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(hit_shape))
|
||||
return
|
||||
|
||||
# Empty canvas -> add primitives.
|
||||
@@ -1403,8 +1410,8 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
||||
_context_menu.add_item("Add Circle", MENU_ADD_CIRCLE)
|
||||
if _has_clipboard:
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Paste", MENU_PASTE)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||
_context_menu.popup_at(mb.global_position)
|
||||
|
||||
|
||||
func _handle_left_press(world_pos: Vector2) -> void:
|
||||
@@ -1857,6 +1864,19 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
||||
return (pos - _pan_offset) / _zoom
|
||||
|
||||
|
||||
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||
var screen_pos := global_position + _pan_offset + world_pos * _zoom
|
||||
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||
|
||||
|
||||
func _global_shape_target_rect(shape_idx: int) -> Rect2:
|
||||
if shape_idx < 0 or shape_idx >= shapes.size():
|
||||
return Rect2()
|
||||
var world_bounds: Rect2 = _shape_bbox(shapes[shape_idx])
|
||||
var screen_position := global_position + _pan_offset + world_bounds.position * _zoom
|
||||
return Rect2(screen_position, world_bounds.size * _zoom).grow(4.0)
|
||||
|
||||
|
||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||
var gs := float(_grid_size)
|
||||
if gs <= 0.0:
|
||||
|
||||
+276
-118
@@ -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)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class_name PropInspectorDropdown
|
||||
extends EditorSelectionDropdown
|
||||
## Prop-specific icon adapter for the shared editor selection dropdown.
|
||||
|
||||
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
|
||||
|
||||
|
||||
func add_icon_item(texture: Texture2D, label: String) -> void:
|
||||
add_styled_item(label, texture, PropInspectorIconsScript.popup_item(texture))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnnujoeli7cwk
|
||||
@@ -0,0 +1,95 @@
|
||||
class_name PropInspectorIcons
|
||||
extends RefCounted
|
||||
## Small flat 2D glyphs for the Prop Editor inspector.
|
||||
## SVG is rasterized at runtime so colors can follow the editor and material tokens.
|
||||
|
||||
const ICON_SIZE := 24
|
||||
|
||||
static func material(preset: int, tint: Color) -> Texture2D:
|
||||
if preset == 0:
|
||||
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="#252B38" stroke="#C4D1E5" stroke-width="4"/>'))
|
||||
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="%s" stroke="#FFFFFF" stroke-opacity="0.35" stroke-width="3"/>' % _hex(tint)))
|
||||
|
||||
|
||||
static func shape(shape_id: String, tint: Color = Color("#22C6FF")) -> Texture2D:
|
||||
var color := _hex(tint)
|
||||
var content := ""
|
||||
match shape_id:
|
||||
"box":
|
||||
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||
"plank":
|
||||
content = '<rect x="5" y="16" width="38" height="16" rx="2" fill="%s"/>' % color
|
||||
"triangle":
|
||||
content = '<polygon points="24,6 44,41 4,41" fill="%s"/>' % color
|
||||
"line":
|
||||
content = '<path d="M8 38 L40 10" fill="none" stroke="%s" stroke-width="6" stroke-linecap="round"/>' % color
|
||||
"circle":
|
||||
content = '<circle cx="24" cy="24" r="16" fill="%s"/>' % color
|
||||
_:
|
||||
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||
return _texture(_svg(content))
|
||||
|
||||
|
||||
static func section(section_id: String) -> Texture2D:
|
||||
var content := ""
|
||||
match section_id:
|
||||
"material":
|
||||
content = '<rect x="7" y="9" width="14" height="14" rx="3" fill="#F3B65D"/><rect x="27" y="9" width="14" height="14" rx="3" fill="#E76D66"/><rect x="7" y="27" width="14" height="14" rx="3" fill="#66C681"/><rect x="27" y="27" width="14" height="14" rx="3" fill="#70BFE8"/>'
|
||||
"selected_shape":
|
||||
content = '<path d="M9 32 L16 11 L38 15 L35 37 Z" fill="#DDE7F5" stroke="#22C6FF" stroke-width="3" stroke-linejoin="round"/><circle cx="9" cy="32" r="3" fill="#FFFFFF"/><circle cx="16" cy="11" r="3" fill="#FFFFFF"/><circle cx="38" cy="15" r="3" fill="#FFFFFF"/><circle cx="35" cy="37" r="3" fill="#FFFFFF"/>'
|
||||
"add_shape":
|
||||
content = '<rect x="7" y="7" width="34" height="34" fill="none" stroke="#22C6FF" stroke-width="5"/><path d="M24 15 V33 M15 24 H33" stroke="#22C6FF" stroke-width="4" stroke-linecap="round"/>'
|
||||
"tags":
|
||||
content = '<path d="M7 17 L17 7 H34 L42 15 V32 L25 42 L7 24 Z" fill="#FFB84D"/><circle cx="31" cy="16" r="3.5" fill="#252B38"/>'
|
||||
return _texture(_svg(content))
|
||||
|
||||
|
||||
static func tag(tag_id: String) -> Texture2D:
|
||||
var color := "#66C681"
|
||||
var body := ""
|
||||
match tag_id:
|
||||
"nature":
|
||||
color = "#66C681"
|
||||
body = '<path d="M9 35 C9 18 21 8 39 8 C39 26 29 39 13 39 Z" fill="%s"/><path d="M12 38 L33 15" stroke="#E8FFF0" stroke-width="3" stroke-linecap="round"/>' % color
|
||||
"buildings":
|
||||
color = "#F3A653"
|
||||
body = '<path d="M10 40 V10 H29 V18 H38 V40 Z" fill="%s"/><path d="M16 16 H21 M16 23 H21 M16 30 H21 M31 24 H35 M31 31 H35" stroke="#3C2A22" stroke-width="3"/>' % color
|
||||
"walls":
|
||||
color = "#F36B5B"
|
||||
body = '<path d="M5 10 H21 V19 H5 Z M24 10 H42 V19 H24 Z M14 22 H31 V31 H14 Z M4 34 H20 V42 H4 Z M23 34 H42 V42 H23 Z" fill="%s"/><path d="M22 10 V19 M13 22 V31 M31 34 V42" stroke="#733A37" stroke-width="2"/>' % color
|
||||
"roads":
|
||||
color = "#9BAEC9"
|
||||
body = '<path d="M16 7 H32 L42 41 H6 Z" fill="%s"/><path d="M24 12 V19 M24 25 V31 M24 36 V40" stroke="#283247" stroke-width="3" stroke-linecap="round"/>' % color
|
||||
"objects":
|
||||
color = "#E8A96D"
|
||||
body = '<rect x="7" y="11" width="34" height="26" fill="none" stroke="%s" stroke-width="5"/>' % color
|
||||
return _texture(_svg(body))
|
||||
|
||||
|
||||
static func popup_item(texture: Texture2D, size: int = 44) -> Texture2D:
|
||||
if texture == null:
|
||||
return null
|
||||
var source: Image = texture.get_image()
|
||||
var canvas := Image.create(size, size, false, Image.FORMAT_RGBA8)
|
||||
var draw_size: Vector2i = Vector2i(mini(source.get_width(), size), mini(source.get_height(), size))
|
||||
var source_rect := Rect2i(Vector2i.ZERO, draw_size)
|
||||
var destination := Vector2i((size - draw_size.x) / 2, (size - draw_size.y) / 2)
|
||||
canvas.blit_rect(source, source_rect, destination)
|
||||
return ImageTexture.create_from_image(canvas)
|
||||
|
||||
|
||||
static func _svg(content: String) -> String:
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 48 48">%s</svg>' % [ICON_SIZE, ICON_SIZE, content]
|
||||
|
||||
|
||||
static func _texture(svg: String) -> Texture2D:
|
||||
var image := Image.new()
|
||||
var error: Error = image.load_svg_from_string(svg)
|
||||
if error != OK:
|
||||
push_warning("Prop Inspector icon could not be rasterized (%s)." % error_string(error))
|
||||
return null
|
||||
return ImageTexture.create_from_image(image)
|
||||
|
||||
|
||||
static func _hex(color: Color) -> String:
|
||||
return "#" + color.to_html(false)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5ac0640i4bj7
|
||||
@@ -426,17 +426,19 @@ func _build_file_menu(parent: HBoxContainer) -> void:
|
||||
var file_popup: PopupMenu = _file_menu.get_popup()
|
||||
var popup_font_size: int = _theme_integer("fonts.sizes.file_menu_popup", 26, 12, 64)
|
||||
file_popup.add_theme_font_size_override("font_size", popup_font_size)
|
||||
file_popup.add_item("New", 0)
|
||||
file_popup.add_item("Open…", 1)
|
||||
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", popup_font_size)
|
||||
recent_popup.id_pressed.connect(_on_recent_scene_requested)
|
||||
file_popup.add_child(recent_popup)
|
||||
file_popup.add_submenu_item("Open Recent", "OpenRecent")
|
||||
file_popup.add_item("Save", 2)
|
||||
file_popup.add_item("Save As…", 3)
|
||||
file_popup.add_item("Rename Scene…", 4)
|
||||
file_popup.add_submenu_item("🕑 Open Recent", "OpenRecent")
|
||||
file_popup.add_separator()
|
||||
file_popup.add_item("💾 Save", 2)
|
||||
file_popup.add_item("📄 Save As…", 3)
|
||||
file_popup.add_separator()
|
||||
file_popup.add_item("✏️ Rename Scene…", 4)
|
||||
file_popup.id_pressed.connect(_on_file_menu_requested)
|
||||
file_popup.about_to_popup.connect(_on_recent_menu_requested)
|
||||
parent.add_child(_file_menu)
|
||||
|
||||
@@ -64,7 +64,7 @@ const ZOOM_STEP: float = 1.10
|
||||
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@onready var drawing_area: Control = %DrawingArea
|
||||
@onready var context_menu: PopupMenu = %ContextMenu
|
||||
@onready var context_menu: EditorContextMenu = %ContextMenu
|
||||
@onready var color_picker_popup: PopupPanel = %ColorPickerPopup
|
||||
@onready var color_picker: ColorPicker = %ColorPicker
|
||||
@onready var color_picker_button_row: HBoxContainer = %ButtonRow
|
||||
@@ -127,6 +127,8 @@ func _ready() -> void:
|
||||
cancel_button.pressed.connect(_on_color_picker_cancel)
|
||||
if not context_menu.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
context_menu.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
if _editor_visual_theme != null:
|
||||
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||
if not color_picker_popup.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
color_picker_popup.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
cancel_button.text = "Cancel"
|
||||
@@ -298,6 +300,8 @@ func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo:
|
||||
_hint_color = hint_color
|
||||
_button_target_height = button_target_height
|
||||
_editor_visual_theme = editor_visual_theme
|
||||
if context_menu != null and _editor_visual_theme != null:
|
||||
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||
if drawing_area != null:
|
||||
drawing_area.queue_redraw()
|
||||
|
||||
@@ -421,6 +425,24 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
||||
return (pos - _pan_offset) / _zoom
|
||||
|
||||
|
||||
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||
var screen_pos := drawing_area.global_position + _pan_offset + world_pos * _zoom
|
||||
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||
|
||||
|
||||
func _global_shape_target_rect(sd: Dictionary) -> Rect2:
|
||||
var points: PackedVector2Array = sd.get("points", PackedVector2Array())
|
||||
if points.is_empty():
|
||||
return Rect2()
|
||||
var min_point := points[0]
|
||||
var max_point := points[0]
|
||||
for point: Vector2 in points:
|
||||
min_point = min_point.min(point)
|
||||
max_point = max_point.max(point)
|
||||
var screen_position := drawing_area.global_position + _pan_offset + min_point * _zoom
|
||||
return Rect2(screen_position, (max_point - min_point) * _zoom).grow(4.0)
|
||||
|
||||
|
||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||
var gs := float(_grid_size)
|
||||
if gs <= 0.0:
|
||||
@@ -638,9 +660,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
if hit_idx >= 0:
|
||||
_context_shape_idx = _selected_shape_idx
|
||||
context_menu.clear()
|
||||
context_menu.add_item("Remove Point", 6)
|
||||
context_menu.add_destructive_item("Remove Point", 6)
|
||||
context_menu.set_meta("vertex_index", hit_idx)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
for i: int in range(shapes.size() - 1, -1, -1):
|
||||
@@ -651,9 +673,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
if hit_idx >= 0:
|
||||
_context_shape_idx = i
|
||||
context_menu.clear()
|
||||
context_menu.add_item("Remove Point", 6)
|
||||
context_menu.add_destructive_item("Remove Point", 6)
|
||||
context_menu.set_meta("vertex_index", hit_idx)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||
return
|
||||
|
||||
# Step 2: Check if right-click is over any shape (reverse order = front first).
|
||||
@@ -677,22 +699,22 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
context_menu.add_item("Create Point", 3)
|
||||
context_menu.add_separator()
|
||||
|
||||
context_menu.add_item("Color...", 4)
|
||||
context_menu.add_item("Change Color…", 4)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Send Back", 7)
|
||||
context_menu.add_item("Bring Forward", 8)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Copy", 9)
|
||||
context_menu.add_item("Copy Shape", 9)
|
||||
if _has_clipboard:
|
||||
context_menu.add_item("Paste", 10)
|
||||
context_menu.add_item("Paste Shape", 10)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Mirror X", 11)
|
||||
context_menu.add_item("Mirror Y", 12)
|
||||
context_menu.add_item("Flip Left-Right", 11)
|
||||
context_menu.add_item("Flip Up-Down", 12)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Delete", 5)
|
||||
context_menu.add_item("Bring Forward", 8)
|
||||
context_menu.add_item("Send Back", 7)
|
||||
context_menu.add_separator()
|
||||
context_menu.add_destructive_item("Erase Shape", 5)
|
||||
|
||||
context_menu.set_meta("click_position", world_pos)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position, _global_shape_target_rect(found_sd))
|
||||
return
|
||||
|
||||
# Step 3: No shape under mouse -> shape creation menu.
|
||||
@@ -702,9 +724,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
||||
context_menu.add_item("Circle", 2)
|
||||
if _has_clipboard:
|
||||
context_menu.add_separator()
|
||||
context_menu.add_item("Paste", 10)
|
||||
context_menu.add_item("Paste Shape", 10)
|
||||
context_menu.set_meta("click_position", world_pos)
|
||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
context_menu.popup_at(mb.global_position)
|
||||
|
||||
|
||||
func _handle_left_press(world_pos: Vector2) -> void:
|
||||
|
||||
@@ -17,6 +17,8 @@ extends Control
|
||||
|
||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.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")
|
||||
|
||||
enum WorkspaceView { OVERVIEW, FOCUS }
|
||||
enum PendingDocumentAction { NONE, LOAD, CLEAR, QUIT }
|
||||
@@ -110,11 +112,10 @@ const WORKSPACE_GAP: float = 8.0
|
||||
@onready var _overview_scroll: ScrollContainer = %OverviewScroll
|
||||
@onready var _focus_host: VBoxContainer = %FocusHost
|
||||
@onready var _focus_panel_host: Control = %FocusPanelHost
|
||||
@onready var _focus_part_label: Label = %FocusPartLabel
|
||||
@onready var _workspace_row: HSplitContainer = %WorkspaceRow
|
||||
@onready var _overview_button: Button = %OverviewButton
|
||||
@onready var _focus_button: Button = %FocusButton
|
||||
@onready var _part_picker: OptionButton = %PartPicker
|
||||
@onready var _part_picker: EditorSelectionDropdown = %PartPicker
|
||||
@onready var _add_shape_button: Button = %AddShapeButton
|
||||
@onready var _color_button: Button = %ColorButton
|
||||
@onready var _whole_stickman_button: Button = %WholeStickmanButton
|
||||
@@ -132,6 +133,7 @@ const WORKSPACE_GAP: float = 8.0
|
||||
@onready var _unsaved_dialog: ConfirmationDialog = %UnsavedChangesDialog
|
||||
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
var _dialog_theme: SandboxTheme
|
||||
var _body_part_origins: Dictionary = {}
|
||||
var _workspace_view: WorkspaceView = WorkspaceView.OVERVIEW
|
||||
var _view_choice_was_manual: bool = false
|
||||
@@ -173,6 +175,8 @@ var _view_menu: PopupMenu
|
||||
func _ready() -> void:
|
||||
_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 = _editor_visual_theme.control_theme(&"stickman")
|
||||
_setup_menu_bar()
|
||||
_setup_dialogs()
|
||||
@@ -188,6 +192,7 @@ func _ready() -> void:
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||
_editor_visual_theme.chrome_color(&"preview", &"stickman")
|
||||
)
|
||||
_whole_preview.set_context_menu_visual_theme(_editor_visual_theme, &"stickman")
|
||||
_stickman_name_edit.text_changed.connect(_on_stickman_name_changed)
|
||||
|
||||
_load_settings()
|
||||
@@ -233,17 +238,18 @@ func _process(_delta: float) -> void:
|
||||
func _setup_menu_bar() -> void:
|
||||
var file_menu: PopupMenu = PopupMenu.new()
|
||||
file_menu.name = "FileMenu"
|
||||
file_menu.add_item("Save", 0)
|
||||
file_menu.add_item("Load", 1)
|
||||
file_menu.add_item("📂 Load…", 1)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Start Over", 2)
|
||||
file_menu.add_item("💾 Save", 0)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("🆕 Start Over", 2)
|
||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
_menu_bar.add_child(file_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||
|
||||
_edit_menu = PopupMenu.new()
|
||||
_edit_menu.name = "EditMenu"
|
||||
_edit_menu.add_item("Configure Grid...", 0)
|
||||
_edit_menu.add_item("⚙️ Configure Grid…", 0)
|
||||
_edit_menu.add_separator()
|
||||
_edit_menu.add_item(_snap_menu_label(), 1)
|
||||
_edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
||||
@@ -253,7 +259,7 @@ func _setup_menu_bar() -> void:
|
||||
|
||||
var view_menu: PopupMenu = PopupMenu.new()
|
||||
view_menu.name = "ViewMenu"
|
||||
view_menu.add_item("Reset Views", 0)
|
||||
view_menu.add_item("🔍 Reset Views", 0)
|
||||
view_menu.add_separator()
|
||||
view_menu.add_item(_guide_menu_label(), 1)
|
||||
view_menu.id_pressed.connect(_on_view_menu_id_pressed)
|
||||
@@ -266,10 +272,6 @@ func _setup_menu_bar() -> void:
|
||||
func _setup_dialogs() -> void:
|
||||
_save_dialog.theme = theme
|
||||
_load_dialog.theme = theme
|
||||
_clear_confirm.theme = theme
|
||||
_unsaved_dialog.theme = theme
|
||||
_error_dialog.theme = theme
|
||||
_grid_config_dialog.theme = theme
|
||||
var content_dir: String = ProjectSettings.globalize_path(ContentPathsScript.STICKMEN_DIR)
|
||||
_save_dialog.current_dir = content_dir
|
||||
_load_dialog.current_dir = content_dir
|
||||
@@ -280,17 +282,34 @@ func _setup_dialogs() -> void:
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_clear_confirm.confirmed.connect(_on_clear_confirmed)
|
||||
_clear_confirm.title = "Start Over?"
|
||||
_clear_confirm.dialog_text = "This will clear your current stickman. Start over?"
|
||||
_clear_confirm.ok_button_text = "Start Over"
|
||||
_clear_confirm.set_meta(StageDialogStylerScript.ICON_META, "🧹")
|
||||
_clear_confirm.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_ok_button(), &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_cancel_button(), &"secondary", &"stickman")
|
||||
var discard_button: Button = _unsaved_dialog.add_button("Discard Changes", false, "discard")
|
||||
StageDialogStylerScript.apply(
|
||||
_clear_confirm, _dialog_theme, _dialog_theme.font(),
|
||||
null, [_clear_confirm.get_ok_button()], [], false
|
||||
)
|
||||
_unsaved_dialog.title = "Unsaved Changes"
|
||||
_unsaved_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
|
||||
var discard_button: Button = _unsaved_dialog.add_button("Discard", false, "discard")
|
||||
_unsaved_dialog.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_unsaved_dialog.ok_button_text = "Save"
|
||||
StageDialogStylerScript.apply(
|
||||
_unsaved_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_unsaved_dialog.get_ok_button(), [discard_button]
|
||||
)
|
||||
_error_dialog.get_ok_button().text = "Got it"
|
||||
StageDialogStylerScript.apply(
|
||||
_error_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_error_dialog.get_ok_button()
|
||||
)
|
||||
_grid_config_dialog.set_meta(StageDialogStylerScript.ICON_META, "📏")
|
||||
StageDialogStylerScript.apply(
|
||||
_grid_config_dialog, _dialog_theme, _dialog_theme.font(),
|
||||
_grid_config_dialog.get_ok_button()
|
||||
)
|
||||
_unsaved_dialog.confirmed.connect(_on_unsaved_save_pressed)
|
||||
_unsaved_dialog.custom_action.connect(_on_unsaved_custom_action)
|
||||
_unsaved_dialog.canceled.connect(_cancel_pending_document_action)
|
||||
@@ -303,14 +322,24 @@ func _setup_workspace_controls() -> void:
|
||||
_focus_button.button_group = _view_button_group
|
||||
_overview_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"mode_button_min_height", &"stickman"))
|
||||
_part_picker.custom_minimum_size = Vector2(160.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||
_part_picker.custom_minimum_size = Vector2(220.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||
_part_picker.configure(
|
||||
theme,
|
||||
_editor_visual_theme.chrome_color(&"surface", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"selected_surface", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"border", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"edit", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"focus", &"stickman"),
|
||||
_editor_visual_theme.chrome_color(&"text", &"stickman"),
|
||||
_editor_visual_theme.chrome_px(&"menu_popup", &"stickman"),
|
||||
12
|
||||
)
|
||||
_add_shape_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_add_shape_button, &"primary", &"stickman")
|
||||
_color_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_color_button, &"secondary", &"stickman")
|
||||
_whole_stickman_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_back_to_part_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_part_label.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"section", &"stickman"))
|
||||
_status_cursor_coords.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_save_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_snap_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
@@ -324,9 +353,9 @@ func _setup_workspace_controls() -> void:
|
||||
_whole_stickman_button.pressed.connect(_on_whole_stickman_button_pressed)
|
||||
_back_to_part_button.pressed.connect(_on_back_to_part_button_pressed)
|
||||
_add_shape_menu = PopupMenu.new()
|
||||
_add_shape_menu.add_item("Line", 0)
|
||||
_add_shape_menu.add_item("Rectangle", 1)
|
||||
_add_shape_menu.add_item("Circle", 2)
|
||||
_add_shape_menu.add_item("〰️ Line", 0)
|
||||
_add_shape_menu.add_item("▭ Rectangle", 1)
|
||||
_add_shape_menu.add_item("⚪ Circle", 2)
|
||||
_add_shape_menu.id_pressed.connect(_on_add_shape_menu_id_pressed)
|
||||
_add_shape_menu.theme = theme
|
||||
add_child(_add_shape_menu)
|
||||
@@ -526,8 +555,6 @@ func _update_active_part_presentation() -> void:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel != null:
|
||||
panel.set_active_presentation(part_name == _active_part_name, surface, border, accent, section_size)
|
||||
if is_instance_valid(_focus_part_label):
|
||||
_focus_part_label.text = BODY_PART_DISPLAY.get(_active_part_name, _active_part_name)
|
||||
|
||||
|
||||
func _refresh_workspace_controls() -> void:
|
||||
@@ -686,7 +713,7 @@ func _request_document_action(action: PendingDocumentAction) -> void:
|
||||
_unsaved_dialog.dialog_text = "Save changes before starting over with a blank stickman?"
|
||||
PendingDocumentAction.QUIT:
|
||||
_unsaved_dialog.dialog_text = "Save changes before closing Stickman Studio?"
|
||||
_unsaved_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_unsaved_dialog)
|
||||
return
|
||||
_continue_document_action(action)
|
||||
|
||||
@@ -696,7 +723,7 @@ func _continue_document_action(action: PendingDocumentAction) -> void:
|
||||
PendingDocumentAction.LOAD:
|
||||
_load_dialog.popup_centered_ratio(0.6)
|
||||
PendingDocumentAction.CLEAR:
|
||||
_clear_confirm.popup_centered()
|
||||
StageDialogStylerScript.popup(_clear_confirm)
|
||||
PendingDocumentAction.QUIT:
|
||||
get_tree().quit()
|
||||
_pending_document_action = PendingDocumentAction.NONE
|
||||
@@ -763,7 +790,7 @@ func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: # Configure Grid...
|
||||
_grid_size_spin_box.value = float(_grid_size)
|
||||
_grid_config_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_grid_config_dialog)
|
||||
1: # Snap to Grid (checkable)
|
||||
_snap_enabled = not _snap_enabled
|
||||
if _edit_menu:
|
||||
@@ -808,7 +835,7 @@ func _update_guide_menu_item() -> void:
|
||||
|
||||
|
||||
func _guide_menu_label() -> String:
|
||||
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
|
||||
return "👁️ Hide Pose Guide" if _show_guide else "👁️ Show Pose Guide"
|
||||
|
||||
|
||||
func _on_grid_config_confirmed() -> void:
|
||||
@@ -862,7 +889,7 @@ func _on_clear_confirmed() -> void:
|
||||
|
||||
func _show_error(message: String) -> void:
|
||||
_error_dialog.dialog_text = message
|
||||
_error_dialog.popup_centered()
|
||||
StageDialogStylerScript.popup(_error_dialog)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal handlers - body-part panels
|
||||
@@ -1198,7 +1225,7 @@ func _broadcast_settings() -> void:
|
||||
|
||||
|
||||
func _snap_menu_label() -> String:
|
||||
return "[√] Snap to Grid" if _snap_enabled else "Snap to Grid"
|
||||
return "🧲 [√] Snap to Grid" if _snap_enabled else "🧲 Snap to Grid"
|
||||
|
||||
|
||||
func _update_snap_status_label() -> void:
|
||||
|
||||
@@ -26,6 +26,7 @@ signal part_order_changed(part_name: String, new_order: Array[String])
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.15)
|
||||
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||
const MIN_ZOOM: float = 0.3
|
||||
const MAX_ZOOM: float = 3.0
|
||||
const ZOOM_STEP: float = 1.10
|
||||
@@ -142,7 +143,7 @@ var _pan_start: Vector2 = Vector2.ZERO
|
||||
|
||||
# Phase 5: z-ordering
|
||||
var _part_order: Array[String] = []
|
||||
var _context_menu: PopupMenu
|
||||
var _context_menu: EditorContextMenu
|
||||
|
||||
# Phase 6: selection gizmos rendered on top
|
||||
var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||
@@ -289,6 +290,11 @@ func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo:
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
if _context_menu != null:
|
||||
_context_menu.configure(visual_theme, editor)
|
||||
|
||||
|
||||
## Returns the preview-space position of a guide joint (a master-space
|
||||
## GUIDE_JOINTS entry mapped through _guide_to_preview). Guarded against
|
||||
## unknown joint names.
|
||||
@@ -334,7 +340,7 @@ func _ready() -> void:
|
||||
if not preview_area.gui_input.is_connected(_on_preview_gui_input):
|
||||
preview_area.gui_input.connect(_on_preview_gui_input)
|
||||
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||
_context_menu.name = "PreviewContextMenu"
|
||||
add_child(_context_menu)
|
||||
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
||||
@@ -1024,12 +1030,14 @@ func _handle_right_click(mb: InputEventMouseButton) -> void:
|
||||
if rect.has_point(world_pos):
|
||||
_selected_part = part_name
|
||||
_context_menu.clear()
|
||||
_context_menu.add_item("Send Back", 0)
|
||||
_context_menu.add_item("Bring Forward", 1)
|
||||
_context_menu.add_item("Send Back", 0)
|
||||
_context_menu.add_separator()
|
||||
_context_menu.add_item("Mirror X", 2)
|
||||
_context_menu.add_item("Mirror Y", 3)
|
||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
||||
_context_menu.add_item("Flip Left-Right", 2)
|
||||
_context_menu.add_item("Flip Up-Down", 3)
|
||||
var screen_position := preview_area.global_position + _pan_offset + rect.position * _zoom
|
||||
var target_rect := Rect2(screen_position, rect.size * _zoom).grow(4.0)
|
||||
_context_menu.popup_at(mb.global_position, target_rect)
|
||||
preview_area.queue_redraw()
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
class_name EditorContextMenu
|
||||
extends PopupPanel
|
||||
## Shared Studio List popup used by right-click menus in both standalone editors.
|
||||
|
||||
signal id_pressed(id: int)
|
||||
|
||||
enum ItemRole { NORMAL, DESTRUCTIVE }
|
||||
|
||||
const DEFAULT_EDITOR := &"prop"
|
||||
const DEFAULT_MIN_WIDTH := 208
|
||||
const DEFAULT_ROW_HEIGHT := 48
|
||||
const DEFAULT_GROUP_GAP := 8
|
||||
const SHELL_INSET := 8
|
||||
const ROW_RADIUS := 6
|
||||
const TARGET_GAP := 12.0
|
||||
|
||||
var _visual_theme: EditorVisualTheme
|
||||
var _editor: StringName = DEFAULT_EDITOR
|
||||
var _font_size_override: int = -1
|
||||
var _shell: MarginContainer
|
||||
var _scroll: ScrollContainer
|
||||
var _items: VBoxContainer
|
||||
var _row_buttons: Array[Button] = []
|
||||
var _row_ids: Dictionary = {}
|
||||
var _keyboard_navigation_active: bool = false
|
||||
var _focused_row_index: int = -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_contents()
|
||||
if not about_to_popup.is_connected(_reset_keyboard_navigation):
|
||||
about_to_popup.connect(_reset_keyboard_navigation)
|
||||
if not popup_hide.is_connected(_reset_keyboard_navigation):
|
||||
popup_hide.connect(_reset_keyboard_navigation)
|
||||
if _visual_theme != null:
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func configure(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||
_visual_theme = visual_theme
|
||||
_editor = editor
|
||||
if is_node_ready():
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func set_font_size(size: int) -> void:
|
||||
_font_size_override = maxi(1, size)
|
||||
if is_node_ready():
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_reset_keyboard_navigation()
|
||||
_row_buttons.clear()
|
||||
_row_ids.clear()
|
||||
if _items == null:
|
||||
return
|
||||
for child: Node in _items.get_children():
|
||||
_items.remove_child(child)
|
||||
child.free()
|
||||
|
||||
|
||||
func add_item(label: String, id: int, role: ItemRole = ItemRole.NORMAL) -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var button := Button.new()
|
||||
button.text = label
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.add_theme_font_size_override("font_size", _font_size())
|
||||
button.add_theme_color_override("font_color", _text_color(role))
|
||||
button.add_theme_color_override("font_hover_color", _text_color(role))
|
||||
button.add_theme_color_override("font_pressed_color", _text_color(role))
|
||||
button.add_theme_color_override("font_focus_color", _text_color(role))
|
||||
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||
button.add_theme_stylebox_override("disabled", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.set_meta("context_menu_id", id)
|
||||
button.set_meta("context_menu_role", role)
|
||||
button.pressed.connect(_on_row_pressed.bind(button))
|
||||
button.mouse_entered.connect(_on_row_mouse_entered.bind(button))
|
||||
_items.add_child(button)
|
||||
_row_buttons.append(button)
|
||||
_row_ids[id] = button
|
||||
|
||||
|
||||
func add_destructive_item(label: String, id: int) -> void:
|
||||
add_item(label, id, ItemRole.DESTRUCTIVE)
|
||||
|
||||
|
||||
func add_separator() -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var spacer := MarginContainer.new()
|
||||
spacer.custom_minimum_size.y = float(DEFAULT_GROUP_GAP + 1)
|
||||
spacer.add_theme_constant_override("margin_top", int(DEFAULT_GROUP_GAP / 2))
|
||||
spacer.add_theme_constant_override("margin_bottom", int(DEFAULT_GROUP_GAP / 2))
|
||||
var line := ColorRect.new()
|
||||
line.color = _theme_color(&"border", Color(1, 1, 1, 0.2))
|
||||
line.custom_minimum_size.y = 1.0
|
||||
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
spacer.add_child(line)
|
||||
_items.add_child(spacer)
|
||||
|
||||
|
||||
func popup_at(global_position: Vector2, avoid_rect: Rect2 = Rect2()) -> void:
|
||||
if _items == null:
|
||||
_build_contents()
|
||||
var parent_viewport: Viewport = get_parent().get_viewport() if get_parent() != null else get_viewport()
|
||||
var safe_rect: Rect2 = parent_viewport.get_visible_rect()
|
||||
var margin := 8.0
|
||||
var popup_size := _items.get_combined_minimum_size() + Vector2(SHELL_INSET * 2, SHELL_INSET * 2)
|
||||
var border_width := float(_theme_px(&"normal_border", 1))
|
||||
popup_size += Vector2(border_width * 2.0, border_width * 2.0)
|
||||
popup_size.x = maxf(popup_size.x, float(_menu_min_width() + SHELL_INSET * 2))
|
||||
var max_popup_size := Vector2(maxf(1.0, safe_rect.size.x - margin * 2.0), maxf(1.0, safe_rect.size.y - margin * 2.0))
|
||||
popup_size = popup_size.min(max_popup_size)
|
||||
if _scroll != null:
|
||||
var content_height: float = _items.get_combined_minimum_size().y
|
||||
var available_content_height: float = max_popup_size.y - float(SHELL_INSET * 2) - border_width * 2.0
|
||||
_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO if content_height > available_content_height else ScrollContainer.SCROLL_MODE_DISABLED
|
||||
size = Vector2i(popup_size.ceil())
|
||||
if _scroll != null:
|
||||
_scroll.scroll_vertical = 0
|
||||
var max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - popup_size.x - margin)
|
||||
var max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - popup_size.y - margin)
|
||||
var popup_origin := _choose_popup_origin(
|
||||
global_position,
|
||||
avoid_rect,
|
||||
popup_size,
|
||||
safe_rect,
|
||||
margin,
|
||||
max_x,
|
||||
max_y
|
||||
)
|
||||
var popup_position := Vector2i(popup_origin.round())
|
||||
popup_on_parent(Rect2i(popup_position, Vector2i.ONE))
|
||||
# PopupPanel sizes itself from the child layout on popup. Reapply the measured
|
||||
# target size after opening so the scroll region can honor the 48 px rows.
|
||||
size = Vector2i(popup_size.ceil())
|
||||
var actual_size := Vector2(size)
|
||||
if popup_position.x + actual_size.x > safe_rect.end.x - margin:
|
||||
popup_origin.x = global_position.x - actual_size.x - margin
|
||||
if popup_position.y + actual_size.y > safe_rect.end.y - margin:
|
||||
popup_origin.y = global_position.y - actual_size.y - margin
|
||||
var actual_max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - actual_size.x - margin)
|
||||
var actual_max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - actual_size.y - margin)
|
||||
popup_origin.x = clampf(popup_origin.x, safe_rect.position.x + margin, actual_max_x)
|
||||
popup_origin.y = clampf(popup_origin.y, safe_rect.position.y + margin, actual_max_y)
|
||||
position = Vector2i(popup_origin.round())
|
||||
|
||||
|
||||
func _choose_popup_origin(
|
||||
global_position: Vector2,
|
||||
avoid_rect: Rect2,
|
||||
popup_size: Vector2,
|
||||
safe_rect: Rect2,
|
||||
margin: float,
|
||||
max_x: float,
|
||||
max_y: float
|
||||
) -> Vector2:
|
||||
var low_x: float = safe_rect.position.x + margin
|
||||
var low_y: float = safe_rect.position.y + margin
|
||||
var cursor_fallback := Vector2(
|
||||
clampf(global_position.x + margin, low_x, max_x),
|
||||
clampf(global_position.y + margin, low_y, max_y)
|
||||
)
|
||||
if not avoid_rect.has_area():
|
||||
return cursor_fallback
|
||||
|
||||
var aligned_y := clampf(global_position.y - 20.0, low_y, max_y)
|
||||
var aligned_x := clampf(global_position.x + margin, low_x, max_x)
|
||||
var candidates: Array[Vector2] = [
|
||||
Vector2(avoid_rect.end.x + TARGET_GAP, aligned_y),
|
||||
Vector2(avoid_rect.position.x - popup_size.x - TARGET_GAP, aligned_y),
|
||||
Vector2(aligned_x, avoid_rect.end.y + TARGET_GAP),
|
||||
Vector2(aligned_x, avoid_rect.position.y - popup_size.y - TARGET_GAP),
|
||||
cursor_fallback,
|
||||
]
|
||||
for candidate: Vector2 in candidates:
|
||||
if candidate.x < low_x or candidate.y < low_y or candidate.x > max_x or candidate.y > max_y:
|
||||
continue
|
||||
var candidate_rect := Rect2(candidate, popup_size)
|
||||
if not candidate_rect.intersects(avoid_rect):
|
||||
return candidate
|
||||
return cursor_fallback
|
||||
|
||||
|
||||
func get_row_button(id: int) -> Button:
|
||||
return _row_ids.get(id) as Button
|
||||
|
||||
|
||||
func _build_contents() -> void:
|
||||
if _items != null:
|
||||
return
|
||||
_shell = MarginContainer.new()
|
||||
_shell.name = "MenuShell"
|
||||
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||
_shell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_shell.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(_shell)
|
||||
_scroll = ScrollContainer.new()
|
||||
_scroll.name = "MenuScroll"
|
||||
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_shell.add_child(_scroll)
|
||||
_items = VBoxContainer.new()
|
||||
_items.name = "MenuItems"
|
||||
_items.add_theme_constant_override("separation", 0)
|
||||
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||
_scroll.add_child(_items)
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func _apply_visual_style() -> void:
|
||||
if _visual_theme == null:
|
||||
return
|
||||
var surface := _theme_color(&"surface", Color("#252B38"))
|
||||
var border := _theme_color(&"border", Color("#FFFFFF33"))
|
||||
var radius := _theme_px(&"panel", 10)
|
||||
var panel := StyleBoxFlat.new()
|
||||
panel.bg_color = surface
|
||||
panel.border_color = border
|
||||
panel.set_border_width_all(_theme_px(&"normal_border", 1))
|
||||
panel.set_corner_radius_all(radius)
|
||||
panel.shadow_color = Color(0.0, 0.0, 0.0, 0.28)
|
||||
panel.shadow_size = 8
|
||||
panel.shadow_offset = Vector2(0.0, 3.0)
|
||||
add_theme_stylebox_override("panel", panel)
|
||||
if _shell != null:
|
||||
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||
if _items != null:
|
||||
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||
for button: Button in _row_buttons:
|
||||
if not is_instance_valid(button):
|
||||
continue
|
||||
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||
button.add_theme_font_size_override("font_size", _font_size())
|
||||
var role := int(button.get_meta("context_menu_role", ItemRole.NORMAL))
|
||||
var item_role: ItemRole = ItemRole.DESTRUCTIVE if role == ItemRole.DESTRUCTIVE else ItemRole.NORMAL
|
||||
button.add_theme_color_override("font_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_hover_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_pressed_color", _text_color(item_role))
|
||||
button.add_theme_color_override("font_focus_color", _text_color(item_role))
|
||||
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||
|
||||
|
||||
func _make_row_box(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(ROW_RADIUS)
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
|
||||
|
||||
func _make_hover_box(pressed: bool = false) -> StyleBoxFlat:
|
||||
var hover_color := _theme_color(&"surface_hover", Color("#30394A"))
|
||||
if pressed:
|
||||
hover_color = hover_color.darkened(0.1)
|
||||
var style := _make_row_box(hover_color, _theme_color(&"edit", Color("#22C6FF")), 0)
|
||||
style.border_width_left = 3
|
||||
style.content_margin_left = 9
|
||||
return style
|
||||
|
||||
|
||||
func _make_focus_box() -> StyleBoxFlat:
|
||||
var style := _make_row_box(Color(0.0, 0.0, 0.0, 0.0), _theme_color(&"focus", Color("#8BD9FF")), 2)
|
||||
return style
|
||||
|
||||
|
||||
func _on_row_pressed(button: Button) -> void:
|
||||
if not is_instance_valid(button):
|
||||
return
|
||||
var id: int = int(button.get_meta("context_menu_id", -1))
|
||||
id_pressed.emit(id)
|
||||
hide()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or not event is InputEventKey:
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
if not key_event.pressed:
|
||||
return
|
||||
if key_event.keycode == KEY_DOWN:
|
||||
_move_keyboard_focus(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif key_event.keycode == KEY_UP:
|
||||
_move_keyboard_focus(-1)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _move_keyboard_focus(direction: int) -> void:
|
||||
if _row_buttons.is_empty():
|
||||
return
|
||||
var candidate_index: int
|
||||
if not _keyboard_navigation_active:
|
||||
candidate_index = 0 if direction > 0 else _row_buttons.size() - 1
|
||||
else:
|
||||
candidate_index = _focused_row_index + direction
|
||||
for _attempt: int in range(_row_buttons.size()):
|
||||
candidate_index = posmod(candidate_index, _row_buttons.size())
|
||||
var button: Button = _row_buttons[candidate_index]
|
||||
if is_instance_valid(button) and button.visible and not button.disabled:
|
||||
_enable_keyboard_navigation()
|
||||
_focused_row_index = candidate_index
|
||||
button.grab_focus()
|
||||
return
|
||||
candidate_index += direction
|
||||
|
||||
|
||||
func _enable_keyboard_navigation() -> void:
|
||||
_keyboard_navigation_active = true
|
||||
for button: Button in _row_buttons:
|
||||
if is_instance_valid(button):
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
|
||||
|
||||
func _reset_keyboard_navigation() -> void:
|
||||
_keyboard_navigation_active = false
|
||||
_focused_row_index = -1
|
||||
if not is_node_ready():
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner != null and _row_buttons.has(focus_owner):
|
||||
focus_owner.release_focus()
|
||||
for button: Button in _row_buttons:
|
||||
if is_instance_valid(button):
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
|
||||
func _on_row_mouse_entered(_button: Button) -> void:
|
||||
_reset_keyboard_navigation()
|
||||
|
||||
|
||||
func _theme_color(role: StringName, fallback: Color) -> Color:
|
||||
if _visual_theme == null:
|
||||
return fallback
|
||||
return _visual_theme.chrome_color(role, _editor)
|
||||
|
||||
|
||||
func _theme_px(role: StringName, fallback: int) -> int:
|
||||
if _visual_theme == null:
|
||||
return fallback
|
||||
return _visual_theme.chrome_px(role, _editor)
|
||||
|
||||
|
||||
func _font_size() -> int:
|
||||
if _font_size_override > 0:
|
||||
return _font_size_override
|
||||
return _theme_px(&"context_menu", 20)
|
||||
|
||||
|
||||
func _menu_row_height() -> int:
|
||||
return maxi(_theme_px(&"context_menu_row_height", DEFAULT_ROW_HEIGHT), DEFAULT_ROW_HEIGHT)
|
||||
|
||||
|
||||
func _menu_min_width() -> int:
|
||||
return maxi(_theme_px(&"context_menu_min_width", DEFAULT_MIN_WIDTH), DEFAULT_MIN_WIDTH)
|
||||
|
||||
|
||||
func _text_color(role: ItemRole) -> Color:
|
||||
if role == ItemRole.DESTRUCTIVE:
|
||||
return _theme_color(&"destructive", Color("#E66B75"))
|
||||
return _theme_color(&"text", Color("#F5F7FA"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjidsdsxomilt
|
||||
@@ -0,0 +1,231 @@
|
||||
class_name EditorSelectionDropdown
|
||||
extends MenuButton
|
||||
## Single-choice editor picker with a Studio-style selected row and no radio lane.
|
||||
|
||||
signal item_selected(index: int)
|
||||
|
||||
var selected: int = -1:
|
||||
set(value):
|
||||
_set_selected(value)
|
||||
get:
|
||||
return _selected_index
|
||||
|
||||
var _selected_index: int = -1
|
||||
var _popup_menu: PopupMenu
|
||||
var _field_icons: Array[Texture2D] = []
|
||||
var _control_theme: Theme
|
||||
var _surface_color: Color = Color("#252B38")
|
||||
var _selected_surface_color: Color = Color("#103B4D")
|
||||
var _border_color: Color = Color("#FFFFFF33")
|
||||
var _accent_color: Color = Color("#22C6FF")
|
||||
var _focus_color: Color = Color("#8BD9FF")
|
||||
var _text_color: Color = Color("#F5F7FA")
|
||||
var _font_size: int = 18
|
||||
var _row_separation: int = 6
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
flat = false
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
_popup_menu = get_popup()
|
||||
_popup_menu.shrink_width = false
|
||||
_popup_menu.transparent = false
|
||||
_popup_menu.transparent_bg = false
|
||||
_popup_menu.id_pressed.connect(_on_popup_item_pressed)
|
||||
about_to_popup.connect(_on_about_to_popup)
|
||||
_apply_popup_theme()
|
||||
_apply_field_theme()
|
||||
_sync_selected_item()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func configure(
|
||||
control_theme: Theme,
|
||||
surface_color: Color,
|
||||
selected_surface_color: Color,
|
||||
border_color: Color,
|
||||
accent_color: Color,
|
||||
focus_color: Color,
|
||||
text_color: Color,
|
||||
font_size: int,
|
||||
row_separation: int = 6
|
||||
) -> void:
|
||||
_control_theme = control_theme
|
||||
_surface_color = surface_color
|
||||
_selected_surface_color = selected_surface_color
|
||||
_border_color = border_color
|
||||
_accent_color = accent_color
|
||||
_focus_color = focus_color
|
||||
_text_color = text_color
|
||||
_font_size = font_size
|
||||
_row_separation = maxi(0, row_separation)
|
||||
if is_node_ready():
|
||||
_apply_popup_theme()
|
||||
|
||||
|
||||
func add_item(label: String, id: int = -1) -> void:
|
||||
var index: int = get_popup().item_count
|
||||
var item_id: int = index if id < 0 else id
|
||||
add_styled_item(label, null, null, item_id)
|
||||
|
||||
|
||||
func add_styled_item(
|
||||
label: String,
|
||||
field_icon: Texture2D = null,
|
||||
popup_icon: Texture2D = null,
|
||||
id: int = -1
|
||||
) -> void:
|
||||
var popup: PopupMenu = get_popup()
|
||||
var index: int = popup.item_count
|
||||
var item_id: int = index if id < 0 else id
|
||||
_field_icons.append(field_icon)
|
||||
if popup_icon != null:
|
||||
popup.add_icon_item(popup_icon, label, item_id)
|
||||
popup.set_item_icon_max_width(index, 44)
|
||||
else:
|
||||
popup.add_item(label, item_id)
|
||||
popup.set_item_as_radio_checkable(index, false)
|
||||
if _selected_index < 0:
|
||||
_set_selected(index)
|
||||
|
||||
|
||||
func get_item_text(index: int) -> String:
|
||||
return get_popup().get_item_text(index)
|
||||
|
||||
|
||||
func get_item_icon(index: int) -> Texture2D:
|
||||
return _field_icons[index] if index >= 0 and index < _field_icons.size() else null
|
||||
|
||||
|
||||
func select(index: int) -> void:
|
||||
_set_selected(index)
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
get_popup().clear()
|
||||
_field_icons.clear()
|
||||
_selected_index = -1
|
||||
_sync_selected_item()
|
||||
|
||||
|
||||
func hide_popup() -> void:
|
||||
get_popup().hide()
|
||||
|
||||
|
||||
func _set_selected(index: int) -> void:
|
||||
var count: int = get_popup().item_count
|
||||
if index < 0 or count == 0:
|
||||
_selected_index = -1
|
||||
else:
|
||||
_selected_index = clampi(index, 0, count - 1)
|
||||
_sync_selected_item()
|
||||
|
||||
|
||||
func _sync_selected_item() -> void:
|
||||
if not is_instance_valid(get_popup()):
|
||||
return
|
||||
if _selected_index < 0 or _selected_index >= get_popup().item_count:
|
||||
text = ""
|
||||
icon = null
|
||||
return
|
||||
text = get_popup().get_item_text(_selected_index)
|
||||
icon = _field_icons[_selected_index] if _selected_index < _field_icons.size() else null
|
||||
|
||||
|
||||
func _apply_popup_theme() -> void:
|
||||
if _popup_menu == null:
|
||||
return
|
||||
if _control_theme != null:
|
||||
_popup_menu.theme = _control_theme
|
||||
var panel := StyleBoxFlat.new()
|
||||
var opaque_surface: Color = _surface_color
|
||||
opaque_surface.a = 1.0
|
||||
panel.bg_color = opaque_surface
|
||||
panel.border_color = _border_color
|
||||
panel.set_border_width_all(1)
|
||||
panel.set_corner_radius_all(8)
|
||||
panel.content_margin_left = 6.0
|
||||
panel.content_margin_top = 6.0
|
||||
panel.content_margin_right = 6.0
|
||||
panel.content_margin_bottom = 6.0
|
||||
_popup_menu.add_theme_stylebox_override("panel", panel)
|
||||
var selected_row := StyleBoxFlat.new()
|
||||
selected_row.bg_color = _selected_surface_color
|
||||
selected_row.border_color = _accent_color
|
||||
selected_row.set_border_width_all(2)
|
||||
selected_row.set_corner_radius_all(7)
|
||||
selected_row.content_margin_left = 5.0
|
||||
selected_row.content_margin_top = 12.0
|
||||
selected_row.content_margin_right = 5.0
|
||||
selected_row.content_margin_bottom = 12.0
|
||||
_popup_menu.add_theme_stylebox_override("hover", selected_row)
|
||||
_popup_menu.add_theme_color_override("font_hover_color", _text_color)
|
||||
_popup_menu.add_theme_color_override("font_color", _text_color)
|
||||
_popup_menu.add_theme_font_size_override("font_size", _font_size)
|
||||
_popup_menu.add_theme_constant_override("icon_max_width", 44)
|
||||
_popup_menu.add_theme_constant_override("h_separation", 10)
|
||||
_popup_menu.add_theme_constant_override("v_separation", _row_separation)
|
||||
_popup_menu.add_theme_constant_override("item_start_padding", 4)
|
||||
_popup_menu.add_theme_constant_override("item_end_padding", 4)
|
||||
add_theme_stylebox_override("focus", _make_focus_style())
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _apply_field_theme() -> void:
|
||||
for state: String in ["normal", "hover", "pressed", "disabled"]:
|
||||
var original: StyleBox = get_theme_stylebox(state, "Button")
|
||||
if original == null:
|
||||
continue
|
||||
var field_style: StyleBox = original.duplicate()
|
||||
field_style.content_margin_right = maxf(field_style.content_margin_right, 28.0)
|
||||
add_theme_stylebox_override(state, field_style)
|
||||
|
||||
|
||||
func _make_focus_style() -> StyleBoxFlat:
|
||||
var focus_box := StyleBoxFlat.new()
|
||||
focus_box.bg_color = Color(0.0, 0.0, 0.0, 0.0)
|
||||
focus_box.border_color = _focus_color
|
||||
focus_box.set_border_width_all(2)
|
||||
focus_box.set_corner_radius_all(8)
|
||||
focus_box.content_margin_right = 28.0
|
||||
return focus_box
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var center := Vector2(size.x - 16.0, size.y * 0.5)
|
||||
var points := PackedVector2Array([
|
||||
center + Vector2(-4.0, -2.0),
|
||||
center + Vector2(0.0, 2.0),
|
||||
center + Vector2(4.0, -2.0),
|
||||
])
|
||||
draw_polyline(points, _text_color.darkened(0.28), 2.0, true)
|
||||
|
||||
|
||||
func _on_about_to_popup() -> void:
|
||||
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||
call_deferred("_sync_popup_size")
|
||||
if _selected_index >= 0:
|
||||
_popup_menu.set_focused_item(_selected_index)
|
||||
else:
|
||||
_popup_menu.set_focused_item(-1)
|
||||
|
||||
|
||||
func _sync_popup_size() -> void:
|
||||
if _popup_menu == null:
|
||||
return
|
||||
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||
if _selected_index >= 0:
|
||||
_popup_menu.set_focused_item(_selected_index)
|
||||
|
||||
|
||||
func _on_popup_item_pressed(id: int) -> void:
|
||||
var index: int = get_popup().get_item_index(id)
|
||||
if index < 0 or index == _selected_index:
|
||||
return
|
||||
_set_selected(index)
|
||||
item_selected.emit(index)
|
||||
@@ -0,0 +1 @@
|
||||
uid://qlg14pj6jfj7
|
||||
@@ -13,6 +13,7 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"background": "#171B24F2",
|
||||
"surface": "#252B38FA",
|
||||
"surface_hover": "#30394A",
|
||||
"selected_surface": "#103B4D",
|
||||
"text": "#F5F7FA",
|
||||
"text_muted": "#AAB2C0",
|
||||
"edit": "#22C6FF",
|
||||
@@ -32,6 +33,8 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"section": 22,
|
||||
"title": 26,
|
||||
"menu": 20,
|
||||
"menu_bar": 22,
|
||||
"menu_popup": 26,
|
||||
"status": 16,
|
||||
"mode": 24,
|
||||
"prop_name": 20,
|
||||
@@ -40,7 +43,13 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"spacing": {"small": 8, "medium": 12, "large": 16},
|
||||
"radii": {"control": 8, "panel": 10},
|
||||
"borders": {"normal": 1, "focus": 2},
|
||||
"targets": {"button_min_height": 44, "row_min_height": 44, "mode_button_min_height": 56},
|
||||
"targets": {
|
||||
"button_min_height": 44,
|
||||
"row_min_height": 44,
|
||||
"mode_button_min_height": 56,
|
||||
"context_menu_row_height": 48,
|
||||
"context_menu_min_width": 208,
|
||||
},
|
||||
},
|
||||
"editors": {"prop": {}, "stickman": {}},
|
||||
"stickman_canvas": {
|
||||
@@ -54,6 +63,7 @@ const COLOR_ROLE_SECTIONS: Dictionary = {
|
||||
"background": "colors",
|
||||
"surface": "colors",
|
||||
"surface_hover": "colors",
|
||||
"selected_surface": "colors",
|
||||
"text": "colors",
|
||||
"text_muted": "colors",
|
||||
"edit": "colors",
|
||||
@@ -73,6 +83,8 @@ const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||
"section": "fonts",
|
||||
"title": "fonts",
|
||||
"menu": "fonts",
|
||||
"menu_bar": "fonts",
|
||||
"menu_popup": "fonts",
|
||||
"status": "fonts",
|
||||
"mode": "fonts",
|
||||
"prop_name": "fonts",
|
||||
@@ -87,6 +99,8 @@ const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||
"button_min_height": "targets",
|
||||
"row_min_height": "targets",
|
||||
"mode_button_min_height": "targets",
|
||||
"context_menu_row_height": "targets",
|
||||
"context_menu_min_width": "targets",
|
||||
}
|
||||
const CONFIG_SIZE_KEYS: Dictionary = {
|
||||
"normal_border": "normal",
|
||||
@@ -208,13 +222,12 @@ func control_theme(editor: StringName) -> Theme:
|
||||
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||
var panel_box: StyleBoxFlat = _make_box(surface, border, chrome_px(&"panel", editor), normal_width)
|
||||
theme.set_stylebox("panel", "PanelContainer", panel_box)
|
||||
theme.set_stylebox("normal", "PopupMenu", panel_box.duplicate())
|
||||
theme.set_stylebox("panel", "MenuBar", panel_box.duplicate())
|
||||
theme.set_font_size("font_size", "Label", chrome_px(&"body", editor))
|
||||
theme.set_color("font_color", "Label", text)
|
||||
theme.set_color("font_hover_color", "Label", text)
|
||||
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu", editor))
|
||||
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu", editor))
|
||||
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu_popup", editor))
|
||||
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu_bar", editor))
|
||||
theme.set_color("font_color", "MenuBar", text)
|
||||
return theme
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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.
|
||||
## Renders child-facing stage and editor dialogs as a shared card while preserving
|
||||
## the native AcceptDialog/ConfirmationDialog signals used by their workflows.
|
||||
|
||||
const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd")
|
||||
const OVERLAY_META := &"stage_dialog_overlay"
|
||||
@@ -20,7 +20,9 @@ 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 SECONDARY_META := &"stage_dialog_secondary_buttons"
|
||||
const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons"
|
||||
const DEFAULT_PRIMARY_META := &"stage_dialog_default_primary"
|
||||
const OPEN_META := &"stage_dialog_open"
|
||||
|
||||
|
||||
@@ -29,7 +31,9 @@ static func apply(
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
destructive_buttons: Array = [],
|
||||
secondary_buttons: Array = [],
|
||||
default_ok_is_primary: bool = true
|
||||
) -> void:
|
||||
if dialog == null:
|
||||
return
|
||||
@@ -37,10 +41,14 @@ static func apply(
|
||||
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
|
||||
var primary: Button = primary_button
|
||||
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||
primary = ok_button
|
||||
dialog.set_meta(PRIMARY_META, primary)
|
||||
dialog.set_meta(SECONDARY_META, secondary_buttons.duplicate())
|
||||
dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate())
|
||||
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons)
|
||||
dialog.set_meta(DEFAULT_PRIMARY_META, default_ok_is_primary)
|
||||
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||
|
||||
|
||||
static func popup(dialog: Window) -> void:
|
||||
@@ -48,9 +56,11 @@ static func popup(dialog: Window) -> void:
|
||||
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 primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||
var secondary_buttons: Array = dialog.get_meta(SECONDARY_META, []) as Array
|
||||
var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array
|
||||
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons)
|
||||
var default_ok_is_primary: bool = bool(dialog.get_meta(DEFAULT_PRIMARY_META, true))
|
||||
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||
if overlay == null:
|
||||
return
|
||||
@@ -71,8 +81,10 @@ static func popup(dialog: Window) -> 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_primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||
var focus_target: Button = visual_button(focus_primary)
|
||||
if focus_target == null and dialog is ConfirmationDialog:
|
||||
focus_target = visual_button((dialog as ConfirmationDialog).get_cancel_button())
|
||||
if focus_target != null and focus_target.visible:
|
||||
focus_target.call_deferred("grab_focus")
|
||||
|
||||
@@ -232,6 +244,7 @@ static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, u
|
||||
var overlay := Control.new()
|
||||
overlay.name = "StageDialogOverlay"
|
||||
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
overlay.theme = stage_theme.control_theme()
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
overlay.z_index = 100
|
||||
overlay.z_as_relative = false
|
||||
@@ -385,7 +398,9 @@ static func _refresh_dialog_surface(
|
||||
stage_theme: SandboxTheme,
|
||||
ui_font: Font,
|
||||
primary_button: Button = null,
|
||||
destructive_buttons: Array = []
|
||||
destructive_buttons: Array = [],
|
||||
secondary_buttons: Array = [],
|
||||
default_ok_is_primary: bool = true
|
||||
) -> void:
|
||||
if not dialog.has_meta(OVERLAY_META):
|
||||
return
|
||||
@@ -458,16 +473,24 @@ static func _refresh_dialog_surface(
|
||||
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
|
||||
var primary: Button = primary_button
|
||||
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||
primary = ok_button
|
||||
if ok_button != null:
|
||||
ok_button.visible = false
|
||||
if cancel_button != null:
|
||||
cancel_button.visible = false
|
||||
for raw_secondary: Variant in secondary_buttons:
|
||||
if raw_secondary is Button:
|
||||
(raw_secondary as 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_secondary: Variant in secondary_buttons:
|
||||
if raw_secondary is Button:
|
||||
_append_unique_button(ordered, raw_secondary as Button)
|
||||
for raw_destructive: Variant in destructive_buttons:
|
||||
if raw_destructive is Button:
|
||||
_append_unique_button(ordered, raw_destructive as Button)
|
||||
@@ -476,6 +499,8 @@ static func _refresh_dialog_surface(
|
||||
for original: Button in ordered:
|
||||
var visual: Button = Button.new()
|
||||
visual.text = original.text
|
||||
visual.disabled = original.disabled
|
||||
visual.tooltip_text = original.tooltip_text
|
||||
visual.name = "Action_%s" % String(original.name)
|
||||
visual.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
var role: StringName = &"secondary"
|
||||
@@ -535,18 +560,18 @@ static func _find_overlay_host(dialog: Window) -> Node:
|
||||
return ancestor
|
||||
ancestor = ancestor.get_parent()
|
||||
var tree: SceneTree = dialog.get_tree()
|
||||
var scene: Node = tree.current_scene if tree != null else null
|
||||
var scene: Node = tree.current_scene if tree != null and tree.current_scene != null else (tree.root 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")
|
||||
var existing: Node = scene.find_child("StageDialogLayer", true, false)
|
||||
if existing is CanvasLayer:
|
||||
return existing
|
||||
var layer := CanvasLayer.new()
|
||||
layer.name = "StageDialogLayer"
|
||||
layer.layer = 100
|
||||
scene.add_child(layer)
|
||||
scene.add_child.call_deferred(layer)
|
||||
return layer
|
||||
return null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user