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:
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user