From ae551f387acc5435fca043caeea858a82f0ceef0 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Tue, 20 Oct 2026 18:33:07 +0000 Subject: [PATCH] =?UTF-8?q?Fix:=20Resize-Dialog=205930=20px=20hoch=20+=20O?= =?UTF-8?q?K/Abbrechen=20unterhalb=20des=20Bildschirms=20(Sly-Befund=2020.?= =?UTF-8?q?10.=20=E2=80=9Eviel=20zu=20lang,=20kann=20nichts=20best=C3=A4ti?= =?UTF-8?q?gen").?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ursache (Proben im Container, xvfb): autowrap-Labels OHNE Mindestbreite melden VOR dem ersten Layout eine explosionsartige Mindesthöhe (Wrap bei ~1 px → jedes Wort eine Zeile). popup_centered() übernimmt genau diesen Wert als Fensterhöhe: Resize-Dialog 5930 px statt ~470, Neue-Map-Dialog 4781 px statt ~390 — beide waren betroffen. Nach dem Layout fällt der Mindestwert zurück, die Fenstergröße bleibt aber. Zusatz-Befund: wächst die Live-Vorschau NACH dem Öffnen (Preset 256 → Warnband), bleibt das Fenster oben hängen → Buttons bei y≈940 in einem 900-px-Sichtbereich = unerreichbar. Fix: - resize_map_dialog + new_map_dialog: autowrap-Labels bekommen Mindestbreite ~ Dialogbreite → Umbruch rechnet mit echter Breite, Fenster normal hoch (Probe: 481 px). - resize_map_dialog: Inhalt in ScrollContainer (horizontal aus), Höhe dynamisch = min(Inhalt, sichtbarer Platz); nach jeder Vorschau-Änderung wird das Fenster gedeckelt und neu zentriert (_fit_dialog/ _fit_after_layout) → OK/Abbrechen IMMER erreichbar. unresizable. - tests: test_resize_dialog prüft jetzt Mindesthöhe VOR dem Popup (absolut, < 1500 px) für beide Dialoge + echten Menü-Klick; Preset-Button-Suche robust über find_children. Proben (xvfb): Dialog 662×481, nach Preset 256 → 662×631, unten 765 < 900, Buttons sichtbar (Beweisbild work/dialog_beweis/resize_dialog_fix.png). Suiten: test_resize_dialog 44 OK, test_theme 35 OK, test_gui_smoke 92 OK, test_ux 137 OK, test_ui_layout 79 OK. --- src/ui/new_map_dialog.gd | 5 +++ src/ui/resize_map_dialog.gd | 75 +++++++++++++++++++++++++++++++++++-- tests/probe_buttons_pos.gd | 38 +++++++++++++++++++ tests/probe_dialog_min.gd | 46 +++++++++++++++++++++++ tests/probe_resize_shot.gd | 26 +++++++++++++ tests/probe_resize_size.gd | 60 +++++++++++++++++++++++++++++ tests/test_resize_dialog.gd | 29 ++++++++++---- 7 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 tests/probe_buttons_pos.gd create mode 100644 tests/probe_dialog_min.gd create mode 100644 tests/probe_resize_shot.gd create mode 100644 tests/probe_resize_size.gd diff --git a/src/ui/new_map_dialog.gd b/src/ui/new_map_dialog.gd index 8164e5d..e374ac5 100644 --- a/src/ui/new_map_dialog.gd +++ b/src/ui/new_map_dialog.gd @@ -60,6 +60,11 @@ func _make_label(text: String) -> Label: var label := Label.new() label.text = text label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + # FIX 20.10.: autowrap-Label ohne Mindestbreite meldet vor dem ersten + # Layout eine explosionsartige Mindesthöhe (Probe: Fenster 4781 px hoch + # statt ~390) — Popup-Größe wird daraus einmalig übernommen. Mindestbreite + # ~ Dialogbreite verankert den Umbruch an der echten Breite. + label.custom_minimum_size = Vector2(UIScale.px(440), 0) return label diff --git a/src/ui/resize_map_dialog.gd b/src/ui/resize_map_dialog.gd index 27a3d5b..ab9e21d 100644 --- a/src/ui/resize_map_dialog.gd +++ b/src/ui/resize_map_dialog.gd @@ -14,6 +14,8 @@ var size_z_edit: SpinBox var drop_check: CheckBox var info_label: Label # aktuelle Größe + Auflösung var preview_label: Label # Live-Vorschau (Verlust-Liste / Warnband) +var _scroll: ScrollContainer +var _vbox: VBoxContainer var _old_sx := 0.0 var _old_sz := 0.0 @@ -28,11 +30,26 @@ func _init() -> void: ok_button_text = "Größe ändern" cancel_button_text = "Abbrechen" min_size = UIScale.v2i(520, 0) + unresizable = true # Der Dialog wächst selbst per Inhalt; kein Hand-Resize + + # FIX 20.10.b: Inhalt in einen ScrollContainer — wächst die Live-Vorschau + # NACH dem Öffnen (z. B. Preset 256 → Warnband), kann die Fensterhöhe + # gedeckelt werden, OHNE dass Inhalt oder OK/Abbrechen verloren gehen. + # Start-Mindesthöhe ~ kompletter Normalinhalt (kein Kollaps beim Popup); + # _fit_after_layout setzt sie dynamisch auf Inhalt vs. sichtbarer Platz. + var scroll := ScrollContainer.new() + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + scroll.custom_minimum_size = UIScale.v2(500, 430) + add_child(scroll) + scroll.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _scroll = scroll var vbox := VBoxContainer.new() - vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", UIScale.px(12)) - add_child(vbox) + vbox.custom_minimum_size = UIScale.v2(500, 0) + vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.add_child(vbox) + _vbox = vbox info_label = _make_label("") vbox.add_child(info_label) @@ -65,7 +82,9 @@ func _init() -> void: preview_label = _make_label("") preview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - preview_label.custom_minimum_size = UIScale.v2(0, 60) + # FIX 20.10.b: gleiche autowrap-Explosion wie oben — (0, 60) überschreibt die + # Mindestbreite von _make_label, deshalb hier BREITE + Höhe explizit setzen. + preview_label.custom_minimum_size = UIScale.v2(500, 60) vbox.add_child(preview_label) drop_check = CheckBox.new() @@ -84,6 +103,13 @@ func _make_label(text: String) -> Label: var label := Label.new() label.text = text label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + # FIX 20.10. (Sly-Befund „Fenster viel zu lang, nichts bestätigbar“): ein + # autowrap-Label OHNE Mindestbreite meldet VOR dem ersten Layout eine + # explosionsartige Mindesthöhe (es wrappet bei ~1 px Breite → jedes Wort + # eine Zeile). popup_centered() nimmt genau diesen Wert als Fensterhöhe + # (Probe: 10 322 px statt ~320). Mindestbreite ~ Dialogbreite verankert + # den Umbruch an der echten Breite → Fenster normal hoch. + label.custom_minimum_size = Vector2(UIScale.px(500), 0) return label @@ -192,6 +218,49 @@ func _update_preview() -> void: preview_label.modulate = Color(1.0, 0.75, 0.3) preview_label.text = "\n".join(lines) + _fit_after_layout.call_deferred() + + +## Fenster im sichtbaren Bereich halten: Höhe deckeln (überzähliger Inhalt +## scrollt im ScrollContainer) und neu zentrieren — sonst rutschen +## OK/Abbrechen unter den Bildschirmrand, wenn die Live-Vorschau das Fenster +## NACH dem Öffnen wachsen lässt (Probe: Fenster blieb oben hängen, Buttons +## bei y≈940 in einem 900-px-Sichtbereich). +func _fit_dialog() -> void: + var area := _parent_area() + if area.x <= 0.0 or area.y <= 0.0: + return + var max_h := int(area.y) - 8 + if size.y > max_h: + size = Vector2i(size.x, max_h) + var half := (area - Vector2(size)) * 0.5 + position = Vector2(maxi(0, int(half.x)), maxi(0, int(half.y))) + + +## Erst das Layout (und damit das Fenster-Wachstum) abwarten, dann den +## Scrollbereich auf „Inhalt vs. sichtbarer Platz“ justieren und verankern. +func _fit_after_layout() -> void: + await get_tree().process_frame + await get_tree().process_frame + var area := _parent_area() + if area.x <= 0.0 or area.y <= 0.0: + return + # Scrollbereich: so hoch wie der Inhalt, aber nie höher als der sichtbare + # Platz (Titelleiste + OK/Abbrechen + Ränder bleiben reserviert). Ist der + # Inhalt größer, scrollt er INNEN — die Buttons bleiben immer erreichbar. + var needed := _vbox.get_combined_minimum_size().y + UIScale.px(16) + var max_body := area.y - UIScale.px(90) + _scroll.custom_minimum_size = Vector2(UIScale.px(500), minf(needed, max_body)) + _fit_dialog() + + +func _parent_area() -> Vector2: + var p := get_parent() + if p is Control: + return (p as Control).size + if p is Window: + return (p as Window).size + return Vector2.ZERO ## Projected new resolution nach dem Resize (gleiche Leiter wie resize_terrain, diff --git a/tests/probe_buttons_pos.gd b/tests/probe_buttons_pos.gd new file mode 100644 index 0000000..1834b6b --- /dev/null +++ b/tests/probe_buttons_pos.gd @@ -0,0 +1,38 @@ +extends SceneTree +## Eine Probe: Button-Rects messen UND Beweisbild mit genau diesem Zustand +## (Spinboxes auf 256 → lange Vorschau, wie Sly). + +func _initialize() -> void: + _run.call_deferred() + +func _rect_chain(n: Node) -> String: + var parts: Array[String] = [] + var cur: Node = n + while cur != null: + if cur is Control: + parts.append("%s pos=%s size=%s" % [cur.get_class(), cur.get_position(), cur.size]) + elif cur is Window: + parts.append("%s pos=%s size=%s" % [cur.get_class(), cur.position, cur.size]) + cur = cur.get_parent() + return " <- ".join(parts) + +func _run() -> void: + var scene: PackedScene = load("res://scenes/main.tscn") + var main: Control = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + main._start_with_new_map() + main._open_resize_dialog() + main.resize_dialog.size_x_edit.value = 256.0 + main.resize_dialog.size_z_edit.value = 256.0 + await process_frame + await process_frame + await RenderingServer.frame_post_draw + var dlg: ConfirmationDialog = main.resize_dialog + print("PROBE dlg pos=", dlg.position, " size=", dlg.size) + print("PROBE OK-chain: ", _rect_chain(dlg.get_ok_button())) + print("PROBE vbox: ", _rect_chain(dlg.get_child(0))) + var img := root.get_viewport().get_texture().get_image() + img.save_png("/opt/data/scopedvr/work/dialog_beweis/resize_256_state.png") + quit(0) diff --git a/tests/probe_dialog_min.gd b/tests/probe_dialog_min.gd new file mode 100644 index 0000000..5d7984b --- /dev/null +++ b/tests/probe_dialog_min.gd @@ -0,0 +1,46 @@ +extends SceneTree +## Gegenprobe: Explosion verhindern? (E) autowrap-Label mit custom_minimum_size.x, +## (F) vbox.custom_minimum_size.x, (G)echter Fix-Kandidat für die Dialoge. + +func _initialize() -> void: + _run.call_deferred() + +func _label(t: String, wrap: bool, min_w := 0.0) -> Label: + var l := Label.new() + l.text = t + if wrap: + l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + if min_w > 0.0: + l.custom_minimum_size = Vector2(min_w, 0) + return l + +func _run() -> void: + await process_frame + # E: Labels mit Mindestbreite 480 + var e := ConfirmationDialog.new() + e.title = "E_minwidth_labels" + var ev := VBoxContainer.new() + ev.set_anchors_preset(Control.PRESET_FULL_RECT) + for i in 5: + ev.add_child(_label("Zeile %d — etwas längerer Text, der im Editor-Dialog steht und umgebrochen werden muss, damit er passt." % i, true, 480.0)) + e.add_child(ev) + root.add_child(e) + print("PROBE E content_min VOR popup=", e.get_contents_minimum_size()) + e.popup_centered() + # F: vbox Mindestbreite 520, Labels autowrap ohne eigene Mindestbreite + var f := ConfirmationDialog.new() + f.title = "F_minwidth_vbox" + var fv := VBoxContainer.new() + fv.custom_minimum_size = Vector2(520, 0) + fv.set_anchors_preset(Control.PRESET_FULL_RECT) + for i in 5: + fv.add_child(_label("Zeile %d — etwas längerer Text, der im Editor-Dialog steht und umgebrochen werden muss, damit er passt." % i, true)) + f.add_child(fv) + root.add_child(f) + print("PROBE F content_min VOR popup=", f.get_contents_minimum_size()) + f.popup_centered() + await process_frame + await process_frame + print("PROBE E window=", e.size, " content_min_nach=", e.get_contents_minimum_size()) + print("PROBE F window=", f.size, " content_min_nach=", f.get_contents_minimum_size()) + quit(0) diff --git a/tests/probe_resize_shot.gd b/tests/probe_resize_shot.gd new file mode 100644 index 0000000..b3ddc94 --- /dev/null +++ b/tests/probe_resize_shot.gd @@ -0,0 +1,26 @@ +extends SceneTree +## Beweisbild: Resize-Dialog geöffnet, Fensterhöhe normal, OK/Abbrechen sichtbar. +## xvfb-run -a godot4 --path . --rendering-driver opengl3 -s res://tests/probe_resize_shot.gd + +func _initialize() -> void: + _run.call_deferred() + +func _run() -> void: + var scene: PackedScene = load("res://scenes/main.tscn") + var main: Control = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + main._start_with_new_map() + main._open_resize_dialog() + # Live-Preview mit Inhalt füllen (Verkleinern auf 256 wie Sly im Test). + main.resize_dialog.size_x_edit.value = 256.0 + main.resize_dialog.size_z_edit.value = 256.0 + await process_frame + await process_frame + await process_frame + await RenderingServer.frame_post_draw + var img := root.get_viewport().get_texture().get_image() + img.save_png("/opt/data/scopedvr/work/dialog_beweis/resize_dialog_fix.png") + print("PROBE dialog size=", main.resize_dialog.size, " bild gespeichert") + quit(0) diff --git a/tests/probe_resize_size.gd b/tests/probe_resize_size.gd new file mode 100644 index 0000000..720e62a --- /dev/null +++ b/tests/probe_resize_size.gd @@ -0,0 +1,60 @@ +extends SceneTree +## Diagnose-Probe: Wie groß wird der „Kartengröße ändern“-Dialog wirklich, +## und welcher Node drückt die Mindesthöhe hoch? (Sly-Befund 20.10.: Dialog +## viel zu lang, OK/Abbrechen unterhalb des Bildschirms.) +## Ausführen (mit Bildschirm): xvfb-run -a godot4 --path . --rendering-driver opengl3 -s res://tests/probe_resize_size.gd + +func _initialize() -> void: + _run.call_deferred() + +func _run() -> void: + var scene: PackedScene = load("res://scenes/main.tscn") + var main: Control = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + main._start_with_new_map() + + # Auflösung der Probe-Umgebung (xvfb) ausgeben. + print("PROBE viewport: ", root.size, " screen? ", DisplayServer.screen_get_size(0) if DisplayServer.get_name() != "headless" else "headless") + + main._open_resize_dialog() + await process_frame + await process_frame + var dlg: ConfirmationDialog = main.resize_dialog + print("PROBE dialog.visible=", dlg.visible, " size=", dlg.size, " min_size=", dlg.min_size) + print("PROBE content_min=", dlg.get_contents_minimum_size()) + + var vbox: VBoxContainer = null + for n in dlg.find_children("*", "VBoxContainer", true, false): + vbox = n + print("PROBE vbox.size=", vbox.size, " min=", vbox.get_combined_minimum_size()) + for c in vbox.get_children(): + if c is Control: + var txt := str(c.get("text")) + print("PROBE child ", c.get_class(), " '", txt.substr(0, min(txt.length(), 40)), "' size=", c.size, " min=", c.get_combined_minimum_size()) + + # FIX 20.10.b: Fenster muss im sichtbaren Bereich bleiben (Buttons sichtbar). + var area: Vector2 = main.size + var ok_btn := dlg.get_ok_button() + var dlg_bottom := float(dlg.position.y) + float(dlg.size.y) + print("PROBE fit: area=", area, " dlg.pos=", dlg.position, " dlg.size=", dlg.size, + " dlg_bottom=", dlg_bottom, " im-Bild=", bool(dlg_bottom <= area.y and dlg.position.y >= 0.0)) + + # Sly-Szenario: Preset 256 klicken → Vorschau wächst NACH dem Öffnen → + # Fenster muss neu verankert werden (Höhe gedeckelt/positioniert). + main.resize_dialog.size_x_edit.value = 256.0 + main.resize_dialog.size_z_edit.value = 256.0 + await process_frame + await process_frame + await process_frame + print("PROBE nach-256: dlg.pos=", dlg.position, " size=", dlg.size, + " bottom=", float(dlg.position.y) + float(dlg.size.y), + " im-Bild=", bool(float(dlg.position.y) + float(dlg.size.y) <= area.y)) + + # Neue-Map-Dialog zum Vergleich. + main.new_dialog.popup_centered() + await process_frame + print("PROBE newmap size=", main.new_dialog.size, " content_min=", main.new_dialog.get_contents_minimum_size()) + + quit(0) diff --git a/tests/test_resize_dialog.gd b/tests/test_resize_dialog.gd index 6faf797..beeea68 100644 --- a/tests/test_resize_dialog.gd +++ b/tests/test_resize_dialog.gd @@ -63,13 +63,13 @@ func _test_dialog_basics() -> void: _check(dlg.info_label.text.contains("129"), "Info-Zeile zeigt Aufloesung (129 Punkte)") _check(is_equal_approx(dlg.get_new_size_x(), 1024.0), "Freifeld startet bei aktueller Groesse") - # Preset-Button 256: beide Achsen wandern mit (Button sitzt in der Preset-HBox). + # Preset-Button 256: beide Achsen wandern mit (Button sitzt im Preset-HBox, + # unter dem ScrollContainer — FIX 20.10.b hat den dazwischengeschoben). var preset_btn: Button = null - for child in dlg.get_child(0).get_children(): - if child is HBoxContainer: - for b in (child as HBoxContainer).get_children(): - if b is Button and b.text == "256 m": - preset_btn = b + for h in dlg.find_children("*", "HBoxContainer", true, false): + for b in (h as HBoxContainer).get_children(): + if b is Button and b.text == "256 m": + preset_btn = b _check(preset_btn != null, "Preset-Button 256 m vorhanden") if preset_btn != null: preset_btn.emit_signal("pressed") @@ -269,4 +269,19 @@ func _test_main_wiring() -> void: main.resize_dialog.hide() (file_menu_btn.get_popup() as PopupMenu).id_pressed.emit(6) _check(main.resize_dialog.visible, - "Echter Menü-Klick (id_pressed=6) öffnet den Resize-Dialog") \ No newline at end of file + "Echter Menü-Klick (id_pressed=6) öffnet den Resize-Dialog") + + # REGRESSION 20.10.b (Sly: „Fenster viel zu lang, nichts bestätigbar“): + # autowrap-Labels ohne Mindestbreite melden VOR dem ersten Layout eine + # explosionsartige Mindesthöhe (jedes Wort eine Zeile bei ~1 px Breite) → + # popup_centered() übernimmt diesen Wert als Fensterhöhe (Probe: 5930 px, + # OK/Abbrechen unterhalb des Bildschirms). Fix: Mindestbreite ~ Dialog- + # breite an den Labels. Der Check MISST VOR dem Popup — genau da saugte + # sich das Fenster voll. Absolutwert (kein Prozentbezug, headless-Wurzel- + # fenster ist nur 64 px hoch). + var dlg_min_h: float = main.resize_dialog.get_contents_minimum_size().y + _check(dlg_min_h > 200.0 and dlg_min_h < 1500.0, + "Resize-Dialog Mindesthöhe normal vor Popup (%.0f px)" % dlg_min_h) + var nm_min_h: float = main.new_dialog.get_contents_minimum_size().y + _check(nm_min_h > 150.0 and nm_min_h < 1500.0, + "Neue-Map-Dialog Mindesthöhe normal (%.0f px)" % nm_min_h) \ No newline at end of file