Shortcuts 3/3 (t_11b5c25b): Strg+O/N/Umschalt+S + Redo-Wege dokumentiert, F1-F4 Werkzeugwechsel, H-Kurzhilfe-Overlay, F5 Karte testen, Dirty-Schutz vor Neu/Oeffnen, permanente Werkzeug-Anzeige in der Statuszeile.

This commit is contained in:
Hermes (godot-vr) 2026-10-16 17:58:34 +00:00
parent 08a75ac430
commit afcbda8dd0
9 changed files with 527 additions and 14 deletions

View file

@ -1,9 +1,11 @@
# Tastatur & Maus — Map-Editor
Deutsche Tastenbelegung des ScopedVR Map-Editors. **Diese Datei ist die Source
of truth** für alle Shortcut-Karten (Karte 1/3: t_c46d31c2 — Navigation,
Auswahl, Löschen; Karte 2/3: t_55257792 — Objektmanipulation + Terrain;
Karte 3/3 ergänzt hier).
of truth** für alle Shortcut-Karten — FINALE Version nach allen drei Karten:
Karte 1/3 (t_c46d31c2): Navigation, Auswahl, Löschen · Karte 2/3 (t_55257792):
Objektmanipulation + Terrain · Karte 3/3 (t_11b5c25b): Datei & Undo,
Werkzeugwechsel, Hilfe, Test. Das Kurzhilfe-Overlay im Editor (Taste **H**)
zeigt dieselbe Tabelle (Quelle: `src/ui/help_text.gd`).
Konventionen (alle Karten):
@ -62,10 +64,23 @@ alle, Strg+G gruppieren / Strg+Umschalt+G lösen.
| **G** | Auswahl **greifen** (Blender-Semantik): sie hängt an der Maus, Linksklick setzt die neue Position (mit Raster-Snap bzw. Strg-Zwang), **Esc** bricht ab und setzt die exakten alten Positionen zurück. Greifen der ganzen Gruppe/mehreren Objekte wandert gemeinsam |
| **Entf (DELETE)** | Auswahl SOFORT löschen, ohne Rückfrage — Strg+Z holt alles zurück. Wirkt auf Szene-Auswahl UND Objektbaum-Markierung |
| **Strg+D** | Auswahl duplizieren: Kopie +2 m in X versetzt (Rasterversatz), Füßpunkt unverändert; bei Mehrfachauswahl ALLE Objekte mit EINEM Undo-Schritt |
| Strg+Z / Strg+Y | Rückgängig / Wiederherstellen |
| Strg+S | Karte speichern |
| Strg+G / Strg+Umschalt+G | Auswahl gruppieren / Gruppe lösen |
## Datei & Undo (Karte 3/3)
Vor „Neue Map“ und „Öffnen“ fragt der Editor NACH, wenn die Map noch
ungespeicherte Änderungen hat (deutscher Dialog „Verwerfen / Abbrechen“) —
nichts geht versehentlich verloren.
| Eingabe | Wirkung |
| --- | --- |
| **Strg+Z** | Rückgängig (EIN Eintrag pro Operation) |
| **Strg+Y** UND **Strg+Umschalt+Z** | Wiederherstellen (Redo) — beide Wege sind belegt |
| **Strg+S** | Map speichern (.svmap, bekannter Pfad; noch nie gespeichert → „Speichern unter“) |
| **Strg+Umschalt+S** | Map speichern unter … |
| **Strg+O** | Map öffnen |
| **Strg+N** | Neue Map |
## Terrain-Werkzeug (Karte 2/3) — nur aktiv, wenn das Terrain-Werkzeug an ist
| Eingabe | Wirkung |
@ -82,12 +97,31 @@ Konfliktfreiheit: Die Ziffern 1–4 sind NUR im Terrain-Werkzeug belegt — im
Objekt-/Bauen-Modus sind sie unbenutzt, im Flugmodus (rechte Maustaste
gehalten) bleiben E/Q die Kamera.
## Werkzeuge
## Werkzeuge (Karte 3/3: F-Tasten wechseln das Werkzeug)
| Eingabe | Wirkung |
| --- | --- |
| **F1** | Werkzeug: Terrain formen |
| **F2** | Werkzeug: Objekte platzieren/auswählen (Standard) |
| **F3** | Werkzeug: Bauen (Häuser) |
| **F4** | Werkzeug: Spawn-Zonen malen |
| Esc (im Terrain-/Spawn-/Bauen-Werkzeug) | Werkzeug beenden — zurück zum Objekt-Werkzeug |
| Esc (beim Greifen) | Greifen abbrechen, alte Positionen zurück |
| Esc (beim Platzieren) | Ghost-Preview abbrechen |
| Esc (Kurzhilfe offen) | Kurzhilfe schließen |
*(Karte 3/3 ergänzt hier ihre Belegungen.)*
F1–F4 und F5/H wirken NUR, wenn kein Textfeld/SpinBox fokussiert ist und kein
Dialog offen ist (ShortcutPolicy) — Tippen geht immer vor.
Die Terrain-Ziffern 1–4 und [ / ] bleiben (Karte 2/3) NUR im Terrain-Werkzeug
belegt — der Werkzeugwechsel läuft jetzt über die F-Tasten, nicht über Ziffern.
## Hilfe & Test (Karte 3/3)
| Eingabe | Wirkung |
| --- | --- |
| **H** | Kurzhilfe-Overlay an/aus — alle Tasten dieser Datei als Panel über der Karte; Esc schließt ebenfalls. Nicht-blockierend: der Editor bleibt bedienbar |
| **F5** | ▶ Karte testen (aktuelle Map sofort als Spiel durchlaufen; der Editor bleibt offen) |
Die Statuszeile unten zeigt PERMANENT das aktive Werkzeug („Werkzeug: …“) plus
den Hinweis „Umschalt+F = Flugmodus“ — man sieht immer, wo man ist.

