diff --git a/CHANGES.md b/CHANGES.md index 72ab215..4700933 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | +| `src/model/documentation.ts` (Java-Regeln) | Java `JDocuElement` (Konstruktor, `fontColor`, `panelColor`, `showPopupMenu`, `mouseDragged`, `setColor`), `GUIDocumentationPanel.updateViewport` (gemessen mit `DocuRun`) | geändert (F4.8a): `isItalic` (Javas Stil 2, mit Fett 3; ging vorher beim Speichern verloren); ein Element behält Größe und Schriftgröße seiner Datei (Swift vergrößert auf 32 × 24, Schrift nur 8–72); fehlende Schrift Dialog 12; neue Elemente 100 × 50, Fläche grau (204), Text schwarz; Javas 15 Text- und 12 Flächenfarben, Schriftgrößen 12/18/24, Stile Normal/Kursiv/Fett; `resizedFrame` (Javas Ziehen am Rand, kleinste Größe Fläche 20 × 20, Text 88 × 20); `inJavaDrawingOrder` (Texte über Flächen, je Art das frühere oben) | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. seit F2.3e wie Java: SYN+ACK und FIN gehen nie ein zweites Mal hinaus; der Server gibt einen Handschlag nach 3 RTT auf und schließt mit FIN; nach dem FIN wartet ein Socket höchstens fünfmal 3 RTT, jedes Segment beendet eine Wartezeit, dann CLOSED (Java lässt den Zustand stehen). Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht, und ein wiederholtes SYN in `SYN_RCVD` bekommt das SYN+ACK noch einmal, statt den Socket zu schließen), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. ARP seit F2.4b nach Javas `holeARPTabellenEintrag`, an Java-Filius gemessen: eine Auflösung fragt, wartet eine RTT, fragt ein zweites Mal und gibt nach einer weiteren RTT auf; dann „Host unreachable" beim Absender selbst oder vom Router zurück (bei ICMP nur für Echo-Anfragen); ein wartender Absender wacht nach den übrigen Ereignissen desselben Zeitpunkts auf; der Ping wartet danach eine RTT auf die Antwort. Swift schickt in der Ereignisverarbeitung beide Anfragen zugleich und lässt das Paket ohne Ende warten, wenn die Adresse irgendwo im Netz vorkommt (Wissen, das kein Knoten hat), und blockiert direkte Aufrufe ohne Wartezeit; die Option `swiftArp` behält das für den Swift-Vergleich. Seit F2.4b-2 kehrt ein Senden wie in Java sofort zurück, die Frames reisen, während die Uhr läuft (Swift: der Aufruf kehrt erst zurück, wenn alle Frames angekommen sind; die Option `swiftSends` behält das für den Swift-Vergleich), und UDP und TCP senden über Javas Transport-Thread je Knoten und Protokoll: ein Segment nach dem anderen; solange IP auf ARP wartet, blockiert das nächste `senden`. Wartende Absender kommen in der Reihenfolge an die Reihe, in der sie kamen (Javas Sperre überlässt das dem Zufall). Seit F2.4b-3 geht jedes ankommende Paket wie in Java durch den IP-Thread (UDP, TCP) oder den ICMP-Thread (ICMP) seines Knotens: zustellen, Echo-Reply, weiterleiten, eins nach dem anderen; solange ARP wartet, warten die folgenden. Abweichend von Java weckt eine ARP-Antwort alle, die auf die Adresse warten (Javas `notify` weckt einen beliebigen). DNS seit F2.4a nach Javas `DNSServer`/`Resolver`, an Java-Filius gemessen: Server auf Port 53 mit Einträgen aus dem Modell (die Antwort wiederholt die Frage nicht, RCODE immer 0, ein Verweis mit NS und A im Antwortteil), iterativer Resolver mit 5 RTT je Anfrage, der nur die Server behält, an die er verwiesen wurde; seit F2.4c auf den Ereignissen statt blockierend und mit rekursivem Server nach `DNSServer.answerWithRemoteData` (Nameserver der Zone, dann der eigene DNS-Server, sonst Verweis), abweichend von Java fragt er sich nie selbst (Java endlos). Gateway-NAT seit F2.6 (Z. ~3585–3900) nach Javas `filius.software.nat`, an Java-Filius gemessen: die Übersetzung vor dem IP-Thread (wie Javas NAT-Threads zwischen Karte und Ethernet), ausgehend WAN-Adresse und der Port des Eintrags (ICMP behält die Kennung), eingehend nach Gegenstelle, Port und Protokoll, sonst nach der Portweiterleitung (legt einen dynamischen Eintrag an), ein Ping an die WAN-Adresse beantwortet der Heimrouter, Pakete von außen an das LAN verschwinden, beide Seiten durch die Firewall des Heimrouters, ausgehend TTL 1 bekommt „Zeit überschritten“; dynamische Einträge verfallen nach 300 s mal Javas Verzögerungsfaktor (Swift fest 300 s), aufgefrischt nur eingehend, seit 01.10.2026 genau in diesem Alter (Java findet sie erst bei seiner sekündlichen Prüfung, bis zu 1 s später; so auch der Switch: Javas `checkSAT` prüft jede Sekunde, der Kern verfällt genau zur Lernzeit); der Heimrouter leitet weiter wie ein Router. Abweichend von Java: ein LAN-Endpunkt bekommt für jede neue Gegenstelle einen Eintrag auf demselben WAN-Port (Java behält nur den für die erste, die Antworten weiterer bleiben am Heimrouter hängen). RIP seit F2.5a (Z. ~4700–5090) nach Javas `filius.software.rip`, seit F2.5b an Java-Filius gemessen (die Pause in Javas `float`-Rechnung): eine Route mit 0 Hops je eigenem Netz, die nie verfällt; der erste Beacon nach einer Sekunde, dann alle 25,2–35,2 s aus einem Zufall je Router (Java: ein ungesetzter `Random` je Router; der Kern setzt ihn aus Szenario und Router-ID, `ripSeed`; Swift: ein Zufall für alle); eine Nachricht je Schnittstelle ohne die Routen über sie (Split Horizon); eine neue Route oder mehr Hops vom selben Router senden sofort; eine gelernte Route wird genau nach ihren 75 s auf 16 gesetzt und sofort gemeldet (Java erst beim nächsten eigenen Beacon, bis zu gut 30 s später; Swift bei jedem Ereignis; seit 01.10.2026, vorher wie Java); Nachrichten von jedem Quellport (Swift: nur 521); ein RIP-Router leitet nur nach seiner Tabelle weiter, wenige Hops vor langer Maske. `receiveUdp` kehrt wie Javas `empfangen` zurück, sobald ein Datagramm da ist. Seit der Abnahme F2 nach Javas `ARPThread`: ein Eintrag entsteht nur aus einer Anfrage nach der eigenen Adresse oder aus einer Antwort (Swift: aus jedem Paket; `swiftArp` behält das), ein Paket von der Netzadresse wird verworfen; ein Rechner leitet weiter, wenn seine IP-Weiterleitung an ist (`ipForwardingByNodeId`). Seit F2.8b nach Javas Tabellen gemessen: eine ARP-Anfrage trägt als Ziel-MAC die Broadcast-Adresse (Swift: Nullen; `swiftArp` behält sie), eine ICMP-Fehlermeldung Identifier und Sequenznummer 0 (RFC 792, Java; Swift kopiert die des Echos, ein Ping erkennt die Meldung am eingebetteten Echo), und „Host nicht erreichbar“ vom Router trägt das Paket, wie er es weitergeleitet hat (TTL gesenkt). Die Broadcast-Adresse schreibt sie immer groß (Java in der Antwort an 0.0.0.0 klein). Den Mitschnitt je Karte (`capture.ts`) schreibt sie seit F2.8a dort, wo Java ihn schreibt: beim Abgang auf das Kabel und bei der Ankunft an der Karte. Seit F3.4a läuft die Befehlszeile mit (`terminal`, auf den Dateien des Knotens). Seit F3.4b nach Javas `ICMP`/`ICMPThread`, an Java-Filius gemessen: ein Echo im Programm-Thread (`icmpEchoSteps`: Warteschlange leeren, senden mit ARP-Wartezeit, eine RTT auf die erste ICMP-Nachricht warten, gleich welche), ein ICMP-Paket an den Broadcast eines eigenen Netzes geht mit TTL 1 an alle und wird von dort beantwortet, einer für ein fremdes Netz verworfen; Javas Routenwahl (`routeRow`), die ARP-Tabelle mit Javas Broadcast-Zeile (`arpTable`, `removeArpEntry`, `resetArpTable`), DNS im Programm-Thread mit Antwort- und Autoritätsteil (`resolveDnsSteps`), ein UDP-Socket weiß, dass er ohne Gegenstelle lauscht (`listening`); ein Rechner nimmt Pakete an 127.0.0.1 an. Eine ARP-Wartezeit endet wie Javas `wait` nach allem, was zur selben Zeit sonst geschieht (eine Antwort zur selben Millisekunde zählt noch). Seit F3.4c die Uhrzeit eines Frames (`wallTime`) für `tcpdump`. Seit F3.6a läuft das Gnutella-Programm mit (`gnutella`, Schnappschuss `gnutellaByNodeId`), mit eigener Zufallsquelle für die GUIDs (`drawGnutellaGuid`). Seit F3.5a schreibt der Mailserver seine Kontendatei `/mailserver/konten.txt` beim Start und bei jeder Änderung seiner Konten in die Dateien des Knotens (Java schreibt die Benachrichtigung an einen lokalen Absender erst beim nächsten Speichern hinein). Abweichend von Java: eine Echo-Antwort kommt von der gefragten Adresse (RFC 792; Java antwortet von 127.0.0.1, wenn die eigene Adresse gefragt war, und von der nahen Karte, wenn die ferne eines Routers gefragt war), und „Netz nicht erreichbar“ vom Router trägt wie „Host nicht erreichbar“ das Paket, wie er es weitergeleitet hätte, mit Absender (Java bettet ein halb gebautes ohne Absender ein). Seit F3.3b läuft das E-Mail-Programm mit (`emailClient`), seine gesendeten und abgeholten Mails stehen im Schnappschuss (`emailClientsByNodeId`); ein Programm-Thread kann MX nachschlagen (`resolveDnsMailServerSteps`). Seit F3.3a läuft der E-Mail-Server mit (`emailServer`); was er annimmt oder löscht, steht im Schnappschuss (`emailServersByNodeId`), aus dem es zurück ins Projekt geht; die Uhrzeit für seine Benachrichtigungen kommt aus der Option `wallClock`. Seit F3.2c ändert die Seite `konfig` der Router und Heimrouter die Firewall zur Laufzeit; die Änderung steht im Schnappschuss (`firewallConfiguration`), aus dem sie zurück ins Projekt geht. Seit F3.2b starten Router und Heimrouter ihren Webserver auf Port 80 mit Javas Seiten (Javas Server lauscht wegen eines Wettlaufs in `setAktiv` manchmal nie, der Kern immer; `swiftRouters` lässt ihn für den Swift-Vergleich weg), die Firewall führt ein Protokoll je Knoten (Regeltreffer bei TCP und UDP, verworfenes ICMP), das wie Javas Protokollseite einen Neustart überlebt, eine RIP-Route kennt die öffentliche Adresse des meldenden Routers (`hopPublicIp`), und TCP zählt Sequenznummern und Segmentgröße wie Java in Zeichen (UTF-16-Einheiten) statt in Bytes, ohne ein Zeichenpaar zu zerreißen; Bytes, die kein Text sind, zählen als Bytes. Seit F3.2a schließt ein Programm wie Javas `schliessen()` nur auf Bitte (`requestTcpClose`): Das FIN geht zur selben Zeit, nach dem, was der Programm-Thread noch tut. Seit F3.1 laufen Verbinden, Senden, Empfangen und Annehmen auch als Schritte eines Programm-Threads (`connectTcpSteps`, `sendTcpSteps`, `receiveTcpSteps`, `acceptTcpSteps`, `resolveHostSteps`); die blockierenden Aufrufe sind nur noch eine Hülle darum. Abweichend von Java: Ein Verbindungsaufbau ohne Route scheitert sofort (Javas Socket-Thread stirbt an der `RouteNotFoundException`, `verbinden` wartet ewig), eine Nachricht, die vor dem Schließen ankam, wird noch ausgegeben (Java verwirft sie), und Senden unterscheidet wie Java „nicht verbunden", „unterbrochen" und „Zeitüberschreitung". WLAN seit F2.7 an Java-Filius gemessen: zur Laufzeit ist ein Funkkabel ein Kabel wie jedes andere, und eine SSID verbindet nichts (nach ihr legt nur Javas Editor das Funkkabel, `JHostKonfiguration.updateWifiConnection`); Swifts Verbindung über die SSID zur Laufzeit, auch ohne Kabel (`wirelessAssociations`), nicht übernommen. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Remote Link | | `src/runtime/engine.ts` (ganze Millisekunden) | Java `System.currentTimeMillis` (long) | behoben (F4.6b): `start` und `advance` runden auf ganze Millisekunden ab, wie Javas Uhr zählt. Gefunden im Browser: der Raum gab `performance.now()` mit Bruchteilen weiter, und ein Ping blieb nach der ersten Zeitüberschreitung hängen; alle Tests liefen zuvor mit glatten Takten | @@ -95,6 +96,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen; merkt sich den Java-Typ jedes Primitivs an Settern, Konstruktor- und Aufrufargumenten und welche Eigenschaften ein eigener Getter angelegt hat (für `beanCapture.ts`) | | `src/javaDefaults.json` (`#priority`) | Java `Thread.getPriority` (gemessen mit `GetterDefaults`) | nachgetragen (F4.6c): die Priorität der gespeicherten Thread-Klassen (5); Javas Datei kann eine Zahl über einen solchen Getter-Wert benennen (`idref`), unaufgelöst las der Import 0 | | `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet, WLAN eines Rechners eingeschaltet nach Javas Funk-Merkmal der Karte (auch ohne SSID; Swift: jede SSID); der Webserver behält Javas Schalter für virtuelle Hosts (Swift: aus); gegen Swifts importiertes Modell auf 13 Dateien geprüft; seit der Abnahme F2 die IP-Weiterleitung eines Knotens, wo sie vom Standard seiner Art abweicht (`ipForwardingByNodeId`; Java `ipForwardingEnabled`, übersehen bis das Firewall-Beispiel lief) | +| `src/importProject.ts`, `src/exportProject.ts` (Dokumentation) | Java `JDocuElement.setColor(null)`, `Font` (gemessen mit `DocuRun`) | geändert (F4.8a): Kursiv gelesen und geschrieben (Stil = Fett 1 | Kursiv 2); ein Element ohne Farbe bekommt Javas Hellgrau (192) statt Schwarz bzw. Swifts Blassgelb; Java lädt das Geschriebene so (exported.java.json neu) | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways; seit F2.7 über Funkkabel); seit der Abnahme F2 IP-Weiterleitung und eingeschaltete DNS-Server im Schnappschuss; seit F3.2c eine Firewall, über `konfig` im Lauf geändert, gespeichert und wieder geöffnet (`internetwork_komplett.fls`); seit F3.3a eine Mail, im Lauf per SMTP angenommen, gespeichert und wieder geöffnet (`email_komplett.fls`); seit F3.3b eine Mail vom Programm über den MX an die andere Domain, abgeholt, gespeichert und wieder geöffnet; seit F3.4a Ordner und Datei aus der Befehlszeile, gespeichert und wieder geöffnet | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem (bei Router und Heimrouter das, was die Datei zu den Vorlagen des Konstruktors hinzufügt, oder eine eigene Wurzel, die sie ersetzt), installierte Programme von Rechner und Notebook (eigene Anwendungen eines Projekts als erfasste Bean, `beanCapture.ts`); `Character.isWhitespace` am JDK gemessen; gegen Java geprüft; seit der Abnahme F2 `ipForwardingEnabled` jedes Knotens mit IP-Stapel, mit Javas Standard der Klasse (Router und Heimrouter an) | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen; seit F2.5a in `packages/core/src/util/javaIp.ts` (der Kern liest damit RIP-Nachrichten) | @@ -148,6 +150,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/view.ts` (`nodeLabel`) | Java `Knoten.holeAnzeigeName` | herausgelöst (F4.5b): die Beschriftung eines Geräts, auch für „Verbunden mit …“ | | `src/view.ts` (`desktopView`) | Java `GUIDesktopWindow.updateTitle`, `GUIDesktopPanel.updateAppPane` | neu (F4.6a): was der Desktop eines Rechners vom Dokument braucht — Art, Beschriftung, ob sie die Adresse ist (Javas Titel lässt die Adresse dann weg), installierte Programme | | `src/view.ts` (`captureCards`, `cardAddress`) | Java `GUIEvents.kontextMenueAktionsmodus` (je Karte „Datenaustausch anzeigen (Adresse)“), `AggregatedExchangePanel.updateTabTitle` | neu (F4.7): die Karten eines Geräts in der Reihenfolge seiner Ports mit Adresse und dem Titel ihres Reiters — die Adresse, wo die Beschriftung sie ist, sonst Beschriftung, „ - “ und Adresse; Switch und Modem haben keine. `cardAddress` zog aus `sheet.ts` hierher | +| `src/documentation.ts`, `src/projectDoc.ts` (`italic`), `src/view.ts` (`italic`, Zeichenreihenfolge), `test/documentation.test.ts`, `test/documentation.java.json`, `test/fixtures/documentation.fls` | Java `JDocuElement`, `GUIDocumentationPanel`, `SzenarioVerwaltung.speichern/laden` (gemessen mit `DocuRun`; die Fixture hat Java gespeichert) | neu (F4.8a): Befehle der Dokumentation — neues Element mit Javas Voreinstellungen ans Ende der Liste (je Art zuunterst), Rahmen in ganzen Pixeln, Farbe, Schriftgröße und -stil (ein Stil ersetzt den vorigen; eine Fläche nimmt keine Schrift), Löschen, der `Y.Text` eines Textes; die Zeichensicht in Javas Stapelung. Gegen Java geprüft: neue Elemente, Farblisten, Schriftfolge, kleinste Größen, Stapel, eine von Java gespeicherte Datei gelesen, im Dokument gehalten und wieder geschrieben | | `src/software.ts`, `test/install.test.ts`, `test/fixtures/installed-programs.fls` | Java `GUIInstallationsDialog.aenderungenSpeichern` (gemessen mit `InstallRun`), `config/Desktop_*.txt` | neu (F4.6c): `setSoftware` — installieren (aus, Javas Voreinstellungen, fehlende Dateien; DNS-Einträge, E-Mail-Konten und das Konto des E-Mail-Programms aus vorhandenen Dateien) und entfernen (mit Einstellungen, Dateien bleiben) in einem Schritt, in der Reihenfolge von Javas Liste der Sprache (`SOFTWARE_ORDER`). Gegen Javas gespeicherte Installation geprüft; benannt: Javas Gnutella würfelt 3–5 Teilnehmer, hier 3 wie in einer Datei ohne Angabe | | `src/simulation.ts` | Plan §6.1 (`origin: 'sim'`); Java `SzenarioVerwaltung.speichern` nach einem Lauf (gemessen mit `SaveAfterRunRun`) | neu (F4.6f1): `writeRunState` schreibt, was ein Lauf von jedem Knoten hinterlässt, und nur, was sich unterscheidet — Dateien (neue hinter die anderen, geänderte an ihrem Platz, ein Text nur an der geänderten Stelle, verschwundene entfernt), ob Programme laufen, Einstellungen, die der Knoten hat (Feldreihenfolge egal); zweimal geschrieben ändert es nichts; `SIM_ORIGIN` landet in keinem Rückgängig-Stapel | | `src/files.ts`, `src/simulation.ts`, `test/files.test.ts` | Plan §7.3 (Texteditor auf `Y.Text`) | neu (F4.6f2): die Dateien eines Geräts im Dokument — als Dateisystem des Kerns gelesen (`nodeFiles`), über die Operationen des Kerns geändert und nur mit dem Geänderten zurückgeschrieben (`changeFiles`), der `Y.Text` einer Textdatei in jeder Schreibweise (`fileText`), und die Dateien des Dokuments in das Dateisystem einer laufenden Engine, nur was sich unterscheidet, nach ihrer Schreibweise, und nach einem Wechsel zwischen Datei und Ordner alles neu in der Reihenfolge des Dokuments (`readFilesInto`, duldsam wie `fileSystemOf`); `writeFiles` und `replaceText` aus `simulation.ts` hierher gezogen | diff --git a/packages/core/src/model/documentation.ts b/packages/core/src/model/documentation.ts index 51865eb..930c2bf 100644 --- a/packages/core/src/model/documentation.ts +++ b/packages/core/src/model/documentation.ts @@ -2,6 +2,15 @@ // ios/FiliusPad/TopologyEditor/Model/TopologyDocumentation.swift // Java counterpart: filius.gui.netzwerksicht.GUIDocuItem (RECT = 1, TEXT = 2). // Rectangles and text boxes drawn over the network. +// +// Java's rules where they differ from Swift's (measured with +// tools/java-oracle's DocuRun, F4.8a): a new item is 100 × 50, a text black +// in Dialog 12, a rectangle grey (204, 204, 204); an item keeps the size and +// font size its file gives it (Swift enlarges to 32 × 24 and keeps sizes +// within 8…72); a text may be italic (Java's style 2) as well as bold; an +// item without a colour shows light grey (Color.LIGHT_GRAY); a drag leaves +// a rectangle at least 20 × 20 and a text 88 × 20; texts lie over +// rectangles, and of one kind the earlier over the later. import { compareIds, type Id } from '../ids.js'; @@ -31,12 +40,21 @@ export interface DocumentationItem { fontName: string; fontSize: number; isBold: boolean; + isItalic: boolean; order: number; } -export const DOCUMENTATION_DEFAULT_TEXT_SIZE = { width: 220, height: 96 } as const; -export const DOCUMENTATION_DEFAULT_RECTANGLE_SIZE = { width: 240, height: 140 } as const; -export const DOCUMENTATION_MINIMUM_SIZE = { width: 32, height: 24 } as const; +/** A new item's size, either kind (JDocuElement's constructor). */ +export const DOCUMENTATION_NEW_SIZE = { width: 100, height: 50 } as const; + +/** + * The smallest size a drag leaves (JDocuElement: RECT_MIN_WIDTH/HEIGHT; a + * text's TEXT_MIN_WIDTH/HEIGHT hold for its frame enlarged by the focus, + * which is 12 wider and 28 higher than the text). + */ +export function documentationMinimumSize(kind: DocumentationItemKind): { readonly width: number; readonly height: number } { + return kind === 'rectangle' ? { width: 20, height: 20 } : { width: 88, height: 20 }; +} /** The editor's coordinate range; keeps Java `int` export and rendering safe. */ export const DOCUMENTATION_WORLD_BOUNDS: Rect = { x: -10_000, y: -10_000, width: 20_000, height: 20_000 }; @@ -49,8 +67,31 @@ export function documentationColor(red: number, green: number, blue: number, alp return { red: clampUnit(red), green: clampUnit(green), blue: clampUnit(blue), alpha: clampUnit(alpha) }; } -export const DOCUMENTATION_BLACK = documentationColor(0, 0, 0); -export const DOCUMENTATION_PALE_YELLOW = documentationColor(255 / 255, 255 / 255, 227 / 255, 0.82); +const byte = (red: number, green: number, blue: number) => documentationColor(red / 255, green / 255, blue / 255); + +export const DOCUMENTATION_BLACK = byte(0, 0, 0); +/** A new rectangle's colour (JDocuElement.panelColor[1]). */ +export const DOCUMENTATION_GREY = byte(204, 204, 204); +/** What Java shows for an item its file gives no colour (JDocuElement.setColor(null)). */ +export const DOCUMENTATION_LIGHT_GRAY = byte(192, 192, 192); + +/** The colours Java's menu offers a text, in its order, three rows (JDocuElement.fontColor). */ +export const DOCUMENTATION_TEXT_COLORS: readonly DocumentationColor[] = [ + [0, 0, 0], [102, 102, 102], [153, 153, 153], [179, 179, 179], [255, 255, 255], [255, 0, 0], [179, 163, 199], [79, 130, 189], + [156, 186, 89], [148, 138, 84], [148, 56, 54], [94, 74, 122], [31, 74, 125], [110, 135, 56], [89, 84, 48], +].map(([red, green, blue]) => byte(red!, green!, blue!)); + +/** The colours Java's menu offers a rectangle, in its order, three rows (JDocuElement.panelColor). */ +export const DOCUMENTATION_RECTANGLE_COLORS: readonly DocumentationColor[] = [ + [0, 0, 0], [204, 204, 204], [235, 235, 235], [255, 255, 255], [255, 201, 201], [252, 201, 163], [255, 255, 227], [212, 207, 181], + [222, 212, 255], [209, 232, 255], [191, 255, 219], [204, 222, 173], +].map(([red, green, blue]) => byte(red!, green!, blue!)); + +/** The font sizes Java's menu offers: „Normal“, „Groß“, „Sehr Groß“. */ +export const DOCUMENTATION_FONT_SIZES = [12, 18, 24] as const; + +/** The font styles Java's menu offers, one at a time: „Normal“, „Kursiv“, „Fett“. */ +export type DocumentationFontStyle = 'plain' | 'italic' | 'bold'; /** CGRect.standardized: a negative width or height moves the origin instead. */ function standardized(frame: Rect): Rect { @@ -59,14 +100,14 @@ function standardized(frame: Rect): Rect { return { x, y, width: Math.abs(frame.width), height: Math.abs(frame.height) }; } -/** At least the minimum size, at most the world, moved inside the world. */ +/** Its own size (at least one, at most the world), moved inside the world. */ export function normalizedFrame(frame: Rect): Rect { const world = DOCUMENTATION_WORLD_BOUNDS; - const fallback = { x: world.x, y: world.y, ...DOCUMENTATION_MINIMUM_SIZE }; + const fallback = { x: world.x, y: world.y, ...DOCUMENTATION_NEW_SIZE }; if (![frame.x, frame.y, frame.width, frame.height].every(Number.isFinite)) return fallback; const rect = standardized(frame); - const width = Math.min(Math.max(rect.width, DOCUMENTATION_MINIMUM_SIZE.width), world.width); - const height = Math.min(Math.max(rect.height, DOCUMENTATION_MINIMUM_SIZE.height), world.height); + const width = Math.min(Math.max(rect.width, 1), world.width); + const height = Math.min(Math.max(rect.height, 1), world.height); const x = Math.min(Math.max(rect.x, world.x), world.x + world.width - width); const y = Math.min(Math.max(rect.y, world.y), world.y + world.height - height); return { x, y, width, height }; @@ -85,12 +126,12 @@ export function isSafeFrame(frame: Rect): boolean { ); } +/** A font size as Java keeps it: any positive one; else Java's 12. */ export function normalizedFontSize(value: number): number { - if (!Number.isFinite(value)) return 14; - return Math.min(Math.max(value, 8), 72); + return Number.isFinite(value) && value > 0 ? value : 12; } -/** A documentation item as Swift's initializer normalizes it. */ +/** A documentation item as Swift's initializer normalizes it, with Java's sizes and font. */ export function documentationItem(fields: { id: Id; kind: DocumentationItemKind; @@ -100,23 +141,40 @@ export function documentationItem(fields: { fontName?: string; fontSize?: number; isBold?: boolean; + isItalic?: boolean; order: number; }): DocumentationItem { - const fontName = (fields.fontName ?? 'System').trim(); + const fontName = (fields.fontName ?? 'Dialog').trim(); return { id: fields.id, kind: fields.kind, frame: normalizedFrame(fields.frame), text: fields.kind === 'text' ? (fields.text ?? '') : '', color: fields.color, - fontName: fontName === '' ? 'System' : fontName, - fontSize: normalizedFontSize(fields.fontSize ?? 14), + fontName: fontName === '' ? 'Dialog' : fontName, + fontSize: normalizedFontSize(fields.fontSize ?? 12), isBold: fields.isBold ?? false, + isItalic: fields.isItalic ?? false, order: Math.max(0, fields.order), }; } -/** Rectangles below text, then by order, then by id. */ +/** Java's style of a font (Font.PLAIN 0, BOLD 1, ITALIC 2, both 3). */ +export function documentationFontStyle(item: Pick): number { + return (item.isBold ? 1 : 0) | (item.isItalic ? 2 : 0); +} + +/** + * The order to draw them in, the last on top: as Java's panel stacks them + * (GUIDocumentationPanel.updateViewport, the first component on top) — + * texts over rectangles, of one kind the earlier over the later. + */ +export function inJavaDrawingOrder>(items: readonly T[]): T[] { + const layer = (item: T): number => (item.kind === 'rectangle' ? 0 : 1); + return [...items].sort((lhs, rhs) => layer(lhs) - layer(rhs) || rhs.order - lhs.order || compareIds(rhs.id, lhs.id)); +} + +/** Rectangles first, then text, each by order, then by id: the list of the file. */ export function inDeterministicRenderOrder(items: readonly DocumentationItem[]): DocumentationItem[] { const layer = (item: DocumentationItem): number => (item.kind === 'rectangle' ? 0 : 1); return [...items].sort( @@ -148,3 +206,36 @@ export function deterministicDocumentationUuid(seed: string): Id { const hex = high.toString(16).padStart(16, '0') + low.toString(16).padStart(16, '0'); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`.toUpperCase(); } + +/** The edges a drag on an item's border moves (JDocuElement: LEFT_SIZING, RIGHT_SIZING, UPPER_SIZING, LOWER_SIZING). */ +export interface DocumentationEdges { + readonly left?: boolean; + readonly right?: boolean; + readonly top?: boolean; + readonly bottom?: boolean; +} + +/** + * JDocuElement's drag on its border: a moved edge follows the pointer, the + * opposite one stays; below the kind's smallest size the moved edge stops + * there. `pointer` is where the pointer is, in the drawing's coordinates. + */ +export function resizedFrame(frame: Rect, kind: DocumentationItemKind, edges: DocumentationEdges, pointer: { readonly x: number; readonly y: number }): Rect { + const minimum = documentationMinimumSize(kind); + let { x, y, width, height } = frame; + if (edges.left === true) { + const right = frame.x + frame.width; + width = Math.max(right - pointer.x, minimum.width); + x = right - width; + } else if (edges.right === true) { + width = Math.max(pointer.x - frame.x, minimum.width); + } + if (edges.top === true) { + const bottom = frame.y + frame.height; + height = Math.max(bottom - pointer.y, minimum.height); + y = bottom - height; + } else if (edges.bottom === true) { + height = Math.max(pointer.y - frame.y, minimum.height); + } + return { x, y, width, height }; +} diff --git a/packages/doc/src/documentation.ts b/packages/doc/src/documentation.ts new file mode 100644 index 0000000..8bc0bb9 --- /dev/null +++ b/packages/doc/src/documentation.ts @@ -0,0 +1,94 @@ +// The documentation items of a document, changed as Java's documentation mode +// changes them (JDocuElement, measured with tools/java-oracle's DocuRun): a +// new text or rectangle where it is dropped, moved and resized in whole +// pixels, its colour, its font size and style, removed. A text's words are +// its Y.Text, typed into by everyone at once. + +import * as Y from 'yjs'; +import { + DOCUMENTATION_BLACK, + DOCUMENTATION_GREY, + DOCUMENTATION_NEW_SIZE, + normalizedFrame, + type DocumentationColor, + type DocumentationFontStyle, + type DocumentationItemKind, + type Id, + type Rect, +} from '@mdg-filius/core'; +import { ROOTS } from './projectDoc.js'; + +const docs = (doc: Y.Doc) => doc.getMap>(ROOTS.docs); + +/** + * A new item, its top left corner at (x, y): 100 × 50, a text empty and black + * in Dialog 12, a rectangle grey; after all others in the list, so of its kind + * the lowest, as Java adds it. + */ +export function addDocumentation(doc: Y.Doc, item: { readonly id: Id; readonly kind: DocumentationItemKind; readonly x: number; readonly y: number }, origin: unknown): void { + doc.transact(() => { + const order = Math.max(-1, ...[...docs(doc).values()].map((each) => each.get('order') as number)) + 1; + const color = item.kind === 'text' ? DOCUMENTATION_BLACK : DOCUMENTATION_GREY; + const map = new Y.Map(); + map.set('kind', item.kind); + map.set('x', Math.round(item.x)); + map.set('y', Math.round(item.y)); + map.set('width', DOCUMENTATION_NEW_SIZE.width); + map.set('height', DOCUMENTATION_NEW_SIZE.height); + map.set('text', new Y.Text('')); + map.set('red', color.red); + map.set('green', color.green); + map.set('blue', color.blue); + map.set('alpha', color.alpha); + map.set('font', 'Dialog'); + map.set('size', 12); + map.set('bold', false); + map.set('italic', false); + map.set('order', order); + docs(doc).set(item.id, map); + }, origin); +} + +/** Its place and size, in whole pixels (Java's bounds are ints), inside the editor's world. */ +export function setDocumentationFrame(doc: Y.Doc, id: Id, frame: Rect, origin: unknown): void { + const map = docs(doc).get(id); + if (map === undefined) return; + const normalized = normalizedFrame({ x: Math.round(frame.x), y: Math.round(frame.y), width: Math.round(frame.width), height: Math.round(frame.height) }); + doc.transact(() => { + for (const key of ['x', 'y', 'width', 'height'] as const) if (map.get(key) !== normalized[key]) map.set(key, normalized[key]); + }, origin); +} + +export function setDocumentationColor(doc: Y.Doc, id: Id, color: DocumentationColor, origin: unknown): void { + const map = docs(doc).get(id); + if (map === undefined) return; + doc.transact(() => { + map.set('red', color.red); + map.set('green', color.green); + map.set('blue', color.blue); + map.set('alpha', color.alpha); + }, origin); +} + +/** A text's font size, or its style, which replaces the one it had (Java's deriveFont: italic, then bold, is bold). */ +export function setDocumentationFont(doc: Y.Doc, id: Id, font: { readonly size?: number; readonly style?: DocumentationFontStyle }, origin: unknown): void { + const map = docs(doc).get(id); + if (map === undefined || map.get('kind') !== 'text') return; + doc.transact(() => { + if (font.size !== undefined) map.set('size', font.size); + if (font.style !== undefined) { + map.set('bold', font.style === 'bold'); + map.set('italic', font.style === 'italic'); + } + }, origin); +} + +export function removeDocumentation(doc: Y.Doc, id: Id, origin: unknown): void { + doc.transact(() => docs(doc).delete(id), origin); +} + +/** A text's words, to type into; undefined for a rectangle or no item. */ +export function documentationText(doc: Y.Doc, id: Id): Y.Text | undefined { + const map = docs(doc).get(id); + return map?.get('kind') === 'text' ? (map.get('text') as Y.Text) : undefined; +} diff --git a/packages/doc/src/index.ts b/packages/doc/src/index.ts index 7807302..1bda9d6 100644 --- a/packages/doc/src/index.ts +++ b/packages/doc/src/index.ts @@ -1,5 +1,6 @@ // Public surface of the shared document. export * from './commands.js'; +export * from './documentation.js'; export * from './fields.js'; export * from './files.js'; export * from './janitor.js'; diff --git a/packages/doc/src/projectDoc.ts b/packages/doc/src/projectDoc.ts index d844e28..6f1c5ab 100644 --- a/packages/doc/src/projectDoc.ts +++ b/packages/doc/src/projectDoc.ts @@ -13,7 +13,7 @@ // own? } // links id → { order, sourceNode, sourcePort, targetNode, targetPort } // docs id → { kind, x, y, width, height, text, red, green, blue, alpha, -// font, size, bold, order } +// font, size, bold, italic, order } // archive name → bytes (the project's own Java applications) // // Departures from §5.2, to keep the model whole: a port's settings are the @@ -190,6 +190,7 @@ function documentationMap(item: DocumentationItem): Y.Map { map.set('font', item.fontName); map.set('size', item.fontSize); map.set('bold', item.isBold); + map.set('italic', item.isItalic); map.set('order', item.order); return map; } @@ -336,6 +337,8 @@ export function readProject(doc: Y.Doc): ImportedProject { fontName: map.get('font') as string, fontSize: map.get('size') as number, isBold: map.get('bold') as boolean, + // A document made before F4.8a has no italic. + isItalic: map.get('italic') === true, order: map.get('order') as number, })), ); diff --git a/packages/doc/src/view.ts b/packages/doc/src/view.ts index 2dedb05..dde67fa 100644 --- a/packages/doc/src/view.ts +++ b/packages/doc/src/view.ts @@ -4,7 +4,7 @@ // so that it can run on every change while someone drags a device. import * as Y from 'yjs'; -import { compareIds, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core'; +import { inJavaDrawingOrder, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core'; import { inOrder, ROOTS } from './projectDoc.js'; export interface NodeView { @@ -44,6 +44,7 @@ export interface DocumentationView { readonly font: string; readonly size: number; readonly bold: boolean; + readonly italic: boolean; readonly order: number; } @@ -153,12 +154,11 @@ export function topologyView(doc: Y.Doc): TopologyView { font: item.get('font') as string, size: item.get('size') as number, bold: item.get('bold') as boolean, + italic: item.get('italic') === true, order: item.get('order') as number, })); - // The model's render order (inDeterministicRenderOrder): rectangles below texts, then by order, then by id. - const layer = (item: DocumentationView) => (item.kind === 'rectangle' ? 0 : 1); - docs.sort((left, right) => layer(left) - layer(right) || left.order - right.order || compareIds(left.id, right.id)); - return { nodes, links, docs }; + // In Java's drawing order, the last on top: texts over rectangles, of one kind the earlier over the later. + return { nodes, links, docs: inJavaDrawingOrder(docs) }; } /** What a computer's desktop needs of the document (Java's GUIDesktopWindow, GUIDesktopPanel). */ diff --git a/packages/doc/test/documentation.java.json b/packages/doc/test/documentation.java.json new file mode 100644 index 0000000..59042ad --- /dev/null +++ b/packages/doc/test/documentation.java.json @@ -0,0 +1,894 @@ +{ + "new": { + "text": { + "bounds": [ + 100, + 100, + 100, + 50 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": { + "name": "Dialog", + "style": 0, + "size": 12 + }, + "text": "" + }, + "rectangle": { + "bounds": [ + 300, + 100, + 100, + 50 + ], + "color": [ + 204, + 204, + 204, + 255 + ], + "font": null, + "text": null + } + }, + "dropped": { + "text": { + "bounds": [ + 94, + 94, + 112, + 78 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": { + "name": "Dialog", + "style": 0, + "size": 12 + }, + "text": "" + }, + "textUnfocused": { + "bounds": [ + 100, + 100, + 100, + 50 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": { + "name": "Dialog", + "style": 0, + "size": 12 + }, + "text": "" + } + }, + "fonts": [ + { + "name": "Dialog", + "style": 0, + "size": 18 + }, + { + "name": "Dialog", + "style": 2, + "size": 18 + }, + { + "name": "Dialog", + "style": 1, + "size": 18 + }, + { + "name": "Dialog", + "style": 1, + "size": 12 + }, + { + "name": "Dialog", + "style": 1, + "size": 24 + } + ], + "textColor": [ + 79, + 130, + 189, + 255 + ], + "rectangleColor": [ + 255, + 201, + 201, + 255 + ], + "menus": { + "text": [ + { + "menu": "Schriftgröße", + "items": [ + "Normal *", + "Groß", + "Sehr Groß" + ] + }, + { + "menu": "Schriftstil", + "items": [ + "Normal *", + "Kursiv", + "Fett" + ] + }, + { + "colors": "Farbe", + "list": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 102, + 102, + 102, + 255 + ], + [ + 153, + 153, + 153, + 255 + ], + [ + 179, + 179, + 179, + 255 + ], + [ + 255, + 255, + 255, + 255 + ], + [ + 255, + 0, + 0, + 255 + ], + [ + 179, + 163, + 199, + 255 + ], + [ + 79, + 130, + 189, + 255 + ], + [ + 156, + 186, + 89, + 255 + ], + [ + 148, + 138, + 84, + 255 + ], + [ + 148, + 56, + 54, + 255 + ], + [ + 94, + 74, + 122, + 255 + ], + [ + 31, + 74, + 125, + 255 + ], + [ + 110, + 135, + 56, + 255 + ], + [ + 89, + 84, + 48, + 255 + ] + ], + "chosen": [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "rows": 3 + }, + { + "separator": true + }, + { + "item": "Löschen" + } + ], + "textAfterLarge": [ + { + "menu": "Schriftgröße", + "items": [ + "Normal", + "Groß *", + "Sehr Groß" + ] + }, + { + "menu": "Schriftstil", + "items": [ + "Normal *", + "Kursiv", + "Fett" + ] + }, + { + "colors": "Farbe", + "list": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 102, + 102, + 102, + 255 + ], + [ + 153, + 153, + 153, + 255 + ], + [ + 179, + 179, + 179, + 255 + ], + [ + 255, + 255, + 255, + 255 + ], + [ + 255, + 0, + 0, + 255 + ], + [ + 179, + 163, + 199, + 255 + ], + [ + 79, + 130, + 189, + 255 + ], + [ + 156, + 186, + 89, + 255 + ], + [ + 148, + 138, + 84, + 255 + ], + [ + 148, + 56, + 54, + 255 + ], + [ + 94, + 74, + 122, + 255 + ], + [ + 31, + 74, + 125, + 255 + ], + [ + 110, + 135, + 56, + 255 + ], + [ + 89, + 84, + 48, + 255 + ] + ], + "chosen": [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "rows": 3 + }, + { + "separator": true + }, + { + "item": "Löschen" + } + ], + "rectangle": [ + { + "colors": "Farbe", + "list": [ + [ + 0, + 0, + 0, + 255 + ], + [ + 204, + 204, + 204, + 255 + ], + [ + 235, + 235, + 235, + 255 + ], + [ + 255, + 255, + 255, + 255 + ], + [ + 255, + 201, + 201, + 255 + ], + [ + 252, + 201, + 163, + 255 + ], + [ + 255, + 255, + 227, + 255 + ], + [ + 212, + 207, + 181, + 255 + ], + [ + 222, + 212, + 255, + 255 + ], + [ + 209, + 232, + 255, + 255 + ], + [ + 191, + 255, + 219, + 255 + ], + [ + 204, + 222, + 173, + 255 + ] + ], + "chosen": [ + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "rows": 3 + }, + { + "separator": true + }, + { + "item": "Löschen" + } + ] + }, + "smallest": { + "rectangle": { + "bounds": [ + 300, + 100, + 20, + 20 + ], + "color": [ + 255, + 201, + 201, + 255 + ], + "font": null, + "text": null + }, + "rectangleByItsTopLeft": { + "bounds": [ + 300, + 100, + 20, + 20 + ], + "color": [ + 255, + 201, + 201, + 255 + ], + "font": null, + "text": null + }, + "textFocused": { + "bounds": [ + 94, + 94, + 112, + 78 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 12 + }, + "text": "" + }, + "textFocusedSmallest": { + "bounds": [ + 94, + 94, + 100, + 48 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 12 + }, + "text": "" + }, + "textSmallest": { + "bounds": [ + 100, + 100, + 88, + 20 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 12 + }, + "text": "" + }, + "textDraggedUnfocused": { + "bounds": [ + 100, + 100, + 88, + 20 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 12 + }, + "text": "" + } + }, + "painted": { + "rectangleEnabled": { + "50,25": [ + 255, + 201, + 201, + 200 + ], + "1,25": [ + 204, + 160, + 160, + 255 + ], + "2,25": [ + 255, + 201, + 201, + 200 + ], + "0,25": [ + 0, + 0, + 0, + 0 + ] + }, + "rectangleFocused": { + "50,25": [ + 255, + 201, + 201, + 200 + ], + "3,25": [ + 56, + 152, + 248, + 77 + ], + "1,25": [ + 122, + 186, + 250, + 255 + ] + }, + "rectangleDisabled": { + "50,25": [ + 255, + 212, + 212, + 255 + ], + "1,25": [ + 204, + 160, + 160, + 255 + ], + "2,25": [ + 255, + 212, + 212, + 255 + ], + "0,25": [ + 0, + 0, + 0, + 0 + ] + }, + "textEnabled": { + "0,25": [ + 179, + 179, + 179, + 255 + ], + "1,25": [ + 0, + 0, + 0, + 0 + ], + "50,0": [ + 179, + 179, + 179, + 255 + ], + "99,25": [ + 179, + 179, + 179, + 255 + ] + }, + "textDisabled": { + "0,25": [ + 0, + 0, + 0, + 0 + ], + "1,25": [ + 0, + 0, + 0, + 0 + ], + "50,0": [ + 0, + 0, + 0, + 0 + ] + } + }, + "stacking": [ + "text 1", + "text 2", + "rectangle 1", + "rectangle 2" + ], + "topAt": "text 1", + "saved": [ + { + "type": 2, + "text": "Kursiv und mittel\nzweite Zeile", + "bounds": [ + 40, + 40, + 160, + 60 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 2, + "size": 18 + } + }, + { + "type": 2, + "text": "Fett", + "bounds": [ + 40, + 140, + 120, + 50 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 24 + } + }, + { + "type": 1, + "text": null, + "bounds": [ + 20, + 20, + 300, + 200 + ], + "color": [ + 255, + 201, + 201, + 255 + ], + "font": null + }, + { + "type": 1, + "text": null, + "bounds": [ + 400, + 20, + 100, + 50 + ], + "color": [ + 204, + 204, + 204, + 255 + ], + "font": null + } + ], + "loaded": [ + { + "type": 2, + "text": "Kursiv und mittel\nzweite Zeile", + "bounds": [ + 40, + 40, + 160, + 60 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": { + "name": "Dialog", + "style": 2, + "size": 18 + } + }, + { + "type": 2, + "text": "Fett", + "bounds": [ + 40, + 140, + 120, + 50 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": { + "name": "Dialog", + "style": 1, + "size": 24 + } + }, + { + "type": 1, + "text": null, + "bounds": [ + 20, + 20, + 300, + 200 + ], + "color": [ + 255, + 201, + 201, + 255 + ], + "font": null + }, + { + "type": 1, + "text": null, + "bounds": [ + 400, + 20, + 100, + 50 + ], + "color": [ + 204, + 204, + 204, + 255 + ], + "font": null + } + ] +} diff --git a/packages/doc/test/documentation.test.ts b/packages/doc/test/documentation.test.ts new file mode 100644 index 0000000..1f6478f --- /dev/null +++ b/packages/doc/test/documentation.test.ts @@ -0,0 +1,208 @@ +// The documentation items against Java's documentation mode +// (documentation.java.json; tools/java-oracle's DocuRun on JDocuElement): +// a new text and rectangle, the colours and font sizes Java's menus offer, +// the font and colour chosen there, the smallest sizes a drag leaves, the +// stacking, and a project Java saved with documentation (fixtures/ +// documentation.fls) read, kept in the document and written again. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import * as Y from 'yjs'; +import { + DOCUMENTATION_FONT_SIZES, + DOCUMENTATION_RECTANGLE_COLORS, + DOCUMENTATION_TEXT_COLORS, + documentationFontStyle, + documentationItem, + inJavaDrawingOrder, + resizedFrame, + type DocumentationColor, + type DocumentationItem, +} from '@mdg-filius/core'; +import { exportProject, openProjectArchive } from '@mdg-filius/fls'; +import { addDocumentation, documentationText, readProject, removeDocumentation, setDocumentationColor, setDocumentationFont, setDocumentationFrame, topologyView, writeProject } from '../src/index.js'; + +type Rgba = [number, number, number, number]; +interface JavaFont { + readonly name: string; + readonly style: number; + readonly size: number; +} +interface JavaElement { + readonly bounds: [number, number, number, number]; + readonly color: Rgba; + readonly font: JavaFont | null; + readonly text: string | null; +} +interface JavaItem extends JavaElement { + readonly type: number; +} +interface JavaMenuEntry { + readonly menu?: string; + readonly items?: string[]; + readonly colors?: string; + readonly list?: Rgba[]; + readonly rows?: number; + readonly item?: string; + readonly separator?: boolean; +} + +const here = dirname(fileURLToPath(import.meta.url)); +const java = JSON.parse(readFileSync(join(here, 'documentation.java.json'), 'utf8')) as { + readonly new: { readonly text: JavaElement; readonly rectangle: JavaElement }; + readonly fonts: JavaFont[]; + readonly textColor: Rgba; + readonly rectangleColor: Rgba; + readonly menus: { readonly text: JavaMenuEntry[]; readonly rectangle: JavaMenuEntry[] }; + readonly smallest: Record; + readonly stacking: string[]; + readonly topAt: string; + readonly saved: JavaItem[]; + readonly loaded: JavaItem[]; +}; + +const channel = (value: number) => Math.round(value * 255); +const rgba = (color: DocumentationColor): Rgba => [channel(color.red), channel(color.green), channel(color.blue), channel(color.alpha)]; +/** An item as Java's GUIDocuItem shows it: its bounds, colour and font (none for a rectangle). */ +const asJava = (item: DocumentationItem) => ({ + bounds: [item.frame.x, item.frame.y, item.frame.width, item.frame.height], + color: rgba(item.color), + font: item.kind === 'text' ? { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize } : null, + text: item.kind === 'text' ? item.text : null, +}); +const itemOf = (doc: Y.Doc, id: string) => readProject(doc).documentationItems.find((item) => item.id === id)!; + +describe("the documentation items, against Java's documentation mode", () => { + it('makes a new text and rectangle as Java does: 100 × 50 where dropped, a text empty and black in Dialog 12, a rectangle grey', () => { + const doc = new Y.Doc(); + addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me'); + addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300.4, y: 99.6 }, 'me'); + expect(asJava(itemOf(doc, 'text'))).toEqual(java.new.text); + expect(asJava(itemOf(doc, 'rectangle'))).toEqual(java.new.rectangle); + // After the others: of its kind the lowest. + expect([itemOf(doc, 'text').order, itemOf(doc, 'rectangle').order]).toEqual([0, 1]); + // An item made without a font has Java's: Dialog 12, plain. + expect(documentationItem({ id: 'x', kind: 'text', frame: { x: 0, y: 0, width: 5, height: 5 }, color: DOCUMENTATION_TEXT_COLORS[0]!, order: 0 })).toMatchObject({ + fontName: 'Dialog', + fontSize: 12, + isBold: false, + isItalic: false, + frame: { x: 0, y: 0, width: 5, height: 5 }, + }); + }); + + it("offers Java's colours and font sizes, and sets font and colour as Java's menu does", () => { + const colors = (entries: JavaMenuEntry[]) => entries.find((entry) => entry.list !== undefined)!; + expect(DOCUMENTATION_TEXT_COLORS.map(rgba)).toEqual(colors(java.menus.text).list); + expect(DOCUMENTATION_RECTANGLE_COLORS.map(rgba)).toEqual(colors(java.menus.rectangle).list); + expect([colors(java.menus.text).rows, colors(java.menus.rectangle).rows]).toEqual([3, 3]); + expect(java.menus.text.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Schriftgröße', 'Schriftstil', 'Farbe', '-', 'Löschen']); + expect(java.menus.rectangle.map((entry) => entry.menu ?? entry.colors ?? entry.item ?? '-')).toEqual(['Farbe', '-', 'Löschen']); + + // „Groß“, „Kursiv“, „Fett“, „Normal“, „Sehr Groß“: a style replaces the one before. + const doc = new Y.Doc(); + addDocumentation(doc, { id: 'text', kind: 'text', x: 100, y: 100 }, 'me'); + const font = () => asJava(itemOf(doc, 'text')).font; + const fonts = []; + for (const change of [{ size: DOCUMENTATION_FONT_SIZES[1] }, { style: 'italic' }, { style: 'bold' }, { size: DOCUMENTATION_FONT_SIZES[0] }, { size: DOCUMENTATION_FONT_SIZES[2] }] as const) { + setDocumentationFont(doc, 'text', change, 'me'); + fonts.push(font()); + } + expect(fonts).toEqual(java.fonts); + // The drawing knows the style too. + setDocumentationFont(doc, 'text', { style: 'italic' }, 'me'); + expect(topologyView(doc).docs.map((item) => [item.bold, item.italic])).toEqual([[false, true]]); + setDocumentationFont(doc, 'text', { style: 'plain' }, 'me'); + expect(font()?.style).toBe(0); + setDocumentationColor(doc, 'text', DOCUMENTATION_TEXT_COLORS[7]!, 'me'); + expect(rgba(itemOf(doc, 'text').color)).toEqual(java.textColor); + + // A rectangle takes a colour, not a font. + addDocumentation(doc, { id: 'rectangle', kind: 'rectangle', x: 300, y: 100 }, 'me'); + setDocumentationFont(doc, 'rectangle', { size: 24, style: 'bold' }, 'me'); + setDocumentationColor(doc, 'rectangle', DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me'); + expect(asJava(itemOf(doc, 'rectangle'))).toMatchObject({ color: java.rectangleColor, font: null }); + expect([itemOf(doc, 'rectangle').fontSize, itemOf(doc, 'rectangle').isBold]).toEqual([12, false]); + }); + + it('leaves a rectangle at least 20 × 20 and a text at least 88 × 20, the opposite edge where it was', () => { + const rectangle = { x: 300, y: 100, width: 100, height: 50 }; + const [x, y, width, height] = java.smallest.rectangle!.bounds; + expect(resizedFrame(rectangle, 'rectangle', { right: true, bottom: true }, { x: 303, y: 103 })).toEqual({ x, y, width, height }); + // By its top left corner, far past the other: the right and lower edges stay. + const small = { x: 300, y: 100, width: 20, height: 20 }; + const [ax, ay, aw, ah] = java.smallest.rectangleByItsTopLeft!.bounds; + expect(resizedFrame(small, 'rectangle', { left: true, top: true }, { x: 500, y: 300 })).toEqual({ x: ax, y: ay, width: aw, height: ah }); + const [tx, ty, tw, th] = java.smallest.textSmallest!.bounds; + expect(resizedFrame({ x: 100, y: 100, width: 100, height: 50 }, 'text', { right: true, bottom: true }, { x: 0, y: 0 })).toEqual({ x: tx, y: ty, width: tw, height: th }); + // Edges follow the pointer; the others stay. + expect(resizedFrame(rectangle, 'rectangle', { left: true }, { x: 250, y: 0 })).toEqual({ x: 250, y: 100, width: 150, height: 50 }); + expect(resizedFrame(rectangle, 'rectangle', { top: true }, { x: 0, y: 130 })).toEqual({ x: 300, y: 130, width: 100, height: 20 }); + expect(resizedFrame(rectangle, 'rectangle', { bottom: true }, { x: 999, y: 300 })).toEqual({ x: 300, y: 100, width: 100, height: 200 }); + expect(resizedFrame(rectangle, 'rectangle', {}, { x: 0, y: 0 })).toEqual(rectangle); + + // The document keeps whole pixels. + const doc = new Y.Doc(); + addDocumentation(doc, { id: 'r', kind: 'rectangle', x: 0, y: 0 }, 'me'); + setDocumentationFrame(doc, 'r', { x: 10.4, y: 20.6, width: 30.5, height: 40.2 }, 'me'); + expect(itemOf(doc, 'r').frame).toEqual({ x: 10, y: 21, width: 31, height: 40 }); + }); + + it('stacks as Java does: texts over rectangles, of one kind the earlier over the later', () => { + const doc = new Y.Doc(); + const names = ['text 1', 'rectangle 1', 'text 2', 'rectangle 2']; + names.forEach((name, index) => addDocumentation(doc, { id: name, kind: name.startsWith('text') ? 'text' : 'rectangle', x: 50 + 10 * index, y: 50 + 10 * index }, 'me')); + // Drawn in this order, the last on top; Java names the top one first. + const drawn = topologyView(doc).docs.map((item) => item.id); + expect([...drawn].reverse()).toEqual(java.stacking); + expect(drawn.at(-1)).toBe(java.topAt); + expect(inJavaDrawingOrder(readProject(doc).documentationItems).map((item) => item.id)).toEqual(drawn); + }); + + it('reads what Java saved, keeps it in the document, and writes it as Java does', () => { + let counter = 0; + const project = openProjectArchive(new Uint8Array(readFileSync(join(here, 'fixtures/documentation.fls'))), () => `id-${++counter}`); + expect(java.loaded).toEqual(java.saved); + const byFile = (items: readonly DocumentationItem[]) => [...items].sort((lhs, rhs) => lhs.order - rhs.order); + const javaItems = java.loaded.map(({ type, ...rest }) => ({ ...rest, type })); + expect(byFile(project.documentationItems).map((item) => ({ ...asJava(item), type: item.kind === 'rectangle' ? 1 : 2 }))).toEqual(javaItems); + + // Through the document and back: italic and all. + const doc = new Y.Doc(); + writeProject(doc, project); + expect(readProject(doc).documentationItems).toEqual(project.documentationItems); + expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'text')!.id)).toBeInstanceOf(Y.Text); + expect(documentationText(doc, project.documentationItems.find((item) => item.kind === 'rectangle')!.id)).toBeUndefined(); + + // Written as Java writes it (rectangles first in the list, which leaves Java's stacking of each kind as it was). + const written = exportProject(readProject(doc)).topology.documentation.map((item) => ({ + ...item, + color: item.color === null ? null : [...item.color], + font: item.font === null ? null : { ...item.font }, + })); + const expected = [...java.saved.filter((item) => item.type === 1), ...java.saved.filter((item) => item.type === 2)].map((item) => ({ + type: item.type, + text: item.text ?? '', + x: item.bounds[0], + y: item.bounds[1], + width: item.bounds[2], + height: item.bounds[3], + color: item.color, + // A rectangle has no font in Java; the model gives every item one. + font: item.font ?? { name: 'Dialog', style: 0, size: 12 }, + })); + expect(written).toEqual(expected); + + // A colour of Java's menu is opaque, also over a translucent one from a file. + const translucent = project.documentationItems[0]!; + setDocumentationColor(doc, translucent.id, { ...translucent.color, alpha: 0.5 }, 'me'); + expect(itemOf(doc, translucent.id).color.alpha).toBe(0.5); + setDocumentationColor(doc, translucent.id, DOCUMENTATION_RECTANGLE_COLORS[4]!, 'me'); + expect(rgba(itemOf(doc, translucent.id).color)).toEqual([255, 201, 201, 255]); + + removeDocumentation(doc, project.documentationItems[0]!.id, 'me'); + expect(readProject(doc).documentationItems).toHaveLength(3); + }); +}); diff --git a/packages/doc/test/fixtures/documentation.fls b/packages/doc/test/fixtures/documentation.fls new file mode 100644 index 0000000..7f0dc85 Binary files /dev/null and b/packages/doc/test/fixtures/documentation.fls differ diff --git a/packages/doc/test/view.test.ts b/packages/doc/test/view.test.ts index 9d83dc3..6c5f993 100644 --- a/packages/doc/test/view.test.ts +++ b/packages/doc/test/view.test.ts @@ -8,7 +8,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import * as Y from 'yjs'; -import { stableGeneratedMacAddress } from '@mdg-filius/core'; +import { inJavaDrawingOrder, stableGeneratedMacAddress } from '@mdg-filius/core'; import { exportProject, openProjectArchive, type ImportedProject } from '@mdg-filius/fls'; import { addDevice, captureCards, desktopView, readProject, renameDevice, setHostAddress, setHostLabels, setInterfaceAddress, topologyView, writeProject } from '../src/index.js'; @@ -42,7 +42,21 @@ describe('the drawing of a document', () => { project.graph.links.map((link) => [link.id, link.sourceNodeId, link.sourcePortId, link.targetNodeId, link.targetPortId]), ); expect(view.links.map((link) => link.wireless)).toEqual(exportProject(project).topology.links.map((link) => link.wireless)); - expect(view.docs.map(({ id, kind, x, y, width, height, text, color, font, size, bold, order }) => ({ id, kind, frame: { x, y, width, height }, text, color, fontName: font, fontSize: size, isBold: bold, order }))).toEqual(project.documentationItems); + // In Java's drawing order, the last on top. + expect( + view.docs.map(({ id, kind, x, y, width, height, text, color, font, size, bold, italic, order }) => ({ + id, + kind, + frame: { x, y, width, height }, + text, + color, + fontName: font, + fontSize: size, + isBold: bold, + isItalic: italic, + order, + })), + ).toEqual(inJavaDrawingOrder(project.documentationItems)); }); } diff --git a/packages/fls/src/exportProject.ts b/packages/fls/src/exportProject.ts index 141b86c..91d043c 100644 --- a/packages/fls/src/exportProject.ts +++ b/packages/fls/src/exportProject.ts @@ -57,8 +57,8 @@ // interface, as Java's own setters (setStandardGateway, setDNSServer) do: // Java reads them from the first interface only (getStandardGateway, // getDNSServer), and the model keeps no others; -// - a documentation item always has a text and a font, bold or plain: where -// Java has none, the empty text and Dialog 12 the model gives it; +// - a documentation item always has a text, a font and a colour: where +// Java has none, the empty text, Dialog 12 and the light grey Java shows; // - a switch's SSID the file lacked (Java rolls one) is the one the model // derives from the node's id; // - files in the model's order (directories first, then by name) and of the @@ -105,6 +105,7 @@ import { type ManualRoute, type RemoteLinkConfiguration, type SwitchConfiguration, + documentationFontStyle, type DocumentationItem, type HostLabelPolicy, type HostWirelessConfiguration, @@ -554,6 +555,6 @@ function exportDocumentationItem(item: DocumentationItem): FlsDocumentationItem width: Math.round(item.frame.width), height: Math.round(item.frame.height), color: [channel(item.color.red), channel(item.color.green), channel(item.color.blue), channel(item.color.alpha)], - font: { name: item.fontName, style: item.isBold ? 1 : 0, size: item.fontSize }, + font: { name: item.fontName, style: documentationFontStyle(item), size: item.fontSize }, }; } diff --git a/packages/fls/src/importProject.ts b/packages/fls/src/importProject.ts index dc9bf23..ab4434b 100644 --- a/packages/fls/src/importProject.ts +++ b/packages/fls/src/importProject.ts @@ -65,8 +65,7 @@ import { VirtualFileSystem, VirtualFileSystemError, topologyNode, - DOCUMENTATION_BLACK, - DOCUMENTATION_PALE_YELLOW, + DOCUMENTATION_LIGHT_GRAY, type DeviceConfiguration, type DhcpClientConfiguration, type DhcpServerConfiguration, @@ -770,7 +769,8 @@ function importDocumentationItem( const seed = `java-doc|${order}|${item.type}|${item.x}|${item.y}|${item.width}|${item.height}|${text}`; let color; if (item.color === null) { - color = kind === 'text' ? DOCUMENTATION_BLACK : DOCUMENTATION_PALE_YELLOW; + // Java shows it light grey, either kind (JDocuElement.setColor(null)). + color = DOCUMENTATION_LIGHT_GRAY; warnings.push(`documentation item ${order + 1} without a color, given the default`); } else { const [red, green, blue, alpha] = item.color; @@ -785,6 +785,7 @@ function importDocumentationItem( fontName: item.font?.name ?? 'Dialog', fontSize: item.font?.size ?? 12, isBold: ((item.font?.style ?? 0) & 1) === 1, + isItalic: ((item.font?.style ?? 0) & 2) === 2, order, }); } diff --git a/packages/fls/test/exportProject.test.ts b/packages/fls/test/exportProject.test.ts index dd81e94..0f86782 100644 --- a/packages/fls/test/exportProject.test.ts +++ b/packages/fls/test/exportProject.test.ts @@ -14,7 +14,6 @@ import { fileURLToPath } from 'node:url'; import { defaultSwitchConfiguration, dhcpServerConfiguration, - DOCUMENTATION_MINIMUM_SIZE, firewallConfiguration, firewallRule, stableGeneratedMacAddress, @@ -104,8 +103,8 @@ function topologyOf(view: JavaFile) { * - generatedMac: a card whose MAC the file lacked (Java rolls a new one at * each load) has the one the simulation uses, derived from its port id; * - documentation: the items in the model's render order (rectangles below - * text), a missing text empty, a missing font Dialog 12, and at least the - * editor's smallest size (DOCUMENTATION_MINIMUM_SIZE); + * text), a missing text empty, a missing font Dialog 12, a missing colour + * the light grey Java shows for it; * - switchSsid: a switch's SSID the file lacked (Java rolls one) is the one * the model derives from the node's id; * - modemRoles: the model keeps a pair of modems, not which one serves: the @@ -212,8 +211,9 @@ const DEPARTURES = { return { ...item, text: item.text ?? '', - bounds: [x, y, Math.max(width!, DOCUMENTATION_MINIMUM_SIZE.width), Math.max(height!, DOCUMENTATION_MINIMUM_SIZE.height)], + bounds: [x, y, width, height], font: item.font ?? ['Dialog', 12, 0], + color: item.color ?? [192, 192, 192, 255], }; }); return { ...view, documentation: sortedDocumentation(items) }; @@ -677,6 +677,7 @@ const SYNTHETIC: ProjectToExport = { fontName: 'Dialog', fontSize: 12, isBold: false, + isItalic: false, order: 0, }, { @@ -688,8 +689,34 @@ const SYNTHETIC: ProjectToExport = { fontName: 'SansSerif', fontSize: 16, isBold: true, + isItalic: false, order: 1, }, + // Italic, bold and italic, smaller than Swift's smallest size: kept as they are. + { + id: 'd3', + kind: 'text', + frame: { x: 400, y: 10, width: 15, height: 10 }, + text: 'kursiv', + color: { red: 0.31, green: 0.51, blue: 0.74, alpha: 1 }, + fontName: 'Dialog', + fontSize: 6, + isBold: false, + isItalic: true, + order: 2, + }, + { + id: 'd4', + kind: 'text', + frame: { x: 400, y: 40, width: 88, height: 20 }, + text: 'beides', + color: { red: 0, green: 0, blue: 0, alpha: 1 }, + fontName: 'Serif', + fontSize: 24, + isBold: true, + isItalic: true, + order: 3, + }, ], }; @@ -933,6 +960,9 @@ describe('a model beyond the corpus, as Java loads it', () => { documentation: [ { type: 1, text: '', bounds: [5, 6, 300, 200], color: [255, 128, 0, 128], font: ['Dialog', 12, 0] }, { type: 2, text: 'Netz A\nzweite Zeile', bounds: [40, 51, 120, 30], color: [0, 0, 255, 255], font: ['SansSerif', 16, 1] }, + // Java's italic (2) and bold italic (3), and sizes below Swift's smallest, as the model keeps them. + { type: 2, text: 'kursiv', bounds: [400, 10, 15, 10], color: [79, 130, 189, 255], font: ['Dialog', 6, 2] }, + { type: 2, text: 'beides', bounds: [400, 40, 88, 20], color: [0, 0, 0, 255], font: ['Serif', 24, 3] }, ], }); }); diff --git a/packages/fls/test/exported.java.json b/packages/fls/test/exported.java.json index eac37c0..8e70c99 100644 --- a/packages/fls/test/exported.java.json +++ b/packages/fls/test/exported.java.json @@ -604,7 +604,7 @@ 508, 390, 88, - 24 + 20 ], "color": [ 0, @@ -646,7 +646,7 @@ 453, 124, 100, - 24 + 20 ], "color": [ 0, @@ -667,7 +667,7 @@ 308, 123, 88, - 24 + 20 ], "color": [ 0, @@ -688,7 +688,7 @@ 631, 391, 88, - 24 + 20 ], "color": [ 0, @@ -808,7 +808,7 @@ ] } ], - "sha256": "edad61949744c17c11d75b6abd180f9916c0f06d643c332209c301778f7ab753" + "sha256": "4c8042374d8f8db186bba920831d9cff9b7a0009c9d5bab094de920954b1fc98" }, "ipad-fixtures/dns_server.fls": { "success": true, @@ -4403,7 +4403,7 @@ 570, 583, 88, - 24 + 20 ], "color": [ 0, @@ -4445,7 +4445,7 @@ 209, 907, 88, - 24 + 20 ], "color": [ 0, @@ -4550,7 +4550,7 @@ 458, 383, 88, - 24 + 20 ], "color": [ 0, @@ -4571,7 +4571,7 @@ 298, 268, 88, - 24 + 20 ], "color": [ 0, @@ -4586,7 +4586,7 @@ ] } ], - "sha256": "5bf3c778a721c2bf41c9203d28f07d3052fa0257f80dcc05911126bc336ab066" + "sha256": "d31a32691f4b82cb683beda090b810671e269fe82a45b5b0810654998d1d8b7b" }, "java-examples/Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls": { "success": true, @@ -7192,7 +7192,7 @@ 570, 583, 88, - 24 + 20 ], "color": [ 0, @@ -7234,7 +7234,7 @@ 255, 897, 88, - 24 + 20 ], "color": [ 0, @@ -7318,7 +7318,7 @@ 440, 907, 108, - 24 + 23 ], "color": [ 0, @@ -7333,7 +7333,7 @@ ] } ], - "sha256": "e56c3b8aec2610d3dedcf7f2c1b3d715aa7bb0eedeb5298ea21c5fd6ff0aba8d" + "sha256": "fa1b339614ba08b53a678ac1242cd26d03b36ec224f4e9a42fba20df050bd31f" }, "java-examples/Public_and_Private_Networks_2_EN.fls": { "success": true, @@ -7940,7 +7940,7 @@ 508, 390, 88, - 24 + 20 ], "color": [ 0, @@ -7982,7 +7982,7 @@ 453, 124, 100, - 24 + 20 ], "color": [ 0, @@ -8003,7 +8003,7 @@ 308, 123, 88, - 24 + 20 ], "color": [ 0, @@ -8024,7 +8024,7 @@ 631, 391, 88, - 24 + 20 ], "color": [ 0, @@ -8144,7 +8144,7 @@ ] } ], - "sha256": "edad61949744c17c11d75b6abd180f9916c0f06d643c332209c301778f7ab753" + "sha256": "4c8042374d8f8db186bba920831d9cff9b7a0009c9d5bab094de920954b1fc98" }, "java-examples/anwendung_komplett.fls": { "success": true, @@ -19681,8 +19681,50 @@ 16, 1 ] + }, + { + "type": 2, + "text": "kursiv", + "bounds": [ + 400, + 10, + 15, + 10 + ], + "color": [ + 79, + 130, + 189, + 255 + ], + "font": [ + "Dialog", + 6, + 2 + ] + }, + { + "type": 2, + "text": "beides", + "bounds": [ + 400, + 40, + 88, + 20 + ], + "color": [ + 0, + 0, + 0, + 255 + ], + "font": [ + "Serif", + 24, + 3 + ] } ], - "sha256": "7e1837d138d433f9721d2852de79f582ccb1e0454fcd100e30f37ab9e324ca58" + "sha256": "dd6692f81ff1eca0214a1c85bce9dbccf99c62b41a84b3a01213f562ffdef483" } } diff --git a/packages/fls/test/importProject.test.ts b/packages/fls/test/importProject.test.ts index addb29c..9a670bf 100644 --- a/packages/fls/test/importProject.test.ts +++ b/packages/fls/test/importProject.test.ts @@ -1282,18 +1282,28 @@ describe('importProject rules', () => { { ...item, type: 2, color: null }, { ...item, type: 1, width: 0, color: [1, 2, 3, 255] }, { ...item, type: 3, color: [1, 2, 3, 255] }, + { ...item, type: 1, text: null, color: null }, + { ...item, type: 2, width: 15, height: 10, color: [1, 2, 3, 255], font: { name: 'Serif', style: 3, size: 6 } }, + { ...item, type: 2, color: [1, 2, 3, 255], font: { name: 'Dialog', style: 2, size: 18 } }, ], }), counter(), ); - expect(project.documentationItems.map((doc) => [doc.kind, doc.text, doc.color, doc.fontName, doc.fontSize])).toEqual([ - ['text', 'Hallo', { red: 0, green: 0, blue: 0, alpha: 1 }, 'Dialog', 12], + // Without a colour, light grey, as Java shows it (JDocuElement.setColor(null)), either kind; italic kept; a small size and font size kept. + const grey = { red: 192 / 255, green: 192 / 255, blue: 192 / 255, alpha: 1 }; + const rgb = { red: 1 / 255, green: 2 / 255, blue: 3 / 255, alpha: 1 }; + expect(project.documentationItems.map((doc) => [doc.kind, doc.text, doc.color, doc.fontName, doc.fontSize, doc.isBold, doc.isItalic, doc.frame.width, doc.frame.height])).toEqual([ + ['rectangle', '', grey, 'Dialog', 12, false, false, 100, 30], + ['text', 'Hallo', grey, 'Dialog', 12, false, false, 100, 30], + ['text', 'Hallo', rgb, 'Serif', 6, true, true, 15, 10], + ['text', 'Hallo', rgb, 'Dialog', 18, false, true, 100, 30], ]); expect(project.warnings).toEqual([ 'documentation item 1 without a color, given the default', 'documentation item without size, left out', 'documentation item of type 3, left out (known: RECT = 1, TEXT = 2)', + 'documentation item 2 without a color, given the default', ]); }); }); diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 64730b7..89ba422 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -1661,3 +1661,41 @@ Fallen der Sonde: Die Einträge des Tabellenmenüs hören nur auf `mousePressed` (sie haben keinen `ActionListener`). Beim Stoppen versteckt Javas Oberfläche das Fenster (`closeDialogs`); ein Aktivieren im Entwurf, wie es die Sonde zuerst tat, entfernt alle Reiter, kommt aber in Javas Oberfläche nicht vor. + +### Dokumentation: Text und Gliederungsfläche (F4.8a) + +`DocuRun` misst Javas Dokumentationsmodus an seinen eigenen Elementen +(`JDocuElement` auf einem `GUIDocumentationPanel`, wie das Ziehen aus der +Seitenleiste sie macht): neue Elemente, ihr Menü (per Reflection +`showPopupMenu`, Einträge per `doClick`, Farben über +`triggerMenuResponse`), die kleinste Größe nach einem Ziehen am Rand +(MouseEvents `moved`/`pressed`/`dragged`), die Pixel eines allein gemalten +Elements in jedem Zustand, die Stapelung nach `updateViewport` und ein +Projekt, das Java mit `SzenarioVerwaltung.speichern` schreibt und wieder +lädt (`packages/doc/test/documentation.java.json`, +`packages/doc/test/fixtures/documentation.fls`). + +Gemessen (04.10.2026): + +* Neu: 100 × 50; ein Text leer, schwarz, Dialog 12 normal; eine Fläche grau + (204, 204, 204). Ein abgelegter Text bekommt den Fokus, sein Rahmen wächst + um 6 nach links/oben, 12 in der Breite, 28 in der Höhe; gespeichert wird + der Rahmen ohne Fokus. +* Menü eines Textes: „Schriftgröße“ (Normal/Groß/Sehr Groß = 12/18/24), + „Schriftstil“ (Normal/Kursiv/Fett, je einer: Kursiv dann Fett ist Fett), + „Farbe“ (15 Farben in drei Reihen), Trenner, „Löschen“. Einer Fläche: + „Farbe“ (12 Farben), Trenner, „Löschen“. Die aktuelle Wahl ist markiert. +* Kleinste Größe nach einem Ziehen: Fläche 20 × 20; ein Text wird nur im + Fokus gezogen, sein vergrößerter Rahmen bis 100 × 48, der Text also bis + 88 × 20. +* Gemalt im Dokumentationsmodus: Fläche in ihrer Farbe mit Deckkraft 200, + Rand in 4/5 ihrer Farbe, Ecken rund; im Fokus ein blauer Rahmen (122, 186, + 250) mit durchscheinendem Band. In Entwurf und Aktion deckend, um 20 % zu + Weiß hin aufgehellt (255, 201, 201 → 255, 212, 212), derselbe Rand. Ein + Text hat nur im Dokumentationsmodus einen grauen Rand (179). +* Stapel: Texte über Flächen, je Art das frühere Element oben. +* Gespeichert und geladen: Kursiv (Stil 2), Fett (1), Farben und Größen + bleiben; eine Fläche hat keine Schrift. + +Falle der Sonde: Der Menüpunkt „Normal“ steht in beiden Untermenüs; die +Sonde nimmt den ersten (Schriftgröße). diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 4c0a5eb..839b96c 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then fi classpath="$upstream/target/filius.jar:$upstream/target/lib/*" mkdir -p "$here/.build" -javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" +javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" # FILIUS writes its settings below $HOME; keep them out of the real one. mkdir -p "$here/.build/home" # Removed first: a crashed run must not leave the previous result standing. @@ -488,6 +488,17 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE - "$root/packages/core/test/scenarios/web-lan.scenario.json" "$output" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true test -s "$output" +# FILIUS' documentation mode on its own elements — a new text and rectangle, +# their menus and colour lists, font and colour chosen, the smallest sizes a +# drag leaves, how they are painted, how they stack, a project saved with +# them and loaded again (DocuRun): the documentation items +# (packages/doc/test/documentation.test.ts, its fixture saved by Java). +output="$root/packages/doc/test/documentation.java.json" +rm -f "$output" +HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" DocuRun \ + "$output" "$root/packages/doc/test/fixtures/documentation.fls" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true +test -s "$output" + # FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS # does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts) # and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts). diff --git a/tools/java-oracle/src/DocuRun.java b/tools/java-oracle/src/DocuRun.java new file mode 100644 index 0000000..ce65628 --- /dev/null +++ b/tools/java-oracle/src/DocuRun.java @@ -0,0 +1,391 @@ +// FILIUS' documentation mode measured on its own elements (JDocuElement on a +// GUIDocumentationPanel, as the sidebar's drag makes them): a new text and a +// new rectangle (size, colour, font), their menus (font size, style, colour, +// the colour lists), the font and colour chosen there, the smallest size a +// drag on an edge leaves (a text is dragged while it has the focus, in its +// enlarged frame), how an element is painted in the documentation mode and +// in the others (fill, border, transparency), the order in which the panel +// stacks texts and rectangles, and a project saved with them and loaded +// again (SzenarioVerwaltung.speichern, laden). The reference for the +// editor's documentation mode (packages/doc/test/documentation.test.ts). +// +// Usage: java -cp filius.jar:lib/*:classes DocuRun + +import filius.gui.netzwerksicht.GUIDocuItem; +import filius.gui.netzwerksicht.GUIDocumentationPanel; +import filius.gui.netzwerksicht.JDocuColorSelector; +import filius.gui.netzwerksicht.JDocuElement; +import filius.rahmenprogramm.SzenarioVerwaltung; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Font; +import java.awt.Window; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.FileOutputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.MenuElement; +import javax.swing.SwingUtilities; + +public class DocuRun { + static final Map output = new LinkedHashMap<>(); + static GUIDocumentationPanel panel; + + static void onEdt(Runnable action) throws Exception { + SwingUtilities.invokeAndWait(action); + Thread.sleep(200); + } + + static List color(Color color) { + List rgba = new ArrayList<>(); + rgba.add(color.getRed()); + rgba.add(color.getGreen()); + rgba.add(color.getBlue()); + rgba.add(color.getAlpha()); + return rgba; + } + + static Map font(Font font) { + Map shown = new LinkedHashMap<>(); + if (font == null) return null; + shown.put("name", font.getName()); + shown.put("style", font.getStyle()); + shown.put("size", font.getSize()); + return shown; + } + + static Map element(JDocuElement element) { + Map shown = new LinkedHashMap<>(); + shown.put("bounds", List.of(element.getX(), element.getY(), element.getWidth(), element.getHeight())); + shown.put("color", color(element.getColor())); + shown.put("font", font(element.getFont())); + shown.put("text", element.getText()); + return shown; + } + + static Map item(GUIDocuItem item) { + Map shown = new LinkedHashMap<>(); + shown.put("type", item.getType()); + shown.put("text", item.getText()); + shown.put("bounds", List.of(item.getX(), item.getY(), item.getWidth(), item.getHeight())); + shown.put("color", color(item.getColor())); + shown.put("font", font(item.getFont())); + return shown; + } + + static JPopupMenu shownMenu() { + for (Window window : Window.getWindows()) { + JPopupMenu found = find(window, JPopupMenu.class); + if (found != null) return found; + } + return null; + } + + static T find(Component component, Class type) { + if (type.isInstance(component) && component.isShowing()) return type.cast(component); + if (component instanceof Container) { + for (Component child : ((Container) component).getComponents()) { + T found = find(child, type); + if (found != null) return found; + } + } + return null; + } + + /** The element's menu (showPopupMenu, private): each entry with its sub-entries or colours. */ + static JPopupMenu openMenu(JDocuElement element) throws Exception { + Method show = JDocuElement.class.getDeclaredMethod("showPopupMenu", int.class, int.class); + show.setAccessible(true); + onEdt(() -> { + try { + show.invoke(element, 5, 5); + } catch (Exception error) { + throw new RuntimeException(error); + } + }); + return shownMenu(); + } + + static List menu(JPopupMenu menu) { + List entries = new ArrayList<>(); + for (Component component : menu.getComponents()) { + Map entry = new LinkedHashMap<>(); + if (component instanceof JDocuColorSelector) { + JDocuColorSelector selector = (JDocuColorSelector) component; + entry.put("colors", selector.getText()); + List colors = new ArrayList<>(); + List chosen = new ArrayList<>(); + JPanel samples = (JPanel) selector.getPopupMenu().getComponent(0); + for (Component sample : samples.getComponents()) { + colors.add(color(sample.getBackground())); + chosen.add(((javax.swing.JComponent) sample).getBorder().getBorderInsets(sample).top == 3); + } + entry.put("list", colors); + entry.put("chosen", chosen); + entry.put("rows", ((java.awt.GridLayout) samples.getLayout()).getRows()); + } else if (component instanceof JMenu) { + JMenu sub = (JMenu) component; + entry.put("menu", sub.getText()); + List items = new ArrayList<>(); + for (MenuElement each : sub.getSubElements()[0].getSubElements()) { + JMenuItem item = (JMenuItem) each; + items.add(item.getText() + (item.isSelected() ? " *" : "")); + } + entry.put("items", items); + } else if (component instanceof JMenuItem) { + entry.put("item", ((JMenuItem) component).getText()); + } else { + entry.put("separator", true); + } + entries.add(entry); + } + return entries; + } + + static void choose(JPopupMenu menu, String text) throws Exception { + for (Component component : menu.getComponents()) { + if (component instanceof JMenu && !(component instanceof JDocuColorSelector)) { + for (MenuElement each : ((JMenu) component).getSubElements()[0].getSubElements()) { + JMenuItem item = (JMenuItem) each; + if (item.getText().equals(text)) { + onEdt(item::doClick); + return; + } + } + } + } + throw new IllegalStateException("no entry " + text); + } + + static void closeMenu(JPopupMenu menu) throws Exception { + onEdt(() -> menu.setVisible(false)); + } + + static void pickColor(JPopupMenu menu, Color color) throws Exception { + for (Component component : menu.getComponents()) { + if (component instanceof JDocuColorSelector) { + onEdt(() -> ((JDocuColorSelector) component).triggerMenuResponse(color)); + return; + } + } + } + + static MouseEvent mouse(Component on, int id, int x, int y) { + return new MouseEvent(on, id, System.currentTimeMillis(), id == MouseEvent.MOUSE_DRAGGED ? MouseEvent.BUTTON1_DOWN_MASK : 0, x, y, 1, false, MouseEvent.BUTTON1); + } + + /** A drag from a point of the element to another, in its coordinates (the edge decides what changes). */ + static void drag(JDocuElement element, int fromX, int fromY, int toX, int toY) throws Exception { + onEdt(() -> { + element.dispatchEvent(mouse(element, MouseEvent.MOUSE_MOVED, fromX, fromY)); + element.dispatchEvent(mouse(element, MouseEvent.MOUSE_PRESSED, fromX, fromY)); + element.dispatchEvent(mouse(element, MouseEvent.MOUSE_DRAGGED, toX, toY)); + element.dispatchEvent(mouse(element, MouseEvent.MOUSE_RELEASED, toX, toY)); + }); + } + + /** The element painted alone: the pixels at the given points, as RGBA. */ + static Map pixels(JDocuElement element, int[][] points) throws Exception { + Map shown = new LinkedHashMap<>(); + BufferedImage image = new BufferedImage(element.getWidth(), element.getHeight(), BufferedImage.TYPE_INT_ARGB); + onEdt(() -> { + java.awt.Graphics2D graphics = image.createGraphics(); + element.paint(graphics); + graphics.dispose(); + }); + for (int[] point : points) { + int argb = image.getRGB(point[0], point[1]); + shown.put(point[0] + "," + point[1], List.of((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >>> 24) & 255)); + } + return shown; + } + + public static void main(String[] args) throws Exception { + try { + run(args); + } catch (Throwable error) { + error.printStackTrace(); + System.exit(1); + } + System.exit(0); + } + + static void run(String[] args) throws Exception { + JFrame frame = new JFrame(); + List made = new ArrayList<>(); + SwingUtilities.invokeAndWait(() -> { + panel = new GUIDocumentationPanel(800, 600); + frame.getContentPane().setLayout(null); + frame.getContentPane().add(panel); + frame.setSize(820, 640); + frame.setVisible(true); + // As the sidebar's drop: made by drag, placed, added to the panel (which gives it the focus). + JDocuElement text = new JDocuElement(true, true); + text.setLocation(100, 100); + JDocuElement rectangle = new JDocuElement(false, true); + rectangle.setLocation(300, 100); + made.add(text); + made.add(rectangle); + }); + JDocuElement text = made.get(0); + JDocuElement rectangle = made.get(1); + Map fresh = new LinkedHashMap<>(); + fresh.put("text", element(text)); + fresh.put("rectangle", element(rectangle)); + output.put("new", fresh); + + onEdt(() -> panel.add(text)); + Thread.sleep(300); + Map focused = new LinkedHashMap<>(); + focused.put("text", element(text)); + onEdt(panel::removeElementsFocus); + focused.put("textUnfocused", element(text)); + output.put("dropped", focused); + + // The menus. + JPopupMenu menu = openMenu(text); + Map menus = new LinkedHashMap<>(); + menus.put("text", menu(menu)); + choose(menu, "Groß"); + closeMenu(menu); + List fonts = new ArrayList<>(); + fonts.add(font(text.getFont())); + menu = openMenu(text); + menus.put("textAfterLarge", menu(menu)); + choose(menu, "Kursiv"); + closeMenu(menu); + fonts.add(font(text.getFont())); + menu = openMenu(text); + choose(menu, "Fett"); + closeMenu(menu); + fonts.add(font(text.getFont())); + menu = openMenu(text); + choose(menu, "Normal"); // the first "Normal", the size menu's + closeMenu(menu); + fonts.add(font(text.getFont())); + menu = openMenu(text); + choose(menu, "Sehr Gro\u00df"); + closeMenu(menu); + fonts.add(font(text.getFont())); + menu = openMenu(text); + choose(menu, "Normal"); + closeMenu(menu); + menu = openMenu(text); + pickColor(menu, new Color(0.31f, 0.51f, 0.74f)); + closeMenu(menu); + output.put("fonts", fonts); + output.put("textColor", color(text.getColor())); + onEdt(() -> panel.add(rectangle)); + menu = openMenu(rectangle); + menus.put("rectangle", menu(menu)); + pickColor(menu, new Color(1f, 0.79f, 0.79f)); + closeMenu(menu); + output.put("rectangleColor", color(rectangle.getColor())); + output.put("menus", menus); + + // The smallest sizes a drag leaves: a rectangle by its corner; a text, focused, by its corner, then unfocused. + Map smallest = new LinkedHashMap<>(); + onEdt(() -> rectangle.setLocalFocus(true)); + drag(rectangle, rectangle.getWidth() - 2, rectangle.getHeight() - 2, 3, 3); + smallest.put("rectangle", element(rectangle)); + drag(rectangle, 1, 1, 200, 200); + smallest.put("rectangleByItsTopLeft", element(rectangle)); + onEdt(() -> text.setLocalFocus(true)); + smallest.put("textFocused", element(text)); + drag(text, text.getWidth() - 2, text.getHeight() - 2, 3, 3); + smallest.put("textFocusedSmallest", element(text)); + onEdt(panel::removeElementsFocus); + smallest.put("textSmallest", element(text)); + // A drag on an unfocused text does nothing (Java moves and sizes a text only in its enlarged frame). + drag(text, 10, 10, 60, 60); + smallest.put("textDraggedUnfocused", element(text)); + output.put("smallest", smallest); + + // Painted: a rectangle and a text, in the documentation mode (enabled) unfocused and focused, and in the others. + Map painted = new LinkedHashMap<>(); + JDocuElement plain = new JDocuElement(false); + JDocuElement words = new JDocuElement(true); + onEdt(() -> { + plain.setBounds(400, 300, 100, 50); + plain.setColor(new Color(1f, 0.79f, 0.79f)); + words.setBounds(400, 400, 100, 50); + words.setText("Text"); + panel.add(plain); + panel.add(words); + }); + int[][] points = { { 50, 25 }, { 1, 25 }, { 2, 25 }, { 0, 25 } }; + painted.put("rectangleEnabled", pixels(plain, points)); + onEdt(() -> plain.setLocalFocus(true)); + painted.put("rectangleFocused", pixels(plain, new int[][] { { 50, 25 }, { 3, 25 }, { 1, 25 } })); + onEdt(() -> plain.setEnabled(false)); + painted.put("rectangleDisabled", pixels(plain, points)); + painted.put("textEnabled", pixels(words, new int[][] { { 0, 25 }, { 1, 25 }, { 50, 0 }, { 99, 25 } })); + onEdt(() -> words.setEnabled(false)); + painted.put("textDisabled", pixels(words, new int[][] { { 0, 25 }, { 1, 25 }, { 50, 0 } })); + output.put("painted", painted); + + // The stacking: texts first, then rectangles, each in the list's order; the first component is on top. + List items = new ArrayList<>(); + JDocuElement[] elements = { new JDocuElement(true), new JDocuElement(false), new JDocuElement(true), new JDocuElement(false) }; + String[] names = { "text 1", "rectangle 1", "text 2", "rectangle 2" }; + onEdt(() -> { + for (int index = 0; index < elements.length; index++) { + elements[index].setBounds(50 + 10 * index, 50 + 10 * index, 100, 50); + elements[index].setText(names[index]); + elements[index].setName(names[index]); + items.add(GUIDocuItem.createDocuItem(elements[index])); + } + panel.updateViewport(items, true); + }); + List stacking = new ArrayList<>(); + for (Component component : panel.getComponents()) stacking.add(component.getName()); + output.put("stacking", stacking); + output.put("topAt", panel.getComponentAt(85, 85).getName()); + + // A project with documentation, saved and loaded again. + List saved = new ArrayList<>(); + JDocuElement italic = new JDocuElement(true); + JDocuElement bold = new JDocuElement(true); + JDocuElement pink = new JDocuElement(false); + JDocuElement grey = new JDocuElement(false); + onEdt(() -> { + italic.setBounds(40, 40, 160, 60); + italic.setText("Kursiv und mittel\nzweite Zeile"); + italic.setFont(italic.getFont().deriveFont(Font.ITALIC, 18f)); + italic.setColor(new Color(0.31f, 0.51f, 0.74f)); + bold.setBounds(40, 140, 120, 50); + bold.setText("Fett"); + bold.setFont(bold.getFont().deriveFont(Font.BOLD, 24f)); + pink.setBounds(20, 20, 300, 200); + pink.setColor(new Color(1f, 0.79f, 0.79f)); + grey.setBounds(400, 20, 100, 50); + for (JDocuElement each : new JDocuElement[] { italic, bold, pink, grey }) saved.add(GUIDocuItem.createDocuItem(each)); + }); + List savedItems = new ArrayList<>(); + for (GUIDocuItem each : saved) savedItems.add(item(each)); + output.put("saved", savedItems); + if (!SzenarioVerwaltung.getInstance().speichern(args[1], new ArrayList<>(), new ArrayList<>(), saved)) throw new IllegalStateException("not saved"); + List loaded = new ArrayList<>(); + SzenarioVerwaltung.getInstance().laden(args[1], new ArrayList<>(), new ArrayList<>(), loaded); + List loadedItems = new ArrayList<>(); + for (GUIDocuItem each : loaded) loadedItems.add(item(each)); + output.put("loaded", loadedItems); + + try (FileOutputStream out = new FileOutputStream(args[0])) { + out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8)); + } + System.out.println("documentation measured"); + } +}