From 43636c840fd5430d801f1850e991e9cee6a2b984 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Mon, 19 Oct 2026 18:49:21 +0000 Subject: [PATCH] Bibliothek: 96-px-Kacheln, Kategorie-Gruppen, klappbar + gemerkt, Suche (t_59fc0f21) --- src/editor/library_browser.gd | 70 +++++++- src/ui/main.gd | 204 +++++++++++++++++++++-- tests/test_library_browser_ui.gd | 233 +++++++++++++++++++++++++++ tests/test_library_browser_ui.gd.uid | 1 + 4 files changed, 489 insertions(+), 19 deletions(-) create mode 100644 tests/test_library_browser_ui.gd create mode 100644 tests/test_library_browser_ui.gd.uid diff --git a/src/editor/library_browser.gd b/src/editor/library_browser.gd index 1819596..1589cc1 100644 --- a/src/editor/library_browser.gd +++ b/src/editor/library_browser.gd @@ -102,6 +102,26 @@ const GROUP_IDS := { "pilz_shiitake", "pilz_morchel", "pilz_austernseitling"], } +## Karten-Übersicht (t_59fc0f21): Die feste Gruppenliste oben deckt nur die +## kuratierten Alt-/Natur-Objekte ab — der 347-Objekt-Katalog bringt seine +## eigene Kategorie mit (Manifest-Feld `kategorie`, siehe library/objects.json). +## Diese Zuordnung zeigt Pack-/Pipeline-Objekte in sinnvollen Gruppen statt +## alles in einer Mega-„Sonstiges"-Gruppe abzulegen (das war der eigentliche +## Übersichts-Killer). Reihenfolge = Anzeige-Reihenfolge nach GROUP_ORDER. +const KAT_ORDER := ["mauern", "barrieren", "gebaeude", "requisiten", "fahrzeuge", "natur_uebrig"] +const KAT_GROUPS := { + "walls": {"key": "mauern", "label": "Mauern"}, + "barriers": {"key": "barrieren", "label": "Barrieren"}, + "buildings": {"key": "gebaeude", "label": "Gebäude"}, + "props": {"key": "requisiten", "label": "Requisiten"}, + "vehicles": {"key": "fahrzeuge", "label": "Fahrzeuge"}, + "nature": {"key": "natur_uebrig", "label": "Natur (übrige)"}, +} + +## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px, +## jetzt Kacheln mit 96 px (128-px-PNGs werden auf die Anzeige-Größe skaliert). +const THUMB_DISPLAY_PX := 96 + static func object_label(oid: String) -> String: return NAMES.get(oid, oid.capitalize()) @@ -115,6 +135,29 @@ static func group_of(oid: String) -> String: return "sonstiges" +## Gruppe eines Katalog-Eintrags (t_59fc0f21): erst die festen Gruppen (oben), +## dann das Manifest-Feld `kategorie` (KAT_GROUPS), sonst "sonstiges". +## entry darf leer sein (z. B. fremde ID ohne Manifest-Eintrag). +static func group_of_entry(oid: String, entry: Dictionary) -> String: + var fixed := group_of(oid) + if fixed != "sonstiges": + return fixed + var kat := str(entry.get("kategorie", "")) + if KAT_GROUPS.has(kat): + return str(KAT_GROUPS[kat]["key"]) + return "sonstiges" + + +## Deutscher Anzeigename: Manifest-Feld `name_de` (Pipeline-Objekte), sonst +## NAMES/fallback.ALLE Pipeline-Objekte zeigen damit ihren echten Namen statt +## des großgeschriebenen IDs (Karte t_59fc0f21). +static func entry_label(oid: String, entry: Dictionary) -> String: + var name_de := str(entry.get("name_de", "")) + if not name_de.is_empty(): + return name_de + return object_label(oid) + + ## Gruppen für den Browser, geprüft gegen das Manifest: nur IDs, die es in ## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte, ## noch nicht gruppierte IDs wandern nach "Sonstiges" — neue Bibliotheks- @@ -123,20 +166,33 @@ static func groups_in_order(manifest: Dictionary) -> Array: var lib: Dictionary = manifest.get("objects", {}) var by_group := {} for oid in lib.keys(): - var key := group_of(str(oid)) + var entry: Dictionary = lib.get(oid, {}) as Dictionary + var key := group_of_entry(str(oid), entry) if not by_group.has(key): by_group[key] = [] (by_group[key] as Array).append(str(oid)) var out: Array = [] - for key in GROUP_ORDER: + # Reihenfolge (t_59fc0f21): Kategorie-Gruppen zuerst (347-Objekt-Katalog ist + # überwiegend Pipeline-Objekte — Mauern/Gebäude/Requisiten oben), danach die + # kuratierten Alt-/Natur-Gruppen, "Sonstiges" immer ganz hinten. + for key in KAT_ORDER + GROUP_ORDER: var ids: Array = (by_group.get(key, []) as Array) ids.sort() if ids.is_empty() and key != "sonstiges": continue - out.append({"key": key, "label": GROUP_LABELS[key], "ids": ids}) + out.append({"key": key, "label": _group_label(key), "ids": ids}) return out +static func _group_label(key: String) -> String: + if GROUP_LABELS.has(key): + return str(GROUP_LABELS[key]) + for kat in KAT_GROUPS.values(): + if str(kat["key"]) == key: + return str(kat["label"]) + return key + + ## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig). static func thumb_path(oid: String) -> String: return "res://library/thumbs/%s.png" % oid @@ -144,9 +200,11 @@ static func thumb_path(oid: String) -> String: ## Thumbnail als Textur laden — direkt über Image (kein Import-System nötig, ## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe -## runtergerechnet. null = noch kein Thumbnail → Button fällt aufs Text-Layout -## zurück (Fehler sieht man nicht, man sieht einfach nur kein Bild). -static func load_thumb(oid: String, size_px := 40) -> Texture2D: +## runtergerechnet. size_px steuert die Anzeige-Größe (Default = THUMB_DISPLAY_PX +## = 96, Karte t_59fc0f21; vorher fest 40). null = noch kein Thumbnail → +## Button fällt aufs Text-Layout zurück (Fehler sieht man nicht, man sieht +## einfach nur kein Bild). +static func load_thumb(oid: String, size_px := THUMB_DISPLAY_PX) -> Texture2D: var path := thumb_path(oid) if not FileAccess.file_exists(path): return null diff --git a/src/ui/main.gd b/src/ui/main.gd index a3dd234..156e667 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -48,6 +48,15 @@ var view_builder: Node3D var camera: Camera3D var library_box: VBoxContainer var library_buttons: Dictionary = {} # id → Button +## Bibliothek-Gruppen (t_59fc0f21): Klapp-Zustand + Zeilen für Suche/Alle auf/zu. +var library_search: LineEdit +var library_open_all_btn: Button +var library_close_all_btn: Button +var _library_group_open: Dictionary = {} # Gruppen-Key → auf/zu +var _library_group_rows: Array = [] # {key, header, box, ids} +var _library_bold_font: Font = null +const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg + var undo_btn: Button var redo_btn: Button var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) @@ -433,6 +442,33 @@ func _build_left_panel() -> PanelContainer: vbox.add_child(_header_label("Objekt-Bibliothek")) vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) + # Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit + # Treffern klappen auf, leere Gruppen verschwinden. + library_search = LineEdit.new() + library_search.placeholder_text = "Suchen (Name oder ID)…" + library_search.clear_button_enabled = true + library_search.custom_minimum_size = Vector2(0, 36) + library_search.text_changed.connect(_apply_library_filter) + library_search.text_submitted.connect(func(_t: String) -> void: library_search.release_focus()) + vbox.add_child(library_search) + + # „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal. + var group_btns := HBoxContainer.new() + group_btns.add_theme_constant_override("separation", 6) + library_open_all_btn = Button.new() + library_open_all_btn.text = "Alle auf" + library_open_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + library_open_all_btn.tooltip_text = "Klappt ALLE Gruppen auf" + library_open_all_btn.pressed.connect(_on_library_expand_all) + group_btns.add_child(library_open_all_btn) + library_close_all_btn = Button.new() + library_close_all_btn.text = "Alle zu" + library_close_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + library_close_all_btn.tooltip_text = "Klappt ALLE Gruppen zu" + library_close_all_btn.pressed.connect(_on_library_collapse_all) + group_btns.add_child(library_close_all_btn) + vbox.add_child(group_btns) + var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.custom_minimum_size = Vector2(0, 280) @@ -478,42 +514,184 @@ func _build_left_panel() -> PanelContainer: func _fill_library() -> void: library_buttons.clear() - # Gruppen-Browser (Karte t_3a998ae4): Klapp-Überschriften je Gruppe, - # darunter die Objekte mit Thumbnail (128-px-Render, runterskaliert). + _library_group_rows.clear() + _library_group_open = _library_load_open_states() + # Gruppen-Browser (Karte t_3a998ae4, Überarbeitung t_59fc0f21): + # - Klapp-Überschriften je Gruppe (fett, 18 px, Name + Anzahl), + # - STANDARD EINGEKLAPPT — nur die erste Gruppe ist offen (Übersicht!), + # - Klapp-Zustand je Gruppe wird in user://scopedvr_editor.cfg gemerkt, + # - Objekte als Kacheln mit 96-px-Vorschaubild (2 Spalten je Gruppe). + var first_open: String = "" for group in LibraryBrowser.groups_in_order(manifest): + var g_key: String = group["key"] var g_label: String = group["label"] var g_ids: Array = group["ids"] if g_ids.is_empty(): continue + if first_open.is_empty(): + first_open = g_key + var is_open: bool = _library_group_open.get(g_key, g_key == first_open) + _library_group_open[g_key] = is_open var header := Button.new() - header.text = "▾ %s (%d)" % [g_label, g_ids.size()] + header.text = _library_header_text(is_open, g_label, g_ids.size()) header.toggle_mode = true - header.button_pressed = true + header.button_pressed = is_open header.alignment = HORIZONTAL_ALIGNMENT_LEFT - header.custom_minimum_size = Vector2(240, 40) + header.custom_minimum_size = Vector2(0, 34) + header.tooltip_text = "Gruppe „%s“ auf-/zuklappen" % g_label + header.add_theme_font_size_override("font_size", 18) + header.add_theme_font_override("font", _library_header_font()) library_box.add_child(header) var group_box := VBoxContainer.new() group_box.add_theme_constant_override("separation", 4) + group_box.visible = is_open + var grid := GridContainer.new() + grid.columns = 2 + grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + grid.add_theme_constant_override("h_separation", 6) + grid.add_theme_constant_override("v_separation", 6) + group_box.add_child(grid) library_box.add_child(group_box) - header.toggled.connect(func(on: bool) -> void: - group_box.visible = on - header.text = "%s %s (%d)" % ["▾" if on else "▸", g_label, g_ids.size()]) + header.toggled.connect(_on_library_group_toggled.bind(g_key, g_label, g_ids.size(), header, group_box)) for oid in g_ids: - _add_library_button(str(oid), group_box) + _add_library_button(str(oid), grid) + _library_group_rows.append({"key": g_key, "header": header, "box": group_box, "ids": g_ids, "label": g_label, "count": g_ids.size()}) + _apply_library_filter(library_search.text) + + +func _library_header_text(on: bool, label: String, count: int) -> String: + return "%s %s (%d)" % ["▾" if on else "▸", label, count] + + +func _on_library_group_toggled(on: bool, g_key: String, g_label: String, count: int, header: Button, group_box: VBoxContainer) -> void: + group_box.visible = on + header.text = _library_header_text(on, g_label, count) + _library_group_open[g_key] = on + _library_save_open_state(g_key, on) + + +func _on_library_expand_all() -> void: + for row in _library_group_rows: + (row["header"] as Button).button_pressed = true + + +func _on_library_collapse_all() -> void: + for row in _library_group_rows: + (row["header"] as Button).button_pressed = false + + +func _library_header_font() -> Font: + if _library_bold_font == null: + var sf := SystemFont.new() + sf.font_names = PackedStringArray(["Noto Sans", "DejaVu Sans", "Ubuntu", "sans-serif"]) + sf.font_weight = 700 + _library_bold_font = sf + return _library_bold_font + + +func _library_load_open_states() -> Dictionary: + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + var out := {} + if cfg.has_section(LIBRARY_CFG_SECTION): + for key in cfg.get_section_keys(LIBRARY_CFG_SECTION): + out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, str(key), true)) + return out + + +func _library_save_open_state(g_key: String, on: bool) -> void: + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + cfg.set_value(LIBRARY_CFG_SECTION, g_key, on) + cfg.save("user://scopedvr_editor.cfg") + + +## Live-Suche (t_59fc0f21): leer = alles normal sichtbar (Klapp-Zustand wie +## gemerkt); Text = nur Treffer (Name de/en, ID — case-insensitive), Gruppen +## mit Treffern bleiben offen, leere verschwinden. +func _apply_library_filter(query: String) -> void: + var q := query.strip_edges().to_lower() + var searching := not q.is_empty() + for row in _library_group_rows: + var ids: Array = row["ids"] + var any := false + for oid in ids: + var btn := library_buttons.get(str(oid)) as Button + if btn == null: + continue + var vis := true + if searching: + vis = _library_oid_matches(str(oid), q) + btn.visible = vis + any = any or vis + var header := row["header"] as Button + var box := row["box"] as VBoxContainer + header.visible = any + if searching: + # Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren + # (Pfeil zeigt dann offen, kosmetisch konsistent). + box.visible = any + if any: + header.text = _library_header_text(true, str(row["label"]), int(row["count"])) + else: + var is_open := bool(_library_group_open.get(str(row["key"]), false)) + box.visible = is_open + header.text = _library_header_text(is_open, str(row["label"]), int(row["count"])) + + +func _library_oid_matches(oid: String, q: String) -> bool: + if oid.to_lower().contains(q): + return true + var entry: Dictionary = (manifest.get("objects", {}) as Dictionary).get(oid, {}) + if LibraryBrowser.entry_label(oid, entry).to_lower().contains(q): + return true + if LibraryBrowser.object_label(oid).to_lower().contains(q): + return true + return false func _add_library_button(oid: String, parent: Container) -> void: var lib: Dictionary = manifest.get("objects", {}) var entry: Dictionary = lib.get(oid, {}) var poly := int(entry.get("poly", 0)) + var name_txt := LibraryBrowser.entry_label(oid, entry) var btn := Button.new() - btn.text = "%s · %d Polys" % [LibraryBrowser.object_label(oid), poly] - btn.icon = LibraryBrowser.load_thumb(oid) - btn.custom_minimum_size = Vector2(240, 44) - btn.alignment = HORIZONTAL_ALIGNMENT_LEFT btn.toggle_mode = true btn.button_pressed = false + btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + btn.custom_minimum_size = Vector2(0, 126) + btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid] btn.pressed.connect(_on_library_pressed.bind(oid)) + var tex := LibraryBrowser.load_thumb(oid) + if tex == null: + # Kein Thumbnail → Text-Zeile wie früher (Fehlertoleranz, t_3a998ae4). + btn.text = "%s · %d Polys" % [name_txt, poly] + btn.alignment = HORIZONTAL_ALIGNMENT_LEFT + btn.custom_minimum_size = Vector2(0, 44) + else: + # Kachel (t_59fc0f21): 96-px-Vorschaubild oben, Name klein darunter. + # Kinder ignorieren die Maus, damit die Klicks am Button ankommen. + var tile := VBoxContainer.new() + tile.mouse_filter = Control.MOUSE_FILTER_IGNORE + tile.set_anchors_preset(Control.PRESET_FULL_RECT) + tile.add_theme_constant_override("separation", 2) + btn.add_child(tile) + var tr := TextureRect.new() + tr.texture = tex + tr.custom_minimum_size = Vector2(LibraryBrowser.THUMB_DISPLAY_PX, LibraryBrowser.THUMB_DISPLAY_PX) + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + tr.mouse_filter = Control.MOUSE_FILTER_IGNORE + tile.add_child(tr) + var lbl := Label.new() + lbl.text = name_txt + lbl.add_theme_font_size_override("font_size", 11) + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.clip_text = true + lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + tile.add_child(lbl) parent.add_child(btn) library_buttons[oid] = btn diff --git a/tests/test_library_browser_ui.gd b/tests/test_library_browser_ui.gd new file mode 100644 index 0000000..be95e00 --- /dev/null +++ b/tests/test_library_browser_ui.gd @@ -0,0 +1,233 @@ +extends SceneTree +## Editor-Test (headless): Bibliotheks-Browser-UI — Karte t_59fc0f21. +## Ausführen: godot --headless --path . --script res://tests/test_library_browser_ui.gd +## Exit 0 = alles grün. +## +## Instanziert die ECHTE Main-UI (Muster tests/test_gui_smoke.gd) und prüft: +## 1. Kacheln: Thumbnail-TextureRect ≥ 64 px (Vorgabe 96), 2-Spalten-Grid, +## deutscher Name (name_de) unter dem Bild. +## 2. Gruppen: Standard eingeklappt (nur erste Gruppe offen), Header fett/18 px +## mit Name + Anzahl, Manifest-Deckung (alle Objekte als Button). +## 3. Klapp-Zustand wird in user://scopedvr_editor.cfg gemerkt und nach +## Neustart (Neu-Instanz) wiederhergestellt. +## 4. Suche: „pilz" filtert live (nur Pilze-Gruppe sichtbar), Leeren stellt +## den gemerkten Klapp-Zustand wieder her. +## 5. „Alle auf" / „Alle zu" klappen alles um. + +var failures := 0 +var checks := 0 +var main: Control + + +func _initialize() -> void: + _run_all.call_deferred() + + +func _run_all() -> void: + _clear_library_cfg_section() + var scene: PackedScene = load("res://scenes/main.tscn") + _check(scene != null, "scenes/main.tscn laedt") + if scene == null: + _finish() + return + main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + + _test_built() + _test_default_collapsed() + _test_tiles() + _test_search() + await _test_persistence() + _test_all_buttons() + + _finish() + + +func _finish() -> void: + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: %s" % label) + else: + failures += 1 + print(" FAIL: %s" % label) + + +func _clear_library_cfg_section() -> void: + # Nur UNSEREN Abschnitt leeren — [test]/[update] der anderen Features bleiben. + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + if cfg.has_section("library"): + cfg.erase_section("library") + cfg.save("user://scopedvr_editor.cfg") + + +func _group_row(g_key: String) -> Dictionary: + for row in main._library_group_rows: + if str(row["key"]) == g_key: + return row + return {} + + +func _test_built() -> void: + print("Testgruppe: UI-Bau (Suchfeld, Alle auf/zu, Gruppen)") + _check(main.library_search != null, "Suchfeld gebaut") + _check(main.library_open_all_btn != null and main.library_close_all_btn != null, + "Alle auf/Alle zu gebaut") + _check(main._library_group_rows.size() >= 8, + "Gruppen gebaut (%d)" % main._library_group_rows.size()) + var lib: Dictionary = main.manifest.get("objects", {}) + _check(main.library_buttons.size() == lib.size(), + "Manifest-Deckung: %d/%d Objekte als Button" % [main.library_buttons.size(), lib.size()]) + + +func _test_default_collapsed() -> void: + print("Testgruppe: Standard eingeklappt (nur erste Gruppe offen)") + var open_count := 0 + var first_key := "" + for row in main._library_group_rows: + var box := row["box"] as VBoxContainer + if box.visible: + open_count += 1 + if first_key.is_empty(): + first_key = str(row["key"]) + _check(open_count == 1, "genau 1 Gruppe offen (ist %d)" % open_count) + _check(not first_key.is_empty(), "eine Gruppe ist offen (%s)" % first_key) + # Header: 18 px Font, Text enthält Name + Anzahl. + var row0: Dictionary = main._library_group_rows[0] + var header := row0["header"] as Button + _check(header.get_theme_font_size("font_size") >= 18, "Header-Schrift ≥ 18 px") + _check(header.text.contains("(%d)" % int(row0["count"])), "Header zeigt Anzahl (%s)" % header.text) + # Alle Boxen außer der ersten sind zu — trotz 347 Objekten klein. + var hidden_ok := true + for row in main._library_group_rows: + if str(row["key"]) != first_key and not (row["box"] as VBoxContainer).visible: + continue + elif str(row["key"]) == first_key and (row["box"] as VBoxContainer).visible: + continue + hidden_ok = false + _check(hidden_ok, "übrige Gruppen zugeklappt") + + +func _test_tiles() -> void: + print("Testgruppe: Kacheln (96-px-Vorschaubild + Name)") + var tex := LibraryBrowser.load_thumb("crate_wood") + _check(tex != null and tex.get_width() >= 96, "load_thumb liefert ≥ 96 px (ist %s)" + % (str(tex.get_width()) if tex != null else "null")) + var tile_btn := main.library_buttons.get("watchtower_wood") as Button + _check(tile_btn != null, "Kachel-Button existiert") + if tile_btn != null: + var tr: TextureRect = null + var lbl: Label = null + for child in tile_btn.get_children(): + for sub in (child as Control).get_children(): + if sub is TextureRect: + tr = sub + elif sub is Label: + lbl = sub + _check(tr != null, "TextureRect in Kachel") + _check(lbl != null, "Name-Label in Kachel") + if tr != null: + _check(tr.custom_minimum_size.x >= 64, "Thumbnail-Zielgröße ≥ 64 px (ist %d)" + % int(tr.custom_minimum_size.x)) + _check(tr.size.x >= 64, "Thumbnail rendert ≥ 64 px breit (ist %.0f)" % tr.size.x) + _check(tr.texture != null and tr.texture.get_width() == 96, + "Textur auf 96 px skaliert") + if lbl != null: + _check(lbl.text == "Wachturm", "deutscher Name unter dem Bild (%s)" % lbl.text) + _check(tile_btn.size.y >= 100, "Kachel hoch genug (%.0f px)" % tile_btn.size.y) + + +func _test_search() -> void: + print("Testgruppe: Suche filtert live") + # „pilz" → nur die Pilze-Gruppe (6 Objekte) sichtbar. + main.library_search.text = "pilz" + main._apply_library_filter("pilz") + var visible_headers := 0 + var pilze_row := _group_row("pilze") + for row in main._library_group_rows: + if (row["header"] as Button).visible: + visible_headers += 1 + _check(visible_headers == 1, "nur 1 Gruppen-Header sichtbar (ist %d)" % visible_headers) + var visible_tiles := 0 + for oid in pilze_row["ids"]: + if (main.library_buttons.get(str(oid)) as Button).visible: + visible_tiles += 1 + _check(visible_tiles == 6, "6 Pilz-Kacheln sichtbar (ist %d)" % visible_tiles) + _check((pilze_row["box"] as VBoxContainer).visible, "Treffer-Gruppe offen") + # ID-Suche funktioniert auch: „watchtower" (ID) → Treffer in bauwerke. + main._apply_library_filter("watchtower") + var wt := main.library_buttons.get("watchtower_wood") as Button + _check(wt != null and wt.visible, "ID-Suche „watchtower“ findet Wachturm") + # Leeren → gemerkter Zustand wiederhergestellt (nur erste Gruppe offen). + main.library_search.text = "" + main._apply_library_filter("") + var open_count := 0 + for row in main._library_group_rows: + if (row["box"] as VBoxContainer).visible: + open_count += 1 + _check(open_count == 1, "nach Leeren wieder 1 Gruppe offen (ist %d)" % open_count) + + +func _test_persistence() -> void: + print("Testgruppe: Klapp-Zustand überlebt Neustart (cfg)") + # Fahrzeuge-Gruppe aufklappen → cfg speichert. + var row := _group_row("fahrzeuge") + _check(not row.is_empty(), "Gruppe fahrzeuge existiert") + if row.is_empty(): + return + var header := row["header"] as Button + header.button_pressed = true + await process_frame + var cfg := ConfigFile.new() + cfg.load("user://scopedvr_editor.cfg") + _check(cfg.get_value("library", "fahrzeuge", null) == true, + "cfg [library] fahrzeuge=true nach Aufklappen") + # „Neustart": Instanz wegwerfen, neu bauen — Zustand muss zurückkommen. + # (queue_free + Frames: free() während laufender Frame-Arbeit ist gesperrt.) + main.queue_free() + await process_frame + await process_frame + main = (load("res://scenes/main.tscn") as PackedScene).instantiate() + root.add_child(main) + await process_frame + await process_frame + var row2 := _group_row("fahrzeuge") + _check(not row2.is_empty() and (row2["box"] as VBoxContainer).visible, + "nach Neu-Instanz ist fahrzeuge wieder offen") + # Und wieder zu machen — Zustand bleibt gemerkt (false). + (_group_row("fahrzeuge")["header"] as Button).button_pressed = false + await process_frame + var cfg2 := ConfigFile.new() + cfg2.load("user://scopedvr_editor.cfg") + _check(cfg2.get_value("library", "fahrzeuge", null) == false, + "cfg [library] fahrzeuge=false nach Zuklappen") + + +func _test_all_buttons() -> void: + print("Testgruppe: Alle auf / Alle zu") + main._on_library_collapse_all() + var open_count := 0 + for row in main._library_group_rows: + if (row["box"] as VBoxContainer).visible: + open_count += 1 + _check(open_count == 0, "Alle zu: 0 Gruppen offen (ist %d)" % open_count) + main._on_library_expand_all() + open_count = 0 + for row in main._library_group_rows: + if (row["box"] as VBoxContainer).visible: + open_count += 1 + _check(open_count == main._library_group_rows.size(), + "Alle auf: alle %d Gruppen offen (ist %d)" % [main._library_group_rows.size(), open_count]) + # Aufräumen für die nächsten Läufe: wieder zu. + main._on_library_collapse_all() \ No newline at end of file diff --git a/tests/test_library_browser_ui.gd.uid b/tests/test_library_browser_ui.gd.uid new file mode 100644 index 0000000..9dc014e --- /dev/null +++ b/tests/test_library_browser_ui.gd.uid @@ -0,0 +1 @@ +uid://sgkivq27rfuj