View file

@ -50,6 +50,25 @@ const ACTIONS := {
"editor_brush_lower": [KEY_2],
"editor_brush_smooth": [KEY_3],
"editor_brush_paint": [KEY_4],
# --- Datei & Undo (Karte t_11b5c25b, Karte 3/3) --------------------------
# Strg+Z/Strg+Y laufen seit dem M2-UX-Fundament; hier NACHGEBUGGEN als
# dokumentierte Actions (alle 3 Shortcut-Karten: ACTIONS-Dict = Source of
# truth). Strg+Umschalt+Z ist der zweite Redo-Weg (beide belegt).
"editor_undo": [{"key": KEY_Z, "ctrl": true}],
"editor_redo_y": [{"key": KEY_Y, "ctrl": true}],
"editor_redo_shift_z": [{"key": KEY_Z, "ctrl": true, "shift": true}],
"editor_save": [{"key": KEY_S, "ctrl": true}],
"editor_save_as": [{"key": KEY_S, "ctrl": true, "shift": true}],
"editor_open_map": [{"key": KEY_O, "ctrl": true}],
"editor_new_map": [{"key": KEY_N, "ctrl": true}],
# --- Werkzeugwechsel (F-Tasten statt Ziffern, Slys Weiche) ---------------
"editor_tool_terrain": [KEY_F1],
"editor_tool_objects": [KEY_F2],
"editor_tool_bauen": [KEY_F3],
"editor_tool_spawns": [KEY_F4],
# --- Kurzhilfe-Overlay + Test-Platzhalter --------------------------------
"editor_help_overlay": [KEY_H],
"editor_test_map": [KEY_F5],
}

View file

