redesign tile action menu with toolbar and submenus

- replace old PopupMenu (flat preset list) with PopupPanel-based menu
- add 3-icon toolbar: ⚙ Settings, ℹ Info, ✕ Remove (+) + Add
- red ✕ on tiles removes the tile; green + on empty space opens add-tile submenu
- 'Themes ▸' button opens a submenu with all ShaderPresets
- add-tile submenu populates from PluginManager.get_all_tile_defs()
- all toolbar buttons are 48×48 flat buttons with 24px font for touch
- orphan PopupPanel children cleaned up during grid rebuild
This commit is contained in:
Eric Smith 2026-05-21 09:19:29 -04:00
parent 6aba0e23d0
commit ebf97a6fc8
3 changed files with 164 additions and 28 deletions

View file

@ -92,7 +92,7 @@ func _on_gui_input(event: InputEvent) -> void:
var mb: InputEventMouseButton = event
if mb.button_index == MOUSE_BUTTON_LEFT and mb.double_click and mb.pressed:
# Double-click still works for mouse users
_show_preset_popup(mb.position)
_show_tile_menu(mb.position)
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.double_click:
if mb.pressed:
# Try resize edge first (instant, no long-press)
@ -120,33 +120,168 @@ func _on_gui_input(event: InputEvent) -> void:
_update_hover_cursor(mm.position)
func _show_preset_popup(mouse_pos: Vector2) -> void:
var cell_pos := _cell_at_position(mouse_pos)
if cell_pos.x < 0 or cell_pos.y < 0:
return
var module := _get_module_at(cell_pos.x, cell_pos.y)
if module == null:
return
# Don't show popup if already dragging or resizing
const MENU_BUTTON_MIN_SIZE: Vector2 = Vector2(48, 48)
const MENU_BUTTON_FONT_SIZE: int = 24
# --------------------------------------------------------------------------
# Tile action menu (replaces old preset-only popup)
# --------------------------------------------------------------------------
func _show_tile_menu(mouse_pos: Vector2) -> void:
if _is_dragging or _is_resizing:
return
var popup := PopupMenu.new()
popup.name = "PresetPopup"
var cell_pos := _cell_at_position(mouse_pos)
if cell_pos.x < 0 or cell_pos.y < 0:
return
var module := _get_module_at(cell_pos.x, cell_pos.y)
var panel := PopupPanel.new()
panel.name = "TileMenu"
panel.position = Vector2i(get_screen_position()) + Vector2i(mouse_pos)
add_child(panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 8)
margin.add_theme_constant_override("margin_right", 8)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
panel.add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 6)
margin.add_child(vbox)
# -- toolbar row --
var toolbar := HBoxContainer.new()
toolbar.add_theme_constant_override("separation", 6)
toolbar.alignment = BoxContainer.ALIGNMENT_CENTER
vbox.add_child(toolbar)
var settings_btn := _make_menu_button("", "Settings")
toolbar.add_child(settings_btn)
var info_btn := _make_menu_button("", "Info")
toolbar.add_child(info_btn)
if module != null:
# On a tile: show remove button (red X)
var remove_btn := _make_menu_button("", "Remove Tile")
remove_btn.modulate = Color(1.0, 0.25, 0.25, 1.0)
remove_btn.pressed.connect(_remove_tile.bind(module, panel))
toolbar.add_child(remove_btn)
else:
# On empty space: show add button (green +)
var add_btn := _make_menu_button("+", "Add Tile")
add_btn.modulate = Color(0.2, 1.0, 0.2, 1.0)
add_btn.pressed.connect(_show_add_tile_submenu.bind(add_btn, panel))
toolbar.add_child(add_btn)
# -- Themes button (only on a tile) --
if module != null:
var themes_btn := Button.new()
themes_btn.text = "Themes ▸"
themes_btn.flat = true
themes_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
themes_btn.pressed.connect(_show_themes_submenu.bind(themes_btn, module, panel))
vbox.add_child(themes_btn)
panel.popup()
func _make_menu_button(text: String, tooltip: String) -> Button:
var btn := Button.new()
btn.text = text
btn.tooltip_text = tooltip
btn.flat = true
btn.custom_min_size = MENU_BUTTON_MIN_SIZE
btn.add_theme_font_size_override("font_size", MENU_BUTTON_FONT_SIZE)
return btn
func _show_themes_submenu(parent_btn: Button, module: Control, panel: PopupPanel) -> void:
var submenu := PopupMenu.new()
submenu.name = "ThemeSubmenu"
var names := ShaderPresets.get_preset_names()
for i in names.size():
popup.add_item(names[i], i)
popup.position = Vector2i(get_screen_position()) + Vector2i(mouse_pos)
add_child(popup)
popup.popup()
submenu.add_item(names[i], i)
var on_select := func(id: int):
# Position the submenu to the right of the parent button
var btn_global: Vector2 = parent_btn.global_position + Vector2(parent_btn.size.x, 0.0)
submenu.position = Vector2i(btn_global)
add_child(submenu)
submenu.popup()
submenu.id_pressed.connect(func(id: int):
if id >= 0 and id < names.size():
ShaderPresets.apply_preset(module, names[id])
if is_instance_valid(popup):
popup.queue_free()
if is_instance_valid(submenu):
submenu.queue_free()
if is_instance_valid(panel):
panel.queue_free()
)
popup.id_pressed.connect(on_select)
func _show_add_tile_submenu(parent_btn: Button, panel: PopupPanel) -> void:
if not PluginManager.has_method("get_all_tile_defs"):
return
var defs: Array[Dictionary] = PluginManager.get_all_tile_defs()
if defs.is_empty():
return
var submenu := PopupMenu.new()
submenu.name = "AddTileSubmenu"
for i in defs.size():
var label: String = defs[i].get("name", defs[i].get("id", "Tile %d" % i))
submenu.add_item(label, i)
var btn_global: Vector2 = parent_btn.global_position + Vector2(parent_btn.size.x, 0.0)
submenu.position = Vector2i(btn_global)
add_child(submenu)
submenu.popup()
submenu.id_pressed.connect(func(id: int):
if id >= 0 and id < defs.size():
_add_tile_from_def(defs[id])
if is_instance_valid(submenu):
submenu.queue_free()
if is_instance_valid(panel):
panel.queue_free()
)
func _remove_tile(module: Control, panel: PopupPanel) -> void:
remove_module(module)
if is_instance_valid(panel):
panel.queue_free()
func _add_tile_from_def(def: Dictionary) -> void:
if not PluginManager.has_method("instantiate_tile"):
return
var tid: String = def.get("id", "")
if tid.is_empty():
return
var instance := PluginManager.instantiate_tile(tid)
if instance == null:
return
# Find a free position at the end of the grid
var gs := get_grid_size()
var target_col := 0
var target_row := gs.y
# Try current row first
var dw: int = def.get("default_w", 1)
var dh: int = def.get("default_h", 1)
for c in range(gs.x):
if _span_fits(c, target_row, dw, dh, null):
target_col = c
break
place_module(instance, target_col, target_row, dw, dh)
# --------------------------------------------------------------------------
@ -177,7 +312,7 @@ func _cancel_long_press() -> void:
func _on_long_press_timeout() -> void:
_long_press_active = false
_long_press_timer = null
_show_preset_popup(_long_press_pos)
_show_tile_menu(_long_press_pos)
# --------------------------------------------------------------------------
@ -665,9 +800,9 @@ func _rebuild_grid() -> void:
_cancel_drag()
if _is_resizing:
_end_resize(Vector2.ZERO)
# Clean up orphaned popups from preset selection
# Clean up orphaned popups from tile menu
for child in get_children():
if child is PopupMenu:
if child is PopupMenu or child is PopupPanel:
child.queue_free()
var avail := size - Vector2(margin * 2.0, margin * 2.0)