feat: F4.6f2 the text editor on the document's Y.Text, files both ways while the simulation runs

Oracle TextEditorWindowRun uses Java's text editor window and its file
dialog as a person does. The open file's text is its Y.Text in the
document (plan 7.3): typed is in the file at once, for everyone and for
the running engine. Core openText/saveTextAs without Java's mistakes
(lost saves after save-as onto a file or a removed file, files named
'ordner/d.txt' or ''); doc files.ts (changeFiles, fileText, nodeFiles,
readFilesInto); the room takes a person's files into the engine at once
and writes the engine's files back at the tick they changed; the editor's
window, Java's dialog, the text area following others' changes with the
caret in place, its own undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:50:27 +00:00
co-authored by Claude Opus 5.5
parent 3d6655e736
commit 2c7733b575
29 changed files with 3268 additions and 76 deletions
+34
View File
@@ -1393,3 +1393,37 @@ Gemessen (03.10.2026):
* Die Konten der Programme in email_komplett liegen nur in der alten
`kontoListe`, die FILIUS 2.14 nicht liest; das Orakel setzt sie, wie man
sie in den Dialog tippt (`setzeKonto`, `speichern`).
### Der Texteditor (F4.6f2)
`TextEditorWindowRun` öffnet Javas `GUIApplicationTextEditorWindow` auf
einem `GUIDesktopPanel` (erster Rechner von mail-lan, mit Befehlszeile und
Webserver) und benutzt ihn wie eine Person: tippen, „Neu“, „Öffnen“,
„Speichern“, „Speichern unter“, dazu Javas Dateidialog (`DMTNFileChooser`:
die Liste des Ordners, ein Doppelklick auf einen Ordner oder eine Datei,
„Ordner aufwärts“, das Namensfeld, der eigene Knopf, „Abbrechen“). Der
modale Dialog wird gesucht, während der Knopf des Editors auf ihn wartet.
Daneben ändert die Befehlszeile die Dateien. Je Schritt: Titel und Text des
Fensters, was der Dialog zeigte, alle Dateien
(`packages/sim/test/textEditorWindow.java.json`).
Gemessen (03.10.2026):
* Der Titel ist „Text-Editor“, mit offener Datei „Text-Editor - name“; nach
„Öffnen“ in anderer Schreibweise der Name der Datei, nach „Speichern
unter“ der getippte.
* „Speichern“ ohne Datei fragt wie „Speichern unter“; der Dialog beginnt in
der Wurzel („Ordner: root“, sonst nur der Name des Ordners, nicht sein
Pfad), listet Ordner und Dateien in ihrer Entstehungsreihenfolge, ein
Doppelklick öffnet einen Ordner oder setzt den Namen einer Datei.
„Speichern unter“ legt eine Datei vom Typ `text` an.
* Ändert ein anderes Programm die offene Datei, übernimmt das Fenster sie,
solange nichts getippt ist; danach behält es seinen Text.
* „Öffnen“ nimmt den Namen als Pfad vom Ordner aus (`ordner/b.txt`), in jeder
Schreibweise; ein Bild erscheint als Base64-Text. Ohne Fund (auch ohne
Namen) vergisst das Fenster die offene Datei und behält den Text.
* Javas Fehler: „Speichern unter“ auf eine vorhandene Datei schreibt sie,
das Fenster bleibt aber in einer Kopie, die kein Ordner hält — jedes
spätere „Speichern“ geht verloren. Ebenso nach dem Löschen der offenen
Datei. „Speichern unter“ mit `ordner/d.txt` legt in der Wurzel eine Datei
mit `/` im Namen an, ohne Namen eine namenlose.
+11 -1
View File
@@ -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"
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"
# 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.
@@ -401,6 +401,16 @@ for run in email:email_komplett dhcp:einfaches_rechnernetz_komplett; do
test -s "$output"
done
# FILIUS' text editor window used as a person does — typing, „Neu“,
# „Öffnen“, „Speichern“, „Speichern unter“ over its file dialog, the command
# line changing the files beside it (TextEditorWindowRun): the editor's text
# editor (packages/editor/test/textEditor.test.ts).
output="$root/packages/sim/test/textEditorWindow.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" TextEditorWindowRun \
"$root/packages/core/test/scenarios/mail-lan.scenario.json" "$output" 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).
@@ -0,0 +1,328 @@
// Uses FILIUS' own text editor window (GUIApplicationTextEditorWindow on a
// GUIDesktopPanel) on mail-lan's first computer as a person does: typing,
// its buttons „Neu“, „Öffnen“, „Speichern“, „Speichern unter“, and its file
// dialog (DMTNFileChooser: the folder's list, a double click on a folder or
// a file, „Ordner aufwärts“, the name field, the dialog's own button), with
// the command line beside it changing the files. After each step it writes
// what the window shows (its title, its text), what the dialog showed, and
// the computer's files. The reference for the editor's text editor
// (packages/editor/test/textEditor.test.ts).
//
// Usage: java -cp filius.jar:lib/*:classes TextEditorWindowRun <scenario.json> <output json>
import filius.gui.anwendungssicht.GUIApplicationTextEditorWindow;
import filius.gui.anwendungssicht.GUIApplicationWindow;
import filius.gui.anwendungssicht.GUIDesktopPanel;
import filius.software.lokal.Terminal;
import filius.software.system.Betriebssystem;
import filius.software.system.Datei;
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.swing.AbstractButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
public class TextEditorWindowRun {
static final String CLIENT = "00000000-0000-0000-0000-000000000201";
static final List<Object> steps = new ArrayList<>();
static GUIApplicationTextEditorWindow window;
static GUIDesktopPanel desktop;
static Betriebssystem system;
static Terminal terminal;
/** What the last dialog showed, for the next step. */
static List<Object> shown = new ArrayList<>();
static Object field(Class<?> owner, Object object, String name) throws Exception {
Field field = owner.getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
@SuppressWarnings("unchecked")
static <T> T find(Container container, Class<T> kind) {
for (Component component : container.getComponents()) {
if (kind.isInstance(component)) return (T) component;
if (component instanceof Container) {
T found = find((Container) component, kind);
if (found != null) return found;
}
}
return null;
}
static <T> List<T> all(Container container, Class<T> kind, List<T> found) {
for (Component component : container.getComponents()) {
if (kind.isInstance(component)) found.add(kind.cast(component));
if (component instanceof Container) all((Container) component, kind, found);
}
return found;
}
static AbstractButton button(Container container, String text) {
for (AbstractButton each : all(container, AbstractButton.class, new ArrayList<>())) if (text.equals(each.getText())) return each;
throw new IllegalStateException("no button " + text);
}
static void onEdt(Runnable action) throws Exception {
SwingUtilities.invokeLater(action);
Thread.sleep(400);
}
static JTextArea editor() throws Exception {
return (JTextArea) field(GUIApplicationTextEditorWindow.class, window, "editorField");
}
/** The file dialog Java shows (modal, so the button's press waits for it). */
static JDialog dialog() throws Exception {
long until = System.currentTimeMillis() + 5000;
while (System.currentTimeMillis() < until) {
for (Window each : Window.getWindows()) {
if (each instanceof JDialog && each.isVisible() && find((Container) each, JList.class) != null) return (JDialog) each;
}
Thread.sleep(100);
}
throw new IllegalStateException("no file dialog");
}
/** What the dialog shows: its title, its folder's label and its list. */
static void look(JDialog dialog) {
Map<String, Object> look = new LinkedHashMap<>();
look.put("title", dialog.getTitle());
List<String> labels = new ArrayList<>();
for (JLabel label : all(dialog.getContentPane(), JLabel.class, new ArrayList<>())) labels.add(label.getText());
look.put("labels", labels);
List<String> entries = new ArrayList<>();
ListModel<?> model = find(dialog.getContentPane(), JList.class).getModel();
for (int index = 0; index < model.getSize(); index++) entries.add(String.valueOf(model.getElementAt(index)));
look.put("entries", entries);
look.put("name", find(dialog.getContentPane(), JTextField.class).getText());
List<String> buttons = new ArrayList<>();
for (AbstractButton each : all(dialog.getContentPane(), AbstractButton.class, new ArrayList<>())) buttons.add(each.getText());
look.put("buttons", buttons);
shown.add(look);
}
/** A double click on the list's entry, as Java's dialog listens (mouseClicked, two clicks). */
static void doubleClick(JDialog dialog, String entry) throws Exception {
JList<?> list = find(dialog.getContentPane(), JList.class);
int index = -1;
for (int each = 0; each < list.getModel().getSize(); each++) if (entry.equals(String.valueOf(list.getModel().getElementAt(each)))) index = each;
if (index < 0) throw new IllegalStateException("no entry " + entry);
int found = index;
onEdt(() -> {
Point at = list.indexToLocation(found);
MouseEvent event = new MouseEvent(list, MouseEvent.MOUSE_CLICKED, System.currentTimeMillis(), 0, at.x + 2, at.y + 2, 2, false, MouseEvent.BUTTON1);
for (MouseListener listener : list.getMouseListeners()) listener.mouseClicked(event);
});
look(dialog);
}
/**
* A file dialog after the editor's button: what it shows, then the
* person's moves (">name" a double click on an entry, "^" folder up,
* "=name" the name typed), then its own button ("!") or „Abbrechen“ ("x").
*/
static void inDialog(String editorButton, String... moves) throws Exception {
onEdt(button(window, editorButton)::doClick);
JDialog dialog = dialog();
look(dialog);
for (String move : moves) {
if (move.startsWith(">")) doubleClick(dialog, move.substring(1));
else if (move.equals("^")) {
onEdt(button(dialog.getContentPane(), "Ordner aufwärts")::doClick);
look(dialog);
} else if (move.startsWith("=")) {
JTextField name = find(dialog.getContentPane(), JTextField.class);
onEdt(() -> name.setText(move.substring(1)));
} else if (move.equals("!")) {
String action = editorButton.equals("Öffnen") ? "Öffnen" : "Speichern";
onEdt(button(dialog.getContentPane(), action)::doClick);
} else if (move.equals("x")) onEdt(button(dialog.getContentPane(), "Abbrechen")::doClick);
}
Thread.sleep(300);
}
static void type(String text) throws Exception {
JTextArea editor = editor();
onEdt(() -> editor.setText(text));
}
/** GUIApplicationTerminalWindow on Enter: the line handed to the program. */
static void command(String line) throws Exception {
StringTokenizer tokens = new StringTokenizer(line, " ");
String name = tokens.nextToken();
String[] parameters = new String[3 + tokens.countTokens()];
Arrays.fill(parameters, "");
for (int index = 0; tokens.hasMoreTokens(); index++) parameters[index] = tokens.nextToken();
terminal.terminalEingabeAuswerten(name, parameters);
Thread.sleep(500);
}
/** The computer's files, each folder's entries in their order: path, type, content (an image's only its length). */
static List<List<String>> files() {
List<List<String>> files = new ArrayList<>();
walk(system.getDateisystem().getRoot(), "", files);
return files;
}
static void walk(DefaultMutableTreeNode folder, String path, List<List<String>> files) {
for (Enumeration<?> each = folder.children(); each.hasMoreElements();) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) each.nextElement();
if (node.getUserObject() instanceof Datei) {
Datei datei = (Datei) node.getUserObject();
String content = datei.getDateiInhalt();
boolean text = "text".equals(datei.getDateiTyp());
files.add(Arrays.asList(path + "/" + datei.getName(), String.valueOf(datei.getDateiTyp()), text ? content : "(" + (content == null ? 0 : content.length()) + " characters)"));
} else {
files.add(Arrays.asList(path + "/" + node + "/", "folder", ""));
walk(node, path + "/" + node, files);
}
}
}
static void step(String name) throws Exception {
Map<String, Object> result = new LinkedHashMap<>();
result.put("step", name);
result.put("title", ((JLabel) field(GUIApplicationWindow.class, window, "titleLabel")).getText());
String text = editor().getText();
result.put("text", text.length() > 200 ? text.substring(0, 200) + "… (" + text.length() + " characters)" : text);
if (!shown.isEmpty()) result.put("dialog", shown);
shown = new ArrayList<>();
result.put("files", files());
steps.add(result);
}
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 {
ScenarioNetwork network = ScenarioNetwork.read(args[0]);
system = (Betriebssystem) network.nodes.get(CLIENT).getSystemSoftware();
system.installApp("filius.software.lokal.TextEditor", false);
system.installApp("filius.software.lokal.Terminal", false);
system.installApp("filius.software.www.WebServer", false);
network.start();
Thread.sleep(1000);
terminal = (Terminal) system.holeSoftware("filius.software.lokal.Terminal");
JFrame frame = new JFrame();
desktop = new GUIDesktopPanel(system);
frame.getContentPane().add(desktop);
window = (GUIApplicationTextEditorWindow) desktop.starteAnwendung("filius.software.lokal.TextEditor");
SwingUtilities.invokeAndWait(() -> {
frame.pack();
frame.setVisible(true);
});
Thread.sleep(500);
step("open");
// A new text saved: „Speichern“ without a file asks for a name.
type("Erste Zeile\nZweite Zeile");
inDialog("Speichern", "=notiz.txt", "!");
step("save new as notiz.txt");
type("Erste Zeile\nZweite Zeile\nDritte Zeile");
onEdt(button(window, "Speichern")::doClick);
step("save");
// Another program changes the open file: the editor takes it while nothing is typed, keeps its own text after typing.
Datei notiz = system.getDateisystem().holeDatei("/notiz.txt");
onEdt(() -> notiz.setDateiInhalt("von außen"));
step("changed outside, nothing typed");
type("getippt");
onEdt(() -> notiz.setDateiInhalt("wieder von außen"));
step("changed outside after typing");
// Folders in the dialog: a double click into one, a file chosen there, folder up.
command("mkdir ordner");
command("touch ordner/b.txt");
inDialog("Öffnen", ">Ordner;ordner", ">Datei;b.txt", "!");
step("open ordner/b.txt by double clicks");
inDialog("Öffnen", ">Ordner;ordner", "^", "x");
step("dialog: folder and up, cancelled");
inDialog("Öffnen", "=NOTIZ.TXT", "!");
step("open NOTIZ.TXT");
inDialog("Öffnen", "=ordner/b.txt", "!");
step("open ordner/b.txt by name");
inDialog("Öffnen", "=fehlt.txt", "!");
step("open fehlt.txt");
inDialog("Speichern", "x");
step("save without a file, cancelled");
inDialog("Öffnen", "=", "!");
step("open without a name");
// An image opened as text.
inDialog("Öffnen", ">Ordner;webserver", ">Datei;splashscreen-mini.png", "!");
step("open the web server's image");
// „Speichern unter“ onto a file there (in other case), then „Speichern“ again.
inDialog("Öffnen", "=notiz.txt", "!");
type("vor dem Speichern unter");
inDialog("Speichern unter", "=NOTIZ.TXT", "!");
step("save as NOTIZ.TXT over notiz.txt");
type("nach dem Speichern unter");
onEdt(button(window, "Speichern")::doClick);
step("save after save as over a file");
// „Speichern unter“ a new name in a folder, then „Speichern“ again.
type("neu im Ordner");
inDialog("Speichern unter", ">Ordner;ordner", "=c.txt", "!");
step("save as ordner/c.txt");
type("neu im Ordner, geändert");
onEdt(button(window, "Speichern")::doClick);
step("save after save as a new file");
// The open file removed by the command line, then saved.
command("rm ordner/c.txt");
step("open file removed");
type("nach dem Löschen");
onEdt(button(window, "Speichern")::doClick);
step("save after the file was removed");
// A name with a folder in it, and none.
inDialog("Speichern unter", "=ordner/d.txt", "!");
step("save as ordner/d.txt");
inDialog("Speichern unter", "=", "!");
step("save as without a name");
onEdt(button(window, "Neu")::doClick);
step("new");
Map<String, Object> output = new LinkedHashMap<>();
output.put("steps", steps);
try (FileOutputStream out = new FileOutputStream(args[1])) {
out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
network.stop();
System.out.println("text editor window: " + steps.size() + " steps");
}
}