@ -29,3 +29,18 @@ static func esc_action(tool_mode: String, placing: bool, has_selection: bool,
if has_selection:
return "clear_selection"
return "none"
## Karte 3/3 (t_11b5c25b): Dürfen die globalen Tasten (F1–F5, H) gerade wirken?
## Nein, wenn ein Textfeld/SpinBox fokussiert ist (Tippen geht vor) oder ein
## Dialog/Fenster offen ist (Taste soll nicht HINTER dem Dialog wirken).
## Pure Funktion — main.gd liefert nur die beiden Fakten (testbar, wie esc_action).
static func tool_keys_allowed(text_focused: bool, dialog_open: bool) -> bool:
return not text_focused and not dialog_open
## Karte 3/3: Verlust-Operation (Neue Map / Öffnen) bei ungespeicherten
## Änderungen → erst NACHFRAGEN (deutscher Dialog), sonst direkt ausführen.
## Pure Entscheidung — der Dialog selbst lebt in main.gd.
static func needs_dirty_confirm(dirty: bool) -> bool:
return dirty

86
src/ui/help_text.gd Normal file
View file

@ -0,0 +1,86 @@
extends RefCounted
## Kurzhilfe-Overlay (Karte t_11b5c25b, Karte 3/3) — H schaltet das Panel an,
## H/Esc aus. Der Text spiegelt docs/tasten.md (Source of truth der Shortcut-
## Karten) als Kompakt-Tabelle für den Editor-Bildschirm. Bei neuen Tasten:
## docs/tasten.md UND hier pflegen.
##
## BBCode-Tabelle ([table=2]): Taste | Wirkung. Bei neuen Tasten die Zeile in
## der passenden Sektion ergänzen (text() aufbauen, _rows() macht die Zellen).
static func text() -> String:
var out := ""
out += _header("Navigation (Kamera)")
out += _table(_rows([
["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"],
["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"],
["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"],
["Rechte Maustaste GEDACHT halten", "Freelook-Flug: Umsehen + W vor / S zurück / A links / D rechts; E hoch / Q runter"],
["Umschalt (im Flug)", "schneller fliegen (×3)"],
["Alt (im Flug)", "langsamer fliegen (×0,3)"],
["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"],
["Umschalt+F", "Flugmodus dauerhaft an/aus"],
["F", "Auswahl ins Bild zentrieren"],
]))
out += _header("Auswahl")
out += _table(_rows([
["Linksklick", "Objekt auswählen (ersetzt die Auswahl)"],
["Strg+Klick (Viewport + Baum)", "Objekt zur Auswahl hinzufügen/entfernen (Mehrfachauswahl)"],
["Umschalt+Klick", "wie Strg+Klick (Mehrfachauswahl umschalten)"],
["Ziehen über leeren Boden", "Rahmen aufziehen: alle Objekte im Rahmen wählen"],
["Umschalt + Rahmen", "Rahmen-Auswahl zur bestehenden Auswahl hinzufügen"],
["Esc", "Werkzeug abbrechen → Greifen zurückrollen → Ghost abbrechen → Auswahl aufheben"],
["G", "Auswahl greifen (hängt an der Maus; Klick setzt, Esc rollt zurück)"],
]))
out += _header("Objekt-Manipulation")
out += _table(_rows([
["R", "90° drehen (Umschalt+R = −90°, ganze Gruppe dreht mit; Mausrad: 15° fein)"],
["Pfeiltasten", "Auswahl im 2-m-Raster verschieben (Kamera-nächste Weltachse)"],
["Bild↑ / Bild↓", "+0,25 m / −0,25 m anheben/absenken"],
["Strg+Bild↑", "Auswahl aufs Terrain/Boden absetzen"],
["Strg halten", "temporärer Snap-Zwang (rastet auf ganze Meter)"],
["Strg+D", "Auswahl duplizieren (+2 m versetzt)"],
["Entf", "Auswahl sofort löschen (Strg+Z holt zurück)"],
["Strg+G / Strg+Umschalt+G", "Gruppieren / Gruppe lösen"],
]))
out += _header("Terrain-Werkzeug (F1)")
out += _table(_rows([
["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"],
["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"],
["Linksklick-Ziehen", "Boden formen bzw. malen"],
["Rechtsklick-Ziehen", "Gegenteil formen / Gras zurückmalen"],
]))
out += _header("Datei & Undo")
out += _table(_rows([
["Strg+Z", "Rückgängig"],
["Strg+Y oder Strg+Umschalt+Z", "Wiederherstellen (Redo)"],
["Strg+S", "Map speichern (.svmap, bekannter Pfad)"],
["Strg+Umschalt+S", "Map speichern unter …"],
["Strg+O", "Map öffnen"],
["Strg+N", "Neue Map"],
]))
out += _header("Werkzeuge & Tests")
out += _table(_rows([
["F1", "Werkzeug: Terrain formen"],
["F2", "Werkzeug: Objekte platzieren/auswählen"],
["F3", "Werkzeug: Bauen (Häuser)"],
["F4", "Werkzeug: Spawn-Zonen malen"],
["F5", "▶ Karte testen"],
["H", "Diese Kurzhilfe an/aus (Esc schließt auch)"],
]))
out += "[color=#a8b6ad]Vor „Neue Map“ und „Öffnen“ fragt der Editor nach, wenn noch ungespeicherte Änderungen da sind. Die Statuszeile unten zeigt immer das aktive Werkzeug.[/color]\n"
return out
static func _header(title: String) -> String:
return "\n[color=#8cc9f2][b]%s[/b][/color]\n\n" % title
static func _table(rows: String) -> String:
return "[table=2]%s[/table]\n" % rows
static func _rows(pairs: Array) -> String:
var out := ""
for pair in pairs:
out += "[cell][b]%s[/b][/cell][cell]%s[/cell]" % [str(pair[0]), str(pair[1])]
return out

1
src/ui/help_text.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cmq500ivk0ro2

View file

@ -21,6 +21,8 @@ const ShortcutKeys := preload("res://src/editor/shortcut_keys.gd")
const ShortcutPolicy := preload("res://src/editor/shortcut_policy.gd")
# Shortcuts-Mathe Karte 2/3 (t_55257792): Pfeil-Raster + Pinsel-Presets.
const MoveMath := preload("res://src/editor/move_math.gd")
# Kurzhilfe-Overlay Karte 3/3 (t_11b5c25b): Gesamte Tastenliste als BBCode.
const HelpText := preload("res://src/ui/help_text.gd")
var manifest: Dictionary = {}
var doc: Dictionary = {} # das Map-Dictionary (1:1 svmap-Format)
@ -116,6 +118,14 @@ var check_text: Label
# Karte testen (Karte t_b84af40f)
var test_btn: Button
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“
var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird
var help_overlay: PanelContainer # H-Kurzhilfe (nicht-blockierend)
var help_overlay_title: Label
var help_overlay_text: RichTextLabel
var tool_label: Label # permanenter Werkzeugmodus + Flug-Hint
# Dialoge
var new_dialog: AcceptDialog
var open_dialog: FileDialog
@ -133,6 +143,7 @@ func _ready() -> void:
ShortcutKeys.register()
manifest = _load_manifest()
_build_ui()
_update_tool_label()
_start_with_new_map()
_maybe_start_demo_mode()
@ -260,6 +271,12 @@ func _build_ui() -> void:
status_label.clip_text = true # langer Text darf das Layout NICHT sprengen
status_h.add_child(status_label)
# Karte 3/3 (t_11b5c25b): PERMANENTE Werkzeug-Anzeige — Sly sieht immer,
# in welchem Werkzeug er ist, plus Hint für den Flugmodus (Umschalt+F).
tool_label = Label.new()
tool_label.modulate = Color(0.55, 0.8, 1.0)
status_h.add_child(tool_label)
stats_objects = Label.new()
stats_objects.modulate = Color(1, 1, 1, 0.75)
status_h.add_child(stats_objects)
@ -316,6 +333,52 @@ func _build_ui() -> void:
check_dialog.add_child(check_text)
add_child(check_dialog)
# Karte 3/3 (t_11b5c25b): Dirty-Schutz vor „Neue Map“ / „Öffnen“ — deutsche
# Klartext-Frage, „Verwerfen“ führt die wartende Aktion aus, „Abbrechen“ tut
# nichts (Map bleibt, wie sie ist).
dirty_dialog = ConfirmationDialog.new()
dirty_dialog.title = "Ungespeicherte Änderungen"
dirty_dialog.dialog_text = "Die Map hat ungespeicherte Änderungen — beim Fortfahren gehen sie verloren.\nJetzt verwerfen und fortfahren?"
dirty_dialog.ok_button_text = "Verwerfen"
dirty_dialog.cancel_button_text = "Abbrechen"
dirty_dialog.confirmed.connect(_on_dirty_confirmed)
add_child(dirty_dialog)
# Kurzhilfe-Overlay (Karte 3/3): H schaltet es an, H/Esc aus. Kein modal
# Dialog — der Editor bleibt bedienbar, das Panel liegt nur obenauf.
help_overlay = PanelContainer.new()
var overlay_style := StyleBoxFlat.new()
overlay_style.bg_color = Color(0.09, 0.11, 0.13, 0.97) # Slate, wie Statuszeile
overlay_style.border_color = Color(0.35, 0.55, 0.7)
overlay_style.set_border_width_all(2)
overlay_style.set_corner_radius_all(8)
overlay_style.set_content_margin_all(14)
help_overlay.add_theme_stylebox_override("panel", overlay_style)
# Zentriertes Panel statt Vollbild: der Editor bleibt rundherum bedienbar
# (nicht-blockierend), das Panel ist groß genug für die ganze Tabelle.
# Grow BOTH: das Panel wächst SYMMETRISCH um den Mittelpunkt (sonst hängt
# seine linke obere Ecke am Zentrum und es läuft rechts/unten aus dem Bild).
help_overlay.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
help_overlay.grow_horizontal = Control.GROW_DIRECTION_BOTH
help_overlay.grow_vertical = Control.GROW_DIRECTION_BOTH
help_overlay.custom_minimum_size = Vector2(880, 640)
help_overlay.visible = false
var overlay_box := VBoxContainer.new()
overlay_box.add_theme_constant_override("separation", 6)
help_overlay.add_child(overlay_box)
help_overlay_title = Label.new()
help_overlay_title.text = "Kurzhilfe — alle Tasten auf einen Blick (H oder Esc schließt)"
help_overlay_title.add_theme_font_size_override("font_size", 20)
help_overlay_title.add_theme_color_override("font_color", Color(0.55, 0.8, 1.0))
overlay_box.add_child(help_overlay_title)
help_overlay_text = RichTextLabel.new()
help_overlay_text.bbcode_enabled = true
help_overlay_text.size_flags_vertical = Control.SIZE_EXPAND_FILL
help_overlay_text.custom_minimum_size = Vector2(0, 0)
help_overlay_text.text = HelpText.text()
overlay_box.add_child(help_overlay_text)
add_child(help_overlay)
func _build_left_panel() -> PanelContainer:
var panel := PanelContainer.new()
@ -888,10 +951,10 @@ func _start_with_new_map() -> void:
func _on_menu_action(id: int) -> void:
match id:
0: # Neue Map
new_dialog.popup_centered()
1: # Öffnen
open_dialog.popup_centered()
0: # Neue Map — bei ungespeicherten Änderungen erst NACHFRAGEN (Karte 3/3)
_request_lossy(func() -> void: new_dialog.popup_centered())
1: # Öffnen — ditto Dirty-Schutz
_request_lossy(func() -> void: open_dialog.popup_centered())
2: # Speichern
_save()
3: # Speichern unter
@ -1022,6 +1085,66 @@ func _show_errors(title: String, errors: Array) -> void:
error_dialog.popup_centered()
# ------------------------------------------- Karte 3/3: Globale Shortcuts ---
## Verlust-Operation (Neue Map / Öffnen): bei ungespeicherten Änderungen erst
## NACHFRAGEN (deutscher Dialog), sonst direkt ausführen. Die wartende Aktion
## liegt in _pending_after_dirty und läuft bei „Verwerfen“ an.
func _request_lossy(next: Callable) -> void:
if ShortcutPolicy.needs_dirty_confirm(dirty):
_pending_after_dirty = next
dirty_dialog.popup_centered()
return
next.call()
## „Verwerfen“ im Dirty-Dialog: die wartende Aktion (Neu/Öffnen) jetzt ausführen.
func _on_dirty_confirmed() -> void:
var next := _pending_after_dirty
_pending_after_dirty = Callable()
if next.is_valid():
next.call()
## Karte 3/3: Dürfen die globalen Tasten (F1–F5, H) gerade wirken? Pure Policy
## in ShortcutPolicy — hier nur die Fakten: Textfeld/SpinBox fokussiert? Ein
## Dialog/Popup offen? (Eigene Fenster des Editors; das Hilfe-Panel ist
## absichtlich KEIN Fenster und zählt nicht.)
func _tool_key_allowed() -> bool:
var focus := get_viewport().gui_get_focus_owner()
var text_focused: bool = focus != null \
and (focus is LineEdit or focus is TextEdit or focus is SpinBox)
var dialog_open := false
for child in get_children():
if child is Window and child.visible:
dialog_open = true
break
return ShortcutPolicy.tool_keys_allowed(text_focused, dialog_open)
## Kurzhilfe-Overlay umschalten; Statuszeile meldet den Zustand.
func _toggle_help_overlay() -> void:
help_overlay.visible = not help_overlay.visible
if help_overlay.visible:
_set_status("Kurzhilfe an — H oder Esc schließt das Overlay wieder.")
else:
_base_tool_status()
## Permanente Werkzeug-Anzeige (Karte 3/3): aktives Werkzeug + Flug-Hint —
## Sly sieht in der Statuszeile immer, wo er ist.
func _update_tool_label() -> void:
if tool_label == null:
return
var names := {
"objects": "Objekte (F2)",
"terrain": "Terrain (F1)",
"bauen": "Bauen (F3)",
"spawns": "Spawn-Zonen (F4)",
}
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
# --------------------------------------------------------------- History ----
func _do_undo() -> void:
@ -1090,7 +1213,44 @@ func _unhandled_key_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
KEY_S:
if ctrl:
_save()
if event.shift_pressed:
# Strg+Umschalt+S (Karte 3/3): Speichern unter …
save_dialog.popup_centered()
else:
_save()
get_viewport().set_input_as_handled()
KEY_O:
if ctrl:
# Strg+O (Karte 3/3): Map öffnen — bei dirty erst NACHFRAGEN.
_request_lossy(func() -> void: open_dialog.popup_centered())
get_viewport().set_input_as_handled()
KEY_N:
if ctrl:
# Strg+N (Karte 3/3): Neue Map — bei dirty erst NACHFRAGEN.
_request_lossy(func() -> void: new_dialog.popup_centered())
get_viewport().set_input_as_handled()
KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5:
# Karte 3/3: Werkzeugwechsel + Test-Platzhalter. NUR wirken, wenn
# kein Textfeld tippt und kein Dialog offen ist (ShortcutPolicy).
if _tool_key_allowed():
match key:
KEY_F1:
_set_tool_mode("terrain")
KEY_F2:
_set_tool_mode("objects")
KEY_F3:
_set_tool_mode("bauen")
KEY_F4:
_set_tool_mode("spawns")
KEY_F5:
# ▶ Karte testen: der echte Freigabe-/Test-Flow existiert
# (Karte t_b84af40f) — F5 ruft ihn direkt auf.
_on_test_run()
get_viewport().set_input_as_handled()
KEY_H:
# Karte 3/3: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls).
if _tool_key_allowed():
_toggle_help_overlay()
get_viewport().set_input_as_handled()
KEY_G:
if ctrl:
@ -1132,6 +1292,12 @@ func _unhandled_key_input(event: InputEvent) -> void:
_focus_selection()
get_viewport().set_input_as_handled()
KEY_ESCAPE:
# Karte 3/3: Steht das Hilfe-Overlay offen, gehört Esc IHM (schließen)
# — VOR der normalen Esc-Prioritätskette.
if help_overlay.visible:
_toggle_help_overlay()
get_viewport().set_input_as_handled()
return
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
@ -1326,6 +1492,7 @@ func _set_tool_mode(mode: String) -> void:
_sync_terrain_button()
_sync_spawn_button()
_sync_build_button()
_update_tool_label()
return
if mode == "terrain":
_leave_spawn_mode()
@ -1344,6 +1511,7 @@ func _set_tool_mode(mode: String) -> void:
_leave_spawn_mode()
_leave_build_mode()
_enter_objects_mode()
_update_tool_label()
func _enter_terrain_mode() -> void:
@ -1570,6 +1738,7 @@ func _recheck_terrain_mode() -> void:
tool_mode = "objects"
terrain_panel.visible = false
_sync_terrain_button()
_update_tool_label()
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
_set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true)

