diff --git a/README.md b/README.md index afad937..cde19f6 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,73 @@ Beispiel mit. ganze, vordefinierte Adresse; er kann nicht als Startpunkt einer neu getippten `:`-Bereichsangabe kombiniert werden. +### Makros + +Ein **USP gegenüber gewöhnlichen Tabellenkalkulationen**: Zellen, Zeilen/ +Spalten, Tabellen und Blätter dokumentweit **per Skript adressieren und +verändern** — Struktur *und* Inhalt — statt nur per Formel zu lesen. Anders +als eine Formel läuft ein Makro **außerhalb der Neuberechnung**: es wird nie +aus einer Zelle heraus aufgerufen, sondern immer **explizit ausgelöst** — +über **„▶ Ausführen"** in der neuen **„▶ Makros"**-Symbolleisten-Ansicht +oder per Klick auf ein **Button-Objekt** auf der Fläche (einfügbar über das +Symbol neben „Text einfügen"; welches Makro der Klick auslöst, wird im +Inspektor **oder** per Rechtsklick auf den Button direkt gewählt). Diese +Trennung ist bewusst: eine Formel, die als Nebeneffekt andere Zellen +verändert, würde die Neuberechnungsreihenfolge undefiniert machen (genau +das vermeidet dieses Feature) — ein Makro ist stattdessen dieselbe Art +Mutation, die auch ein Klick in der UI auslösen würde, nur mit +`FOR`/`IF`/`RETURN`-Kontrollfluss verkettet (dieselbe Sprache wie „Mit +Schleife"-Funktionen, s. o.). + +**Elf Schreib-Aktionen**, adressiert mit der gewohnten +`Blatt!Tabelle::Zelle`-Syntax: + +| Aktion | Wirkung | +|---|---| +| `SETVALUE(Ziel; Wert)` | schreibt `Wert` in eine Zelle oder — als Ziel ein Bereich — denselben Wert in jede Zelle darin (wie Excels `Strg+Enter`). Beginnt der (ggf. per `&` zusammengesetzte) Text mit `=`, wird er wie getippt zur echten Formel. | +| `INSERTROW`/`DELETEROW`/`INSERTCOL`/`DELETECOL(TabellenRef; Index)` | dieselbe dokumentweite Formel-Referenz-Umschreibung wie das manuelle Einfügen/Löschen (s. o.) | +| `ADDTABLE(BlattRef; Name)`, `RENAMETABLE`/`DELETETABLE(TabellenRef)` | Tabellen anlegen/umbenennen/löschen | +| `ADDSHEET(Name)`, `RENAMESHEET`/`DELETESHEET(BlattRef)` | Blätter anlegen/umbenennen/löschen | + +`TabellenRef`/`BlattRef` sind Text (`"Umsatz"` oder `"Blatt2!Kosten"`, +identisch zur Formel-Qualifizierung aufgelöst) — auch berechenbar, z. B. +`ADDTABLE("Blatt1"; "Kopie_" & COUNT(Umsatz!A:A))`. Nur `SETVALUE`s erstes +Argument ist ein echter Zell-/Bereichsbezug (Literal, Parameter, +Schleifenvariable); alle anderen Argumente sind normale Ausdrücke. + +- **Jeder Zell-/Bereichsbezug muss die Tabelle nennen** (`Umsatz::A1`, nie + bloß `A1`) — anders als eine Zelle hat ein Makro keine „Heimat-Tabelle", + die ein unqualifizierter Bezug meinen könnte. Beim Speichern geprüft, mit + klarer Fehlermeldung. +- **`RETURN`** beendet die Ausführung vorzeitig (z. B. um eine Schleife + früh zu verlassen); ein `RETURN "text"` wird zur Erfolgsmeldung im Toast + statt des generischen „Makro ausgeführt.". +- **Ein ganzer Lauf zählt als ein Undo-Schritt** — läuft er synchron durch + (immer der Fall), fasst die bestehende, zeitbasierte Undo/Redo-Historie + ihn automatisch zu einem `Strg`/`Cmd`+`Z` zusammen, ohne eigene + Transaktionslogik. +- **Ein Fehler mitten im Lauf bricht sofort ab** (unbekannte Tabelle, + fehlerhafter Wert, …) und zeigt die Ursache als Fehler-Toast — bereits + ausgeführte Schritte bleiben stehen (kein automatisches Rollback, + bewusst; per Undo rückgängig machbar). +- **Eigenes, kleineres Schrittbudget** (1.000 statt der 10.000 einer + gewöhnlichen `FOR`-Schleife) — eine Makro-Schleife kann pro Durchlauf + eine volle Neuberechnung auslösen (bei einer Struktur-Aktion), ein zu + großzügiges Budget wäre dort ein echtes Einfrier-Risiko. +- **Kein Makro ruft ein anderes Makro auf** (keine solche Aktion existiert) + — eliminiert jedes Makro-triggert-Makro-Zyklus-Risiko von vornherein. +- Eine im Makro aufgerufene **benutzerdefinierte Funktion** funktioniert + normal (berechnet einen Wert), kann aber selbst nie schreiben — ihr + Funktionskörper wird immer ohne Schreib-Rechte geparst. + +**Bewusst ohne** (Stand jetzt): Makros haben keine Parameter (ein Klick +übergibt keine Argumente); mehrere Struktur-Aktionen in einer Schleife +lösen weiterhin je eine eigene Neuberechnung aus statt gebündelt einmal am +Ende (das kleinere Schrittbudget hält das Restrisiko klein, spätere +Optimierung möglich); ein `TabellenRef`-String wird am *ersten* `!` +gesplittet (ein Tabellenname mit eigenem `!` darin ist so nicht +adressierbar). + ### Zeilen & Spalten einfügen/löschen Rechtsklick auf einen **Zeilen-** oder **Spaltenkopf** öffnet ein Kontextmenü @@ -1083,11 +1150,40 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, Fehler ohne Nebenwirkung, unbekannter Tabellenname → `#REF`, v1→v2 beim Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting, `=1+2`→3, Locale-Zahlen. +- **Makros** — Grammatik (`parser.spec.ts`): `NAME(args)` an Anweisungs- + position nur mit `allowWrites` (nie in einem gewöhnlichen Funktionskörper), + `;`-Argumente inkl. übersprungener Slots, Zuweisung bleibt eindeutig + unterscheidbar. Interpreter (`macros.spec.ts`): `SETVALUE`-Zielauflösung + auf Literal/Bereich/Schleifenvariable, `RETURN` als Kurzschluss inkl. + String-Nachricht, Fall-Through → `null`, alle elf Aktionen gegen einen + Mock-`MacroWriteHost`, das kleinere Makro-Schrittbudget greift bei einer + strukturell-schweren Schleife, ein verschachtelter Aufruf einer + benutzerdefinierten Funktion in einem `SETVALUE`-Wert funktioniert und + kann selbst nicht schreiben. Store-Ebene (`document.spec.ts`): Validierung + (unqualifizierte Referenz abgelehnt, Namenskollision, reservierte + Aktionsnamen als lokale Variable abgelehnt), alle elf Aktionen live gegen + ein echtes Dokument inkl. dokumentweiter Referenz-Umschreibung bei + `INSERTROW`, **der wichtigste Test**: `SETVALUE` → `INSERTROW` → `SETVALUE` + in einem Lauf beweist, dass kein Host-Zugriff nach der strukturellen + Aktion auf einer veralteten Workbook-Referenz hängen bleibt, ein ganzer + synchroner Lauf ist genau ein Undo-Schritt, `DELETESHEET` verweigert das + letzte Blatt als echten Fehlschlag statt stillem No-op, `ADDTABLE` auf ein + *anderes* Blatt lässt die aktive Ansicht/Auswahl unangetastet (reiner + Seiteneffekt-Fix, live per Playwright gefunden), JSON-Round-Trip. --- ## Bekannte Grenzen +- **Makros — UI-Ebene** (`ButtonView.vue`, `MacrosModal.vue`, der Button- + Zweig in `CanvasObject.vue`/`SettingsPanel.vue`) rein UI-seitig + implementiert, ohne eigene Unit-Tests — wie bei Tabellen-Größenänderung/ + Abhängigkeiten-Panel/Ctrl+A oben, nur live per Playwright verifiziert + (Makro anlegen, per Listen-Button und per Button-Objekt ausführen, + Rechtsklick-Zuweisung, Fehlerfall zeigt einen Fehler-Toast statt + abzustürzen). Die Makro-*Sprache* selbst (Grammatik, Interpreter, + Store-Aktionen) ist dagegen vollständig durch Engine-/Store-Tests + abgedeckt (s. „Tests" unten). - **Zeilen/Spalten einfügen/löschen** (s. „Zeilen & Spalten einfügen/löschen" oben) bewusst ohne: Mehrfachauswahl (immer genau eine Zeile/Spalte pro Aufruf), automatisches Nachziehen von `CellFormat`-Regeln über die @@ -1180,7 +1276,8 @@ src/ store/ # reaktiver Dokument-Store (v2 Canvas-Modell), Formatierung io/ # csv / json (Eigenbau, inkl. v1→v2), xlsx (SheetJS, lazy) components/ # AppToolbar, SettingsPanel, FormulaBar, SheetCanvas, CanvasObject, - # TableGrid, ChartView, TextBox, SheetTabs, StatusBar, TheLogo, … + # TableGrid, ChartView, TextBox, ButtonView, MacrosModal, SheetTabs, + # StatusBar, TheLogo, … numolus-logo.svg # Marke: drei überlappende Pop-Art-Quadrate + Schriftzug config/brand.ts # Herausgeber-Metadaten (wicom.li) + Social-Media-Liste composables/ # Theme, Toasts diff --git a/src/components/AppToolbar.vue b/src/components/AppToolbar.vue index 2fef3b5..9d485af 100644 --- a/src/components/AppToolbar.vue +++ b/src/components/AppToolbar.vue @@ -6,6 +6,7 @@ import { useToasts } from '../composables/useToasts'; import BaseModal from './BaseModal.vue'; import CustomFunctionsModal from './CustomFunctionsModal.vue'; import NamedRangesModal from './NamedRangesModal.vue'; +import MacrosModal from './MacrosModal.vue'; import TheLogo from './TheLogo.vue'; import ToolbarIcon from './ToolbarIcon.vue'; import { WICOM, WICOM_SOCIAL } from '../config/brand'; @@ -32,6 +33,7 @@ const fileMenuOpen = ref(false); const aboutOpen = ref(false); const functionsOpen = ref(false); const namedRangesOpen = ref(false); +const macrosOpen = ref(false); const busy = ref(false); const fileInput = ref(null); @@ -54,6 +56,10 @@ function insertText() { store.addText(store.doc.activeSheetId); toasts.info('Textfeld eingefügt.'); } +function insertButton() { + store.addButton(store.doc.activeSheetId); + toasts.info('Button eingefügt.'); +} // ---- CSV export dialog ------------------------------------------- const csvExportOpen = ref(false); @@ -245,6 +251,9 @@ function onReset() { +
@@ -264,6 +273,14 @@ function onReset() { > +
@@ -360,6 +377,7 @@ function onReset() { + diff --git a/src/components/ButtonView.vue b/src/components/ButtonView.vue new file mode 100644 index 0000000..bd0f2f6 --- /dev/null +++ b/src/components/ButtonView.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/components/CanvasObject.vue b/src/components/CanvasObject.vue index 8da8095..e197368 100644 --- a/src/components/CanvasObject.vue +++ b/src/components/CanvasObject.vue @@ -1,6 +1,7 @@ + +