diff --git a/.gitattributes b/.gitattributes index e0202eb..c614c3e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,7 @@ testdaten/** -text *.pdf binary *.ttf binary *.xlsx binary + +# Linux-Skripte und -Dateien (Chromebook-Pakete) brauchen LF +*.sh text eol=lf +*.desktop text eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7846de..7687389 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,7 @@ # Erzeugt eine neue Version auf GitHub Releases, sobald ein Tag wie v1.0.0 gepusht wird: # bauen und testen, veröffentlichen (win-x64, eigenständig), mit Velopack Setup, portable ZIP und Update-Pakete # erzeugen und alles als Release hochladen. Das installierte Programm findet die neue Version dort selbst. +# Danach kommen Linux-Pakete für Chromebooks dazu (x64 und arm64, je .deb und portables tar.gz; ohne Selbst-Update). name: Release on: @@ -93,3 +94,42 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload ${{ github.ref_name }} AndiGeneratorNET-${{ steps.version.outputs.version }}-sbom.spdx.json + + # Linux (Chromebooks mit Linux-Entwicklungsumgebung, auch andere Linux-Systeme): eigenständig veröffentlichen und als + # Debian-Paket und tar.gz an das Release hängen, das der Windows-Job angelegt hat. Getestet wird im Windows-Job. + linux: + needs: release + runs-on: ubuntu-latest + strategy: + matrix: + rid: [ linux-x64, linux-arm64 ] + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Version aus dem Tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Veröffentlichen + run: dotnet publish src/AndiGenerator.Desktop -c Release -r ${{ matrix.rid }} --self-contained -nologo -o publish -p:Version=${{ steps.version.outputs.version }} + + - name: Pakete erzeugen + run: | + mkdir -p pakete + bash src/AndiGenerator.Desktop/Linux/paket-bauen.sh ${{ matrix.rid }} ${{ steps.version.outputs.version }} publish pakete + ls -l pakete + + - name: Herkunftsnachweis + continue-on-error: true + uses: actions/attest-build-provenance@v4 + with: + subject-path: pakete/* + + - name: An das Release hängen + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.ref_name }} pakete/* --clobber --repo ${{ github.repository }} diff --git a/.gitignore b/.gitignore index 6bdd97c..8b55239 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ build-messung.txt optimierung-log.txt abdeckung-log.txt messen-log.txt +bildschirmfotos-log.txt # Private Originaldaten (Klarnamen) – öffentlich nur die anonymisierte Kopie unter testdaten/ /Referenz/ diff --git a/Doku/ANLEITUNG.md b/Doku/ANLEITUNG.md index ddf52ba..3d8aab2 100644 --- a/Doku/ANLEITUNG.md +++ b/Doku/ANLEITUNG.md @@ -10,7 +10,7 @@ AndiGenerator.NET ist die Weiterentwicklung des **AndiGenerators** von Andreas H 2. [Installation](#2-installation) 3. [Erste Schritte](#3-erste-schritte) 4. [Das Hauptfenster](#4-das-hauptfenster) -5. [Die Generierung steuern](#5-die-generierung-steuern) +5. [Die Generierung steuern](#5-die-generierung-steuern) – mit [Der Automodus](#51-der-automodus) 6. [Pläne beurteilen: die Ansichten](#6-pläne-beurteilen-die-ansichten) 7. [Gewichtungen anpassen](#7-gewichtungen-anpassen) 8. [Pläne merken und vergleichen](#8-pläne-merken-und-vergleichen) @@ -38,7 +38,7 @@ Wenn Sie vorher mit dem Original-AndiGenerator gearbeitet haben, übernimmt Andi ## 2. Installation -AndiGenerator.NET läuft unter Windows 10 und 11 (64 Bit). Administratorrechte sind nicht nötig, und .NET muss nicht installiert sein – alles Nötige ist dabei. +AndiGenerator.NET läuft unter Windows 10 und 11 (64 Bit), auf Chromebooks und unter Linux (siehe unten). Administratorrechte sind nicht nötig, und .NET muss nicht installiert sein – alles Nötige ist dabei. **Mit Setup (empfohlen)** @@ -61,6 +61,17 @@ Das installierte Programm sucht nach dem Start selbst nach einer neuen Version u Über *Einstellungen → Apps → Installierte Apps → AndiGenerator.NET → Deinstallieren*. Ihre Daten im Ordner `%LOCALAPPDATA%\AndiGenerator.NET` und Ihre click-TT-Dateien bleiben erhalten. +**Auf einem Chromebook (und unter Linux)** + +Auf Chromebooks läuft das Programm in der Linux-Entwicklungsumgebung, die ChromeOS mitbringt. + +1. Einmalig Linux einschalten: *Einstellungen → Info zu ChromeOS → Entwickler → Linux-Entwicklungsumgebung → Aktivieren* (bei älteren Versionen *Einstellungen → Erweitert → Entwickler*). +2. Auf der Seite [Releases](https://github.com/PBuchmann/AndiGenerator.NET/releases) das passende Paket herunterladen: `andigenerator-net__amd64.deb` für Chromebooks mit Intel- oder AMD-Prozessor, `…_arm64.deb` für Chromebooks mit ARM-Prozessor (z. B. MediaTek oder Qualcomm). Welcher Prozessor eingebaut ist, steht unter *Einstellungen → Info zu ChromeOS → Weitere Details*, oder im Linux-Terminal mit `dpkg --print-architecture`. +3. In der App *Dateien* doppelt auf die heruntergeladene Datei klicken und **Installieren** wählen. Das Programm erscheint danach im Launcher im Ordner *Linux-Apps*. +4. Die click-TT-Dateien müssen für Linux sichtbar sein: entweder in den Ordner *Linux-Dateien* legen oder einen Ordner in *Dateien* mit der rechten Maustaste **Mit Linux teilen**. + +Ohne Installation geht es mit `AndiGeneratorNET--linux-x64.tar.gz` bzw. `…-linux-arm64.tar.gz`: entpacken und `./AndiGenerator.NET` starten. Unter Linux liegen die eigenen Daten im Ordner `~/.local/share/AndiGenerator.NET`. Neue Versionen werden hier nicht selbst gesucht – zum Aktualisieren das neue Paket genauso installieren. + ## 3. Erste Schritte Der Weg zu einem fertigen Plan in sechs Schritten: @@ -113,7 +124,9 @@ Bevor Sie die Generierung starten, lohnt ein Blick in die Ansicht **Terminwünsc Mit dem blauen Startknopf oben rechts beginnt die Suche. Über den Pfeil rechts daneben wählen Sie das Verfahren: - **Kostenoptimierung** – wie im Original: Die Suche senkt die Gesamtkosten mit Ihren Gewichtungen. -- **Automodus** – die Suche lenkt sich selbst über die Gewichte, so wie man es von Hand an den Reglern tun würde: Erst drängt sie Verstöße der Stufen A und B heraus (Hallenbelegung, parallele Spiele, Pflichtspieltage, Koppel-, Sperr- und Ausweichtermine), dann glättet sie die übrigen Kriterien, ohne A und B wieder zu verschlechtern. Die erste Kachel zeigt dann statt der Gesamtkosten die Verstöße A, B und C des besten Plans (ein Klick öffnet die Ansicht **Qualität**; die Kachel *Pflichtregeln* entfällt), oben die Phase des Automodus (**Basisoptimierung** mit Ihren Gewichtungen, dann **Optimierung Stufe A**, **Stufe B** und **Stufe C**), unten nur Hinweise wie „angehalten“ oder verbliebene harte Fehler. Wechseln Sie während der Generierung von der Kostenoptimierung in den Automodus, entfällt die Basisoptimierung: Der bisher beste Plan ist schon optimiert und gilt als Basis, der Automodus beginnt sofort mit Stufe A. Beim Start des Automodus (und beim Wechsel zu ihm) springt das Programm in die Ansicht **Qualität**, beim Start der Kostenoptimierung (und beim Wechsel zu ihr) in die Ansicht **Kosten**; fehlt die Ansicht, wird sie geöffnet. +- **Automodus** – die Suche lenkt sich selbst über die Gewichte, so wie man es von Hand an den Reglern tun würde: Erst drängt sie die Verstöße der Stufen A und B heraus (Hallenbelegung, parallele Spiele, Pflichtspieltage, Koppel-, Sperr- und Ausweichtermine), dann glättet sie die übrigen Kriterien, ohne A und B wieder zu verschlechtern. Wie er arbeitet und wann er sich lohnt, steht in Abschnitt [5.1](#51-der-automodus). + +Beim Start des Automodus (und beim Wechsel zu ihm) springt das Programm in die Ansicht **Qualität**, beim Start der Kostenoptimierung (und beim Wechsel zu ihr) in die Ansicht **Kosten**; fehlt die Ansicht, wird sie geöffnet. Der Knopf merkt sich die Wahl und heißt danach **Kostenoptimierung starten** bzw. **Automodus starten**. Auch während der Generierung – laufend oder pausiert – lässt sich über den Pfeil zum anderen Verfahren wechseln; die Suche setzt beim bisher besten Plan fort. @@ -154,6 +167,8 @@ Darunter vier **Kacheln** mit dem Stand der Generierung: Im Automodus entfällt die Kachel *Pflichtregeln*; die Ergebniskachel nimmt ihren Platz mit ein, *Verbesserungen* und *Berechnete Pläne* bleiben an ihrer Stelle. Die breitere Ergebniskachel zeigt oben die Phase (z. B. „Automodus · Optimierung Stufe A“) und unten unter **In Arbeit** alle Kriterien, an denen gerade gearbeitet wird (in Stufe A und B die zuletzt angehobenen, in C das, das geglättet wird) und vor jeder Verstoßzahl klein den Stand am Ende der Basisoptimierung (z. B. „A 14 → 3“). +![Ergebniskachel im Automodus](Anleitung/Bilder/18-automodus.png) + Den **Hauptbereich** füllen die Ansichten. Jede Ansicht öffnet sich als Reiter; Sie können mehrere öffnen, zwischen ihnen wechseln, sie per Maus nebeneinander anordnen und mit dem **×** am Reiter schließen. In vielen Ansichten vergrößert oder verkleinert **Strg + Mausrad** (oder der Zoomregler) die Darstellung. Ganz unten zeigt die **Statuszeile** den Stand in einer Zeile. @@ -179,6 +194,28 @@ Solange eine Generierung läuft oder pausiert ist, ist *Start mit* gesperrt: Ein Änderungen an den **Einstellungen** und **Spielplandaten** übernimmt eine laufende Generierung sofort; sie rechnet vom bisher besten Plan aus weiter. +### 5.1 Der Automodus + +Bei der **Kostenoptimierung** sucht das Programm den Plan mit den niedrigsten Gesamtkosten; was dabei wie viel zählt, bestimmen Sie über die Gewichtungen (Abschnitt [7](#7-gewichtungen-anpassen)). Oft dreht man dann von Hand nach: Ein Kriterium hat noch Verstöße, also Gewicht hoch, abwarten, nächstes Kriterium. Genau das übernimmt der **Automodus**. Er verändert die Gewichte selbst und richtet sich dabei nach der Einteilung der Kriterien in die Stufen A, B und C, die Sie in der Ansicht **Qualität** festlegen (Abschnitt [6.2](#62-qualität)). Ihre eigenen Gewichtungen bleiben dabei unverändert. + +Der Automodus arbeitet in **Phasen**; die laufende steht oben in der Ergebniskachel: + +1. **Basisoptimierung** – eine gewöhnliche Kostenoptimierung mit Ihren Gewichtungen, bis sie etwa 10 Sekunden lang nichts mehr verbessert. Ihr bester Plan ist der Maßstab für alles Weitere (in der Kachel die kleine Zahl vor dem Pfeil, z. B. „A 14 → 3“). +2. **Optimierung Stufe A** – der Automodus beobachtet die Verstöße der Stufe A. Ändern sie sich 10 Sekunden lang nicht mehr, hebt er das Gewicht genau der Mannschaften und Kriterien um eine Stufe an, die noch Verstöße haben. Das wiederholt er, bis keine Verstöße mehr übrig sind, nichts mehr anzuheben ist oder es dreimal hintereinander nicht besser wird. +3. **Optimierung Stufe B** – ebenso für die Stufe B; die Gewichte der Stufe A bleiben dabei stehen. +4. **Optimierung Stufe C** – nun glättet er die übrigen Kriterien der Reihe nach, wie sie in der Einteilung stehen, ausgehend vom besten Plan. Zuerst nimmt er sich Ausreißer vor: Mannschaften, die bei einem Kriterium mehr als zwei Verstöße mehr haben als nach der Basisoptimierung – ein, zwei mehr sind in Ordnung, viele bei einer Mannschaft wären unfair. Steigen dabei die Verstöße in A oder B wieder, nimmt er die letzte Anhebung zurück und macht beim nächsten Kriterium weiter. Sind alle Kriterien der Stufe C durch, beginnt er wieder mit dem Herausdrängen. + +Der Automodus hat keine Zeitgrenze; er läuft, bis Sie **Pausieren** oder **Beenden**. Unter **In Arbeit** zeigt die Ergebniskachel, an welchen Kriterien er gerade arbeitet (Abschnitt [4](#4-das-hauptfenster)). + +**Welcher Plan ist der beste?** Der Automodus vergleicht Pläne nicht nach den Gesamtkosten, sondern der Reihe nach: harte Fehler, Verstöße der Stufe A, Verstöße der Stufe B, Ausreißer in C, Verstöße in C und erst zuletzt die Kosten mit Ihren Gewichtungen. Ein Plan mit weniger Verstößen in A gewinnt also immer, auch wenn er teurer ist. + +**Eingreifen während der Generierung:** + +- **Einteilung ändern** – verschieben Sie in der Ansicht **Qualität** ein Kriterium, gilt das sofort, ohne neue Basisoptimierung. Rückt etwa ein Kriterium von B1 nach A2, drängt der Automodus zuerst genau dieses Kriterium heraus und danach wieder die ganze Stufe A. +- **Verfahren wechseln** – über den Pfeil am Startknopf, auch in der Pause. Beim Wechsel von der Kostenoptimierung in den Automodus entfällt die Basisoptimierung: Der bisher beste Plan ist schon optimiert und gilt als Basis, der Automodus beginnt sofort mit Stufe A. Ein bewährter Weg ist deshalb, mit der Kostenoptimierung zu beginnen und, wenn sie kaum noch Fortschritte macht, in den Automodus zu wechseln. + +**Wann welches Verfahren?** Der Automodus lohnt sich, wenn Pflichtregeln und Terminwünsche der Vereine Vorrang haben und Sie nicht selbst an den Gewichten drehen wollen. Die Kostenoptimierung ist die richtige Wahl, wenn Sie die Gewichtungen gezielt einsetzen oder Ergebnisse mit dem alten AndiGenerator vergleichen wollen – sie rechnet wie das Original. + ## 6. Pläne beurteilen: die Ansichten Die Ansichten **Kosten, Qualität, Meldungen, Terminplan** und **Diagramme** zeigen einen Plan. Welchen, wählen Sie oben in der Ansicht unter **Plan**: den besten Plan der laufenden Generierung, den in click-TT vorhandenen Plan oder einen gemerkten Plan. So lassen sich Pläne nebeneinander vergleichen – etwa zwei Kostenansichten mit verschiedenen Plänen. diff --git a/Doku/Anleitung.pdf b/Doku/Anleitung.pdf index 92c836a..aea64a2 100644 Binary files a/Doku/Anleitung.pdf and b/Doku/Anleitung.pdf differ diff --git a/Doku/Anleitung/Bilder/01-startseite.png b/Doku/Anleitung/Bilder/01-startseite.png index b9298d6..9e88fb4 100644 Binary files a/Doku/Anleitung/Bilder/01-startseite.png and b/Doku/Anleitung/Bilder/01-startseite.png differ diff --git a/Doku/Anleitung/Bilder/02-einrichtung.png b/Doku/Anleitung/Bilder/02-einrichtung.png index 89085d2..bfda0d1 100644 Binary files a/Doku/Anleitung/Bilder/02-einrichtung.png and b/Doku/Anleitung/Bilder/02-einrichtung.png differ diff --git a/Doku/Anleitung/Bilder/03-hauptfenster.png b/Doku/Anleitung/Bilder/03-hauptfenster.png index bdb51b2..65b9b5a 100644 Binary files a/Doku/Anleitung/Bilder/03-hauptfenster.png and b/Doku/Anleitung/Bilder/03-hauptfenster.png differ diff --git a/Doku/Anleitung/Bilder/04-kostenansicht.png b/Doku/Anleitung/Bilder/04-kostenansicht.png index 22287a9..ef107bc 100644 Binary files a/Doku/Anleitung/Bilder/04-kostenansicht.png and b/Doku/Anleitung/Bilder/04-kostenansicht.png differ diff --git a/Doku/Anleitung/Bilder/05-qualitaet.png b/Doku/Anleitung/Bilder/05-qualitaet.png index cefb38b..dcaf156 100644 Binary files a/Doku/Anleitung/Bilder/05-qualitaet.png and b/Doku/Anleitung/Bilder/05-qualitaet.png differ diff --git a/Doku/Anleitung/Bilder/06-terminplan.png b/Doku/Anleitung/Bilder/06-terminplan.png index 3bbb90d..699bf7a 100644 Binary files a/Doku/Anleitung/Bilder/06-terminplan.png and b/Doku/Anleitung/Bilder/06-terminplan.png differ diff --git a/Doku/Anleitung/Bilder/07-diagramm-spieltage.png b/Doku/Anleitung/Bilder/07-diagramm-spieltage.png index da12ef8..83477e2 100644 Binary files a/Doku/Anleitung/Bilder/07-diagramm-spieltage.png and b/Doku/Anleitung/Bilder/07-diagramm-spieltage.png differ diff --git a/Doku/Anleitung/Bilder/08-terminwuensche.png b/Doku/Anleitung/Bilder/08-terminwuensche.png index fc36b30..b254698 100644 Binary files a/Doku/Anleitung/Bilder/08-terminwuensche.png and b/Doku/Anleitung/Bilder/08-terminwuensche.png differ diff --git a/Doku/Anleitung/Bilder/09-gewichtung.png b/Doku/Anleitung/Bilder/09-gewichtung.png index ea2e551..fdcbf37 100644 Binary files a/Doku/Anleitung/Bilder/09-gewichtung.png and b/Doku/Anleitung/Bilder/09-gewichtung.png differ diff --git a/Doku/Anleitung/Bilder/10-einstellungen.png b/Doku/Anleitung/Bilder/10-einstellungen.png index 61fd48a..56adba4 100644 Binary files a/Doku/Anleitung/Bilder/10-einstellungen.png and b/Doku/Anleitung/Bilder/10-einstellungen.png differ diff --git a/Doku/Anleitung/Bilder/11-druckauswahl.png b/Doku/Anleitung/Bilder/11-druckauswahl.png index 09ec834..9dd409f 100644 Binary files a/Doku/Anleitung/Bilder/11-druckauswahl.png and b/Doku/Anleitung/Bilder/11-druckauswahl.png differ diff --git a/Doku/Anleitung/Bilder/12-spielplandaten.png b/Doku/Anleitung/Bilder/12-spielplandaten.png index 53af868..cfcbf55 100644 Binary files a/Doku/Anleitung/Bilder/12-spielplandaten.png and b/Doku/Anleitung/Bilder/12-spielplandaten.png differ diff --git a/Doku/Anleitung/Bilder/13-wunschtermin.png b/Doku/Anleitung/Bilder/13-wunschtermin.png index 5481e13..089d995 100644 Binary files a/Doku/Anleitung/Bilder/13-wunschtermin.png and b/Doku/Anleitung/Bilder/13-wunschtermin.png differ diff --git a/Doku/Anleitung/Bilder/14-terminwuensche-karten.png b/Doku/Anleitung/Bilder/14-terminwuensche-karten.png index c3f5cdf..99087ad 100644 Binary files a/Doku/Anleitung/Bilder/14-terminwuensche-karten.png and b/Doku/Anleitung/Bilder/14-terminwuensche-karten.png differ diff --git a/Doku/Anleitung/Bilder/15-nachbarmannschaften.png b/Doku/Anleitung/Bilder/15-nachbarmannschaften.png index 7d943d2..2554d7f 100644 Binary files a/Doku/Anleitung/Bilder/15-nachbarmannschaften.png and b/Doku/Anleitung/Bilder/15-nachbarmannschaften.png differ diff --git a/Doku/Anleitung/Bilder/16-meldungen.png b/Doku/Anleitung/Bilder/16-meldungen.png index 15101e9..62f7c25 100644 Binary files a/Doku/Anleitung/Bilder/16-meldungen.png and b/Doku/Anleitung/Bilder/16-meldungen.png differ diff --git a/Doku/Anleitung/Bilder/17-diagramm-spielverteilung.png b/Doku/Anleitung/Bilder/17-diagramm-spielverteilung.png index 35d371f..4dd89ab 100644 Binary files a/Doku/Anleitung/Bilder/17-diagramm-spielverteilung.png and b/Doku/Anleitung/Bilder/17-diagramm-spielverteilung.png differ diff --git a/Doku/Anleitung/Bilder/18-automodus.png b/Doku/Anleitung/Bilder/18-automodus.png new file mode 100644 index 0000000..bda93de Binary files /dev/null and b/Doku/Anleitung/Bilder/18-automodus.png differ diff --git a/Doku/MIGRATIONSPLAN.md b/Doku/MIGRATIONSPLAN.md index de9b890..f47f294 100644 --- a/Doku/MIGRATIONSPLAN.md +++ b/Doku/MIGRATIONSPLAN.md @@ -515,6 +515,11 @@ Die Portierung soll sich deutlich vom Original unterscheiden, ohne ihre Herkunft 4. Später: Klasse-3-Einzelentscheidungen (E9) nach grünen Paritätstests; Signierung (E12) vor der öffentlichen Freigabe. +**Erledigt (05.10.2026, vor dem Release v0.9.3):** + +- **Bildschirmfotos:** In Bildern, die nur einen Teil des Fensters zeigen, zerfiel die Schrift, wenn nur dieser Teil gezeichnet wurde (ein Versuch mit dem Fensterhintergrund ergab schwarzen Grund). Jetzt zeichnet `Bildschirmfotos.cs` immer das ganze Fenster und schneidet zu; alle Bilder und `Doku/Anleitung.pdf` sind neu erzeugt. +- **Ansichten nach dem Start:** Nach „Start mit“ einem vorhandenen Plan zeigten die Ansichten nach dem Start weiter den unveränderten Ausgangsplan (Kostenansicht 104 T, Kachel 85.400). Behoben mit `StartplanAnsichtenZuruecksetzen` und Test. + --- ## 9. Dokumentenverzeichnis (Einzeldokus) @@ -727,3 +732,6 @@ Befund: Billigere Pläne nach der Kostenfunktion des Originals sind nicht automa | 04.10.2026 | Qualitätsansicht: Einteilung nur im Automodus änderbar (Vorgabe Peter); A/B/C als Schaltergruppe und Pfeile als größere Knöpfe; Ziehen und Ablegen am Griff mit Vorschau (die gezogene Zeile bleibt als hervorgehobene Zeile sichtbar, die Tabelle zeigt Stufe und Nummer nach dem Ablegen; obere Zeilenhälfte = davor, untere = dahinter; Esc bricht ab; `Stufeneinteilung.Platzieren`) | | 04.10.2026 | Qualitätsansicht: Umstufen nach unten setzt an die erste Stelle der neuen Stufe, nach oben ans Ende (`Stufeneinteilung.Einstufen`); Pfeile am Stufenrand wechseln in die Nachbarstufe; beim Ziehen hängt die Zeile am Mauszeiger (Ebene über der Tabelle), die Tabelle zeigt an der Ablagestelle eine Lücke und nummeriert schon um | | 04.10.2026 | Spalte Einstufung breiter und links abgesetzt; Auswahlmenü des Startknopfs gestaltet (Titel, Erklärung, Haken). Start/Wechsel zum Automodus aktiviert die Qualitäts-, zur Kostenoptimierung die Kostenansicht. Kacheln: Pflichtregeln an Position 2; im Automodus nimmt die Ergebniskachel deren Platz ein (Grid statt UniformGrid, im engen Fenster 2 × 2). Ergebniskachel im Automodus: Verstöße seit der Basisoptimierung („A 14 → 3“) und Phase mit den Kriterien, an denen gearbeitet wird (`Lenkungsstand.Kriterien`: Drängen = zuletzt angehobene, Glätten = aktuelles C-Kriterium). Geänderte Einteilung während des Automodus ohne Neustart (`Inseloptimierer.EinteilungAendern`): Basis und bester Plan neu nach Stufen bewertet, weiter beim wichtigsten geänderten Kriterium (Drängen A/B bzw. Glätten ab dem C-Kriterium), sofern der Automodus nicht noch davor steht. Danach sofort (ohne Warten auf Stillstand) und in A/B zuerst nur das wichtigste geänderte Kriterium (Fokus), bis dort nichts mehr geht, dann die ganze Stufe. Kriterien in der Ergebniskachel unten als „In Arbeit: …“, alle mit Namen (oben war zu wenig Platz). Wechsel Kostenoptimierung → Automodus ohne Basisoptimierung (`Optimierungseinstellungen.AutoOhneGrundlauf`): bester Plan sofort Basis, erste Gewichtsänderung der Stufe A ohne Warten. „Start mit“: gewählter vorhandener Plan wird vor dem Start in allen (auch später geöffneten) Ansichten und den Kacheln gezeigt (`Generierungsanzeige.PlanZeigen`). Löschen gemerkter Pläne aus den Ansichten entfernt; stattdessen „Plan löschen“ oben an der Stelle von „Plan merken“, wenn vor dem Start ein gemerkter Plan gewählt ist. „Nach Excel“ aus den Ansichten in die linke Leiste (Ausgabe) mit Planmenü (`Exportziel`); Abschnitte der linken Leiste klappbar (in der schmalen Leiste immer offen). „Für click-TT exportieren“ ebenfalls mit Planmenü: auch gemerkte Pläne und der click-TT-Plan lassen sich als CSV exportieren. Excel-Export ausgebaut (Entscheidung Peter: zu ähnlich zum click-TT-Export): `Persistence.Tabellen` (Xlsx), `Tabellenexport`, Menü und Tests entfernt; „Für click-TT exportieren“ in der Leiste farblich hervorgehoben. Der Export nimmt ohne Menü den angezeigten Plan (aktive Ansicht, sonst Generierung bzw. Ausgangsplan) und ist ohne Plan deaktiviert und nicht hervorgehoben. Untere Zeile der Ergebniskachel im Automodus: statt „… seit dem ersten gültigen Plan“ (zweiter, widersprüchlicher Bezug) nur noch Zustandshinweise (angehalten, harte Fehler). Einrichtung: Punkt „Gespeicherte Kriterienreihenfolge“ (nur bei eigener Einteilung) mit Wahl „verwenden“ / „Standardreihenfolge nur für diese Sitzung“ (`Plansitzung.StandardKriterienstufenFuerDieseSitzung`, analog zu den gespeicherten Einstellungen). Einrichtung: Wahlkacheln fest unten bei den Knöpfen; „Abschließen“ übernimmt angeklickte, noch offene Entscheidungen (`Einrichtungsschritt.IstGewaehlt`). Qualitätsansicht: Klick auf eine Zeile zeigt rechts die Verstöße je Mannschaft (`Referenzbewertung.MeldungenJeMannschaft`, A1: harte Fehler); ohne Verstöße bleibt die Liste zu | +| 05.10.2026 | Release: zusätzlich Linux-Pakete für Chromebooks (Linux-Entwicklungsumgebung) – eigenständig veröffentlicht für linux-x64 und linux-arm64, je Debian-Paket (`/opt/andigenerator-net`, Eintrag im Launcher) und portables tar.gz (`src/AndiGenerator.Desktop/Linux/paket-bauen.sh`, Job `linux` im Release-Workflow); ohne Selbst-Update | +| 05.10.2026 | Bildschirmfotos der Anleitung automatisch: `bildschirmfotos.cmd` startet den UI-Test `Bildschirmfotos` (Avalonia.Headless mit Skia, 1400 × 860, 150 %), der mit der anonymisierten Staffel R2_4 Startseite, Einrichtung, Kostenoptimierung mit allen Ansichten, Automodus (neues Bild 18-automodus: Ergebniskachel) und die Dialoge aufnimmt; ohne Umgebungsvariable `ANDIGEN_BILDER` tut der Test nichts. | +| 05.10.2026 | Anleitung: eigener Abschnitt 5.1 „Der Automodus“ (Phasen, Wertung des besten Plans, Eingreifen, Wahl des Verfahrens); Abschnitt 3.5 verweist darauf. Nach dem Start zeigen Ansichten, die „Start mit“ auf den Ausgangsplan gestellt hatte, wieder die laufende Generierung (vorher blieb dort der unveränderte Ausgangsplan stehen). Bildschirmfotos von Teilen des Fensters: ganzes Fenster zeichnen und zuschneiden (beim Zeichnen nur eines Teils zerfiel die Schrift). | diff --git a/README.md b/README.md index 4357576..392447d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ AndiGenerator.NET ist die C#/.NET-Portierung des **AndiGenerators** von **Andrea ## Installation -Die neueste Version gibt es unter [Releases](https://github.com/PBuchmann/AndiGenerator.NET/releases): `AndiGeneratorNET-win-Setup.exe` installiert das Programm für den eigenen Benutzer (ohne Administratorrechte) und hält es danach selbst aktuell; `AndiGeneratorNET-win-Portable.zip` läuft ohne Installation. Windows 10/11, 64 Bit. Bis zur digitalen Signierung warnt Windows beim ersten Start – Einzelheiten in der [Anleitung](Doku/ANLEITUNG.md#2-installation). +Die neueste Version gibt es unter [Releases](https://github.com/PBuchmann/AndiGenerator.NET/releases): `AndiGeneratorNET-win-Setup.exe` installiert das Programm für den eigenen Benutzer (ohne Administratorrechte) und hält es danach selbst aktuell; `AndiGeneratorNET-win-Portable.zip` läuft ohne Installation. Windows 10/11, 64 Bit. Für Chromebooks (Linux-Entwicklungsumgebung) und Linux gibt es Debian-Pakete `andigenerator-net__amd64.deb` bzw. `_arm64.deb` und portable `tar.gz`. Bis zur digitalen Signierung warnt Windows beim ersten Start – Einzelheiten in der [Anleitung](Doku/ANLEITUNG.md#2-installation). ## Selbst bauen diff --git a/bildschirmfotos.cmd b/bildschirmfotos.cmd new file mode 100644 index 0000000..9c21230 --- /dev/null +++ b/bildschirmfotos.cmd @@ -0,0 +1,19 @@ +@echo off +rem Erzeugt die Bildschirmfotos der Anleitung neu (Doku\Anleitung\Bilder) - ohne Bildschirm, mit anonymisierten Testdaten. +rem Laeuft etwa eine Minute (Kostenoptimierung und Automodus rechnen kurz). Protokoll: bildschirmfotos-log.txt. +chcp 65001 >nul +cd /d "%~dp0" +set "LOG=%~dp0bildschirmfotos-log.txt" +set "ANDIGEN_BILDER=%~dp0Doku\Anleitung\Bilder" +echo Erzeuge Bildschirmfotos in %ANDIGEN_BILDER% ... +dotnet test tests\AndiGenerator.UI.Tests -c Release -nologo --filter "FullyQualifiedName~Bildschirmfotos" --logger "console;verbosity=normal" > "%LOG%" 2>&1 +set RC=%ERRORLEVEL% +echo. +findstr /C:": error " /C:"[FAIL]" "%LOG%" +if not "%RC%"=="0" ( + echo ERGEBNIS: FEHLGESCHLAGEN - das Protokoll steht in bildschirmfotos-log.txt. + pause + goto :eof +) +echo ERGEBNIS: Bilder erzeugt. Mit git diff bzw. im Explorer pruefen und danach die PDF neu erzeugen. +timeout /t 10 >nul diff --git a/src/AndiGenerator.Desktop/Linux/andigenerator-net.desktop b/src/AndiGenerator.Desktop/Linux/andigenerator-net.desktop new file mode 100644 index 0000000..091de16 --- /dev/null +++ b/src/AndiGenerator.Desktop/Linux/andigenerator-net.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=AndiGenerator.NET +GenericName=Spielplan-Generator +Comment=Spielpläne für Tischtennis-Staffeln aus den click-TT-Terminwünschen erstellen +Exec=/opt/andigenerator-net/AndiGenerator.NET %f +Icon=andigenerator-net +Terminal=false +Categories=Office; +MimeType=application/xml;text/xml; diff --git a/src/AndiGenerator.Desktop/Linux/andigenerator-net.png b/src/AndiGenerator.Desktop/Linux/andigenerator-net.png new file mode 100644 index 0000000..837617d Binary files /dev/null and b/src/AndiGenerator.Desktop/Linux/andigenerator-net.png differ diff --git a/src/AndiGenerator.Desktop/Linux/paket-bauen.sh b/src/AndiGenerator.Desktop/Linux/paket-bauen.sh new file mode 100755 index 0000000..91cc9d7 --- /dev/null +++ b/src/AndiGenerator.Desktop/Linux/paket-bauen.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Linux-Pakete (u. a. für Chromebooks mit Linux-Entwicklungsumgebung): Debian-Paket und portables tar.gz aus einer +# eigenständigen Veröffentlichung. Aufruf (Release-Workflow): paket-bauen.sh +# rid: linux-x64 (Intel/AMD-Chromebooks) oder linux-arm64 (ARM-Chromebooks) +set -euo pipefail + +rid="$1" +version="$2" +publish="$(realpath "$3")" +ziel="$(realpath "$4")" +hier="$(dirname "$(realpath "$0")")" + +case "$rid" in + linux-x64) arch=amd64 ;; + linux-arm64) arch=arm64 ;; + *) echo "Unbekannte Laufzeit: $rid" >&2; exit 1 ;; +esac + +# Portables Archiv: entpacken und ./AndiGenerator.NET starten. +tar -czf "$ziel/AndiGeneratorNET-$version-$rid.tar.gz" -C "$publish" . + +# Debian-Paket: Programm unter /opt, Aufruf andigenerator-net, Eintrag im Startmenü (auf dem Chromebook im Launcher). +wurzel="$(mktemp -d)" +mkdir -p "$wurzel/DEBIAN" "$wurzel/opt/andigenerator-net" "$wurzel/usr/bin" \ + "$wurzel/usr/share/applications" "$wurzel/usr/share/icons/hicolor/256x256/apps" +cp -r "$publish/." "$wurzel/opt/andigenerator-net/" +ln -s /opt/andigenerator-net/AndiGenerator.NET "$wurzel/usr/bin/andigenerator-net" +cp "$hier/andigenerator-net.desktop" "$wurzel/usr/share/applications/" +cp "$hier/andigenerator-net.png" "$wurzel/usr/share/icons/hicolor/256x256/apps/" + +# Rechte wie bei Systempaketen: Ordner 755, Dateien 644, das Programm ausführbar. +chmod 755 "$wurzel" +find "$wurzel" -type d -exec chmod 755 {} + +find "$wurzel" -type f -exec chmod 644 {} + +chmod 755 "$wurzel/opt/andigenerator-net/AndiGenerator.NET" + +# Debian-Versionen: „-“ trennt die Paketrevision ab, Vorabversionen (1.0.0-beta.1) werden deshalb zu 1.0.0~beta.1. +debversion="${version//-/\~}" +groesse="$(du -sk --exclude=DEBIAN "$wurzel" | cut -f1)" +cat > "$wurzel/DEBIAN/control" < +Installed-Size: $groesse +Depends: libc6, libfontconfig1, libx11-6, libice6, libsm6, libicu76 | libicu74 | libicu72 | libicu71 | libicu70 | libicu67, libgtk-3-0t64 | libgtk-3-0 +Section: misc +Priority: optional +Homepage: https://github.com/PBuchmann/AndiGenerator.NET +Description: Spielplan-Generator für Tischtennis-Staffeln + Portierung des AndiGenerators von Andreas Hofmann (GPL-3.0). Erstellt Spielpläne + aus den in click-TT gemeldeten Terminwünschen und exportiert sie für click-TT. +STEUER + +dpkg-deb --root-owner-group --build "$wurzel" "$ziel/andigenerator-net_${debversion}_${arch}.deb" +rm -rf "$wurzel" diff --git a/src/AndiGenerator.Presentation/HauptfensterViewModel.cs b/src/AndiGenerator.Presentation/HauptfensterViewModel.cs index 49cd419..d3c632a 100644 --- a/src/AndiGenerator.Presentation/HauptfensterViewModel.cs +++ b/src/AndiGenerator.Presentation/HauptfensterViewModel.cs @@ -39,6 +39,9 @@ public sealed class HauptfensterViewModel : ObservableObject, IDisposable private readonly Optimierungsdienst dienst; private readonly DockFabrik fabrik; private readonly List ansichten = []; + + /// Ansichten, die „Start mit“ vor dem Start auf den Ausgangsplan gestellt hat. + private readonly List startplanAnsichten = []; private readonly Dictionary gemerkteStaende = new(StringComparer.Ordinal); private readonly Timer statusTakt; private readonly string eigeneBasis; @@ -1324,7 +1327,8 @@ private void Starten() return; } - IReadOnlyList? ausgangsplan = startplan.Quelle is PlanQuelle quelle ? PlanFuer(quelle)?.Spiele : null; + PlanQuelle? ausgangsquelle = startplan.Quelle; + IReadOnlyList? ausgangsplan = ausgangsquelle is null ? null : PlanFuer(ausgangsquelle)?.Spiele; laufenderStand = null; dienst.Pausiert = false; dienst.Automodus = Automodus; @@ -1334,6 +1338,7 @@ private void Starten() Laeuft = true; Pausiert = false; BefehleAktualisieren(); + StartplanAnsichtenZuruecksetzen(ausgangsquelle); AnsichtenAktualisieren(PlanQuellenArt.LaufendeGenerierung); VerfahrensansichtZeigen(); } @@ -1689,6 +1694,7 @@ private void AnsichtOeffnen(AnsichtViewModel ansicht) if (!Laeuft && startplan.Quelle is PlanQuelle quelle && Planquellen.Contains(quelle)) { ansicht.Quelle = quelle; + startplanAnsichten.Add(ansicht); } } @@ -1767,9 +1773,11 @@ private void StartplanZeigen(PlanQuelle quelle) return; } + startplanAnsichten.Clear(); foreach (AnsichtViewModel ansicht in ansichten.Where(a => a.Quellen.Contains(quelle))) { ansicht.Quelle = quelle; + startplanAnsichten.Add(ansicht); } if (PlanFuer(quelle) is Planstand stand) @@ -1778,6 +1786,22 @@ private void StartplanZeigen(PlanQuelle quelle) } } + /// + /// Nach dem Start zeigen die Ansichten, die „Start mit“ auf den Ausgangsplan gestellt hat, wieder die laufende + /// Generierung – sonst bliebe dort der unveränderte Ausgangsplan stehen, während die Kacheln den besten Plan zeigen. + /// Ansichten, deren Plan der Anwender selbst gewählt hat, bleiben, wie sie sind. + /// + /// Die Quelle des Ausgangsplans. + private void StartplanAnsichtenZuruecksetzen(PlanQuelle? ausgangsquelle) + { + foreach (AnsichtViewModel ansicht in startplanAnsichten.Where(a => ansichten.Contains(a) && a.Quelle == ausgangsquelle && a.Quellen.Contains(PlanQuelle.Laufend))) + { + ansicht.Quelle = PlanQuelle.Laufend; + } + + startplanAnsichten.Clear(); + } + private void StartplaeneAufbauen() { var starts = new List { Startplan.Leer }; diff --git a/tests/AndiGenerator.Presentation.Tests/HauptfensterTests.cs b/tests/AndiGenerator.Presentation.Tests/HauptfensterTests.cs index c58e1c0..372f549 100644 --- a/tests/AndiGenerator.Presentation.Tests/HauptfensterTests.cs +++ b/tests/AndiGenerator.Presentation.Tests/HauptfensterTests.cs @@ -177,6 +177,27 @@ public async Task Gewaehlter_Ausgangsplan_wird_vor_dem_Start_in_allen_Ansichten_ Assert.Equal(PlanQuelle.ClickTt, Assert.Single(Ansichten()).Quelle); } + [Fact] + public async Task Nach_dem_Start_zeigen_die_Ansichten_des_Ausgangsplans_die_laufende_Generierung() + { + await OeffnenAsync(); + KostenAnsichtViewModel kosten = Assert.Single(Ansichten()); + hf.Startplan = hf.Startplaene.Single(p => p.Quelle == PlanQuelle.ClickTt); + hf.QualitaetsansichtCommand.Execute(null); + QualitaetAnsichtViewModel qualitaet = Assert.Single(Ansichten()); + + hf.StartenCommand.Execute(null); + try + { + Assert.Equal(PlanQuelle.Laufend, kosten.Quelle); + Assert.Equal(PlanQuelle.Laufend, qualitaet.Quelle); + } + finally + { + hf.StoppenCommand.Execute(null); + } + } + [Fact] public async Task Einrichtung_abschliessen_zeigt_den_Arbeitsbereich_ohne_zu_generieren() { diff --git a/tests/AndiGenerator.UI.Tests/Bildschirmfotos.cs b/tests/AndiGenerator.UI.Tests/Bildschirmfotos.cs new file mode 100644 index 0000000..ef9e572 --- /dev/null +++ b/tests/AndiGenerator.UI.Tests/Bildschirmfotos.cs @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: 2017 Andreas Hofmann +// SPDX-FileCopyrightText: 2026 Peter Buchmann +// SPDX-License-Identifier: GPL-3.0-only + +using AndiGenerator.Application; +using AndiGenerator.Domain.Optionen; +using AndiGenerator.Presentation; +using AndiGenerator.Presentation.Tests; +using AndiGenerator.Rendering; +using AndiGenerator.UI.Ansichten; +using AndiGenerator.UI.Seiten; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.VisualTree; +using CommunityToolkit.Mvvm.Input; +using Dock.Avalonia.Controls; + +namespace AndiGenerator.UI.Tests; + +/// +/// Erzeugt die Bildschirmfotos der Anleitung (Doku/Anleitung/Bilder) mit der echten Oberfläche ohne Bildschirm und +/// anonymisierten Testdaten – immer gleich groß und mit denselben Daten. Läuft nur, wenn die Umgebungsvariable +/// den Zielordner nennt (bildschirmfotos.cmd); im normalen Testlauf tut der Test nichts. +/// +[Collection(OberflaechenSammlung.Name)] +public sealed class Bildschirmfotos : IDisposable +{ + /// Umgebungsvariable mit dem Zielordner der Bilder. + public const string Variable = "ANDIGEN_BILDER"; + + /// Vergrößerung der Bilder (wie ein Bildschirm mit 150 %), damit sie im PDF scharf sind. + private const double Skala = 1.5; + + private const string Staffel = "R2_4__Kreisklasse_Gruppe_A (2)"; + + private readonly string ordner = Path.Combine(Path.GetTempPath(), "andigen-bilder-" + Guid.NewGuid().ToString("N")); + private readonly Oberflaeche oberflaeche; + + public Bildschirmfotos(Oberflaeche oberflaeche) + { + this.oberflaeche = oberflaeche; + oberflaeche.Protokoll.Leeren(); + } + + public void Dispose() + { + try + { + Directory.Delete(ordner, recursive: true); + } + catch (Exception fehler) when (fehler is IOException or UnauthorizedAccessException) + { + // Aufräumen ist nicht Teil der Bilder. + } + } + + [Fact] + public Task Bilder_der_Anleitung_erzeugen() => oberflaeche.Ausfuehren(async () => + { + if (Environment.GetEnvironmentVariable(Variable) is not { Length: > 0 } ziel) + { + return; + } + + Directory.CreateDirectory(ziel); + await EinrichtungAsync(ziel, StaffelKopieren("erststart", mitAenderungen: false)); + string datei = StaffelKopieren("daten", mitAenderungen: true); + await HauptfensterAsync(ziel, datei); + Dialoge(ziel, datei); + }); + + /// + /// Startseite und Einrichtung: Die Einrichtung erscheint nur, wenn es etwas zu klären gibt – sicher beim ersten Öffnen + /// einer click-TT-Datei (ohne .modifications). + /// + private static async Task EinrichtungAsync(string ziel, string datei) + { + string basis = Path.Combine(Path.GetDirectoryName(datei)!, "..", "basis"); + var o = new TestOberflaeche { DateiZumOeffnen = datei, Sammeln = true }; + using var modell = new HauptfensterViewModel(o, () => new HostWindow(), basis, null); + var fenster = new HauptfensterView { DataContext = modell, Width = 1400, Height = 860 }; + fenster.Show(); + try + { + Foto(ziel, "01-startseite", fenster, o); + await modell.OeffnenCommand.ExecuteAsync(null); + Foto(ziel, "02-einrichtung", fenster, o); + } + finally + { + fenster.Close(); + } + } + + /// Hauptfenster mit Kostenoptimierung und Automodus, alle Ansichten, Spielplandaten. + private static async Task HauptfensterAsync(string ziel, string datei) + { + string basis = Path.Combine(Path.GetDirectoryName(datei)!, "..", "basis"); + var o = new TestOberflaeche { DateiZumOeffnen = datei, Sammeln = true }; + using var modell = new HauptfensterViewModel(o, () => new HostWindow(), basis, null); + var fenster = new HauptfensterView { DataContext = modell, Width = 1400, Height = 860 }; + fenster.Show(); + try + { + await modell.OeffnenCommand.ExecuteAsync(null); + o.Abarbeiten(); + if (modell.Einrichtung is { } einrichtung) + { + await einrichtung.AbschliessenCommand.ExecuteAsync(null); + o.Abarbeiten(); + } + + // Kostenoptimierung ab dem Plan aus click-TT, kurz rechnen lassen und anhalten. + modell.Startplan = modell.Startplaene.Single(p => p.Quelle == PlanQuelle.ClickTt); + modell.KostenoptimierungCommand.Execute(null); + await WartenAsync(o, TimeSpan.FromSeconds(8), () => false); + modell.GenerierungCommand.Execute(null); + Foto(ziel, "03-hauptfenster", fenster, o); + Foto(ziel, "04-kostenansicht", Ansichten(fenster), o); + + Zeigen(modell.MeldungsansichtCommand, o); + Foto(ziel, "16-meldungen", Ansichten(fenster), o); + Zeigen(modell.TerminplanansichtCommand, o); + Foto(ziel, "06-terminplan", Ansichten(fenster), o); + + Zeigen(modell.DiagrammansichtCommand, o); + Foto(ziel, "07-diagramm-spieltage", Ansichten(fenster), o); + var diagramm = (DiagrammAnsichtViewModel)Einzige(fenster).DataContext!; + if (diagramm.Auswahl.FirstOrDefault(w => w.Teil == Diagrammteil.Spielverteilung) is { } verteilung) + { + diagramm.WaehlenCommand.Execute(verteilung); + Foto(ziel, "17-diagramm-spielverteilung", Ansichten(fenster), o); + } + + Zeigen(modell.TerminwunschansichtCommand, o); + var wuensche = (TerminwunschAnsichtViewModel)Einzige(fenster).DataContext!; + wuensche.IstUebersicht = true; + Foto(ziel, "08-terminwuensche", Ansichten(fenster), o); + wuensche.IstKarten = true; + Foto(ziel, "14-terminwuensche-karten", Ansichten(fenster), o); + + Zeigen(modell.NachbarterminansichtCommand, o); + Foto(ziel, "15-nachbarmannschaften", Ansichten(fenster), o); + Zeigen(modell.EinstellungenCommand, o); + Foto(ziel, "10-einstellungen", Ansichten(fenster), o); + + // Wechsel in den Automodus: ohne Basisoptimierung gleich Stufe A; rechnen, bis er an Kriterien arbeitet. + modell.AutomodusCommand.Execute(null); + await WartenAsync(o, TimeSpan.FromSeconds(45), () => modell.Anzeige.Phase.Length > 0); + await WartenAsync(o, TimeSpan.FromSeconds(3), () => false); + modell.GenerierungCommand.Execute(null); + Foto(ziel, "18-automodus", Kacheln(fenster), o); + + // Qualität im Automodus (mit Einstufung), gewählt die erste Zeile mit Verstößen je Mannschaft. + var qualitaet = (QualitaetAnsichtViewModel)Einzige(fenster).DataContext!; + if (qualitaet.Zeilen.FirstOrDefault(z => z.Zustand != Qualitaetszustand.Erfuellt && z.Kriterium is not null) is { } zeile) + { + qualitaet.Waehlen(zeile); + } + + Foto(ziel, "05-qualitaet", Ansichten(fenster), o); + + // Spielplandaten über den Befehl, der den Dialog zeigt. + o.Datendialog = dialog => + { + var datenfenster = new Datendialog(dialog); + datenfenster.Show(); + Foto(ziel, "12-spielplandaten", datenfenster, o); + datenfenster.Close(); + return Task.FromResult(false); + }; + modell.StoppenCommand.Execute(null); + o.Abarbeiten(); + await modell.DatenCommand.ExecuteAsync(null); + } + finally + { + fenster.Close(); + } + } + + /// Gewichtung einer Kostenart, Druckauswahl und Wunschtermin. + private static void Dialoge(string ziel, string datei) + { + var o = new TestOberflaeche(); + Plansitzung sitzung = Plansitzung.Oeffnen(datei, Path.Combine(Path.GetDirectoryName(datei)!, "..", "basis")); + var art = MannschaftsKostenart.Sperrtermine; + (string name, IReadOnlyList meldungen) = sitzung.Staffel.Mannschaften + .Select(m => (m.Name, sitzung.Meldungen(sitzung.Staffel.BestehenderSpielplan, m.Name, art))) + .OrderByDescending(x => x.Item2.Count) + .First(); + var gewichtung = new Gewichtungsdialog($"Gewichtung der Mannschaft {name} ändern", Gewichtungsanzeige.Name(art), Gewichtung.Normal, meldungen); + gewichtung.Show(); + Foto(ziel, "09-gewichtung", gewichtung, o); + gewichtung.Close(); + + var druck = new Druckauswahldialog(new DruckauswahlViewModel([PlanQuelle.Laufend, PlanQuelle.ClickTt])); + druck.Show(); + Foto(ziel, "11-druckauswahl", druck, o); + druck.Close(); + + // Wunschtermin: Doppelklick auf einen Termin in den Spielplandaten öffnet den Dialog. + using var a = new Arbeitsplatz("datendialoge/D01_Allgemein.xml"); + a.Oberflaeche.Wunschtermin = dialog => + { + var wunsch = new Wunschtermindialog(dialog); + wunsch.Show(); + Foto(ziel, "13-wunschtermin", wunsch, o); + wunsch.Close(); + return false; + }; + var daten = new Datendialog(a.Dialog); + daten.Show(); + try + { + WunschtermineSeiteViewModel seite = a.Dialog.Seiten.OfType().Single(); + daten.GetVisualDescendants().OfType().Single(l => l.Classes.Contains("seitenliste")).SelectedItem = seite; + seite.Mannschaft = Namen.RheinduerkheimIV; + Oberflaeche.Zeichnen(daten); + WunschterminDetail detail = daten.GetVisualDescendants().OfType().Single(); + Point punkt = detail.GetVisualDescendants() + .OfType() + .Where(t => t.DataContext is WunschterminZelle) + .Select(t => t.TranslatePoint(new Point(t.Bounds.Width / 2, t.Bounds.Height / 2), daten)) + .OfType() + .First(p => new Rect(daten.ClientSize).Contains(p)); + daten.MouseDown(punkt, MouseButton.Left); + daten.MouseUp(punkt, MouseButton.Left); + daten.MouseDown(punkt, MouseButton.Left); + daten.MouseUp(punkt, MouseButton.Left); + Oberflaeche.Zeichnen(daten); + } + finally + { + daten.Close(); + } + } + + /// + /// Speichert ein Steuerelement (Fenster oder Teil davon) als PNG in 150 %. Gezeichnet wird immer das ganze Fenster und + /// dann zugeschnitten: Zeichnet man nur einen Teil, zerfällt dort die Schrift. + /// + private static void Foto(string ziel, string name, Control steuerelement, TestOberflaeche o) + { + o.Abarbeiten(); + Zeichnen(steuerelement); + o.Abarbeiten(); + Zeichnen(steuerelement); + Visual fenster = TopLevel.GetTopLevel(steuerelement) ?? steuerelement; + var aufloesung = new Vector(96 * Skala, 96 * Skala); + using var bild = new RenderTargetBitmap(Pixel(fenster.Bounds.Size), aufloesung); + bild.Render(fenster); + string datei = Path.Combine(ziel, name + ".png"); + if (ReferenceEquals(fenster, steuerelement)) + { + Speichern(bild, datei); + return; + } + + Point oben = steuerelement.TranslatePoint(default, fenster) ?? default; + PixelRect ausschnitt = new PixelRect( + (int)Math.Round(oben.X * Skala), + (int)Math.Round(oben.Y * Skala), + Pixel(steuerelement.Bounds.Size).Width, + Pixel(steuerelement.Bounds.Size).Height) + .Intersect(new PixelRect(bild.PixelSize)); + using var teil = new WriteableBitmap(ausschnitt.Size, aufloesung, PixelFormat.Bgra8888, AlphaFormat.Premul); + using (ILockedFramebuffer puffer = teil.Lock()) + { + bild.CopyPixels(ausschnitt, puffer.Address, puffer.RowBytes * ausschnitt.Height, puffer.RowBytes); + } + + Speichern(teil, datei); + } + + private static PixelSize Pixel(Size groesse) => + new((int)Math.Ceiling(groesse.Width * Skala), (int)Math.Ceiling(groesse.Height * Skala)); + + private static void Speichern(Bitmap bild, string datei) + { +#pragma warning disable CS0618 // wie Bildschirmfoto: die neue Speicher-Methode verlangt noch undokumentierte Optionen + bild.Save(datei); +#pragma warning restore CS0618 + } + + private static void Zeichnen(Control steuerelement) + { + if (TopLevel.GetTopLevel(steuerelement) is Window fenster) + { + Oberflaeche.Zeichnen(fenster); + } + else + { + Dispatcher.UIThread.RunJobs(); + } + } + + /// Lässt die Generierung rechnen und arbeitet dabei die Meldungen an die Oberfläche ab. + private static async Task WartenAsync(TestOberflaeche o, TimeSpan hoechstens, Func fertig) + { + DateTime ende = DateTime.UtcNow + hoechstens; + while (DateTime.UtcNow < ende && !fertig()) + { + await Task.Delay(200); + o.Abarbeiten(); + Dispatcher.UIThread.RunJobs(); + } + } + + private static void Zeigen(IRelayCommand befehl, TestOberflaeche o) + { + befehl.Execute(null); + o.Abarbeiten(); + Dispatcher.UIThread.RunJobs(); + } + + private static T Einzige(Window fenster) + where T : Control => + fenster.GetVisualDescendants().OfType().Single(); + + /// Der Hauptbereich mit den Ansichten (ohne Kopf, Kacheln und Leiste). + private static Control Ansichten(Window fenster) => fenster.GetVisualDescendants().OfType().First(); + + /// Die Kacheln mit dem Stand der Generierung. + private static Control Kacheln(Window fenster) => + fenster.GetVisualDescendants().OfType().First(g => g.Classes.Contains("kacheln")); + + /// Kopiert die Staffel (click-TT-Datei, auf Wunsch mit Änderungen) in einen eigenen Ordner, damit nichts verändert wird. + private string StaffelKopieren(string unterordner, bool mitAenderungen) + { + string daten = Path.Combine(ordner, unterordner); + Directory.CreateDirectory(daten); + string quelle = Path.Combine(Testdaten.Referenz, "eingabe", Staffel); + string datei = Path.Combine(daten, "4. Kreisklasse Gruppe A.xml"); + File.Copy(quelle + ".xml", datei); + if (mitAenderungen && File.Exists(quelle + ".modifications")) + { + File.Copy(quelle + ".modifications", Path.ChangeExtension(datei, ".modifications")); + } + + return datei; + } +} diff --git a/tools/anleitung/README.md b/tools/anleitung/README.md index 6ccc13f..b0ec12d 100644 --- a/tools/anleitung/README.md +++ b/tools/anleitung/README.md @@ -15,4 +15,13 @@ rm -r fonts anleitung.css pdf.js anleitung.html ## Bildschirmfotos -Mit `starten.cmd` gestartet, speichert **Strg+Umschalt+F12** das aktive Fenster (auch Dialoge) als PNG im Ordner `Bildschirmfotos` (nicht im Repository). Für die Bilder nur anonymisierte Testdaten (`testdaten/`) verwenden und persönliche Pfade unkenntlich machen. +`bildschirmfotos.cmd` erzeugt alle Bilder unter `Doku/Anleitung/Bilder` neu – automatisch, ohne Bildschirm und immer gleich: dieselbe Fenstergröße (1400 × 860), 150 % Auflösung und die anonymisierte Staffel `testdaten/referenz/eingabe/R2_4__Kreisklasse_Gruppe_A (2)`. Dahinter steht der Test `tests/AndiGenerator.UI.Tests/Bildschirmfotos.cs` mit der echten Oberfläche (Avalonia.Headless mit Skia); er läuft nur, wenn die Umgebungsvariable `ANDIGEN_BILDER` den Zielordner nennt, im normalen Testlauf tut er nichts. Von Hand gleichwertig: + +```bat +set ANDIGEN_BILDER=%CD%\Doku\Anleitung\Bilder +dotnet test tests\AndiGenerator.UI.Tests -c Release --filter "FullyQualifiedName~Bildschirmfotos" +``` + +Der Ablauf: Startseite, Einrichtung, Kostenoptimierung ab dem click-TT-Plan (8 s, dann Pause) mit allen Ansichten, Wechsel in den Automodus bis zur ersten Stufe (Ergebniskachel, Qualität), danach Spielplandaten, Gewichtung, Druckauswahl und Wunschtermin. Kommt ein Bild hinzu oder ändert sich die Oberfläche so, dass ein Schritt nicht mehr passt, wird der Test angepasst; danach die PDF wie oben neu erzeugen. Weil Kosten- und Automodus kurz echt rechnen, unterscheiden sich die Zahlen von Lauf zu Lauf leicht. + +Einzelne Fenster lassen sich weiterhin von Hand aufnehmen: Mit `starten.cmd` gestartet, speichert **Strg+Umschalt+F12** das aktive Fenster (auch Dialoge) als PNG im Ordner `Bildschirmfotos` (nicht im Repository). Dafür nur anonymisierte Testdaten (`testdaten/`) verwenden und persönliche Pfade unkenntlich machen.