590 lines
24 KiB
GDScript
590 lines
24 KiB
GDScript
extends "res://core/ui/panels/editor_panel.gd"
|
|
## Map-Inhalt-Baum (aus main.gd ausgezogen, Karte t_eaccbfed U1 — Logik
|
|
## unverändert). U2 baut hier den hierarchischen Szenen-Baum aus; diese Karte
|
|
## zieht nur um: Der Baum lebt jetzt im LINKEN TabContainer (Registry-Gruppe
|
|
## "links"), Zustand/Signale wie vorher.
|
|
|
|
var content_tree: Tree
|
|
var group_btn: Button
|
|
var ungroup_btn: Button
|
|
var _tree_select_lock := false # verhindert Rückkopplung Auswahl ↔ Baum
|
|
var _tree_select_pending := false # multi_selected gesammelt, Auswertung steht aus
|
|
var _obj_index_map: Dictionary = {} # obj_index -> TreeItem (schnelle Zwei-Wege-Kopplung)
|
|
|
|
# U3 (Karte t_a4bf9b8c): Kontextmenü, Umbenennen, Lösch-Rückfrage, Drag&Drop.
|
|
var _context_menu: PopupMenu
|
|
var _rename_dialog: AcceptDialog
|
|
var _rename_field: LineEdit
|
|
var _rename_gid := -1 # Gruppe, die das Umbenennen-Dialogfeld betrifft
|
|
var _confirm_delete: ConfirmationDialog
|
|
var _confirm_gid := -1 # Gruppe, die das Lösch-Rückfragen betrifft
|
|
|
|
|
|
static func spec() -> Dictionary:
|
|
return {"titel": "Map-Inhalt", "gruppe": "links", "order": 10}
|
|
|
|
|
|
func build() -> void:
|
|
add_child(_header_label("Map-Inhalt"))
|
|
|
|
content_tree = Tree.new()
|
|
content_tree.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
content_tree.custom_minimum_size = UIScale.v2(0, 160)
|
|
content_tree.allow_reselect = true
|
|
content_tree.select_mode = Tree.SELECT_MULTI # Strg+Klick im Baum = Mehrfachauswahl (t_c46d31c2)
|
|
content_tree.hide_root = true
|
|
# FIX 25.10. (Test-Befund „Liste anklicken wählt in der Szene nichts aus“):
|
|
# Im Modus SELECT_MULTI feuert Godot NIE item_selected, sondern nur
|
|
# multi_selected (pro Zeile). Beide anschließen; das Sammeln übernimmt der
|
|
# Aufrufer-Merker, damit ein Klick mit mehreren Signalen nur EINMAL wirkt.
|
|
content_tree.item_selected.connect(_on_tree_selected)
|
|
content_tree.multi_selected.connect(_on_tree_multi_selected)
|
|
add_child(content_tree)
|
|
|
|
# Gruppen verwalten (25.10.: der Tab „Auswahl“ ist weg): mehrere Objekte in der
|
|
# Liste (Strg+Klick) oder in der Szene markieren, dann hier gruppieren/lösen.
|
|
# Rechtsklick auf eine Zeile bietet dieselben Aktionen.
|
|
var group_row := HBoxContainer.new()
|
|
group_row.add_theme_constant_override("separation", UIScale.px(8))
|
|
add_child(group_row)
|
|
group_btn = Button.new()
|
|
group_btn.text = "🔗 Gruppieren"
|
|
group_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
group_btn.custom_minimum_size = UIScale.v2(0, 40)
|
|
group_btn.tooltip_text = "Markierte Objekte zu einer Gruppe verbinden (Strg+G) — sie ziehen und drehen dann gemeinsam. Mehrere markieren: Strg+Klick in der Liste oder Rahmen in der Szene."
|
|
group_btn.pressed.connect(func() -> void: main.interaction.group_selection())
|
|
group_row.add_child(group_btn)
|
|
ungroup_btn = Button.new()
|
|
ungroup_btn.text = "⛓ Gruppe lösen"
|
|
ungroup_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
ungroup_btn.custom_minimum_size = UIScale.v2(0, 40)
|
|
ungroup_btn.tooltip_text = "Die Gruppe der Auswahl auflösen (Strg+Umschalt+G) — die Objekte bleiben, nur die Bindung löst sich"
|
|
ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
|
|
group_row.add_child(ungroup_btn)
|
|
|
|
# U3 (Karte t_a4bf9b8c): Rechtsklick-Kontextmenü + Drag&Drop für Gruppen.
|
|
content_tree.gui_input.connect(_on_tree_gui_input)
|
|
# Tree.set_drag_forwarding: die drei Virtuals (_get_drag_data/_can_drop_data/
|
|
# _drop_data) wohnen am Tree selbst — hier per Callables weitergeleitet.
|
|
content_tree.set_drag_forwarding(_tree_drag_data, _tree_can_drop, _tree_drop)
|
|
_context_menu = PopupMenu.new()
|
|
_context_menu.hide_on_item_selection = true
|
|
_context_menu.id_pressed.connect(_on_menu_action)
|
|
add_child(_context_menu)
|
|
_rename_dialog = AcceptDialog.new()
|
|
_rename_dialog.title = "Gruppe umbenennen"
|
|
_rename_dialog.ok_button_text = "Übernehmen"
|
|
# Hinweis: AcceptDialog kennt in Godot 4.7 weder cancel_button_text noch
|
|
# get_cancel_button() — der Default-Abbrechen-Button bleibt, wie er ist.
|
|
_rename_field = LineEdit.new()
|
|
_rename_field.placeholder_text = "Gruppenname"
|
|
_rename_dialog.add_child(_rename_field)
|
|
_rename_dialog.confirmed.connect(_on_rename_confirmed)
|
|
add_child(_rename_dialog)
|
|
_confirm_delete = ConfirmationDialog.new()
|
|
_confirm_delete.title = "Gruppe löschen"
|
|
_confirm_delete.ok_button_text = "Löschen"
|
|
_confirm_delete.cancel_button_text = "Abbrechen"
|
|
_confirm_delete.confirmed.connect(_on_delete_confirmed)
|
|
add_child(_confirm_delete)
|
|
add_child(_separator())
|
|
|
|
|
|
## multi_selected kommt je Zeile (auch beim Abwählen): die Auswahl EINMAL pro
|
|
## Frame auswerten, nicht pro Signal.
|
|
func _on_tree_multi_selected(_item: TreeItem, _column: int, _selected: bool) -> void:
|
|
if _tree_select_lock or _tree_select_pending:
|
|
return
|
|
_tree_select_pending = true
|
|
_apply_tree_selection.call_deferred()
|
|
|
|
|
|
func _apply_tree_selection() -> void:
|
|
_tree_select_pending = false
|
|
_on_tree_selected()
|
|
|
|
|
|
## Der Baum ruft zurück, wenn der Nutzer dort ein Objekt auswählt.
|
|
## SELECT_MULTI (Karte t_c46d31c2): einfacher Klick = Ersatz-Auswahl,
|
|
## Strg+Klick addiert/entfernt nativ — ALLE markierten Objekt-Zeilen spiegeln
|
|
## sich in die Viewport-Auswahl (Baum und Szene sind immer synchron).
|
|
func _on_tree_selected() -> void:
|
|
if _tree_select_lock:
|
|
return
|
|
var item := content_tree.get_selected()
|
|
if item == null:
|
|
return
|
|
# Gruppe geklickt (Karte t_e3c0310a U2): alle Mitglieder auswählen.
|
|
if item.has_meta("group_id"):
|
|
var g_indices: Array = []
|
|
var walk_g := item.get_first_child()
|
|
while walk_g != null:
|
|
if walk_g.has_meta("obj_index"):
|
|
g_indices.append(int(walk_g.get_meta("obj_index")))
|
|
walk_g = walk_g.get_next()
|
|
if not g_indices.is_empty():
|
|
_tree_select_lock = true
|
|
main.interaction.select_many(g_indices)
|
|
_tree_select_lock = false
|
|
main.call("_set_status", "Gruppe %d ausgewählt (%d Objekte) — gemeinsam verschieben/drehen/duplizieren." % [int(item.get_meta("group_id")), g_indices.size()])
|
|
return
|
|
var indices: Array = []
|
|
var walk := content_tree.get_next_selected(null)
|
|
while walk != null:
|
|
if walk.has_meta("obj_index"):
|
|
indices.append(int(walk.get_meta("obj_index")))
|
|
walk = content_tree.get_next_selected(walk)
|
|
if indices.is_empty():
|
|
return
|
|
var objects: Array = main.doc.get("objects", [])
|
|
var valid: Array = []
|
|
for idx in indices:
|
|
if idx >= 0 and idx < objects.size():
|
|
valid.append(idx)
|
|
if valid.is_empty():
|
|
return
|
|
if valid.size() == 1:
|
|
main.interaction.select_object(valid[0], true)
|
|
main.call("_set_status", "%s ausgewählt — ziehen zum Verschieben, R/Mausrad zum Drehen, Entf zum Löschen. Strg+Klick markiert weitere dazu." % main.object_label(str(objects[valid[0]].get("id", ""))))
|
|
else:
|
|
main.interaction.select_many(valid)
|
|
main.call("_set_status", "%d Objekte im Baum markiert — Entf löscht ALLE, Strg+D dupliziert ALLE (jeweils EIN Undo-Schritt)." % valid.size())
|
|
|
|
|
|
func mark_tree_selection(idx: int) -> void:
|
|
mark_tree_selection_multi([idx])
|
|
|
|
|
|
func mark_tree_selection_multi(indices: Array) -> void:
|
|
_tree_select_lock = true
|
|
var wanted := {}
|
|
for i in indices:
|
|
wanted[int(i)] = true
|
|
# Zwei-Wege-Kopplung (Karte t_e3c0310a U2): die Auswahl in 3D spiegelt
|
|
# sich im Baum — über die Index-Karte O(1) statt Voll-Suche.
|
|
for idx in wanted.keys():
|
|
if _obj_index_map.has(idx):
|
|
var item: TreeItem = _obj_index_map[idx]
|
|
item.select(0)
|
|
# Abwählen: alle übrigen markierten Objekt-Zeilen deselektieren.
|
|
var walk := content_tree.get_next_selected(null)
|
|
while walk != null:
|
|
if walk.has_meta("obj_index") and not wanted.has(int(walk.get_meta("obj_index"))):
|
|
walk.deselect(0)
|
|
walk = content_tree.get_next_selected(walk)
|
|
var primary: Variant = main.interaction.get("selected_index") if main.interaction != null else -1
|
|
if primary is int and primary >= 0 and _obj_index_map.has(primary):
|
|
_ensure_tree_visible(content_tree.get_root(), primary)
|
|
_tree_select_lock = false
|
|
|
|
|
|
func _ensure_tree_visible(item: TreeItem, idx: int) -> void:
|
|
if item == null:
|
|
return
|
|
if item.has_meta("obj_index") and int(item.get_meta("obj_index")) == idx:
|
|
# Fix (beim Inspektor-Einbau gefunden, Vorbestand t_f6f29d86): Tree
|
|
# und TreeItem haben KEIN ensure_visible — die Godot-4-API heißt
|
|
# Tree.scroll_to_item. Der alte Ruf warf bei JEDER Auswahl einen
|
|
# SCRIPT ERROR und scrollte den Baum nie zur Auswahl.
|
|
content_tree.scroll_to_item(item)
|
|
return
|
|
for child in item.get_children():
|
|
_ensure_tree_visible(child, idx)
|
|
|
|
|
|
func refresh_tree() -> void:
|
|
content_tree.clear()
|
|
_obj_index_map.clear()
|
|
var root := content_tree.create_item()
|
|
var objects: Array = main.doc.get("objects", [])
|
|
var zones: Dictionary = main.doc.get("spawn_zones", {})
|
|
var group_ids: Array = GroupOps.active_group_ids(main.doc)
|
|
|
|
# Leerer Zustand (Karte t_e3c0310a U2): Klartext-Hinweis statt leere Gruppen.
|
|
if objects.is_empty() and zones.is_empty() and group_ids.is_empty():
|
|
var empty_item := content_tree.create_item(root)
|
|
empty_item.set_text(0, "Noch keine Objekte — links in der Objekt-Bibliothek wählen")
|
|
empty_item.set_selectable(0, false)
|
|
return
|
|
|
|
# Objekte: Kategorie → Objekte (Karte t_e3c0310a U2 — z. B. Mauern, Barrieren).
|
|
var obj_item := content_tree.create_item(root)
|
|
obj_item.set_text(0, "Objekte (%d)" % objects.size())
|
|
obj_item.set_meta("objekte_kopf", true) # Drag-Ziel „aus Gruppe herauslösen“ (U3)
|
|
var kat_map: Dictionary = {}
|
|
for i in objects.size():
|
|
var obj = objects[i]
|
|
if obj is Dictionary:
|
|
var oid := str(obj.get("id", "?"))
|
|
var kat := LibraryBrowser.group_of(oid)
|
|
if not kat_map.has(kat):
|
|
kat_map[kat] = []
|
|
kat_map[kat].append({"idx": i, "obj": obj})
|
|
for kat in kat_map.keys():
|
|
var kat_item := content_tree.create_item(obj_item)
|
|
kat_item.set_text(0, "%s (%d)" % [_kat_label(kat), (kat_map[kat] as Array).size()])
|
|
for data in kat_map[kat]:
|
|
var idx: int = int(data["idx"])
|
|
var obj: Dictionary = data["obj"]
|
|
var is_visible: bool = bool(obj.get("visible", true))
|
|
var item := content_tree.create_item(kat_item)
|
|
item.set_text(0, ("👁 " if is_visible else "🚫 ") + "%s #%d" % [main.object_label(str(obj.get("id", "?"))), idx + 1])
|
|
item.set_meta("obj_index", idx)
|
|
item.set_meta("visible", is_visible)
|
|
_obj_index_map[idx] = item
|
|
|
|
# Spawn-Zonen: Team A/B (Karte t_e3c0310a U2 — aus der alten Struktur).
|
|
if not zones.is_empty():
|
|
var spawn_item := content_tree.create_item(root)
|
|
spawn_item.set_text(0, "Spawn-Zonen")
|
|
for team_key in ["0", "1"]:
|
|
if not zones.has(team_key):
|
|
continue
|
|
var zone: Dictionary = zones[team_key]
|
|
var team_item := content_tree.create_item(spawn_item)
|
|
var team_name := "Team Blau" if team_key == "0" else "Team Rot"
|
|
team_item.set_text(0, "%s (%d Punkte)" % [team_name, (zone.get("points", []) as Array).size()])
|
|
for p in (zone.get("points", []) as Array):
|
|
if p is Dictionary:
|
|
var point_item := content_tree.create_item(team_item)
|
|
point_item.set_text(0, "%s (%.0f, %.0f)" % [
|
|
str(p.get("id", "?")), float(p.get("pos", [0, 0, 0])[0]), float(p.get("pos", [0, 0, 0])[2])])
|
|
|
|
# Gruppen (Karte t_e3c0310a U2): aktive Gruppen als eigene Ebene im Baum.
|
|
# Klick auf eine Gruppe wählt ALLE Mitglieder in der 3D-Szene (Zwei-Wege).
|
|
if not group_ids.is_empty():
|
|
var grp_item := content_tree.create_item(root)
|
|
grp_item.set_text(0, "Gruppen (%d)" % group_ids.size())
|
|
grp_item.set_meta("objekte_kopf", true) # Drag-Ziel „herauslösen“ (U3)
|
|
for gid in group_ids:
|
|
var member_indices: Array = GroupOps.members_of(main.doc, gid)
|
|
# U3 (Karte t_a4bf9b8c): gespeicherter Name voran, Fallback "Gruppe N".
|
|
var g_node := content_tree.create_item(grp_item)
|
|
g_node.set_text(0, "%s (%d Objekte)" % [GroupState.display_name(main.doc, int(gid)), member_indices.size()])
|
|
g_node.set_meta("group_id", int(gid))
|
|
for mi in member_indices:
|
|
var obj: Dictionary = main.doc["objects"][int(mi)]
|
|
var m_item := content_tree.create_item(g_node)
|
|
m_item.set_text(0, " %s" % main.object_label(str(obj.get("id", "?"))))
|
|
m_item.set_meta("obj_index", int(mi))
|
|
|
|
|
|
## Kategorie-Schlüssel (z. B. "mauern") in Anzeige-Label (z. B. "Mauern") —
|
|
## Karte t_e3c0310a U2. Nutzt KAT_GROUPS aus LibraryBrowser.
|
|
static func _kat_label(key: String) -> String:
|
|
for kat_name in LibraryBrowser.KAT_GROUPS:
|
|
var g: Dictionary = LibraryBrowser.KAT_GROUPS[kat_name]
|
|
if str(g["key"]) == key:
|
|
return str(g["label"])
|
|
return key.capitalize()
|
|
|
|
|
|
# ================= U3: Gruppen im Baum (Karte t_a4bf9b8c) ====================
|
|
# Gruppen direkt im Szenen-Baum bilden: Rechtsklick-Menü (Gruppieren/Auflösen/
|
|
# Umbenennen/Löschen), Drag&Drop für die Mitgliedschaft, Gruppennamen im
|
|
# Dokument (GroupState, additiv). Jede Aktion = EIN Undo-Schritt über
|
|
# main.history — die Mehrfachauswahl-Logik (interaction.gd) wird benutzt,
|
|
# nicht kopiert; object_ops.gd nur LESEND.
|
|
|
|
|
|
## Rechtsklick im Baum: Zeile unter der Maus finden → Kontextmenü aufmachen.
|
|
func _on_tree_gui_input(event: InputEvent) -> void:
|
|
if not (event is InputEventMouseButton):
|
|
return
|
|
var mb := event as InputEventMouseButton
|
|
if not mb.pressed or mb.button_index != MOUSE_BUTTON_RIGHT:
|
|
return
|
|
var item: TreeItem = content_tree.get_item_at_position(mb.position)
|
|
if item == null:
|
|
return
|
|
# Rechtsklick auf eine (noch nicht markierte) Zeile wählt sie an — wie in
|
|
# Unity/Godot: das Menü gehört zur angeklickten Zeile. Direkter Ruf von
|
|
# _on_tree_selected (item_selected feuert bei .select() nicht selbst).
|
|
if item.has_meta("obj_index") and not item.is_selected(0):
|
|
content_tree.deselect_all()
|
|
item.select(0)
|
|
_on_tree_selected()
|
|
_open_context_menu(item, Vector2i(content_tree.get_screen_transform() * mb.position))
|
|
|
|
|
|
## Kontextmenü je Zeilen-Typ aufbauen (Klartext-Einträge, je Aktion eine ID).
|
|
func _open_context_menu(item: TreeItem, at_screen: Vector2i) -> void:
|
|
_context_menu.clear()
|
|
var has_entries := false
|
|
# Gruppen-Knoten: Auflösen / Umbenennen / Löschen (Rückfrage bei > 1 Objekt).
|
|
if item.has_meta("group_id"):
|
|
var gid := int(item.get_meta("group_id"))
|
|
_context_menu.add_item("⛓ Gruppe auflösen", 1)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loesen_gruppe", "gid": gid})
|
|
_context_menu.add_item("✂ Umbenennen…", 2)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "umbenennen", "gid": gid})
|
|
_context_menu.add_item("🗑 Gruppe löschen…", 3)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loeschen_gruppe", "gid": gid})
|
|
has_entries = true
|
|
# Objekt-Zeile: Gruppieren (wenn ≥ 2 markiert), Aus Gruppe lösen, Löschen.
|
|
if item.has_meta("obj_index"):
|
|
var sel: Array = main.interaction.selected_indices
|
|
if sel.size() >= 2:
|
|
_context_menu.add_item("🔗 Gruppieren (%d Objekte)" % sel.size(), 10)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "gruppieren"})
|
|
has_entries = true
|
|
var objects: Array = main.doc.get("objects", [])
|
|
var idx := int(item.get_meta("obj_index"))
|
|
if idx >= 0 and idx < objects.size() and GroupOps.group_id(objects[idx]) != GroupOps.UNGROUPED:
|
|
_context_menu.add_item("⛓ Aus Gruppe lösen", 1)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loesen_einzeln", "idx": idx})
|
|
has_entries = true
|
|
_context_menu.add_item("🗑 Löschen", 3)
|
|
_context_menu.set_item_metadata(_context_menu.item_count - 1, {"aktion": "loeschen_einzeln", "idx": idx})
|
|
has_entries = true
|
|
if not has_entries:
|
|
return
|
|
_context_menu.position = at_screen
|
|
_context_menu.popup()
|
|
|
|
|
|
## Menü-Auswahl → Aktion (Metadaten tragen Ziel; ID → Zeile über get_item_index,
|
|
## robust auch nachdem das Menü sich bei hide_on_item_selection schon versteckt).
|
|
func _on_menu_action(id: int) -> void:
|
|
var menu_index := _context_menu.get_item_index(id)
|
|
if menu_index < 0:
|
|
return
|
|
var meta: Variant = _context_menu.get_item_metadata(menu_index)
|
|
if not (meta is Dictionary):
|
|
return
|
|
var m := meta as Dictionary
|
|
match str(m.get("aktion", "")):
|
|
"gruppieren":
|
|
group_selected()
|
|
"loesen_gruppe":
|
|
ungroup_group(int(m.get("gid", -1)))
|
|
"loesen_einzeln":
|
|
ungroup_indices([int(m.get("idx", -1))])
|
|
"umbenennen":
|
|
open_rename_dialog(int(m.get("gid", -1)))
|
|
"loeschen_gruppe":
|
|
delete_group(int(m.get("gid", -1)))
|
|
"loeschen_einzeln":
|
|
_select_for_action([int(m.get("idx", -1))])
|
|
main.interaction.delete_selected()
|
|
|
|
|
|
## Auswahl für eine Aktion setzen (Baum-Markierung + 3D synchron).
|
|
func _select_for_action(indices: Array) -> void:
|
|
_tree_select_lock = true
|
|
main.interaction.select_many(indices)
|
|
_tree_select_lock = false
|
|
|
|
|
|
## Mehrfachauswahl → Gruppe (nutzt die bestehende interaction-Logik, EIN Undo).
|
|
func group_selected() -> void:
|
|
var sel: Array = main.interaction.selected_indices
|
|
if sel.size() < 2:
|
|
main.call("_set_status", "Gruppieren braucht mindestens 2 markierte Objekte — Strg+Klick im Baum oder im Viewport.", true)
|
|
return
|
|
main.interaction.group_selection()
|
|
_refresh_after_group_change()
|
|
_select_for_action(sel) # Gruppe bleibt markiert (Mitglieder hängen jetzt unter dem Gruppen-Knoten)
|
|
|
|
|
|
## GANZE Gruppe auflösen: Mitglieder anwählen → bestehende Löse-Logik.
|
|
func ungroup_group(gid: int) -> void:
|
|
var members: Array = GroupOps.members_of(main.doc, gid)
|
|
if members.is_empty():
|
|
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
|
|
return
|
|
_select_for_action(members)
|
|
main.interaction.ungroup_selection()
|
|
_refresh_after_group_change()
|
|
|
|
|
|
## Objekte aus ihrer Gruppe herauslösen (NUR diese Indizes — Drag-out-Pfad).
|
|
## EIN Undo-Schritt: snapshot ["objects", "group_names"] vor/nach (verwaiste
|
|
## Namen-Einträge werden im selben Schritt mitgeräumt).
|
|
func ungroup_indices(indices: Array) -> void:
|
|
var before := _snapshot_group_blocks()
|
|
var freed := GroupState.clear_group_fields(main.doc, indices)
|
|
if freed == 0:
|
|
main.call("_set_status", "Die markierten Objekte stecken in keiner Gruppe.")
|
|
return
|
|
GroupState.prune_orphans(main.doc)
|
|
main.history.commit("Objekt aus Gruppe gelöst (%d)" % freed,
|
|
before, _snapshot_group_blocks())
|
|
main.call("_refresh_history_buttons")
|
|
_refresh_after_group_change()
|
|
|
|
|
|
## Snapshot für Gruppen-Undo-Schritte: ["objects", "group_names"]. Fehlt der
|
|
## Namen-Block im Dokument (alte Maps), wird er als null SEEDED — ActionHistory
|
|
## wendet nur Schlüssel an, die im Snapshot stehen; ohne Seed bliebe der Name
|
|
## nach Undo als Leiche stehen (Restore von null löscht den Block sauber).
|
|
func _snapshot_group_blocks() -> Dictionary:
|
|
var out: Dictionary = main.history.snapshot(["objects", "group_names"])
|
|
if not out.has(GroupState.NAMES_KEY):
|
|
out[GroupState.NAMES_KEY] = null
|
|
return out
|
|
|
|
|
|
## Umbenennen-Kern (testbar ohne Dialog): Name im Dokument setzen, EIN Undo.
|
|
func rename_group(gid: int, new_name: String) -> bool:
|
|
if gid < 0:
|
|
return false
|
|
var clean := new_name.strip_edges()
|
|
if GroupState.group_name(main.doc, gid) == clean:
|
|
return false # Name unverändert — kein leerer Undo-Eintrag
|
|
var before := _snapshot_group_blocks()
|
|
GroupState.set_group_name(main.doc, gid, new_name)
|
|
GroupState.prune_orphans(main.doc)
|
|
var after := _snapshot_group_blocks()
|
|
var label := ("Gruppe umbenannt in „%s“" % clean) if not clean.is_empty() \
|
|
else ("Gruppenname entfernt (Gruppe %d)" % (gid + 1))
|
|
main.history.commit(label, before, after)
|
|
main.call("_refresh_history_buttons")
|
|
_refresh_after_group_change()
|
|
return true
|
|
|
|
|
|
## Umbenennen-Dialog (Rechtsklick am Gruppen-Knoten).
|
|
func open_rename_dialog(gid: int) -> void:
|
|
var members: Array = GroupOps.members_of(main.doc, gid)
|
|
if members.is_empty():
|
|
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
|
|
return
|
|
_rename_gid = gid
|
|
_rename_field.text = GroupState.group_name(main.doc, gid)
|
|
_rename_dialog.dialog_text = "Neuer Name für die Gruppe (leer = wieder „Gruppe %d“):" % (gid + 1)
|
|
_rename_field.grab_focus()
|
|
_rename_dialog.popup_centered(Vector2i(360, 100))
|
|
|
|
|
|
func _on_rename_confirmed() -> void:
|
|
if _rename_gid >= 0:
|
|
rename_group(_rename_gid, _rename_field.text)
|
|
_rename_gid = -1
|
|
|
|
|
|
## Gruppe löschen: Rückfrage bei > 1 Objekt, dann bestehende Lösch-Logik
|
|
## (interaction.delete_selected — EIN Undo-Schritt, Marker/3D räumen mit auf).
|
|
func delete_group(gid: int) -> void:
|
|
var members: Array = GroupOps.members_of(main.doc, gid)
|
|
if members.is_empty():
|
|
main.call("_set_status", "Gruppe %d existiert nicht mehr." % (gid + 1), true)
|
|
return
|
|
if members.size() > 1:
|
|
_confirm_gid = gid
|
|
_confirm_delete.dialog_text = "Gruppe „%s“ löschen? Die %d Objekte darin werden ALLE gelöscht — Strg+Z holt sie zurück." % [GroupState.display_name(main.doc, gid), members.size()]
|
|
_confirm_delete.popup_centered(Vector2i(480, 140))
|
|
return
|
|
delete_group_confirmed(gid)
|
|
|
|
|
|
## Lösch-Kern NACH der Rückfrage (auch direkt aufrufbar — Tests).
|
|
func delete_group_confirmed(gid: int) -> void:
|
|
var members: Array = GroupOps.members_of(main.doc, gid)
|
|
if members.is_empty():
|
|
return
|
|
_select_for_action(members)
|
|
main.interaction.delete_selected() # ruft selbst _after_structure_change → Baum neu
|
|
|
|
|
|
func _on_delete_confirmed() -> void:
|
|
if _confirm_gid >= 0:
|
|
delete_group_confirmed(_confirm_gid)
|
|
_confirm_gid = -1
|
|
|
|
|
|
# --- Drag & Drop (Mitgliedschaft ändern) -------------------------------------
|
|
|
|
## Zieh-Menge bestimmen: gezogene Zeile ist Teil der Markierung → die GANZE
|
|
## Markierung wandert mit; sonst nur die gezogene Zeile. (Testbar ohne echten
|
|
## Drag — _tree_drag_data ruft nur dies + set_drag_preview.)
|
|
func _collect_drag_indices(item: TreeItem) -> Array:
|
|
if item == null or not item.has_meta("obj_index"):
|
|
return []
|
|
var idx := int(item.get_meta("obj_index"))
|
|
var sel: Array = main.interaction.selected_indices
|
|
if idx in sel:
|
|
var out: Array = []
|
|
for i in sel:
|
|
if i is int:
|
|
out.append(int(i))
|
|
return out
|
|
return [idx]
|
|
|
|
|
|
## Tree-Drag-Start (set_drag_forwarding-Callable): Datenpaket für Drop-Ziele.
|
|
## Vorschau-Control wird NICHT in den Baum gehängt (set_drag_preview verlangt
|
|
## ein Control außerhalb des Baums).
|
|
func _tree_drag_data(from_position: Vector2) -> Variant:
|
|
var item: TreeItem = content_tree.get_item_at_position(from_position)
|
|
var indices := _collect_drag_indices(item)
|
|
if indices.is_empty():
|
|
return null
|
|
var label := Label.new()
|
|
label.text = "%d Objekt(e) ziehen" % indices.size()
|
|
content_tree.set_drag_preview(label)
|
|
return {"svmap_tree_objekte": indices}
|
|
|
|
|
|
## Drop erlaubt: AUF einen Gruppen-Knoten (aufnehmen) oder AUF den Objekte-Kopf/
|
|
## eine Kategorie-Zeile (herauslösen — nur wenn mindestens ein gezogenes Objekt
|
|
## überhaupt gruppiert ist).
|
|
func _tree_can_drop(at_position: Vector2, data: Variant) -> bool:
|
|
if not (data is Dictionary) or not (data as Dictionary).has("svmap_tree_objekte"):
|
|
return false
|
|
var item: TreeItem = content_tree.get_item_at_position(at_position)
|
|
if item == null:
|
|
return false
|
|
if item.has_meta("group_id"):
|
|
return true
|
|
if item.has_meta("objekte_kopf"):
|
|
var objects: Array = main.doc.get("objects", [])
|
|
for i in (data["svmap_tree_objekte"] as Array):
|
|
if i is int and i >= 0 and i < objects.size() \
|
|
and GroupOps.group_id(objects[i]) != GroupOps.UNGROUPED:
|
|
return true
|
|
return false
|
|
|
|
|
|
## Drop ausführen → Mitgliedschaft ändern (EIN Undo-Schritt), Baum/Auswahl neu.
|
|
func _tree_drop(at_position: Vector2, data: Variant) -> void:
|
|
var item: TreeItem = content_tree.get_item_at_position(at_position)
|
|
if item == null:
|
|
return
|
|
var indices: Array = (data as Dictionary).get("svmap_tree_objekte", [])
|
|
if item.has_meta("group_id"):
|
|
_apply_membership_drop(indices, int(item.get_meta("group_id")))
|
|
elif item.has_meta("objekte_kopf"):
|
|
_apply_membership_drop(indices, -1)
|
|
|
|
|
|
## Mitgliedschafts-Kern (testbar): gid ≥ 0 = aufnehmen, gid < 0 = herauslösen.
|
|
func _apply_membership_drop(indices: Array, gid: int) -> void:
|
|
var before := _snapshot_group_blocks()
|
|
var changed := 0
|
|
var label := ""
|
|
if gid >= 0:
|
|
changed = GroupState.assign_group(main.doc, indices, gid)
|
|
label = "%d Objekt(e) in „%s“ gezogen" % [changed, GroupState.display_name(main.doc, gid)]
|
|
else:
|
|
changed = GroupState.clear_group_fields(main.doc, indices)
|
|
label = "%d Objekt(e) aus der Gruppe herausgelöst" % changed
|
|
if changed == 0:
|
|
main.call("_set_status", "Nichts zu ändern — die Objekte gehören schon genau so zur Gruppe.")
|
|
return
|
|
GroupState.prune_orphans(main.doc)
|
|
main.history.commit(label, before, _snapshot_group_blocks())
|
|
main.call("_refresh_history_buttons")
|
|
_refresh_after_group_change()
|
|
_select_for_action(indices) # gezogene Objekte bleiben sichtbar markiert
|
|
|
|
|
|
## Nach jeder Gruppen-Änderung: Baum neu + Panels synchron (kein 3D-Rebuild —
|
|
## Positionen ändern sich beim Gruppieren/Umbenennen/Zuordnen nicht).
|
|
func _refresh_after_group_change() -> void:
|
|
main.call("_refresh_tree")
|
|
var sel: Array = main.interaction.selected_indices
|
|
_tree_select_lock = true
|
|
mark_tree_selection_multi(sel)
|
|
_tree_select_lock = false
|
|
main.call("_sync_selection_panel")
|