View file

@ -41,6 +41,7 @@ func _run_all() -> void:
_test_terrain_presets()
_test_shortcut_undo()
_test_terrain_hotkeys()
_test_shortcuts_karte3()
_finish()
@ -101,12 +102,13 @@ func _test_terrain_presets() -> void:
"Klick auf Klein setzt Slider auf 4 m")
func _press_key(keycode: Key, ctrl: bool) -> InputEventKey:
func _press_key(keycode: Key, ctrl: bool, shift := false) -> InputEventKey:
var ev := InputEventKey.new()
ev.keycode = keycode
ev.physical_keycode = keycode
ev.pressed = true
ev.ctrl_pressed = ctrl
ev.shift_pressed = shift
return ev
@ -205,3 +207,112 @@ func _test_terrain_hotkeys() -> void:
"Ziffern/[ im Objekt-Modus: Pinsel bleibt unangetastet")
_check(int(main.terrain_tool.get("brush_mode")) == TerrainField.Mode.RAISE,
"Ziffern im Objekt-Modus: Pinsel-Modus bleibt unangetastet")
## Karte 3/3 (t_11b5c25b): F1–F4 Werkzeugwechsel, F5 Test, H-Hilfe,
## Strg+O/N/S + Dirty-Dialog am ECHTEN main (Dialoge als --mock: sichtbar
## prüfen statt bedienen — headless gibt es keine echte Maus).
func _test_shortcuts_karte3() -> void:
print("Testgruppe: Karte 3/3 — F-Tasten, Hilfe, Datei, Dirty-Dialog")
# --- F1–F4: Werkzeugwechsel (Slys Weiche: F-Tasten statt Ziffern) --------
main._set_tool_mode("objects")
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1: Terrain-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Terrain"), "Statuszeile: Werkzeug-Label zeigt Terrain")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2: zurück zum Objekt-Werkzeug")
main._unhandled_key_input(_press_key(KEY_F3, false))
_check(str(main.get("tool_mode")) == "bauen", "F3: Bauen-Werkzeug aktiv")
_check(str(main.tool_label.text).contains("Bauen"), "Statuszeile: Werkzeug-Label zeigt Bauen")
main._unhandled_key_input(_press_key(KEY_F4, false))
_check(str(main.get("tool_mode")) == "spawns", "F4: Spawn-Werkzeug aktiv")
main._unhandled_key_input(_press_key(KEY_F2, false))
_check(str(main.get("tool_mode")) == "objects", "F2 aus dem Spawn-Werkzeug: sauber zurück")
# --- Fokus-Regel: Textfeld fokussiert → F-Tasten wirken NICHT ------------
main.prop_name.grab_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei fokussiertem Textfeld: Werkzeug bleibt (Tippen geht vor)")
main.prop_name.release_focus()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "terrain", "F1 nach Release-Focus: wirkt wieder")
main._set_tool_mode("objects")
# --- Dialog-Regel: offener Dialog → F-Tasten wirken NICHT ----------------
main.error_dialog.popup_centered()
main._unhandled_key_input(_press_key(KEY_F1, false))
_check(str(main.get("tool_mode")) == "objects",
"F1 bei offenem Dialog: Werkzeug bleibt")
main.error_dialog.hide()
# --- H: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls) ----------------
main._unhandled_key_input(_press_key(KEY_H, false))
_check(main.help_overlay.visible, "H: Kurzhilfe-Overlay sichtbar")
var help_txt: String = main.help_overlay_text.text
_check(help_txt.contains("Strg+Z") and help_txt.contains("F1") and help_txt.contains("Terrain"),
"Overlay enthält die Tastenliste aus docs/tasten.md")
_check(str(main.status_label.text).contains("Kurzhilfe an"), "Statuszeile meldet Overlay an")
main._unhandled_key_input(_press_key(KEY_ESCAPE, false))
_check(not main.help_overlay.visible, "Esc schließt das Overlay")
_check(str(main.get("tool_mode")) == "objects", "Esc am Overlay: Objekt-Werkzeug unangetastet")
main._unhandled_key_input(_press_key(KEY_H, false))
main._unhandled_key_input(_press_key(KEY_H, false))
_check(not main.help_overlay.visible, "H zweimal: Overlay wieder zu (Toggle)")
# --- F5: Karte testen (echter Flow existiert — hier läuft er in den
# „Client nicht gefunden“-Zweig, Hauptsache die Route ist verdrahtet) -----
main._unhandled_key_input(_press_key(KEY_F5, false))
_check(str(main.status_label.text).contains("Karte testen"),
"F5: Karte-testen-Flow angesprungen (Statuszeile meldet)")
_check(main.error_dialog.visible, "F5: Fehler-Dialog im nicht-gefunden-Zweig geöffnet")
main.error_dialog.hide()
# --- Dirty-Schutz: Strg+N bei ungespeicherten Änderungen FRAGT ZUERST ----
main.dirty = true
main._unhandled_key_input(_press_key(KEY_N, true))
_check(main.dirty_dialog.visible, "Strg+N bei dirty: Bestätigungs-Dialog ZUERST")
_check(not main.new_dialog.visible, "Strg+N bei dirty: Neue-Map-Dialog wartet hinterher")
_check(str(main.dirty_dialog.dialog_text).contains("ungespeicherte Änderungen"),
"Dirty-Dialog: deutsche Klartext-Meldung")
# „Verwerfen“ (confirmed) → jetzt erst geht der Neue-Map-Dialog auf.
main.dirty_dialog.confirmed.emit()
_check(main.new_dialog.visible, "Verwerfen bestätigt: Neue-Map-Dialog öffnet jetzt")
main.new_dialog.hide()
main.dirty = false
# --- Strg+O ohne dirty: Öffnen-Dialog DIREKT -----------------------------
main._unhandled_key_input(_press_key(KEY_O, true))
_check(main.open_dialog.visible, "Strg+N/Strg+O ohne dirty: Öffnen-Dialog direkt")
main.open_dialog.hide()
# --- Strg+S: speichert an bekannten Pfad, dirty wird aufgeräumt ----------
main.current_path = "/tmp/svmap_karte3_test.svmap"
main.dirty = true
main._unhandled_key_input(_press_key(KEY_S, true))
_check(FileAccess.file_exists("/tmp/svmap_karte3_test.svmap"), "Strg+S: .svmap geschrieben")
_check(not main.dirty, "Strg+S: dirty zurückgesetzt")
_check(str(main.status_label.text).contains("gespeichert"), "Strg+S: Statuszeile meldet Speichern")
# --- Strg+Umschalt+S: Speichern-unter-Dialog ------------------------------
main._unhandled_key_input(_press_key(KEY_S, true, true))
_check(main.save_dialog.visible, "Strg+Umschalt+S: Speichern-unter-Dialog")
main.save_dialog.hide()
# --- Strg+Umschalt+Z: der zweite Redo-Weg --------------------------------
var before: Dictionary = main.history.snapshot(["objects"])
(main.doc["objects"] as Array).append(
{"id": "test_redo_objekt", "pos": [4.0, 0.0, 4.0], "scale": 1.0,
"rot_y_deg": 0.0})
main.history.commit("Test: Redo-Objekt", before, main.history.snapshot(["objects"]))
main._unhandled_key_input(_press_key(KEY_Z, true))
_check((main.doc["objects"] as Array).size() == 0, "Strg+Z: Objekt weg (Undo)")
main._unhandled_key_input(_press_key(KEY_Z, true, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Umschalt+Z: Redo-Weg 2 stellt her")
main._unhandled_key_input(_press_key(KEY_Z, true))
main._unhandled_key_input(_press_key(KEY_Y, true))
_check((main.doc["objects"] as Array).size() == 1, "Strg+Y: Redo-Weg 1 stellt auch her")
# Aufräumen: Undo, damit spätere Gruppen (falls Testreihenfolge sich ändert)
# nicht von diesem Objekt ausgehen.
main._unhandled_key_input(_press_key(KEY_Z, true))

View file

@ -12,6 +12,8 @@ var checks := 0
func _init() -> void:
ShortcutKeys.register()
_test_inputmap_actions()
_test_inputmap_actions_3()
_test_policy_3()
_test_fly_math()
_test_esc_policy()
_test_esc_grab_policy()
@ -194,6 +196,64 @@ func _test_inputmap_actions() -> void:
_check(lift_floor, "editor_lift_to_floor → Strg+Bild↑")
# ------------------------------------------------- Karte 3/3 (t_11b5c25b) ----
## Karte 3/3: Datei & Undo, Werkzeugwechsel (F-Tasten), Hilfe + Test als
## physische KEY_*-Actions — gleiche Konvention wie Karte 1/2.
func _test_inputmap_actions_3() -> void:
print("Testgruppe: InputMap-Actions Karte 3/3 (Datei, F-Tasten, Hilfe)")
var mod_combos := {
"editor_undo": [KEY_Z, false, true], # Strg+Z
"editor_redo_y": [KEY_Y, false, true], # Strg+Y
"editor_redo_shift_z": [KEY_Z, true, true], # Strg+Umschalt+Z (2. Redo-Weg)
"editor_save": [KEY_S, false, true], # Strg+S
"editor_save_as": [KEY_S, true, true], # Strg+Umschalt+S
"editor_open_map": [KEY_O, false, true], # Strg+O
"editor_new_map": [KEY_N, false, true], # Strg+N
}
for action in mod_combos:
var combo: Array = mod_combos[action]
if not InputMap.has_action(action):
_check(false, "Action existiert: %s" % action)
continue
var ok := false
for ev in InputMap.action_get_events(action):
var kev := ev as InputEventKey
if ev is InputEventKey and kev.physical_keycode == int(combo[0]) \
and kev.shift_pressed == bool(combo[1]) and kev.ctrl_pressed == bool(combo[2]):
ok = true
_check(ok, "%s → korrekte Tastenkombi" % action)
var f_keys := {
"editor_tool_terrain": KEY_F1,
"editor_tool_objects": KEY_F2,
"editor_tool_bauen": KEY_F3,
"editor_tool_spawns": KEY_F4,
"editor_test_map": KEY_F5,
"editor_help_overlay": KEY_H,
}
for action in f_keys:
if not InputMap.has_action(action):
_check(false, "Action existiert: %s" % action)
continue
var ok := false
for ev in InputMap.action_get_events(action):
var kev := ev as InputEventKey
if ev is InputEventKey and kev.physical_keycode == int(f_keys[action]):
ok = true
_check(ok, "%s → physische Taste %s" % [action, OS.get_keycode_string(int(f_keys[action]))])
## Karte 3/3: Pure Policy — F-Tasten-Gate (Textfeld/Dialog) + Dirty-Confirm.
func _test_policy_3() -> void:
print("Testgruppe: ShortcutPolicy Karte 3/3 (F-Tasten-Gate, Dirty-Confirm)")
_check(ShortcutPolicy.tool_keys_allowed(false, false), "F-Tasten: frei ohne Textfeld/Dialog")
_check(not ShortcutPolicy.tool_keys_allowed(true, false), "F-Tasten: GESPERRT bei fokussiertem Textfeld")
_check(not ShortcutPolicy.tool_keys_allowed(false, true), "F-Tasten: GESPERRT bei offenem Dialog")
_check(not ShortcutPolicy.tool_keys_allowed(true, true), "F-Tasten: GESPERRT bei beidem")
_check(ShortcutPolicy.needs_dirty_confirm(true), "Dirty: Neu/Öffnen NACHFRAGEN (Dialog)")
_check(not ShortcutPolicy.needs_dirty_confirm(false), "Nicht-dirty: Neu/Öffnen direkt ausführen")
# ------------------------------------------------------------ FlyMath --------
func _test_fly_math() -> void:

18
tools/parse_check_shortcuts3.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/bash
# Parse-Check der Shortcuts-3-Karte (t_11b5c25b): alle geänderten/neuen Scripts.
cd "$(dirname "$0")/.."
GODOT=/opt/data/home/bin/godot4
fail=0
for f in src/ui/main.gd src/ui/help_text.gd src/editor/shortcut_keys.gd \
src/editor/shortcut_policy.gd \
tests/test_shortcuts.gd tests/test_gui_smoke.gd; do
out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true)
if [ -n "$out" ]; then
echo "FAIL $f:"
echo "$out"
fail=1
else
echo "OK $f"
fi
done
exit $fail