feat: add Phase 20a stage identities and overlap selection
This commit is contained in:
@@ -34,6 +34,8 @@ const STAGE_SPATIAL_INDEX := preload("res://scripts/stage/stage_spatial_index.gd
|
||||
const STAGE_TERRAIN_PAINTER := preload("res://scripts/editors/stage/stage_terrain_painter.gd")
|
||||
const STAGE_CHROME_SCRIPT := preload("res://scripts/editors/stage/stage_chrome.gd")
|
||||
const STAGE_PROP_SETTINGS_VIEW_SCRIPT := preload("res://scripts/editors/stage/stage_prop_settings_view.gd")
|
||||
const STAGE_CHARACTER_SETTINGS_VIEW_SCRIPT := preload("res://scripts/editors/stage/stage_character_settings_view.gd")
|
||||
const STAGE_OBJECT_IDENTITY_SCRIPT := preload("res://scripts/stage/stage_object_identity.gd")
|
||||
const STAGE_USE_SPOT_OVERLAY_SCRIPT := preload("res://scripts/editors/stage/stage_use_spot_overlay.gd")
|
||||
const STAGE_USE_TARGET_CAPTURE_BAR_SCRIPT := preload("res://scripts/editors/stage/stage_use_target_capture_bar.gd")
|
||||
const STAGE_SCENE_ADAPTER_SCRIPT := preload("res://scripts/editors/stage/stage_scene_adapter.gd")
|
||||
@@ -82,8 +84,6 @@ const RULE_EDITOR := preload("res://scenes/editors/stage/rule_editor.tscn")
|
||||
const WAYPOINT_CONTEXT := preload("res://scripts/stage/waypoint_context.gd")
|
||||
const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
const MISSING_STICKMAN_PLACEHOLDER := preload("res://scripts/characters/stickman/missing_stickman_placeholder.gd")
|
||||
const MISSING_PROP_PLACEHOLDER := preload("res://scripts/props/missing_prop_placeholder.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
@@ -257,6 +257,7 @@ var _suppressed_contact_pairs: Dictionary:
|
||||
return {} if _rule_engine == null else _rule_engine.get_suppressed_contact_pairs()
|
||||
|
||||
var _spawner: StageSpawner
|
||||
var _object_identity: StageObjectIdentity
|
||||
var _selection: StageSelection
|
||||
var _gizmos: StageGizmos
|
||||
var _stage_input_ready: bool = false
|
||||
@@ -344,12 +345,25 @@ var _asset_browser: StageAssetBrowser
|
||||
var _modal_views: StageModalViews
|
||||
var _chrome: StageChrome
|
||||
var _prop_settings_view: StagePropSettingsView
|
||||
var _character_settings_view: StageCharacterSettingsView
|
||||
var _placement_name_prompt: Label
|
||||
var _placement_name_prompt_panel: PanelContainer
|
||||
var _objects_here_candidates: Array[Node2D] = []
|
||||
var _objects_here_context: StringName = &""
|
||||
var _objects_here_hit_position: Vector2 = Vector2.ZERO
|
||||
var _placement_feedback_label: Label
|
||||
var _placement_feedback_panel: PanelContainer
|
||||
var _placement_feedback_node: Node2D
|
||||
var _placement_feedback_timer: float = 0.0
|
||||
var _last_pointer_viewport_position: Vector2 = Vector2.ZERO
|
||||
var _edit_wobble_preview_prop: PropBlock
|
||||
var _use_spot_overlay: StageUseSpotOverlay
|
||||
var _use_target_capture_bar: StageUseTargetCaptureBar
|
||||
var _placement_overlay = null # StagePlacementOverlay (preloaded)
|
||||
|
||||
var _placement_id: String = ""
|
||||
var _placement_name_cache_key: String = ""
|
||||
var _placement_name_cache_value: String = ""
|
||||
|
||||
var _stickman_library: StickmanLibrary:
|
||||
get:
|
||||
@@ -403,9 +417,6 @@ var _palette_dock: PanelContainer:
|
||||
var _prop_settings_card: Control:
|
||||
get:
|
||||
return null if _prop_settings_view == null else _prop_settings_view.get_card()
|
||||
var _prop_settings_reopen_button: Button:
|
||||
get:
|
||||
return null if _prop_settings_view == null else _prop_settings_view.get_reopen_button()
|
||||
var _prop_settings_fields: VBoxContainer:
|
||||
get:
|
||||
return null if _prop_settings_view == null else _prop_settings_view.get_fields()
|
||||
@@ -421,12 +432,6 @@ var _prop_settings_motion_group: Control:
|
||||
var _prop_settings_controls: Dictionary:
|
||||
get:
|
||||
return {} if _prop_settings_view == null else _prop_settings_view.get_controls()
|
||||
var _prop_settings_active_key: String:
|
||||
get:
|
||||
return "" if _prop_settings_view == null else _prop_settings_view.get_active_selection_key()
|
||||
var _prop_settings_dismissed_key: String:
|
||||
get:
|
||||
return "" if _prop_settings_view == null else _prop_settings_view.get_dismissed_selection_key()
|
||||
var _objects_here_popup: PopupPanel:
|
||||
get:
|
||||
return null if _modal_views == null else _modal_views.get_objects_here_popup()
|
||||
@@ -539,6 +544,11 @@ var _pan_last: Vector2 = Vector2.ZERO
|
||||
var _space_key_down: bool = false
|
||||
var _space_press_owned: bool = false
|
||||
|
||||
var _pending_overlap_candidates: Array[Node2D] = []
|
||||
var _pending_overlap_drag_target: Node2D = null
|
||||
var _pending_overlap_world_position: Vector2 = Vector2.ZERO
|
||||
var _pending_overlap_screen_position: Vector2 = Vector2.ZERO
|
||||
|
||||
var _box_selecting: bool = false
|
||||
var _box_start: Vector2 = Vector2.ZERO
|
||||
var _box_rect: Rect2 = Rect2()
|
||||
@@ -548,6 +558,8 @@ var _ghost_holder: Node2D = null
|
||||
|
||||
## Phase 4b terrain drag-painting state.
|
||||
const TERRAIN_DRAG_THRESHOLD_PX: float = 6.0
|
||||
## An overlap press becomes a selected-object drag after this screen-space move.
|
||||
const OVERLAP_SELECT_DRAG_THRESHOLD_PX: float = 8.0
|
||||
|
||||
## Compatibility views retained for existing stage tools and tests.
|
||||
var _terrain_dragging: bool:
|
||||
@@ -643,7 +655,6 @@ var _direct_move_target: Vector2:
|
||||
var _direct_move_last_style: Dictionary:
|
||||
get:
|
||||
return {} if _director_controller == null else _director_controller.get_direct_move_last_style()
|
||||
var _next_stage_prop_serial: int = 0
|
||||
var _speak_dialog: AcceptDialog = null
|
||||
var _speak_edit: LineEdit = null
|
||||
var _speak_stop_current_check: CheckBox = null
|
||||
@@ -1327,6 +1338,7 @@ func _ready() -> void:
|
||||
_load_settings()
|
||||
|
||||
_spawner = STAGE_SPAWNER.new(_world)
|
||||
_object_identity = STAGE_OBJECT_IDENTITY_SCRIPT.new() as StageObjectIdentity
|
||||
_spatial_index = STAGE_SPATIAL_INDEX.new()
|
||||
_asset_browser = STAGE_ASSET_BROWSER_SCRIPT.new() as StageAssetBrowser
|
||||
add_child(_asset_browser)
|
||||
@@ -1379,6 +1391,7 @@ func _ready() -> void:
|
||||
_update_scene_chrome()
|
||||
_handle_cli_startup(OS.get_cmdline_user_args())
|
||||
_check_speech_recovery()
|
||||
_last_pointer_viewport_position = get_viewport().get_mouse_position()
|
||||
_stage_input_ready = true
|
||||
|
||||
|
||||
@@ -1400,6 +1413,7 @@ func _process(delta: float) -> void:
|
||||
_use_spot_overlay.show_edit(_spot_edit_prop, _spot_edit_ghost_world, "Click a place on this prop" if _spot_edit_id.is_empty() else "Click the spot's new place", _spot_selected_id, _spot_edit_id)
|
||||
elif _ghost != null and is_instance_valid(_ghost):
|
||||
_update_ghost_position()
|
||||
_update_placement_feedback(delta)
|
||||
_update_cursor_coords()
|
||||
_update_action_overlay()
|
||||
_sync_awaiting_cursor()
|
||||
@@ -1453,6 +1467,28 @@ func _update_throw_preview_cache() -> void:
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _hosted_prop_editor != null or not _stage_input_ready:
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
_last_pointer_viewport_position = (event as InputEventMouseMotion).position
|
||||
elif event is InputEventMouseButton:
|
||||
_last_pointer_viewport_position = (event as InputEventMouseButton).position
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup) and _objects_here_popup.visible:
|
||||
if event is InputEventMouseButton:
|
||||
var chooser_click: InputEventMouseButton = event as InputEventMouseButton
|
||||
var popup_rect: Rect2 = Rect2(Vector2(_objects_here_popup.position), Vector2(_objects_here_popup.size))
|
||||
if chooser_click.pressed and not popup_rect.has_point(chooser_click.position):
|
||||
_modal_views.dismiss_objects_here()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
elif event is InputEventKey:
|
||||
var chooser_key: InputEventKey = event as InputEventKey
|
||||
if chooser_key.pressed and not chooser_key.echo and chooser_key.keycode == KEY_ESCAPE:
|
||||
_modal_views.dismiss_objects_here()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event is InputEventKey and (event as InputEventKey).pressed and not (event as InputEventKey).echo and (event as InputEventKey).keycode == KEY_ESCAPE and not _pending_overlap_candidates.is_empty():
|
||||
_clear_pending_overlap_gesture()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _speech_session != null:
|
||||
_speech_session.set_camera_panning(_panning)
|
||||
if _speech_session.handle_input(event):
|
||||
@@ -1651,6 +1687,8 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
_action_editor.hide()
|
||||
elif _rule_editor != null and _rule_editor.visible:
|
||||
_rule_editor.hide()
|
||||
else:
|
||||
set_mode(StageMode.EDIT)
|
||||
elif _placement_id != "":
|
||||
set_placement_mode("")
|
||||
elif not _selection.get_selected().is_empty():
|
||||
@@ -1827,8 +1865,10 @@ func _handle_world_click(mb: InputEventMouseButton) -> void:
|
||||
else:
|
||||
_place_at(world_pos)
|
||||
else:
|
||||
_begin_click_select(world_pos, mb.shift_pressed)
|
||||
_begin_click_select(world_pos, mb.shift_pressed, mb.position)
|
||||
else:
|
||||
if _resolve_pending_overlap_click():
|
||||
return
|
||||
if _gizmos.is_dragging():
|
||||
_gizmos.end_drag()
|
||||
elif _box_selecting:
|
||||
@@ -1851,7 +1891,9 @@ func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
|
||||
if _scenery_edit_mode:
|
||||
_handle_scenery_edit_motion(world_pos)
|
||||
return
|
||||
if _gizmos.is_dragging():
|
||||
if _try_begin_pending_overlap_drag(mm.position, world_pos):
|
||||
_director_visuals.mark_dirty()
|
||||
elif _gizmos.is_dragging():
|
||||
_gizmos.drag_to(world_pos)
|
||||
_director_visuals.mark_dirty()
|
||||
elif _terrain_dragging:
|
||||
@@ -1883,6 +1925,7 @@ func set_mode(mode: StageMode) -> void:
|
||||
return
|
||||
if _plain_color_draft_active:
|
||||
_close_selector()
|
||||
_clear_pending_overlap_gesture()
|
||||
_cancel_surface_color_preview()
|
||||
if _scenery_controller != null:
|
||||
if _scenery_controller.is_group_preview_pending():
|
||||
@@ -1891,6 +1934,7 @@ func set_mode(mode: StageMode) -> void:
|
||||
_scenery_controller.exit_tool()
|
||||
if _scenery_edit_mode:
|
||||
_exit_scenery_edit_mode()
|
||||
_dismiss_active_objects_here()
|
||||
_hide_scenery_capture_bar()
|
||||
var previous_mode: StageMode = current_mode
|
||||
_cancel_edit_wobble_preview()
|
||||
@@ -1914,6 +1958,7 @@ func set_mode(mode: StageMode) -> void:
|
||||
_sync_run_controls()
|
||||
_apply_cursor()
|
||||
_refresh_prop_settings_card()
|
||||
_refresh_character_settings_card()
|
||||
mode_changed.emit(int(mode))
|
||||
_refresh_status()
|
||||
|
||||
@@ -2164,6 +2209,10 @@ func _place_at(world_pos: Vector2) -> void:
|
||||
if node == null:
|
||||
return
|
||||
_finalize_placed_node(node)
|
||||
var placed_name: String = _object_identity.get_name(node)
|
||||
if not placed_name.is_empty():
|
||||
_show_toast("%s placed" % placed_name)
|
||||
_show_placement_feedback(node, placed_name)
|
||||
if node is TerrainBlock:
|
||||
_nav_dirty = true
|
||||
_rebuild_grid_cells()
|
||||
@@ -2176,10 +2225,12 @@ func _place_at(world_pos: Vector2) -> void:
|
||||
## Common post-spawn wiring: freeze kinematic props in EDIT, save authored state,
|
||||
## and connect the Phase 3a/4 signals for content/stickmen/props.
|
||||
func _finalize_placed_node(node: Node2D) -> void:
|
||||
if node is PropBlock:
|
||||
_object_identity.assign_new(node, StageObjectIdentity.asset_base_name("prop", String((node as PropBlock).spawn_id)), _world)
|
||||
elif node is STICKMAN_RIG:
|
||||
_object_identity.assign_new(node, StageObjectIdentity.asset_base_name("stickman", String((node as StickmanRig).source_stk_path)), _world)
|
||||
if node is PropBlock:
|
||||
var placed_prop := node as PropBlock
|
||||
if placed_prop.stage_object_id.is_empty():
|
||||
placed_prop.stage_object_id = _new_stage_prop_id()
|
||||
placed_prop.ensure_default_grip()
|
||||
if placed_prop.stage_roles.is_empty():
|
||||
placed_prop.set_stage_roles(PROP_STAGE_ROLES.new_placement(placed_prop.has_usable_grip()))
|
||||
@@ -2216,24 +2267,6 @@ func _finalize_placed_node(node: Node2D) -> void:
|
||||
prop.separated.connect(_on_prop_separated.bind(prop))
|
||||
|
||||
|
||||
func _new_stage_prop_id() -> String:
|
||||
while true:
|
||||
_next_stage_prop_serial += 1
|
||||
var candidate := "prop_%d_%d_%d" % [Time.get_ticks_usec(), randi(), _next_stage_prop_serial]
|
||||
if not _stage_prop_id_in_use(candidate):
|
||||
return candidate
|
||||
return ""
|
||||
|
||||
|
||||
func _stage_prop_id_in_use(stage_id: String) -> bool:
|
||||
for child: Node in _world.get_children():
|
||||
if child is PropBlock and (child as PropBlock).stage_object_id == stage_id:
|
||||
return true
|
||||
if child.get_script() == MISSING_PROP_PLACEHOLDER and String(child.get("stage_object_id")) == stage_id:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _resolve_prop_by_stage_id(stage_id: String) -> PropBlock:
|
||||
if stage_id.is_empty():
|
||||
return null
|
||||
@@ -2293,6 +2326,7 @@ func _use_failure_message(reason_code: String) -> String:
|
||||
func _spawn_ghost() -> void:
|
||||
_free_ghost()
|
||||
if _placement_id == "":
|
||||
_update_placement_name_prompt(Vector2.ZERO)
|
||||
return
|
||||
var ghost := _spawner.spawn(_placement_id, Vector2.ZERO)
|
||||
if ghost == null:
|
||||
@@ -2337,17 +2371,116 @@ func _free_ghost() -> void:
|
||||
if _ghost != null and is_instance_valid(_ghost):
|
||||
_ghost.queue_free()
|
||||
_ghost = null
|
||||
if _placement_id.is_empty() or not _is_named_placement():
|
||||
_update_placement_name_prompt(Vector2.ZERO)
|
||||
|
||||
|
||||
func _update_ghost_position() -> void:
|
||||
if _ghost == null or not is_instance_valid(_ghost):
|
||||
return
|
||||
var pos := _camera.get_global_mouse_position() + _spawner.get_spawn_offset(_placement_id)
|
||||
var canvas_transform: Transform2D = get_viewport().get_canvas_transform()
|
||||
var pointer_world_position: Vector2 = canvas_transform.affine_inverse() * _last_pointer_viewport_position
|
||||
var pos: Vector2 = pointer_world_position + _spawner.get_spawn_offset(_placement_id)
|
||||
if _is_terrain_placement():
|
||||
pos = _snap_to_grid(pos) if _snap_enabled else pos
|
||||
elif _snap_enabled:
|
||||
pos = _snap_to_grid(pos)
|
||||
_ghost.position = pos
|
||||
_update_placement_name_prompt(_last_pointer_viewport_position)
|
||||
|
||||
|
||||
func _is_named_placement() -> bool:
|
||||
return _placement_id == "prop" or _placement_id == "stickman"
|
||||
|
||||
|
||||
func _placement_base_name() -> String:
|
||||
var asset_key: String = ""
|
||||
if _placement_id == "prop":
|
||||
asset_key = "prop:" + _spawner.get_selected_prop_id()
|
||||
elif _placement_id == "stickman":
|
||||
asset_key = "stickman:" + _spawner.get_selected_stickman_path()
|
||||
if asset_key.is_empty():
|
||||
_placement_name_cache_key = ""
|
||||
_placement_name_cache_value = ""
|
||||
return ""
|
||||
if asset_key == _placement_name_cache_key:
|
||||
return _placement_name_cache_value
|
||||
_placement_name_cache_key = asset_key
|
||||
var object_type: String = "prop" if _placement_id == "prop" else "stickman"
|
||||
var asset_path: String = _spawner.get_selected_prop_id() if object_type == "prop" else _spawner.get_selected_stickman_path()
|
||||
_placement_name_cache_value = StageObjectIdentity.asset_base_name(object_type, asset_path)
|
||||
return _placement_name_cache_value
|
||||
|
||||
|
||||
func _update_placement_name_prompt(screen_position: Vector2) -> void:
|
||||
if _placement_name_prompt == null or _placement_name_prompt_panel == null:
|
||||
return
|
||||
if current_mode != StageMode.EDIT or _ghost == null or not is_instance_valid(_ghost) or not _is_named_placement():
|
||||
_placement_name_prompt_panel.visible = false
|
||||
return
|
||||
var next_name: String = _object_identity.next_free_name(_placement_base_name(), _world, _ghost)
|
||||
_placement_name_prompt.text = "Click to place %s" % next_name
|
||||
_placement_name_prompt_panel.size = _placement_name_prompt_panel.get_combined_minimum_size()
|
||||
var viewport: Rect2 = get_viewport_rect()
|
||||
var position: Vector2 = screen_position + Vector2(16.0, 18.0)
|
||||
position.x = clampf(position.x, 8.0, maxf(8.0, viewport.size.x - _placement_name_prompt_panel.size.x - 8.0))
|
||||
position.y = clampf(position.y, 8.0, maxf(8.0, viewport.size.y - _placement_name_prompt_panel.size.y - 8.0))
|
||||
_placement_name_prompt_panel.position = position
|
||||
_placement_name_prompt_panel.visible = true
|
||||
|
||||
|
||||
func _show_placement_feedback(node: Node2D, stage_name: String) -> void:
|
||||
if _placement_feedback_label == null or node == null:
|
||||
return
|
||||
_placement_feedback_node = node
|
||||
_placement_feedback_label.text = "%s placed" % stage_name
|
||||
_placement_feedback_timer = 1.6
|
||||
_placement_feedback_panel.visible = true
|
||||
_position_placement_feedback()
|
||||
|
||||
|
||||
func _update_placement_feedback(delta: float) -> void:
|
||||
if _placement_feedback_timer <= 0.0:
|
||||
return
|
||||
_placement_feedback_timer = maxf(0.0, _placement_feedback_timer - delta)
|
||||
if _placement_feedback_node == null or not is_instance_valid(_placement_feedback_node) or _placement_feedback_node.is_queued_for_deletion():
|
||||
_placement_feedback_timer = 0.0
|
||||
if _placement_feedback_timer <= 0.0:
|
||||
_placement_feedback_panel.visible = false
|
||||
_placement_feedback_node = null
|
||||
else:
|
||||
_position_placement_feedback()
|
||||
|
||||
|
||||
func _position_placement_feedback() -> void:
|
||||
if _placement_feedback_label == null or _placement_feedback_node == null or not is_instance_valid(_placement_feedback_node):
|
||||
return
|
||||
var viewport: Rect2 = get_viewport_rect()
|
||||
var bounds: Rect2 = STAGE_SELECTION.get_world_aabb(_placement_feedback_node)
|
||||
var screen_bounds: Rect2 = _world_rect_to_viewport_rect(bounds)
|
||||
_placement_feedback_panel.size = _placement_feedback_panel.get_combined_minimum_size()
|
||||
var position: Vector2 = Vector2(
|
||||
screen_bounds.get_center().x - _placement_feedback_panel.size.x * 0.5,
|
||||
screen_bounds.position.y - _placement_feedback_panel.size.y - 12.0
|
||||
)
|
||||
position.x = clampf(position.x, 8.0, maxf(8.0, viewport.size.x - _placement_feedback_panel.size.x - 8.0))
|
||||
position.y = clampf(position.y, 8.0, maxf(8.0, viewport.size.y - _placement_feedback_panel.size.y - 8.0))
|
||||
_placement_feedback_panel.position = position
|
||||
|
||||
|
||||
func _world_rect_to_viewport_rect(world_rect: Rect2) -> Rect2:
|
||||
var transform: Transform2D = get_viewport().get_canvas_transform()
|
||||
var corners: Array[Vector2] = [
|
||||
world_rect.position,
|
||||
Vector2(world_rect.end.x, world_rect.position.y),
|
||||
world_rect.end,
|
||||
Vector2(world_rect.position.x, world_rect.end.y),
|
||||
]
|
||||
var first_point: Vector2 = transform * corners[0]
|
||||
var screen_rect: Rect2 = Rect2(first_point, Vector2.ZERO)
|
||||
for corner_index: int in range(1, corners.size()):
|
||||
screen_rect = screen_rect.expand(transform * corners[corner_index])
|
||||
return screen_rect
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Terrain drag-painting (Phase 4b)
|
||||
@@ -2469,7 +2602,8 @@ func _add_node_to_grid_cells(node: Node2D) -> void:
|
||||
# Selection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _begin_click_select(world_pos: Vector2, shift: bool) -> void:
|
||||
func _begin_click_select(world_pos: Vector2, shift: bool, screen_pos: Vector2) -> void:
|
||||
_clear_pending_overlap_gesture()
|
||||
var candidates: Array[Node2D] = _selection.hit_test_all(world_pos)
|
||||
var hit: Node2D = candidates[0] if not candidates.is_empty() else null
|
||||
if hit != null:
|
||||
@@ -2478,8 +2612,10 @@ func _begin_click_select(world_pos: Vector2, shift: bool) -> void:
|
||||
if _selection.is_selected(hit):
|
||||
_gizmos.begin_translate_drag(hit, world_pos)
|
||||
elif candidates.size() > 1:
|
||||
_selection.select_only(hit)
|
||||
_show_objects_here_popup(candidates)
|
||||
_pending_overlap_candidates = candidates.duplicate()
|
||||
_pending_overlap_drag_target = _selected_overlap_drag_target(candidates)
|
||||
_pending_overlap_world_position = world_pos
|
||||
_pending_overlap_screen_position = screen_pos
|
||||
elif _selection.is_selected(hit):
|
||||
# Already selected: keep the whole selection and drag it together.
|
||||
_gizmos.begin_translate_drag(hit, world_pos)
|
||||
@@ -2493,6 +2629,58 @@ func _begin_click_select(world_pos: Vector2, shift: bool) -> void:
|
||||
_gizmos.set_box_rect(_box_rect)
|
||||
|
||||
|
||||
func _selected_overlap_drag_target(candidates: Array[Node2D]) -> Node2D:
|
||||
if _selection == null:
|
||||
return null
|
||||
var primary: Node2D = _selection.get_primary()
|
||||
if primary != null and candidates.has(primary):
|
||||
return primary
|
||||
for selected: Node2D in _selection.get_selected():
|
||||
if candidates.has(selected):
|
||||
return selected
|
||||
return null
|
||||
|
||||
|
||||
func _try_begin_pending_overlap_drag(screen_pos: Vector2, world_pos: Vector2) -> bool:
|
||||
if _pending_overlap_candidates.is_empty() or _pending_overlap_drag_target == null:
|
||||
return false
|
||||
if not is_instance_valid(_pending_overlap_drag_target) or _pending_overlap_drag_target.is_queued_for_deletion():
|
||||
_pending_overlap_drag_target = null
|
||||
return false
|
||||
if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||||
return false
|
||||
if screen_pos.distance_to(_pending_overlap_screen_position) < OVERLAP_SELECT_DRAG_THRESHOLD_PX:
|
||||
return false
|
||||
var drag_target: Node2D = _pending_overlap_drag_target
|
||||
var drag_origin: Vector2 = _pending_overlap_world_position
|
||||
_clear_pending_overlap_gesture()
|
||||
_gizmos.begin_translate_drag(drag_target, drag_origin)
|
||||
_gizmos.drag_to(world_pos)
|
||||
return true
|
||||
|
||||
|
||||
func _resolve_pending_overlap_click() -> bool:
|
||||
if _pending_overlap_candidates.is_empty():
|
||||
return false
|
||||
var world_position: Vector2 = _pending_overlap_world_position
|
||||
var candidates: Array[Node2D] = _selection.hit_test_all(world_position)
|
||||
_clear_pending_overlap_gesture()
|
||||
if candidates.size() > 1:
|
||||
_show_objects_here_popup(candidates, &"edit_select", world_position)
|
||||
elif candidates.size() == 1:
|
||||
var hit: Node2D = candidates[0]
|
||||
if not _selection.is_selected(hit):
|
||||
_selection.select_only(hit)
|
||||
return true
|
||||
|
||||
|
||||
func _clear_pending_overlap_gesture() -> void:
|
||||
_pending_overlap_candidates.clear()
|
||||
_pending_overlap_drag_target = null
|
||||
_pending_overlap_world_position = Vector2.ZERO
|
||||
_pending_overlap_screen_position = Vector2.ZERO
|
||||
|
||||
|
||||
func _finish_box_select(shift: bool) -> void:
|
||||
_box_selecting = false
|
||||
_gizmos.set_box_rect(Rect2())
|
||||
@@ -3040,7 +3228,8 @@ func _refresh_status() -> void:
|
||||
if sel.is_empty():
|
||||
sel_text = "none"
|
||||
elif sel.size() == 1:
|
||||
sel_text = String(sel[0].name)
|
||||
var selected_stage_name: String = "" if _object_identity == null else _object_identity.get_name(sel[0])
|
||||
sel_text = selected_stage_name if not selected_stage_name.is_empty() else "Stage object"
|
||||
else:
|
||||
sel_text = "%d objects" % sel.size()
|
||||
var text := "%s | Objects: %d | Selected: %s" % [_scene_name, count, sel_text]
|
||||
@@ -3195,7 +3384,11 @@ func _build_ui() -> void:
|
||||
_modal_views = STAGE_MODAL_VIEWS_SCRIPT.new() as StageModalViews
|
||||
_modal_views.configure(_sandbox_theme as SandboxTheme, _ui_font)
|
||||
_modal_views.object_selected.connect(_on_objects_here_selected)
|
||||
_modal_views.candidate_preview_changed.connect(_on_objects_here_candidate_preview_changed)
|
||||
_modal_views.objects_here_dismissed.connect(_on_objects_here_dismissed)
|
||||
_build_prop_settings_view(ui)
|
||||
_build_character_settings_view(ui)
|
||||
_build_placement_name_prompt(ui)
|
||||
_build_scenery_inspector(ui)
|
||||
_build_set_down_cancel_button(ui)
|
||||
_build_use_target_capture_bar(ui)
|
||||
@@ -3357,11 +3550,68 @@ func _build_prop_settings_view(ui: CanvasLayer) -> void:
|
||||
_prop_settings_view.spot_rename_requested.connect(_on_spot_rename_requested)
|
||||
_prop_settings_view.spot_erase_requested.connect(_on_spot_erase_requested)
|
||||
_prop_settings_view.spot_selected.connect(_on_spot_selected)
|
||||
_prop_settings_view.close_requested.connect(_on_prop_settings_close_requested)
|
||||
_prop_settings_view.rename_requested.connect(_on_prop_rename_requested)
|
||||
ui.add_child(_prop_settings_view)
|
||||
_prop_settings_view.update_layout(_prop_settings_safe_rect())
|
||||
|
||||
|
||||
func _build_character_settings_view(ui: CanvasLayer) -> void:
|
||||
_character_settings_view = STAGE_CHARACTER_SETTINGS_VIEW_SCRIPT.new() as StageCharacterSettingsView
|
||||
_character_settings_view.configure(_sandbox_theme as SandboxTheme)
|
||||
_character_settings_view.rename_requested.connect(_on_character_rename_requested)
|
||||
ui.add_child(_character_settings_view)
|
||||
_character_settings_view.update_layout(_prop_settings_safe_rect())
|
||||
|
||||
|
||||
func _build_placement_name_prompt(ui: CanvasLayer) -> void:
|
||||
_placement_name_prompt_panel = _make_placement_feedback_panel("PlacementNamePromptPanel")
|
||||
_placement_name_prompt_panel.visible = false
|
||||
ui.add_child(_placement_name_prompt_panel)
|
||||
_placement_name_prompt = Label.new()
|
||||
_placement_name_prompt.name = "PlacementNamePrompt"
|
||||
_placement_name_prompt.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_placement_name_prompt.add_theme_font_size_override("font_size", 18)
|
||||
_placement_name_prompt.add_theme_color_override("font_color", Color.WHITE)
|
||||
_placement_name_prompt.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
if _ui_font != null:
|
||||
_placement_name_prompt.add_theme_font_override("font", _ui_font)
|
||||
_placement_name_prompt_panel.add_child(_placement_name_prompt)
|
||||
_placement_feedback_panel = _make_placement_feedback_panel("PlacementFeedbackPanel")
|
||||
_placement_feedback_panel.visible = false
|
||||
ui.add_child(_placement_feedback_panel)
|
||||
_placement_feedback_label = Label.new()
|
||||
_placement_feedback_label.name = "PlacementFeedback"
|
||||
_placement_feedback_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_placement_feedback_label.add_theme_font_size_override("font_size", 18)
|
||||
_placement_feedback_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
_placement_feedback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
if _ui_font != null:
|
||||
_placement_feedback_label.add_theme_font_override("font", _ui_font)
|
||||
_placement_feedback_panel.add_child(_placement_feedback_label)
|
||||
|
||||
|
||||
func _make_placement_feedback_panel(panel_name: String) -> PanelContainer:
|
||||
var panel: PanelContainer = PanelContainer.new()
|
||||
panel.name = panel_name
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var accent: Color = Color("#22C6FF")
|
||||
var surface: Color = Color("#182432F2")
|
||||
if _sandbox_theme != null:
|
||||
accent = _sandbox_theme.color("mode_colors.edit", accent)
|
||||
surface = _sandbox_theme.color("stage_panels.shell.surface_color", surface)
|
||||
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
style.bg_color = surface
|
||||
style.border_color = accent
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(10)
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 6.0
|
||||
style.content_margin_bottom = 6.0
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
return panel
|
||||
|
||||
|
||||
func _build_scenery_inspector(ui: CanvasLayer) -> void:
|
||||
_scenery_inspector = STAGE_INSPECTOR_PANEL_SCRIPT.new() as StageInspectorPanel
|
||||
_scenery_inspector.name = "SceneryRepairCard"
|
||||
@@ -4157,7 +4407,7 @@ func _handle_use_target_click(world_position: Vector2, screen_position: Vector2)
|
||||
_show_toast("Click a prop to use")
|
||||
return
|
||||
if prop_candidates.size() > 1:
|
||||
_show_objects_here_popup(prop_candidates)
|
||||
_show_objects_here_popup(prop_candidates, &"direct_use_prop", world_position)
|
||||
return
|
||||
_select_use_target_prop(prop_candidates[0] as PropBlock, screen_position)
|
||||
return
|
||||
@@ -4560,9 +4810,7 @@ func _refresh_prop_settings_card() -> void:
|
||||
return
|
||||
var prop: PropBlock = selected[0] as PropBlock
|
||||
var roles: Dictionary = PROP_STAGE_ROLES.normalize(prop.stage_roles)
|
||||
var display_name: String = prop.spawn_id if not prop.spawn_id.is_empty() else "Prop"
|
||||
var entry: Dictionary = PROP_LIBRARY.get_entry(prop.spawn_id)
|
||||
display_name = String(entry.get("name", entry.get("label", display_name)))
|
||||
var display_name: String = prop.stage_name if not prop.stage_name.is_empty() else "Prop"
|
||||
_prop_settings_view.show_prop(prop, display_name, roles, safe_rect, selected, _spot_selected_id)
|
||||
if not _spot_edit_active and not _use_target_active and _use_spot_overlay != null:
|
||||
_use_spot_overlay.show_edit(prop, Vector2.INF, "", _spot_selected_id)
|
||||
@@ -4571,11 +4819,45 @@ func _refresh_prop_settings_card() -> void:
|
||||
func _prop_settings_safe_rect() -> Rect2:
|
||||
var viewport: Rect2 = get_viewport_rect()
|
||||
var inset: float = float(_stage_panel_integer("shell.edge_inset", 12, 0, 48))
|
||||
var top_offset: float = minf(TOP_BAR_HEIGHT + 116.0, maxf(12.0, viewport.size.y - 220.0))
|
||||
var top_offset: float = minf(TOP_BAR_HEIGHT + 120.0, maxf(12.0, viewport.size.y - 220.0))
|
||||
var bottom_limit: float = maxf(top_offset + 72.0, viewport.size.y - STATUS_BAR_HEIGHT - 12.0)
|
||||
return Rect2(Vector2(0.0, top_offset - inset), Vector2(viewport.size.x, bottom_limit - top_offset + inset * 2.0))
|
||||
|
||||
|
||||
func _refresh_character_settings_card() -> void:
|
||||
if _character_settings_view == null or _selection == null:
|
||||
return
|
||||
var selected: Array[Node2D] = _selection.get_selected()
|
||||
if current_mode != StageMode.EDIT or selected.size() != 1 or not selected[0] is STICKMAN_RIG:
|
||||
_character_settings_view.hide_card(true)
|
||||
return
|
||||
_character_settings_view.show_character(selected[0] as StickmanRig, _prop_settings_safe_rect())
|
||||
|
||||
|
||||
func _on_prop_rename_requested(node: Node2D, requested_name: String) -> void:
|
||||
if current_mode != StageMode.EDIT or _selection == null or _selection.get_selected().size() != 1 or _selection.get_selected()[0] != node:
|
||||
return
|
||||
var previous_name: String = String(node.get("stage_name"))
|
||||
var result: Dictionary = _object_identity.rename(node, requested_name, _world)
|
||||
_prop_settings_view.show_name_result(result)
|
||||
if bool(result.get("ok", false)):
|
||||
if previous_name != String(result.get("name", "")):
|
||||
_mark_scene_dirty()
|
||||
_refresh_prop_settings_card()
|
||||
|
||||
|
||||
func _on_character_rename_requested(node: Node2D, requested_name: String) -> void:
|
||||
if current_mode != StageMode.EDIT or _selection == null or _selection.get_selected().size() != 1 or _selection.get_selected()[0] != node:
|
||||
return
|
||||
var previous_name: String = String(node.get("stage_name"))
|
||||
var result: Dictionary = _object_identity.rename(node, requested_name, _world)
|
||||
_character_settings_view.show_name_result(result)
|
||||
if bool(result.get("ok", false)):
|
||||
if previous_name != String(result.get("name", "")):
|
||||
_mark_scene_dirty()
|
||||
_refresh_character_settings_card()
|
||||
|
||||
|
||||
func _position_cancel_button_after_prop_choice(after_choice: bool) -> void:
|
||||
if _set_down_cancel_button == null:
|
||||
return
|
||||
@@ -4584,15 +4866,6 @@ func _position_cancel_button_after_prop_choice(after_choice: bool) -> void:
|
||||
_set_down_cancel_button.offset_right = left + 132.0
|
||||
|
||||
|
||||
func _on_prop_settings_close_requested() -> void:
|
||||
if _prop_settings_view == null or _prop_settings_active_key.is_empty():
|
||||
return
|
||||
_prop_settings_view.set_dismissed(true)
|
||||
_refresh_prop_settings_card()
|
||||
if _prop_settings_reopen_button != null and _prop_settings_reopen_button.visible:
|
||||
_prop_settings_reopen_button.grab_focus()
|
||||
|
||||
|
||||
func _on_prop_settings_choice_selected(id: StringName, key: String) -> void:
|
||||
var value: String = String(id)
|
||||
if key == "depth":
|
||||
@@ -5864,8 +6137,7 @@ func _handle_scenery_edit_click(event: InputEventMouseButton, world_position: Ve
|
||||
_gizmos.set_box_rect(_box_rect)
|
||||
return
|
||||
if candidates.size() > 1 and not event.shift_pressed and not _scenery_multi_select:
|
||||
_selection.select_only(hit)
|
||||
_show_objects_here_popup(candidates)
|
||||
_show_objects_here_popup(candidates, &"scenery_select", world_position)
|
||||
return
|
||||
if event.shift_pressed or _scenery_multi_select:
|
||||
_selection.toggle_selection(hit)
|
||||
@@ -5962,6 +6234,10 @@ func _on_grid_size_changed(value: float) -> void:
|
||||
|
||||
|
||||
func _on_selection_changed(nodes: Array[Node2D]) -> void:
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup):
|
||||
_dismiss_active_objects_here()
|
||||
else:
|
||||
_gizmos.set_candidate_preview(null)
|
||||
_cancel_edit_wobble_preview()
|
||||
if _scenery_controller != null and _scenery_controller.is_single_transform_active() and not nodes.has(_scenery_controller.get_single_transform_node()):
|
||||
_scenery_controller.cancel_single_transform()
|
||||
@@ -5975,6 +6251,7 @@ func _on_selection_changed(nodes: Array[Node2D]) -> void:
|
||||
object_selected.emit(nodes)
|
||||
_refresh_status()
|
||||
_refresh_prop_settings_card()
|
||||
_refresh_character_settings_card()
|
||||
if _scenery_edit_mode:
|
||||
_refresh_scenery_inspector()
|
||||
_refresh_scenery_capture_bar()
|
||||
@@ -6016,20 +6293,106 @@ func _remove_prop_contact_exceptions(prop: PropBlock) -> void:
|
||||
(node as PropBlock).remove_collision_exception_with(prop)
|
||||
|
||||
|
||||
func _show_objects_here_popup(candidates: Array[Node2D]) -> void:
|
||||
if _modal_views == null:
|
||||
func _show_objects_here_popup(candidates: Array[Node2D], context: StringName, world_position: Vector2) -> void:
|
||||
if _modal_views == null or candidates.size() < 2:
|
||||
return
|
||||
var ui_layer: CanvasLayer = get_node_or_null("UI") as CanvasLayer
|
||||
if ui_layer == null:
|
||||
return
|
||||
_modal_views.show_objects_here(ui_layer, candidates, _selection.get_selected(), get_viewport_rect().size, get_viewport().get_mouse_position())
|
||||
_dismiss_active_objects_here()
|
||||
if _placement_feedback_panel != null:
|
||||
_placement_feedback_panel.visible = false
|
||||
_placement_feedback_timer = 0.0
|
||||
_placement_feedback_node = null
|
||||
_objects_here_candidates = candidates.duplicate()
|
||||
_objects_here_context = context
|
||||
_objects_here_hit_position = world_position
|
||||
_gizmos.set_candidate_preview(null)
|
||||
var click_screen_position: Vector2 = get_viewport().get_canvas_transform() * world_position
|
||||
var avoid_rects: Array[Rect2] = []
|
||||
var viewport_size: Vector2 = get_viewport_rect().size
|
||||
avoid_rects.append(Rect2(Vector2.ZERO, Vector2(viewport_size.x, TOP_BAR_HEIGHT + 8.0)))
|
||||
avoid_rects.append(Rect2(Vector2(0.0, viewport_size.y - STATUS_BAR_HEIGHT - 4.0), Vector2(viewport_size.x, STATUS_BAR_HEIGHT + 4.0)))
|
||||
avoid_rects.append(Rect2(Vector2.ZERO, Vector2(120.0, viewport_size.y)))
|
||||
if _mode_badge != null and _mode_badge.visible:
|
||||
avoid_rects.append(_mode_badge.get_global_rect().grow(8.0))
|
||||
if _palette_dock != null and _palette_dock.visible:
|
||||
avoid_rects.append(_palette_dock.get_global_rect().grow(8.0))
|
||||
if _placement_name_prompt_panel != null and _placement_name_prompt_panel.visible:
|
||||
avoid_rects.append(_placement_name_prompt_panel.get_global_rect().grow(8.0))
|
||||
if context == &"direct_use_prop" and _use_target_capture_bar != null and _use_target_capture_bar.visible:
|
||||
avoid_rects.append(_use_target_capture_bar.get_global_rect().grow(8.0))
|
||||
if _prop_settings_view != null:
|
||||
var prop_card: Control = _prop_settings_view.get_card()
|
||||
if prop_card != null and prop_card.visible:
|
||||
avoid_rects.append(prop_card.get_global_rect().grow(8.0))
|
||||
if _character_settings_view != null:
|
||||
var character_card: Control = _character_settings_view.get_card()
|
||||
if character_card != null and character_card.visible:
|
||||
avoid_rects.append(character_card.get_global_rect().grow(8.0))
|
||||
for candidate: Node2D in _objects_here_candidates:
|
||||
var candidate_bounds: Rect2 = STAGE_SELECTION.get_world_aabb(candidate)
|
||||
if candidate_bounds.has_area():
|
||||
avoid_rects.append(_world_rect_to_viewport_rect(candidate_bounds).grow(8.0))
|
||||
_modal_views.show_objects_here(
|
||||
ui_layer,
|
||||
_objects_here_candidates,
|
||||
_selection.get_selected().duplicate(),
|
||||
get_viewport_rect().size,
|
||||
click_screen_position,
|
||||
avoid_rects
|
||||
)
|
||||
var popup_rect: Rect2 = _modal_views.get_objects_here_screen_rect()
|
||||
if popup_rect.has_area():
|
||||
avoid_rects.append(popup_rect.grow(8.0))
|
||||
_gizmos.set_candidate_preview_obstacles(avoid_rects)
|
||||
|
||||
|
||||
func _on_objects_here_selected(node: Node2D) -> void:
|
||||
if node != null and is_instance_valid(node) and node.get_parent() == _world:
|
||||
_selection.select_only(node)
|
||||
if _use_target_active and _use_target_step == 0 and node is PropBlock:
|
||||
call_deferred("_select_use_target_prop", node, get_viewport().get_mouse_position())
|
||||
var context: StringName = _objects_here_context
|
||||
if node == null or not is_instance_valid(node) or node.is_queued_for_deletion() or node.get_parent() != _world or not _objects_here_candidates.has(node):
|
||||
_modal_views.dismiss_objects_here()
|
||||
return
|
||||
var still_hit: bool = _selection.hit_test_all(_objects_here_hit_position).has(node)
|
||||
if not still_hit:
|
||||
_modal_views.dismiss_objects_here()
|
||||
return
|
||||
match context:
|
||||
&"edit_select":
|
||||
_selection.select_only(node)
|
||||
_show_toast("Drag to move · Click to choose")
|
||||
&"scenery_select":
|
||||
_selection.select_only(node)
|
||||
&"direct_use_prop":
|
||||
if current_mode == StageMode.DIRECT and _use_target_active and _use_target_step == 0 and node is PropBlock:
|
||||
_select_use_target_prop(node as PropBlock, get_viewport().get_mouse_position())
|
||||
else:
|
||||
_modal_views.dismiss_objects_here()
|
||||
_:
|
||||
_modal_views.dismiss_objects_here()
|
||||
|
||||
|
||||
func _on_objects_here_candidate_preview_changed(node: Node2D) -> void:
|
||||
if node != null and (not is_instance_valid(node) or not _objects_here_candidates.has(node)):
|
||||
node = null
|
||||
_gizmos.set_candidate_preview(node)
|
||||
|
||||
|
||||
func _on_objects_here_dismissed() -> void:
|
||||
_gizmos.set_candidate_preview(null)
|
||||
_gizmos.set_candidate_preview_obstacles([])
|
||||
_objects_here_candidates.clear()
|
||||
_objects_here_context = &""
|
||||
_objects_here_hit_position = Vector2.ZERO
|
||||
|
||||
|
||||
func _dismiss_active_objects_here() -> void:
|
||||
if _modal_views == null:
|
||||
return
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup):
|
||||
_modal_views.dismiss_objects_here()
|
||||
else:
|
||||
_on_objects_here_dismissed()
|
||||
|
||||
|
||||
func _select_use_target_prop(prop: PropBlock, _screen_position: Vector2) -> void:
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
class_name StageCharacterSettingsView
|
||||
extends Control
|
||||
## Presentation-only selected-character name card for the Sandbox Stage.
|
||||
|
||||
const STAGE_INSPECTOR_PANEL := preload("res://scripts/shared/stage_ui/stage_inspector_panel.gd")
|
||||
const STICKMAN_RIG := preload("res://scripts/characters/stickman/stickman_rig.gd")
|
||||
|
||||
signal rename_requested(node: Node2D, requested_name: String)
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _card: StageInspectorPanel
|
||||
var _name_section: VBoxContainer
|
||||
var _name_edit: LineEdit
|
||||
var _done_button: Button
|
||||
var _cancel_button: Button
|
||||
var _feedback: Label
|
||||
var _character: StickmanRig
|
||||
var _active_selection_key: String = ""
|
||||
var _editing: bool = false
|
||||
|
||||
|
||||
func configure(stage_theme: SandboxTheme) -> void:
|
||||
_stage_theme = stage_theme
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_build()
|
||||
if not get_viewport().size_changed.is_connected(_on_viewport_resized):
|
||||
get_viewport().size_changed.connect(_on_viewport_resized)
|
||||
_on_viewport_resized()
|
||||
|
||||
|
||||
func show_character(rig: StickmanRig, safe_rect: Rect2) -> void:
|
||||
if rig == null or not is_instance_valid(rig):
|
||||
hide_card()
|
||||
return
|
||||
var selection_key: String = str(rig.get_instance_id())
|
||||
if selection_key != _active_selection_key:
|
||||
_active_selection_key = selection_key
|
||||
_editing = false
|
||||
_feedback.visible = false
|
||||
_character = rig
|
||||
_name_edit.text = rig.stage_name
|
||||
_name_section.visible = false
|
||||
_card.set_body_visible(false)
|
||||
_card.visible = true
|
||||
_character = rig
|
||||
_card.set_safe_rect(safe_rect)
|
||||
_card.set_header(rig.stage_name, "🧍")
|
||||
_card.set_rename_enabled(true)
|
||||
_card.set_close_visible(false)
|
||||
if not _editing:
|
||||
_name_edit.text = rig.stage_name
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func show_name_result(result: Dictionary) -> void:
|
||||
if bool(result.get("ok", false)):
|
||||
var accepted_name: String = String(result.get("name", ""))
|
||||
_name_edit.text = accepted_name
|
||||
_feedback.visible = false
|
||||
if _character != null and is_instance_valid(_character):
|
||||
_card.set_header(accepted_name, "🧍")
|
||||
_set_editing(false)
|
||||
else:
|
||||
var reason: StringName = StringName(String(result.get("reason", "")))
|
||||
var suggestion: String = String(result.get("suggestion", ""))
|
||||
if reason == &"blank":
|
||||
_feedback.text = "Give this one a name. Try %s." % suggestion
|
||||
else:
|
||||
_feedback.text = "%s is already here. Try %s." % [String(result.get("name", "")), suggestion]
|
||||
_feedback.visible = true
|
||||
_set_editing(true)
|
||||
_card.request_fit()
|
||||
if not bool(result.get("ok", false)):
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_name_edit.call_deferred("select_all")
|
||||
|
||||
|
||||
func hide_card(reset_selection: bool = false) -> void:
|
||||
if reset_selection:
|
||||
_active_selection_key = ""
|
||||
_set_editing(false)
|
||||
_feedback.visible = false
|
||||
_character = null
|
||||
if _card != null:
|
||||
_card.visible = false
|
||||
|
||||
|
||||
func get_card() -> Control:
|
||||
return _card
|
||||
|
||||
|
||||
func focus_card() -> void:
|
||||
if _card != null and _card.visible:
|
||||
_card.focus_first_control()
|
||||
|
||||
|
||||
func update_layout(safe_rect: Rect2) -> void:
|
||||
if _card != null:
|
||||
_card.set_safe_rect(safe_rect)
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_card = STAGE_INSPECTOR_PANEL.new() as StageInspectorPanel
|
||||
_card.name = "CharacterSettingsCard"
|
||||
_card.visible = false
|
||||
add_child(_card)
|
||||
_card.apply_theme(_stage_theme)
|
||||
_card.set_rename_enabled(true)
|
||||
_card.set_close_visible(false)
|
||||
_card.set_body_visible(false)
|
||||
_card.set_title_single_line(true)
|
||||
_card.rename_requested.connect(_start_editing)
|
||||
var body: VBoxContainer = _card.body()
|
||||
body.add_theme_constant_override("separation", _stage_panel_integer("shell.section_gap", 12, 4, 32))
|
||||
_name_section = VBoxContainer.new()
|
||||
_name_section.name = "StageNameEditor"
|
||||
_name_section.add_theme_constant_override("separation", 6)
|
||||
_name_section.visible = false
|
||||
body.add_child(_name_section)
|
||||
_name_edit = LineEdit.new()
|
||||
_name_edit.name = "StageNameEdit"
|
||||
_name_edit.placeholder_text = "Character name"
|
||||
_name_edit.custom_minimum_size.y = float(_stage_panel_integer("action.min_target_size", 48, 44, 96))
|
||||
_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_name_edit.visible = false
|
||||
_name_edit.text_submitted.connect(func(_value: String) -> void: _submit_name())
|
||||
_name_edit.gui_input.connect(_on_name_edit_input)
|
||||
_apply_font(_name_edit)
|
||||
_name_section.add_child(_name_edit)
|
||||
_feedback = Label.new()
|
||||
_feedback.name = "StageNameFeedback"
|
||||
_feedback.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_feedback.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32))
|
||||
_feedback.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0")))
|
||||
_feedback.visible = false
|
||||
_apply_font(_feedback)
|
||||
_name_section.add_child(_feedback)
|
||||
var actions: HBoxContainer = HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 8)
|
||||
_name_section.add_child(actions)
|
||||
_done_button = Button.new()
|
||||
_done_button.name = "DoneStageName"
|
||||
_done_button.text = "Done"
|
||||
_done_button.custom_minimum_size = Vector2(96.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96)))
|
||||
_done_button.visible = false
|
||||
_done_button.pressed.connect(_submit_name)
|
||||
_card.style_primary_action_button(_done_button)
|
||||
actions.add_child(_done_button)
|
||||
_cancel_button = Button.new()
|
||||
_cancel_button.name = "CancelStageName"
|
||||
_cancel_button.text = "Cancel"
|
||||
_cancel_button.custom_minimum_size = Vector2(96.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96)))
|
||||
_cancel_button.visible = false
|
||||
_cancel_button.pressed.connect(_cancel_editing)
|
||||
_card.style_action_button(_cancel_button, false)
|
||||
actions.add_child(_cancel_button)
|
||||
|
||||
|
||||
func _start_editing() -> void:
|
||||
if _character == null or not is_instance_valid(_character):
|
||||
return
|
||||
_name_edit.text = _character.stage_name
|
||||
_feedback.visible = false
|
||||
_set_editing(true)
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_name_edit.call_deferred("select_all")
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func _set_editing(editing: bool) -> void:
|
||||
_editing = editing
|
||||
if _name_section == null:
|
||||
return
|
||||
_name_section.visible = editing
|
||||
_card.set_body_visible(editing)
|
||||
_name_edit.visible = editing
|
||||
_done_button.visible = editing
|
||||
_cancel_button.visible = editing
|
||||
|
||||
|
||||
func _submit_name() -> void:
|
||||
if _character != null and is_instance_valid(_character):
|
||||
rename_requested.emit(_character, _name_edit.text)
|
||||
|
||||
|
||||
func _cancel_editing() -> void:
|
||||
if _character != null and is_instance_valid(_character):
|
||||
_name_edit.text = _character.stage_name
|
||||
_feedback.visible = false
|
||||
_set_editing(false)
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func _on_name_edit_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and (event as InputEventKey).pressed and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
_cancel_editing()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_viewport_resized() -> void:
|
||||
var viewport: Rect2 = get_viewport_rect()
|
||||
var top_offset: float = minf(64.0 + 120.0, maxf(12.0, viewport.size.y - 220.0))
|
||||
var bottom_limit: float = maxf(top_offset + 72.0, viewport.size.y - 38.0 - 12.0)
|
||||
var inset: float = float(_stage_panel_integer("shell.edge_inset", 12, 0, 48))
|
||||
update_layout(Rect2(Vector2(0.0, top_offset - inset), Vector2(viewport.size.x, bottom_limit - top_offset + inset * 2.0)))
|
||||
|
||||
|
||||
func _stage_panel_integer(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||
return fallback if _stage_theme == null else _stage_theme.integer("stage_panels." + key, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _stage_panel_color(key: String, fallback: Color) -> Color:
|
||||
return fallback if _stage_theme == null else _stage_theme.color("stage_panels." + key, fallback)
|
||||
|
||||
|
||||
func _apply_font(control: Control) -> void:
|
||||
if _stage_theme == null:
|
||||
return
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
control.add_theme_font_override("font", font)
|
||||
@@ -0,0 +1 @@
|
||||
uid://fqbgu2edqtpd
|
||||
@@ -9,11 +9,13 @@ const SPEECH_COMPOSER_SCENE := preload("res://scenes/editors/stage/speech_compos
|
||||
const SPEECH_STYLE_TOOLBAR_SCRIPT := preload("res://scripts/speech/speech_style_toolbar.gd")
|
||||
const SPEECH_SHOW_TOOLBAR_SCRIPT := preload("res://scripts/speech/speech_show_toolbar.gd")
|
||||
const WAYPOINT_CONTEXT_SCRIPT := preload("res://scripts/stage/waypoint_context.gd")
|
||||
const PROP_LIBRARY_SCRIPT := preload("res://scripts/props/prop_library.gd")
|
||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||
const MISSING_PROP_PLACEHOLDER_SCRIPT := preload("res://scripts/props/missing_prop_placeholder.gd")
|
||||
const MISSING_STICKMAN_PLACEHOLDER_SCRIPT := preload("res://scripts/characters/stickman/missing_stickman_placeholder.gd")
|
||||
const DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||
|
||||
signal object_selected(node: Node2D)
|
||||
signal candidate_preview_changed(node: Node2D)
|
||||
signal objects_here_dismissed
|
||||
|
||||
var speak_dialog: AcceptDialog
|
||||
var speak_edit: LineEdit
|
||||
@@ -41,6 +43,8 @@ var speech_show_toolbar: Control
|
||||
var speech_recovery_dialog: ConfirmationDialog
|
||||
var speech_destination_dialog: ConfirmationDialog
|
||||
var _objects_here_popup: PopupPanel
|
||||
var _objects_here_guard: Control
|
||||
var _objects_here_preview: Node2D
|
||||
var _speak_box: VBoxContainer
|
||||
var _sandbox_theme: SandboxTheme
|
||||
var _ui_font: Font
|
||||
@@ -237,12 +241,43 @@ func show_objects_here(
|
||||
candidates: Array[Node2D],
|
||||
selected_nodes: Array[Node2D],
|
||||
viewport_size: Vector2,
|
||||
pointer_position: Vector2
|
||||
pointer_position: Vector2,
|
||||
avoid_rects: Array[Rect2] = []
|
||||
) -> void:
|
||||
if parent == null or candidates.size() < 2:
|
||||
return
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup):
|
||||
_objects_here_popup.queue_free()
|
||||
_objects_here_popup.hide()
|
||||
var content_inset: int = _objects_here_integer("content_inset", 12, 8, 24)
|
||||
var edge_inset: int = _objects_here_integer("edge_inset", 8, 0, 32)
|
||||
var safe_top: int = _objects_here_integer("safe_top", 120, 0, 240)
|
||||
var safe_bottom: int = _objects_here_integer("safe_bottom", 48, 0, 160)
|
||||
var preferred_width: int = _objects_here_integer("preferred_width", 360, 220, 720)
|
||||
var popup_width: int = mini(preferred_width, maxi(1, int(viewport_size.x) - edge_inset * 2))
|
||||
var content_width: int = maxi(1, popup_width - content_inset * 2)
|
||||
var row_height: int = _objects_here_integer("row_height", 56, 44, 96)
|
||||
var row_gap: int = _objects_here_integer("row_gap", 8, 2, 24)
|
||||
var header_gap: int = _objects_here_integer("header_gap", 8, 4, 24)
|
||||
var footer_gap: int = _objects_here_integer("footer_gap", 8, 4, 24)
|
||||
var cancel_height: int = _objects_here_integer("cancel_height", 48, 44, 96)
|
||||
var heading_font_size: int = _objects_here_integer("heading_font_size", 20, 16, 32)
|
||||
var fixed_content_height: float = float(heading_font_size + header_gap + cancel_height + footer_gap + content_inset * 2)
|
||||
var row_content_height: float = float(candidates.size() * row_height + maxi(0, candidates.size() - 1) * row_gap)
|
||||
var safe_popup_height: float = maxf(180.0, viewport_size.y - float(safe_top + safe_bottom))
|
||||
var minimum_rows_height: float = float(_objects_here_integer("minimum_rows_height", 116, 56, 240))
|
||||
var max_rows_height: float = minf(
|
||||
float(_objects_here_integer("max_rows_height", 320, 116, 640)),
|
||||
maxf(minimum_rows_height, safe_popup_height - fixed_content_height)
|
||||
)
|
||||
var rows_height: float = minf(max_rows_height, row_content_height)
|
||||
var popup_height: float = minf(safe_popup_height, fixed_content_height + rows_height)
|
||||
var guard: Control = Control.new()
|
||||
guard.name = "ObjectsHereInputGuard"
|
||||
guard.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
guard.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
guard.gui_input.connect(_on_objects_here_guard_input.bind(guard))
|
||||
parent.add_child(guard)
|
||||
_objects_here_guard = guard
|
||||
var popup: PopupPanel = PopupPanel.new()
|
||||
popup.name = "ObjectsHere"
|
||||
popup.exclusive = false
|
||||
@@ -250,67 +285,334 @@ func show_objects_here(
|
||||
popup.unfocusable = false
|
||||
if _sandbox_theme != null:
|
||||
popup.theme = _sandbox_theme.control_theme()
|
||||
var shell: StyleBoxFlat = _sandbox_theme.stylebox("popups.anchored_chooser") if _sandbox_theme != null else StyleBoxFlat.new()
|
||||
shell.set_corner_radius_all(_objects_here_integer("corner_radius", 14, 8, 24))
|
||||
shell.set_border_width_all(_objects_here_integer("border_width", 2, 1, 4))
|
||||
shell.bg_color = _objects_here_color("popups.anchored_chooser.bg_color", Color("#202633FA"))
|
||||
shell.border_color = _objects_here_color("popups.anchored_chooser.border_color", Color("#667386"))
|
||||
shell.shadow_size = _objects_here_integer("shadow_size", 6, 0, 24)
|
||||
shell.shadow_color = _objects_here_color("popups.anchored_chooser.shadow_color", Color("#00000080"))
|
||||
shell.shadow_offset = _objects_here_vector2("shadow_offset", Vector2(0.0, 4.0))
|
||||
# The child MarginContainer owns the content inset so the shell adds one clean gutter.
|
||||
shell.content_margin_left = 0.0
|
||||
shell.content_margin_right = 0.0
|
||||
shell.content_margin_top = 0.0
|
||||
shell.content_margin_bottom = 0.0
|
||||
popup.add_theme_stylebox_override("panel", shell)
|
||||
parent.add_child(popup)
|
||||
var margin: MarginContainer = MarginContainer.new()
|
||||
margin.name = "ObjectsHereMargin"
|
||||
for side: String in ["left", "right", "top", "bottom"]:
|
||||
margin.add_theme_constant_override("margin_" + side, 8)
|
||||
margin.add_theme_constant_override("margin_" + side, content_inset)
|
||||
popup.add_child(margin)
|
||||
var column: VBoxContainer = VBoxContainer.new()
|
||||
column.add_theme_constant_override("separation", 4)
|
||||
column.name = "ObjectsHereColumn"
|
||||
column.add_theme_constant_override("separation", 0)
|
||||
margin.add_child(column)
|
||||
var heading: Label = Label.new()
|
||||
heading.text = "Objects here · Choose an object"
|
||||
heading.name = "ObjectsHereHeading"
|
||||
heading.text = "Objects here · Choose one"
|
||||
if _ui_font != null:
|
||||
heading.add_theme_font_override("font", _ui_font)
|
||||
heading.add_theme_font_size_override("font_size", heading_font_size)
|
||||
heading.add_theme_color_override("font_color", _objects_here_color("global.text_color", Color("#F5F7FA")))
|
||||
column.add_child(heading)
|
||||
var header_spacer: Control = Control.new()
|
||||
header_spacer.custom_minimum_size.y = float(header_gap)
|
||||
header_spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
column.add_child(header_spacer)
|
||||
var scroll: ScrollContainer = ScrollContainer.new()
|
||||
scroll.name = "ObjectsHereScroll"
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.custom_minimum_size = Vector2(300.0, minf(360.0, float(candidates.size()) * 48.0))
|
||||
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED if row_content_height <= rows_height else ScrollContainer.SCROLL_MODE_AUTO
|
||||
scroll.custom_minimum_size = Vector2(float(content_width), rows_height)
|
||||
column.add_child(scroll)
|
||||
var rows: VBoxContainer = VBoxContainer.new()
|
||||
rows.name = "ObjectsHereRows"
|
||||
rows.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
rows.add_theme_constant_override("separation", row_gap)
|
||||
scroll.add_child(rows)
|
||||
for node: Node2D in candidates:
|
||||
for candidate_index: int in candidates.size():
|
||||
var node: Node2D = candidates[candidate_index]
|
||||
if node == null or not is_instance_valid(node):
|
||||
continue
|
||||
var button: Button = Button.new()
|
||||
var selected_mark: String = "✓ " if selected_nodes.has(node) else ""
|
||||
button.text = selected_mark + _object_here_label(node)
|
||||
button.custom_minimum_size = Vector2(300.0, 44.0)
|
||||
button.name = "ObjectCandidate_%d" % candidate_index
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.custom_minimum_size = Vector2(float(content_width), float(row_height))
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_style_objects_here_candidate(button, selected_nodes.has(node))
|
||||
var row_content: HBoxContainer = HBoxContainer.new()
|
||||
row_content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var row_padding_x: float = float(_objects_here_integer("row_padding_x", 12, 4, 24))
|
||||
var row_padding_y: float = float(_objects_here_integer("row_padding_y", 6, 0, 20))
|
||||
row_content.offset_left = row_padding_x
|
||||
row_content.offset_top = row_padding_y
|
||||
row_content.offset_right = -row_padding_x
|
||||
row_content.offset_bottom = -row_padding_y
|
||||
row_content.add_theme_constant_override("separation", 8)
|
||||
row_content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
button.add_child(row_content)
|
||||
var name_label: Label = Label.new()
|
||||
name_label.text = _object_here_label(node)
|
||||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if _ui_font != null:
|
||||
name_label.add_theme_font_override("font", _ui_font)
|
||||
name_label.add_theme_font_size_override("font_size", _objects_here_font_size())
|
||||
row_content.add_child(name_label)
|
||||
if selected_nodes.has(node):
|
||||
row_content.add_child(_make_selected_candidate_badge())
|
||||
button.pressed.connect(_on_objects_here_selected.bind(node))
|
||||
button.mouse_entered.connect(_set_candidate_preview.bind(node))
|
||||
button.mouse_exited.connect(_clear_row_preview.bind(button, node))
|
||||
button.focus_entered.connect(_set_candidate_preview.bind(node))
|
||||
button.focus_exited.connect(_clear_row_preview.bind(button, node))
|
||||
rows.add_child(button)
|
||||
var popup_size: Vector2i = Vector2i(
|
||||
mini(360, maxi(260, int(viewport_size.x) - 16)),
|
||||
mini(420, maxi(120, int(viewport_size.y) - 16))
|
||||
var footer_spacer: Control = Control.new()
|
||||
footer_spacer.custom_minimum_size.y = float(footer_gap)
|
||||
footer_spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
column.add_child(footer_spacer)
|
||||
var cancel: Button = Button.new()
|
||||
cancel.name = "CancelObjectsHere"
|
||||
cancel.text = "Cancel"
|
||||
cancel.custom_minimum_size = Vector2(
|
||||
minf(float(content_width), float(_objects_here_integer("cancel_width", 120, 88, 240))),
|
||||
float(cancel_height)
|
||||
)
|
||||
cancel.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
_style_objects_here_cancel(cancel)
|
||||
cancel.pressed.connect(_dismiss_objects_here)
|
||||
column.add_child(cancel)
|
||||
var popup_size: Vector2i = Vector2i(popup_width, int(round(popup_height)))
|
||||
popup.size = popup_size
|
||||
var position: Vector2i = Vector2i(pointer_position + Vector2(12.0, 12.0))
|
||||
position.x = clampi(position.x, 8, maxi(8, int(viewport_size.x) - popup_size.x - 8))
|
||||
position.y = clampi(position.y, 8, maxi(8, int(viewport_size.y) - popup_size.y - 8))
|
||||
var position: Vector2i = _objects_here_popup_position(pointer_position, viewport_size, popup_size, avoid_rects, edge_inset, safe_top, safe_bottom)
|
||||
_objects_here_popup = popup
|
||||
popup.popup_hide.connect(_on_objects_here_popup_closed.bind(popup))
|
||||
popup.close_requested.connect(_dismiss_objects_here)
|
||||
popup.window_input.connect(_on_objects_here_window_input.bind(popup))
|
||||
popup.position = position
|
||||
popup.popup()
|
||||
cancel.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _objects_here_popup_position(
|
||||
pointer_position: Vector2,
|
||||
viewport_size: Vector2,
|
||||
popup_size: Vector2i,
|
||||
avoid_rects: Array[Rect2],
|
||||
edge_inset: int = 8,
|
||||
safe_top: int = 120,
|
||||
safe_bottom: int = 48
|
||||
) -> Vector2i:
|
||||
const POINTER_GAP: float = 12.0
|
||||
var maximum_x: int = maxi(edge_inset, int(viewport_size.x) - popup_size.x - edge_inset)
|
||||
var minimum_y: int = mini(safe_top, maxi(edge_inset, int(viewport_size.y) - popup_size.y - safe_bottom))
|
||||
var maximum_y: int = maxi(minimum_y, int(viewport_size.y) - popup_size.y - safe_bottom)
|
||||
var candidate_positions: Array[Vector2] = [
|
||||
pointer_position + Vector2(POINTER_GAP, POINTER_GAP),
|
||||
Vector2(pointer_position.x - float(popup_size.x) - POINTER_GAP, pointer_position.y + POINTER_GAP),
|
||||
Vector2(pointer_position.x + POINTER_GAP, pointer_position.y - float(popup_size.y) - POINTER_GAP),
|
||||
pointer_position - Vector2(float(popup_size.x) + POINTER_GAP, float(popup_size.y) + POINTER_GAP),
|
||||
]
|
||||
var x_edges: Array[float] = [float(edge_inset), float(maximum_x), pointer_position.x - float(popup_size.x) - POINTER_GAP, pointer_position.x + POINTER_GAP]
|
||||
var y_edges: Array[float] = [float(minimum_y), float(maximum_y), pointer_position.y - float(popup_size.y) - POINTER_GAP, pointer_position.y + POINTER_GAP]
|
||||
for avoid_rect: Rect2 in avoid_rects:
|
||||
if not avoid_rect.has_area():
|
||||
continue
|
||||
var centered_y: float = pointer_position.y - float(popup_size.y) * 0.5
|
||||
var centered_x: float = pointer_position.x - float(popup_size.x) * 0.5
|
||||
candidate_positions.append(Vector2(avoid_rect.position.x - float(popup_size.x) - POINTER_GAP, centered_y))
|
||||
candidate_positions.append(Vector2(avoid_rect.end.x + POINTER_GAP, centered_y))
|
||||
candidate_positions.append(Vector2(centered_x, avoid_rect.position.y - float(popup_size.y) - POINTER_GAP))
|
||||
candidate_positions.append(Vector2(centered_x, avoid_rect.end.y + POINTER_GAP))
|
||||
candidate_positions.append(Vector2(avoid_rect.end.x + POINTER_GAP, safe_top))
|
||||
candidate_positions.append(Vector2(avoid_rect.position.x - float(popup_size.x) - POINTER_GAP, safe_top))
|
||||
candidate_positions.append(Vector2(avoid_rect.end.x + POINTER_GAP, float(maximum_y)))
|
||||
candidate_positions.append(Vector2(avoid_rect.position.x - float(popup_size.x) - POINTER_GAP, float(maximum_y)))
|
||||
x_edges.append(avoid_rect.position.x - float(popup_size.x) - POINTER_GAP)
|
||||
x_edges.append(avoid_rect.end.x + POINTER_GAP)
|
||||
y_edges.append(avoid_rect.position.y - float(popup_size.y) - POINTER_GAP)
|
||||
y_edges.append(avoid_rect.end.y + POINTER_GAP)
|
||||
for candidate_x: float in x_edges:
|
||||
for candidate_y: float in y_edges:
|
||||
candidate_positions.append(Vector2(candidate_x, candidate_y))
|
||||
var pointer_safety_rect: Rect2 = Rect2(pointer_position - Vector2(28.0, 28.0), Vector2(56.0, 56.0))
|
||||
var best_position: Vector2i = Vector2i(edge_inset, edge_inset)
|
||||
var best_overlap_score: float = INF
|
||||
for candidate: Vector2 in candidate_positions:
|
||||
var clamped_position: Vector2i = Vector2i(
|
||||
clampf(candidate.x, float(edge_inset), float(maximum_x)),
|
||||
clampf(candidate.y, float(minimum_y), float(maximum_y))
|
||||
)
|
||||
var popup_rect: Rect2 = Rect2(Vector2(clamped_position), Vector2(popup_size))
|
||||
var pointer_overlap_area: float = popup_rect.intersection(pointer_safety_rect).get_area()
|
||||
var avoid_overlap_area: float = 0.0
|
||||
for avoid_rect: Rect2 in avoid_rects:
|
||||
if avoid_rect.has_area():
|
||||
avoid_overlap_area += popup_rect.intersection(avoid_rect).get_area()
|
||||
var nearest_point: Vector2 = Vector2(
|
||||
clampf(pointer_position.x, popup_rect.position.x, popup_rect.end.x),
|
||||
clampf(pointer_position.y, popup_rect.position.y, popup_rect.end.y)
|
||||
)
|
||||
var overlap_score: float = pointer_overlap_area * 1000000.0 + avoid_overlap_area * 1000000.0 + nearest_point.distance_to(pointer_position)
|
||||
if overlap_score < best_overlap_score:
|
||||
best_overlap_score = overlap_score
|
||||
best_position = clamped_position
|
||||
return best_position
|
||||
|
||||
|
||||
func _style_objects_here_candidate(button: Button, selected: bool) -> void:
|
||||
if button == null:
|
||||
return
|
||||
var accent: Color = _objects_here_color("accent_color", Color("#22C6FF"))
|
||||
var surface: Color = _objects_here_color("row_surface", Color("#171B24"))
|
||||
var selected_surface: Color = _objects_here_color("selected_surface", Color("#12394A"))
|
||||
var hover_surface: Color = _objects_here_color("hover_surface", Color("#203849"))
|
||||
if _sandbox_theme != null:
|
||||
accent = _sandbox_theme.color("mode_colors.edit", accent)
|
||||
button.add_theme_stylebox_override("normal", _objects_here_button_style(
|
||||
selected_surface if selected else surface,
|
||||
accent if selected else _objects_here_color("row_border_color", Color("#FFFFFF55")),
|
||||
_objects_here_integer("selected_border_width", 2, 1, 4) if selected else _objects_here_integer("row_border_width", 1, 0, 4),
|
||||
_objects_here_integer("row_corner_radius", 12, 4, 24)
|
||||
))
|
||||
button.add_theme_stylebox_override("hover", _objects_here_button_style(hover_surface, accent, _objects_here_integer("hover_border_width", 2, 1, 4), _objects_here_integer("row_corner_radius", 12, 4, 24)))
|
||||
button.add_theme_stylebox_override("pressed", _objects_here_button_style(selected_surface, accent, _objects_here_integer("selected_border_width", 2, 1, 4), _objects_here_integer("row_corner_radius", 12, 4, 24)))
|
||||
button.add_theme_stylebox_override("focus", _objects_here_button_style(Color(0.0, 0.0, 0.0, 0.0), _objects_here_color("focus_color", accent), _objects_here_integer("focus_border_width", 3, 2, 5), _objects_here_integer("row_corner_radius", 12, 4, 24)))
|
||||
var text_color: Color = _objects_here_color("row_text_color", Color.WHITE)
|
||||
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("font_focus_color", text_color)
|
||||
button.add_theme_font_size_override("font_size", _objects_here_font_size())
|
||||
if _ui_font != null:
|
||||
button.add_theme_font_override("font", _ui_font)
|
||||
|
||||
|
||||
func _objects_here_button_style(background: Color, border: Color, border_width: int, corner_radius: int) -> StyleBoxFlat:
|
||||
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
style.border_color = border
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(corner_radius)
|
||||
style.content_margin_left = float(_objects_here_integer("row_padding_x", 12, 4, 24))
|
||||
style.content_margin_right = float(_objects_here_integer("row_padding_x", 12, 4, 24))
|
||||
style.content_margin_top = float(_objects_here_integer("row_padding_y", 6, 0, 20))
|
||||
style.content_margin_bottom = float(_objects_here_integer("row_padding_y", 6, 0, 20))
|
||||
return style
|
||||
|
||||
|
||||
func _style_objects_here_cancel(button: Button) -> void:
|
||||
if button == null:
|
||||
return
|
||||
var normal: Color = _objects_here_color("cancel_surface", Color("#293242"))
|
||||
var hover: Color = _objects_here_color("cancel_hover_surface", Color("#354154"))
|
||||
var border: Color = _objects_here_color("cancel_border_color", Color("#718096"))
|
||||
var text_color: Color = _objects_here_color("cancel_text_color", Color("#F5F7FA"))
|
||||
var radius: int = _objects_here_integer("cancel_corner_radius", 10, 4, 24)
|
||||
var border_width: int = _objects_here_integer("cancel_border_width", 2, 1, 4)
|
||||
button.add_theme_stylebox_override("normal", _objects_here_button_style(normal, border, border_width, radius))
|
||||
button.add_theme_stylebox_override("hover", _objects_here_button_style(hover, _objects_here_color("focus_color", border), border_width, radius))
|
||||
button.add_theme_stylebox_override("pressed", _objects_here_button_style(hover, border, border_width, radius))
|
||||
button.add_theme_stylebox_override("focus", _objects_here_button_style(normal, _objects_here_color("focus_color", border), _objects_here_integer("focus_border_width", 3, 2, 5), radius))
|
||||
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("font_focus_color", text_color)
|
||||
button.add_theme_font_size_override("font_size", _objects_here_integer("cancel_font_size", 20, 16, 32))
|
||||
if _ui_font != null:
|
||||
button.add_theme_font_override("font", _ui_font)
|
||||
|
||||
|
||||
func _objects_here_integer(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||
if _sandbox_theme == null:
|
||||
return clampi(fallback, minimum, maximum)
|
||||
return _sandbox_theme.integer("popups.anchored_chooser." + key, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _objects_here_color(path: String, fallback: Color) -> Color:
|
||||
if _sandbox_theme == null:
|
||||
return fallback
|
||||
var resolved_path: String = path if path.contains(".") else "popups.anchored_chooser." + path
|
||||
return _sandbox_theme.color(resolved_path, fallback)
|
||||
|
||||
|
||||
func _objects_here_vector2(key: String, fallback: Vector2) -> Vector2:
|
||||
if _sandbox_theme == null:
|
||||
return fallback
|
||||
var raw: Variant = _sandbox_theme.value("popups.anchored_chooser." + key, null)
|
||||
if raw is Array and (raw as Array).size() == 2:
|
||||
return Vector2(float((raw as Array)[0]), float((raw as Array)[1]))
|
||||
return fallback
|
||||
|
||||
|
||||
func _make_selected_candidate_badge() -> PanelContainer:
|
||||
var badge: PanelContainer = PanelContainer.new()
|
||||
badge.name = "SelectedCandidateBadge"
|
||||
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var accent: Color = _objects_here_color("accent_color", Color("#22C6FF"))
|
||||
if _sandbox_theme != null:
|
||||
accent = _sandbox_theme.color("mode_colors.edit", accent)
|
||||
var badge_style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
badge_style.bg_color = _objects_here_color("badge_surface", Color("#12394A"))
|
||||
badge_style.border_color = accent
|
||||
badge_style.set_border_width_all(1)
|
||||
badge_style.set_corner_radius_all(_objects_here_integer("badge_corner_radius", 8, 4, 20))
|
||||
badge_style.content_margin_left = float(_objects_here_integer("badge_padding_x", 8, 2, 20))
|
||||
badge_style.content_margin_right = float(_objects_here_integer("badge_padding_x", 8, 2, 20))
|
||||
badge_style.content_margin_top = float(_objects_here_integer("badge_padding_y", 2, 0, 12))
|
||||
badge_style.content_margin_bottom = float(_objects_here_integer("badge_padding_y", 2, 0, 12))
|
||||
badge.add_theme_stylebox_override("panel", badge_style)
|
||||
var label: Label = Label.new()
|
||||
label.text = "Selected"
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", _objects_here_integer("badge_font_size", 16, 12, 24))
|
||||
label.add_theme_color_override("font_color", _objects_here_color("badge_text_color", Color.WHITE))
|
||||
if _ui_font != null:
|
||||
label.add_theme_font_override("font", _ui_font)
|
||||
badge.add_child(label)
|
||||
return badge
|
||||
|
||||
|
||||
func _objects_here_font_size() -> int:
|
||||
return _objects_here_integer("row_font_size", 20, 14, 40)
|
||||
|
||||
|
||||
func get_objects_here_popup() -> PopupPanel:
|
||||
return _objects_here_popup
|
||||
|
||||
|
||||
func get_objects_here_screen_rect() -> Rect2:
|
||||
if _objects_here_popup == null or not is_instance_valid(_objects_here_popup) or not _objects_here_popup.visible:
|
||||
return Rect2()
|
||||
return Rect2(Vector2(_objects_here_popup.position), Vector2(_objects_here_popup.size))
|
||||
|
||||
|
||||
func dismiss_objects_here() -> void:
|
||||
_dismiss_objects_here()
|
||||
|
||||
|
||||
func _object_here_label(node: Node2D) -> String:
|
||||
if node is PropBlock:
|
||||
var prop: PropBlock = node as PropBlock
|
||||
var depth: String = "Behind characters" if String(prop.stage_roles.get("depth", PROP_STAGE_ROLES.DEPTH_BEHIND)) == PROP_STAGE_ROLES.DEPTH_BEHIND else "In front of characters"
|
||||
var entry: Dictionary = PROP_LIBRARY_SCRIPT.get_entry(prop.spawn_id)
|
||||
var prop_name: String = String(entry.get("name", entry.get("label", prop.spawn_id)))
|
||||
return "📦 %s · %s" % [prop_name, depth]
|
||||
return "📦 %s" % String(node.get("stage_name"))
|
||||
if node is TerrainBlock:
|
||||
return "🧱 %s · Terrain" % String(node.get("spawn_id"))
|
||||
if node is StickmanRig:
|
||||
return "🧍 %s · Stickman" % node.name
|
||||
return "🧍 %s" % String(node.get("stage_name"))
|
||||
if node.get_script() == MISSING_PROP_PLACEHOLDER_SCRIPT:
|
||||
return "📦 %s" % String(node.get("stage_name"))
|
||||
if node.get_script() == MISSING_STICKMAN_PLACEHOLDER_SCRIPT:
|
||||
return "🧍 %s" % String(node.get("stage_name"))
|
||||
return "🎯 %s" % node.name
|
||||
|
||||
|
||||
func _on_objects_here_selected(node: Node2D) -> void:
|
||||
if node == null or not is_instance_valid(node) or node.is_queued_for_deletion():
|
||||
_dismiss_objects_here()
|
||||
return
|
||||
object_selected.emit(node)
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup):
|
||||
_objects_here_popup.hide()
|
||||
@@ -319,10 +621,57 @@ func _on_objects_here_selected(node: Node2D) -> void:
|
||||
func _on_objects_here_popup_closed(popup: PopupPanel) -> void:
|
||||
if _objects_here_popup == popup:
|
||||
_objects_here_popup = null
|
||||
candidate_preview_changed.emit(null)
|
||||
_objects_here_preview = null
|
||||
if _objects_here_guard != null and is_instance_valid(_objects_here_guard):
|
||||
_objects_here_guard.queue_free()
|
||||
_objects_here_guard = null
|
||||
objects_here_dismissed.emit()
|
||||
if is_instance_valid(popup):
|
||||
popup.queue_free()
|
||||
|
||||
|
||||
func _set_candidate_preview(node: Node2D) -> void:
|
||||
if node == null or not is_instance_valid(node) or node.is_queued_for_deletion():
|
||||
return
|
||||
_objects_here_preview = node
|
||||
candidate_preview_changed.emit(node)
|
||||
|
||||
|
||||
func _clear_row_preview(button: Button, node: Node2D) -> void:
|
||||
if button != null and is_instance_valid(button) and button.has_focus():
|
||||
return
|
||||
if _objects_here_preview == node:
|
||||
_objects_here_preview = null
|
||||
candidate_preview_changed.emit(null)
|
||||
|
||||
|
||||
func _dismiss_objects_here() -> void:
|
||||
if _objects_here_popup != null and is_instance_valid(_objects_here_popup):
|
||||
_objects_here_popup.hide()
|
||||
else:
|
||||
_on_objects_here_popup_closed(null)
|
||||
|
||||
|
||||
func _on_objects_here_guard_input(event: InputEvent, guard: Control) -> void:
|
||||
if event is InputEventMouseButton and (event as InputEventMouseButton).pressed:
|
||||
guard.get_viewport().set_input_as_handled()
|
||||
_dismiss_objects_here()
|
||||
elif event is InputEventKey and (event as InputEventKey).pressed and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
guard.get_viewport().set_input_as_handled()
|
||||
_dismiss_objects_here()
|
||||
elif guard != null and is_instance_valid(guard):
|
||||
guard.get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _on_objects_here_window_input(event: InputEvent, _popup: PopupPanel) -> void:
|
||||
if not (event is InputEventKey) or not (event as InputEventKey).pressed or (event as InputEventKey).keycode != KEY_ESCAPE:
|
||||
return
|
||||
if _objects_here_guard != null and is_instance_valid(_objects_here_guard):
|
||||
_objects_here_guard.get_viewport().set_input_as_handled()
|
||||
_dismiss_objects_here()
|
||||
|
||||
|
||||
func theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i:
|
||||
if _sandbox_theme == null:
|
||||
return fallback
|
||||
|
||||
@@ -16,13 +16,11 @@ signal spot_move_requested(spot_id: String)
|
||||
signal spot_rename_requested(spot_id: String, name: String)
|
||||
signal spot_erase_requested(spot_id: String)
|
||||
signal spot_selected(spot_id: String)
|
||||
signal close_requested
|
||||
|
||||
signal rename_requested(node: Node2D, requested_name: String)
|
||||
var _stage_theme: SandboxTheme
|
||||
var _top_bar_height: float = 64.0
|
||||
var _status_bar_height: float = 38.0
|
||||
var _card: StageInspectorPanel
|
||||
var _reopen_button: Button
|
||||
var _fields: VBoxContainer
|
||||
var _notice: Label
|
||||
var _depth_group: StagePanelChoiceGroup
|
||||
@@ -32,7 +30,14 @@ var _controls: Dictionary = {}
|
||||
var _spot_rows: VBoxContainer
|
||||
var _spot_empty_label: Label
|
||||
var _active_selection_key: String = ""
|
||||
var _dismissed_selection_key: String = ""
|
||||
var _name_node: Node2D
|
||||
var _name_section: VBoxContainer
|
||||
var _name_edit: LineEdit
|
||||
var _name_done_button: Button
|
||||
var _name_cancel_button: Button
|
||||
var _name_feedback: Label
|
||||
var _name_editing: bool = false
|
||||
var _restore_collapsed_after_edit: bool = false
|
||||
|
||||
|
||||
func configure(stage_theme: SandboxTheme, top_bar_height: float, status_bar_height: float) -> void:
|
||||
@@ -62,27 +67,28 @@ func show_prop(
|
||||
hide_card()
|
||||
return
|
||||
_update_selection_key(selection)
|
||||
var dismissed: bool = _active_selection_key == _dismissed_selection_key
|
||||
_card.visible = not dismissed
|
||||
_reopen_button.visible = dismissed
|
||||
if dismissed:
|
||||
return
|
||||
_card.visible = true
|
||||
_card.set_safe_rect(safe_rect)
|
||||
_notice.visible = false
|
||||
_fields.visible = true
|
||||
_card.set_header(title, "📦")
|
||||
_card.set_rename_enabled(true)
|
||||
if _name_node != prop:
|
||||
_name_node = prop
|
||||
_set_name_editing(false)
|
||||
_name_edit.text = prop.stage_name
|
||||
_name_feedback.visible = false
|
||||
_sync_roles(prop, roles)
|
||||
_sync_spots(prop.get_use_spots(), selected_spot_id)
|
||||
|
||||
|
||||
func show_multiple_props(selection: Array[Node2D], safe_rect: Rect2) -> void:
|
||||
_set_name_editing(false)
|
||||
_name_feedback.visible = false
|
||||
_update_selection_key(selection)
|
||||
var dismissed: bool = _active_selection_key == _dismissed_selection_key
|
||||
_card.visible = not dismissed
|
||||
_reopen_button.visible = dismissed
|
||||
if dismissed:
|
||||
return
|
||||
_card.visible = true
|
||||
_card.set_safe_rect(safe_rect)
|
||||
_card.set_rename_enabled(false)
|
||||
_notice.visible = true
|
||||
_fields.visible = false
|
||||
_card.set_header("Props", "📦")
|
||||
@@ -90,43 +96,25 @@ func show_multiple_props(selection: Array[Node2D], safe_rect: Rect2) -> void:
|
||||
|
||||
|
||||
func hide_card(reset_selection: bool = false) -> void:
|
||||
if reset_selection:
|
||||
_set_name_editing(false)
|
||||
_name_feedback.visible = false
|
||||
if reset_selection and not _active_selection_key.is_empty():
|
||||
_update_selection_key([])
|
||||
if _card != null:
|
||||
_card.visible = false
|
||||
if _reopen_button != null:
|
||||
_reopen_button.visible = false
|
||||
|
||||
|
||||
func set_dismissed(dismissed: bool) -> void:
|
||||
if _active_selection_key.is_empty():
|
||||
return
|
||||
_dismissed_selection_key = _active_selection_key if dismissed else ""
|
||||
|
||||
|
||||
func update_layout(safe_rect: Rect2) -> void:
|
||||
if _card == null:
|
||||
return
|
||||
_card.set_safe_rect(safe_rect)
|
||||
if _reopen_button == null:
|
||||
return
|
||||
var inset: float = float(_stage_panel_integer("shell.edge_inset", 12, 0, 48))
|
||||
var button_width: float = minf(148.0, maxf(1.0, safe_rect.size.x - inset * 2.0))
|
||||
var button_height: float = float(_stage_panel_integer("action.min_target_size", 48, 44, 96))
|
||||
_reopen_button.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||
_reopen_button.custom_minimum_size.x = button_width
|
||||
_reopen_button.position = Vector2(safe_rect.end.x - inset - button_width, safe_rect.position.y + inset)
|
||||
_reopen_button.size = Vector2(button_width, button_height)
|
||||
|
||||
|
||||
func get_card() -> Control:
|
||||
return _card
|
||||
|
||||
|
||||
func get_reopen_button() -> Button:
|
||||
return _reopen_button
|
||||
|
||||
|
||||
func get_fields() -> VBoxContainer:
|
||||
return _fields
|
||||
|
||||
@@ -151,10 +139,6 @@ func get_active_selection_key() -> String:
|
||||
return _active_selection_key
|
||||
|
||||
|
||||
func get_dismissed_selection_key() -> String:
|
||||
return _dismissed_selection_key
|
||||
|
||||
|
||||
func focus_card() -> void:
|
||||
_focus_card()
|
||||
|
||||
@@ -165,12 +149,15 @@ func _build() -> void:
|
||||
_card.visible = false
|
||||
add_child(_card)
|
||||
_card.apply_theme(_stage_theme)
|
||||
_card.close_requested.connect(_on_card_close_requested)
|
||||
_card.set_collapsible(true)
|
||||
_card.set_rename_enabled(false)
|
||||
_card.rename_requested.connect(_start_name_edit)
|
||||
_card.collapsed_changed.connect(_on_card_collapsed_changed)
|
||||
var body: VBoxContainer = _card.body()
|
||||
body.add_theme_constant_override("separation", 10)
|
||||
|
||||
_notice = Label.new()
|
||||
_notice.text = "Select one prop to change its settings."
|
||||
_notice.text = "Select one character or prop to change its name."
|
||||
_notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_notice.add_theme_font_size_override("font_size", _stage_panel_integer("type.body_size", 20, 14, 40))
|
||||
_notice.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA")))
|
||||
@@ -182,6 +169,7 @@ func _build() -> void:
|
||||
_fields.add_theme_constant_override("separation", _stage_panel_integer("shell.section_gap", 12, 4, 32))
|
||||
_fields.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
body.add_child(_fields)
|
||||
_build_name_section(_fields)
|
||||
_add_heading(_fields, "Where it appears")
|
||||
_depth_group = _make_choice_group("depth")
|
||||
_fields.add_child(_depth_group)
|
||||
@@ -198,15 +186,114 @@ func _build() -> void:
|
||||
_add_wobble_section()
|
||||
_add_use_spots_section()
|
||||
|
||||
_reopen_button = Button.new()
|
||||
_reopen_button.name = "ReopenPropSettings"
|
||||
_reopen_button.text = "Settings"
|
||||
_reopen_button.custom_minimum_size = Vector2(148.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96)))
|
||||
_reopen_button.focus_mode = Control.FOCUS_ALL
|
||||
_reopen_button.visible = false
|
||||
_reopen_button.pressed.connect(_on_reopen_pressed)
|
||||
_card.style_action_button(_reopen_button, true)
|
||||
add_child(_reopen_button)
|
||||
func _build_name_section(parent: VBoxContainer) -> void:
|
||||
_name_section = VBoxContainer.new()
|
||||
_name_section.name = "StageNameEditor"
|
||||
_name_section.add_theme_constant_override("separation", 6)
|
||||
_name_section.visible = false
|
||||
parent.add_child(_name_section)
|
||||
_name_edit = LineEdit.new()
|
||||
_name_edit.name = "StageNameEdit"
|
||||
_name_edit.placeholder_text = "Character or prop name"
|
||||
_name_edit.custom_minimum_size.y = float(_stage_panel_integer("action.min_target_size", 48, 44, 96))
|
||||
_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_name_edit.visible = false
|
||||
_name_edit.text_submitted.connect(func(_value: String) -> void: _submit_name_edit())
|
||||
_name_edit.gui_input.connect(_on_name_edit_gui_input)
|
||||
_apply_ui_font(_name_edit)
|
||||
_name_section.add_child(_name_edit)
|
||||
_name_feedback = Label.new()
|
||||
_name_feedback.name = "StageNameFeedback"
|
||||
_name_feedback.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_name_feedback.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32))
|
||||
_name_feedback.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0")))
|
||||
_name_feedback.visible = false
|
||||
_apply_ui_font(_name_feedback)
|
||||
_name_section.add_child(_name_feedback)
|
||||
var actions: HBoxContainer = HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 8)
|
||||
_name_section.add_child(actions)
|
||||
_name_done_button = Button.new()
|
||||
_name_done_button.name = "DoneStageName"
|
||||
_name_done_button.text = "Done"
|
||||
_name_done_button.custom_minimum_size = Vector2(96.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96)))
|
||||
_name_done_button.visible = false
|
||||
_name_done_button.pressed.connect(_submit_name_edit)
|
||||
_card.style_primary_action_button(_name_done_button)
|
||||
actions.add_child(_name_done_button)
|
||||
_name_cancel_button = Button.new()
|
||||
_name_cancel_button.name = "CancelStageName"
|
||||
_name_cancel_button.text = "Cancel"
|
||||
_name_cancel_button.custom_minimum_size = Vector2(96.0, float(_stage_panel_integer("action.min_target_size", 48, 44, 96)))
|
||||
_name_cancel_button.visible = false
|
||||
_name_cancel_button.pressed.connect(_cancel_name_edit)
|
||||
_card.style_action_button(_name_cancel_button, false)
|
||||
actions.add_child(_name_cancel_button)
|
||||
|
||||
|
||||
func show_name_result(result: Dictionary) -> void:
|
||||
if bool(result.get("ok", false)):
|
||||
_name_edit.text = String(result.get("name", ""))
|
||||
_name_feedback.visible = false
|
||||
_set_name_editing(false)
|
||||
else:
|
||||
var reason: StringName = StringName(String(result.get("reason", "")))
|
||||
var suggestion: String = String(result.get("suggestion", ""))
|
||||
if reason == &"blank":
|
||||
_name_feedback.text = "Give this one a name. Try %s." % suggestion
|
||||
else:
|
||||
_name_feedback.text = "%s is already here. Try %s." % [String(result.get("name", "")), suggestion]
|
||||
_name_feedback.visible = true
|
||||
_card.request_fit()
|
||||
if not bool(result.get("ok", false)):
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_name_edit.call_deferred("select_all")
|
||||
|
||||
|
||||
func _start_name_edit() -> void:
|
||||
if _name_node == null or not is_instance_valid(_name_node):
|
||||
return
|
||||
_restore_collapsed_after_edit = _card.is_collapsed()
|
||||
_card.set_collapsed(false)
|
||||
_name_edit.text = String(_name_node.get("stage_name"))
|
||||
_name_feedback.visible = false
|
||||
_set_name_editing(true)
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_name_edit.call_deferred("select_all")
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func _set_name_editing(editing: bool) -> void:
|
||||
_name_editing = editing
|
||||
if _name_section == null:
|
||||
return
|
||||
_name_section.visible = editing
|
||||
_name_edit.visible = editing
|
||||
_name_done_button.visible = editing
|
||||
_name_cancel_button.visible = editing
|
||||
if not editing and _restore_collapsed_after_edit and _card != null:
|
||||
_restore_collapsed_after_edit = false
|
||||
_card.set_collapsed(true)
|
||||
|
||||
|
||||
func _submit_name_edit() -> void:
|
||||
if _name_node == null or not is_instance_valid(_name_node):
|
||||
return
|
||||
rename_requested.emit(_name_node, _name_edit.text)
|
||||
|
||||
|
||||
func _cancel_name_edit() -> void:
|
||||
if _name_node != null and is_instance_valid(_name_node):
|
||||
_name_edit.text = String(_name_node.get("stage_name"))
|
||||
_name_feedback.visible = false
|
||||
_set_name_editing(false)
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func _on_name_edit_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and (event as InputEventKey).pressed and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
_cancel_name_edit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _add_use_spots_section() -> void:
|
||||
@@ -449,7 +536,12 @@ func _update_selection_key(selection: Array[Node2D]) -> void:
|
||||
if selection_key == _active_selection_key:
|
||||
return
|
||||
_active_selection_key = selection_key
|
||||
_dismissed_selection_key = ""
|
||||
if _name_editing:
|
||||
_name_feedback.visible = false
|
||||
_restore_collapsed_after_edit = false
|
||||
_set_name_editing(false)
|
||||
if _card != null:
|
||||
_card.set_collapsed(false, false)
|
||||
_card.reset_scroll()
|
||||
|
||||
|
||||
@@ -481,17 +573,11 @@ func _on_row_height_changed() -> void:
|
||||
_card.request_fit()
|
||||
|
||||
|
||||
func _on_card_close_requested() -> void:
|
||||
close_requested.emit()
|
||||
|
||||
|
||||
func _on_reopen_pressed() -> void:
|
||||
var should_focus: bool = _reopen_button.has_focus()
|
||||
set_dismissed(false)
|
||||
_card.visible = true
|
||||
_reopen_button.visible = false
|
||||
if should_focus:
|
||||
call_deferred("_focus_card")
|
||||
func _on_card_collapsed_changed(collapsed: bool) -> void:
|
||||
if collapsed and _name_editing:
|
||||
_name_feedback.visible = false
|
||||
_restore_collapsed_after_edit = false
|
||||
_set_name_editing(false)
|
||||
|
||||
|
||||
func _focus_card() -> void:
|
||||
@@ -502,7 +588,7 @@ func _focus_card() -> void:
|
||||
func _on_viewport_resized() -> void:
|
||||
var viewport: Rect2 = get_viewport_rect()
|
||||
var inset: float = float(_stage_panel_integer("shell.edge_inset", 12, 0, 48))
|
||||
var top_offset: float = minf(_top_bar_height + 116.0, maxf(12.0, viewport.size.y - 220.0))
|
||||
var top_offset: float = minf(_top_bar_height + 120.0, maxf(12.0, viewport.size.y - 220.0))
|
||||
var bottom_limit: float = maxf(top_offset + 72.0, viewport.size.y - _status_bar_height - 12.0)
|
||||
var safe_rect := Rect2(Vector2(0.0, top_offset - inset), Vector2(viewport.size.x, bottom_limit - top_offset + inset * 2.0))
|
||||
update_layout(safe_rect)
|
||||
|
||||
@@ -87,10 +87,12 @@ func collect(
|
||||
entry["rotation"] = rotation_degrees
|
||||
entry["scale"] = STAGE_SCENE_IO.vector_to_data(stage_node.scale)
|
||||
entry["stage_object_id"] = String(stage_node.get("stage_object_id"))
|
||||
entry["stage_name"] = String(stage_node.get("stage_name"))
|
||||
elif type == "prop":
|
||||
var prop: PropBlock = stage_node as PropBlock
|
||||
entry["asset_id"] = prop.spawn_id
|
||||
entry["stage_object_id"] = prop.stage_object_id
|
||||
entry["stage_name"] = prop.stage_name
|
||||
entry["roles"] = prop.stage_roles.duplicate(true)
|
||||
entry["use_spots"] = PROP_USE_SPOT_DATA.encode(prop.get_use_spots())
|
||||
entry["next_use_spot_number"] = prop.next_use_spot_number
|
||||
@@ -109,9 +111,13 @@ func collect(
|
||||
entry["type"] = "stickman"
|
||||
entry["position"] = STAGE_SCENE_IO.vector_to_data(position)
|
||||
entry["rotation"] = rotation_degrees
|
||||
entry["stage_object_id"] = String(stage_node.get("stage_object_id"))
|
||||
entry["stage_name"] = String(stage_node.get("stage_name"))
|
||||
else:
|
||||
var rig: StickmanRig = stage_node as StickmanRig
|
||||
entry["stk_path"] = rig.source_stk_path
|
||||
entry["stage_object_id"] = rig.stage_object_id
|
||||
entry["stage_name"] = rig.stage_name
|
||||
var facing_profile: int = int(authored_state.get("facing_profile", rig.get_facing_profile()))
|
||||
entry["facing_profile"] = StickmanRig.FacingProfile.keys()[facing_profile]
|
||||
var queue_result: Dictionary = STAGE_SCENE_IO.encode_queue(rig.get_queue())
|
||||
@@ -248,6 +254,7 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
|
||||
if node is PropBlock:
|
||||
var prop: PropBlock = node as PropBlock
|
||||
prop.stage_object_id = String(entry.get("stage_object_id", ""))
|
||||
prop.stage_name = String(entry.get("stage_name", ""))
|
||||
prop.set_wobble_on_impact(bool(entry.get("wobble_on_impact", false)))
|
||||
var decoded_spots: Array[Dictionary] = PROP_USE_SPOT_DATA.decode(entry.get("use_spots", []) as Array)
|
||||
prop.set_use_spots(decoded_spots, int(entry.get("next_use_spot_number", 1)))
|
||||
@@ -270,6 +277,9 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
|
||||
else:
|
||||
warnings.append("Invalid prop material preset")
|
||||
if node is StickmanRig:
|
||||
var rig: StickmanRig = node as StickmanRig
|
||||
rig.stage_object_id = String(entry.get("stage_object_id", ""))
|
||||
rig.stage_name = String(entry.get("stage_name", ""))
|
||||
var facing_names: Array = StickmanRig.FacingProfile.keys()
|
||||
var facing_index: int = facing_names.find(String(entry.get("facing_profile", "FORWARD")))
|
||||
if facing_index >= 0:
|
||||
|
||||
Reference in New Issue
Block a user