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:
parent
f4cfe17594
commit
ae551f387a
7 changed files with 269 additions and 10 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
38
tests/probe_buttons_pos.gd
Normal file
38
tests/probe_buttons_pos.gd
Normal 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
46
tests/probe_dialog_min.gd
Normal 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)
|
||||
26
tests/probe_resize_shot.gd
Normal file
26
tests/probe_resize_shot.gd
Normal 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)
|
||||
60
tests/probe_resize_size.gd
Normal file
60
tests/probe_resize_size.gd
Normal 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)
|
||||
|
|
@ -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")
|
||||
|
|
@ -270,3 +270,18 @@ func _test_main_wiring() -> void:
|
|||
(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")
|
||||
|
||||
# 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)
|
||||
Loading…
Reference in a new issue