diff --git a/docs/tasten.md b/docs/tasten.md index de39e3d..618e0ad 100644 --- a/docs/tasten.md +++ b/docs/tasten.md @@ -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. diff --git a/src/editor/shortcut_keys.gd b/src/editor/shortcut_keys.gd index 24a180b..48bc628 100644 --- a/src/editor/shortcut_keys.gd +++ b/src/editor/shortcut_keys.gd @@ -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], } diff --git a/src/editor/shortcut_policy.gd b/src/editor/shortcut_policy.gd index b375834..927c831 100644 --- a/src/editor/shortcut_policy.gd +++ b/src/editor/shortcut_policy.gd @@ -28,4 +28,19 @@ static func esc_action(tool_mode: String, placing: bool, has_selection: bool, return "cancel_placement" if has_selection: return "clear_selection" - return "none" \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/ui/help_text.gd b/src/ui/help_text.gd new file mode 100644 index 0000000..92fbd88 --- /dev/null +++ b/src/ui/help_text.gd @@ -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 diff --git a/src/ui/help_text.gd.uid b/src/ui/help_text.gd.uid new file mode 100644 index 0000000..c1a819e --- /dev/null +++ b/src/ui/help_text.gd.uid @@ -0,0 +1 @@ +uid://cmq500ivk0ro2 diff --git a/src/ui/main.gd b/src/ui/main.gd index 4babfa1..46a72b2 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -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) diff --git a/tests/test_gui_smoke.gd b/tests/test_gui_smoke.gd index dad7a83..368414e 100644 --- a/tests/test_gui_smoke.gd +++ b/tests/test_gui_smoke.gd @@ -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)) diff --git a/tests/test_shortcuts.gd b/tests/test_shortcuts.gd index 427b205..05920b1 100644 --- a/tests/test_shortcuts.gd +++ b/tests/test_shortcuts.gd @@ -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: diff --git a/tools/parse_check_shortcuts3.sh b/tools/parse_check_shortcuts3.sh new file mode 100755 index 0000000..6533c59 --- /dev/null +++ b/tools/parse_check_shortcuts3.sh @@ -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