Fix: Resize-Dialog 5930 px hoch + OK/Abbrechen unterhalb des Bildschirms (Sly-Befund 20.10. „viel zu lang, kann nichts bestätigen").

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.
This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 18:33:07 +00:00
parent f4cfe17594
commit ae551f387a
7 changed files with 269 additions and 10 deletions

View file

@ -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

View file

@ -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,

View file

@ -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)

46
tests/probe_dialog_min.gd Normal file
View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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")
"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)