From 639ccb833e382d5f576050e04eced6334188c2ac Mon Sep 17 00:00:00 2001 From: Pelotrio <45769595+Pelotrio@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:16:57 +0200 Subject: [PATCH 1/2] Every page reads through page and follows; the older loader modes go PageLoader keeps page, follows, hold and readsWhenShown, and gains updates, redraws and retitles for work from memory; whenShown, waitsWhileHidden, follow(Function) and ShownUpdates go, and reads run on the file workers. No page reads in its constructor or because a navigation showed it. The World tab names the world from its owner, also while its page is hidden, and the owner tells of another world once it was read. An external edit's take gives way to a later write. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../catalog/KeyBindingLabels.java | 5 +- .../catalog/WorldReading.java | 20 ++++- .../change/ChangeLabels.java | 9 +- .../model/ChangesView.java | 5 -- .../model/DefinitionView.java | 2 +- .../model/InspectionView.java | 2 +- .../totalDebugCompanion/model/LogsView.java | 3 +- .../totalDebugCompanion/model/ModView.java | 2 +- .../model/PackConfigurationView.java | 3 - .../navigation/NavigationService.java | 4 +- .../ui/components/PageLoader.java | 69 +++++++++------- .../ui/components/ShownUpdates.java | 59 ------------- .../ui/components/catalog/ChangesPanel.java | 7 +- .../components/catalog/ConfigFilePanel.java | 3 +- .../ui/components/catalog/ConfigPanel.java | 8 +- .../ui/components/catalog/ContentPanel.java | 8 +- .../components/catalog/DefinitionDetails.java | 20 ++--- .../ui/components/catalog/LogsPanel.java | 36 ++++---- .../ui/components/catalog/ModPanel.java | 12 ++- .../catalog/PackConfigurationPanel.java | 6 +- .../ui/components/catalog/PackPanel.java | 2 +- .../catalog/PackResourcesPanel.java | 9 +- .../ui/components/catalog/WorldPanel.java | 24 +++--- .../components/inspection/SubjectPanel.java | 8 +- .../totalDebugCompanion/util/FileReading.java | 7 ++ .../totalDebugCompanion/SystemsRulesTest.java | 1 - .../catalog/WorldReadingTest.java | 20 +++++ .../navigation/PageReadsTest.java | 41 ++++++++++ .../ui/components/PageLoaderTest.java | 82 +++++++++++++------ .../ui/components/ShownUpdatesTest.java | 49 ----------- .../components/catalog/CatalogPanelsTest.java | 13 +-- .../components/catalog/ChangesPanelTest.java | 4 + .../components/catalog/ConfigPanelTest.java | 7 ++ .../catalog/PackConfigurationPanelTest.java | 4 + .../inspection/SubjectPanelTest.java | 3 +- docs/SYSTEMS.md | 6 +- 36 files changed, 297 insertions(+), 266 deletions(-) delete mode 100644 companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdates.java delete mode 100644 companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdatesTest.java diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/KeyBindingLabels.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/KeyBindingLabels.java index 0e0e5600..0f1dccf5 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/KeyBindingLabels.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/KeyBindingLabels.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.catalog; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.change.ChangeLabels; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget; import com.github.minecraft_ta.totalDebugCompanion.storage.ChangeRecord; @@ -22,8 +23,8 @@ public final class KeyBindingLabels implements ChangeLabels { /** A key rebound outside Companion, such as in the game, changes the key a row shows. */ @Override - public Runnable follow(Runnable listener) { - return this.keys.assignmentsChanged().subscribe(listener); + public List follows() { + return List.of(this.keys.assignmentsChanged()); } public KeyBindingLabels(KeyBindingControl keys) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReading.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReading.java index dc1b82b2..1dd97b78 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReading.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReading.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.catalog; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.nio.file.attribute.FileTime; import com.github.minecraft_ta.totalDebugCompanion.util.FileWatch; @@ -85,6 +86,13 @@ public Signal changed() { return this.changed; } + /** The name of the current world as read last, without reading; empty where none was read yet. */ + public Optional publishedName() { + Followed now = this.followed; + if (now.reading() == null) return Optional.empty(); + return now.reading().published().map(read -> read.saved().name()); + } + /** The current world as read last; read now where it was not read yet. Blocking then. */ public World value() { Followed now = this.followed; @@ -128,8 +136,18 @@ private void follow() { reading.close(); }; this.followed = new Followed(current, reading); + // Told once the new world's first read landed, so its followers find it published, as the tab its name. + Workers.files().execute(() -> { + try { + reading.value(); + } catch (IOException unreadable) { + // The World page shows why. + } + this.changed.fire(); + }); + return; } - if (before.directory() != null || current != null) this.changed.fire(); + if (before.directory() != null) this.changed.fire(); } /** Watches the datapacks folder of {@code world}, which the game plays, or none; a change there reads it again. */ diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/change/ChangeLabels.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/change/ChangeLabels.java index a522287d..643f48f4 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/change/ChangeLabels.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/change/ChangeLabels.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.change; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.catalog.CatalogIndex; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget; import com.github.minecraft_ta.totalDebugCompanion.storage.ChangeRecord; @@ -88,11 +89,11 @@ record Rows(List rows, List problems) { CompletableFuture revert(List changes, CatalogIndex index); /** - * Runs {@code listener} when the values the category's rows show may have changed outside the change record, such as - * a key rebound in the game; returns its removal. Most categories' values change only through the record. + * The signals of values the category's rows show that change outside the change record, such as a key rebound in the + * game. Most categories' values change only through the record. */ - default Runnable follow(Runnable listener) { - return () -> { }; + default List follows() { + return List.of(); } /** diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ChangesView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ChangesView.java index 73585be6..5e5b0eaf 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ChangesView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ChangesView.java @@ -17,11 +17,6 @@ public ChangesView(EditorContext context) { context.navigation()::navigate); } - /** Reads the changed files again. */ - public void refresh() { - this.panel.load(); - } - @Override public String getTitle() { return "Changes"; diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/DefinitionView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/DefinitionView.java index 4eb05833..079a2a76 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/DefinitionView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/DefinitionView.java @@ -19,7 +19,7 @@ public DefinitionView(EditorContext context, SubjectRef.Definition subject) { this.subject = subject; this.panel = SubjectPanel.definition(subject, new DefinitionDetails.Services(context.project().catalog(), () -> context.project().sources(), context.itemIcons(), context.navigation()::navigate, - context.project().packs().changed(ChangeRecord.PackSide.RESOURCES)::subscribe)); + context.project().packs().changed(ChangeRecord.PackSide.RESOURCES))); } public SubjectRef.Definition subject() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/InspectionView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/InspectionView.java index ac6266f0..65dd83ed 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/InspectionView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/InspectionView.java @@ -23,7 +23,7 @@ public InspectionView(EditorContext context, InspectSubjectPayload subject, Runt this.subject = Objects.requireNonNull(subject, "subject"); this.panel = SubjectPanel.occurrence(subject, context.snippets(), () -> context.project().scriptFiles(), new DefinitionDetails.Services(context.project().catalog(), () -> context.project().sources(), - context.itemIcons(), context.navigation()::navigate, context.project().packs().changed(ChangeRecord.PackSide.RESOURCES)::subscribe)); + context.itemIcons(), context.navigation()::navigate, context.project().packs().changed(ChangeRecord.PackSide.RESOURCES))); } public InspectSubjectPayload subject() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/LogsView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/LogsView.java index bdb25f62..72bba912 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/LogsView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/LogsView.java @@ -18,10 +18,9 @@ public LogsView(EditorContext context) { context.navigation()::navigate); } - /** Lists the logs and crash reports again, selecting {@code file} when it is not null. */ + /** Selects {@code file} among the logs and crash reports when it is not null. */ public void show(Path file) { this.panel.select(file); - this.panel.load(); } @Override diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ModView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ModView.java index 9cdc97ba..c0cfbad0 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ModView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/ModView.java @@ -16,7 +16,7 @@ public final class ModView implements IEditorPanel { public ModView(EditorContext context, NavigationTarget.ModPage page) { this.panel = new ModPanel(page.modId(), context.project().catalog(), () -> context.project().sources(), context.itemIcons(), context.project().profile().workspaceDirectory(), context.project().configSettings(), - context.project().keyBindings(), context.navigation()::navigate, context.project().packs().changed(ChangeRecord.PackSide.RESOURCES)::subscribe); + context.project().keyBindings(), context.navigation()::navigate, context.project().packs().changed(ChangeRecord.PackSide.RESOURCES)); this.panel.show(page); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/PackConfigurationView.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/PackConfigurationView.java index 3de4928f..73f9ad73 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/PackConfigurationView.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/model/PackConfigurationView.java @@ -18,9 +18,6 @@ public PackConfigurationView(EditorContext context) { } /** Reads the configuration files again. */ - public void refresh() { - this.panel.load(); - } @Override public String getTitle() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java index 3e68730f..e1ce4253 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/navigation/NavigationService.java @@ -317,7 +317,7 @@ private CompletableFuture performNavigation(NavigationTarget target, Activ PackConfigurationView.class, view -> true, () -> new PackConfigurationView(editors.get()) - ).thenAccept(PackConfigurationView::refresh), activation); + ).thenAccept(view -> { }), activation); case NavigationTarget.PackResources resources -> dispatchNavigation(() -> this.tabs.focusOrCreateIfAbsent( PackResourcesView.class, view -> true, @@ -332,7 +332,7 @@ private CompletableFuture performNavigation(NavigationTarget target, Activ ChangesView.class, view -> true, () -> new ChangesView(editors.get()) - ).thenAccept(ChangesView::refresh), activation); + ).thenAccept(view -> { }), activation); case NavigationTarget.Content content -> dispatchNavigation(() -> this.tabs.focusOrCreateIfAbsent( ContentView.class, view -> true, diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoader.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoader.java index 85dae1a6..39bdc1b4 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoader.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoader.java @@ -1,6 +1,7 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components; import com.github.minecraft_ta.totalDebugCompanion.util.Signal; +import com.github.minecraft_ta.totalDebugCompanion.util.Workers; import javax.swing.JComponent; import javax.swing.SwingUtilities; @@ -17,7 +18,6 @@ import java.util.concurrent.CompletionException; import java.util.function.BooleanSupplier; import java.util.function.Consumer; -import java.util.function.Function; /** * Reads what a page shows off the Swing thread and shows it on that thread (docs/SYSTEMS.md, section 3). One read runs @@ -29,10 +29,8 @@ * first shown, and after a followed signal fires, at once while the page is shown, otherwise once it is shown again. A * page shown again with nothing changed reads nothing, unless its last read failed. While the page holds its reads * ({@link #hold}), as during a save, signals wait in the same way. The page never reads in its constructor or because a - * navigation showed it.

- * - *

Pages not moved to {@link #page} yet use the older modes {@link #whenShown}, {@link #waitsWhileHidden} and - * {@link #readsWhenShown} with {@link #follow}, and read in their constructors; the last of them to move deletes those.

+ * navigation showed it. A page whose files others write and only it reads, such as the logs, reads whenever it is shown + * ({@link #readsWhenShown}); work that only redraws from memory waits the same way ({@link #updates}).

*/ public final class PageLoader { /** What to read: prepared on the Swing thread, where the page's state is captured, then run off it. */ @@ -78,6 +76,11 @@ public interface Read { */ private boolean showReadQueued; + /** A loader for {@code page}, which reads nothing and only redraws from memory ({@link #updates}). */ + public static PageLoader redraws(JComponent page) { + return new PageLoader(() -> null, nothing -> { }, failure -> { }).page(page); + } + /** {@code show} and {@code fail} run on the Swing thread with what a read returned or why it failed. */ public PageLoader(Read read, Consumer show, Consumer fail) { this.read = Objects.requireNonNull(read, "read"); @@ -125,24 +128,6 @@ public boolean fired(Signal signal) { return this.fired.contains(signal); } - /** - * Waits while {@code page} is hidden and reads every time it is shown, since what it shows may have changed while it - * was hidden without a source telling, such as a file the game writes. - */ - public PageLoader whenShown(JComponent page) { - this.page = Objects.requireNonNull(page, "page"); - return readsWhenShown(page); - } - - /** - * Waits while {@code page} is hidden: a followed change then reads once the page is shown again, and not at all when - * none came. For pages whose sources tell every change. - */ - public PageLoader waitsWhileHidden(JComponent page) { - this.page = Objects.requireNonNull(page, "page"); - return watch(page, false); - } - /** * Reads every time {@code component} is shown, such as a tab listing files that change without telling. It may be a * part of the page, which then reads when that part is chosen. @@ -169,11 +154,39 @@ private PageLoader watch(JComponent component, boolean always) { } /** - * Loads whenever a source changes, or once the page is shown again. {@code subscribe} adds a listener to the source - * and returns what removes it, as {@code catalog.changed()::subscribe} does; the listener may be called on any thread. + * Runs {@code update}, which only redraws from memory, such as icons again after new ones came, when {@code signal} + * fires: at once while the page is shown, otherwise once it is shown again, once however often it fired. Swing thread. */ - public PageLoader follow(Function subscribe) { - this.unsubscribe.add(subscribe.apply(() -> SwingUtilities.invokeLater(() -> changed(ALWAYS)))); + public PageLoader updates(Signal signal, Runnable update) { + Objects.requireNonNull(update, "update"); + boolean[] missed = {false}; + this.unsubscribe.add(signal.subscribe(() -> SwingUtilities.invokeLater(() -> { + if (this.disposed) return; + if (this.page != null && !this.page.isShowing()) missed[0] = true; + else update.run(); + }))); + if (this.page != null) { + JComponent shown = this.page; + HierarchyListener listener = event -> { + if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) == 0 || !shown.isShowing() || !missed[0]) return; + missed[0] = false; + if (!this.disposed) update.run(); + }; + shown.addHierarchyListener(listener); + this.unwatch.add(() -> shown.removeHierarchyListener(listener)); + } + return this; + } + + /** + * Runs {@code redraw} on the Swing thread whenever {@code signal} fires, shown or not, for what shows outside the page + * from memory, such as the title of its tab. + */ + public PageLoader retitles(Signal signal, Runnable redraw) { + Objects.requireNonNull(redraw, "redraw"); + this.unsubscribe.add(signal.subscribe(() -> SwingUtilities.invokeLater(() -> { + if (!this.disposed) redraw.run(); + }))); return this; } @@ -253,7 +266,7 @@ public void load() { } catch (Exception exception) { throw new CompletionException(exception); } - }).whenComplete((value, failure) -> { + }, Workers.files()).whenComplete((value, failure) -> { // Completed off the Swing thread, so a wait on the Swing thread does not block what it waits for. finished.complete(null); SwingUtilities.invokeLater(() -> showRead(value, failure)); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdates.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdates.java deleted file mode 100644 index 6c4b4368..00000000 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdates.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.minecraft_ta.totalDebugCompanion.ui.components; - -import javax.swing.JComponent; -import javax.swing.SwingUtilities; -import java.awt.event.HierarchyEvent; -import java.awt.event.HierarchyListener; -import java.util.function.Function; - -/** - * Updates a page when a source it follows changes: at once while the page is shown, otherwise once it is shown again, so - * a hidden page does no work for a change it cannot show. For pages that update themselves rather than through a - * {@link PageLoader}. - */ -public final class ShownUpdates { - private ShownUpdates() { - } - - /** - * Runs {@code update} on the Swing thread whenever the source {@code subscribe} adds a listener to changes, as - * {@code catalog::addListener} does, and {@code page} is shown, or when it is shown after a change. Returns what stops - * following; an update queued before then does not run. - */ - public static Runnable follow(JComponent page, Function subscribe, Runnable update) { - Following following = new Following(page, update); - page.addHierarchyListener(following); - Runnable unsubscribe = subscribe.apply(() -> SwingUtilities.invokeLater(following::changed)); - return () -> { - following.stopped = true; - unsubscribe.run(); - page.removeHierarchyListener(following); - }; - } - - private static final class Following implements HierarchyListener { - private final JComponent page; - private final Runnable update; - /** Whether the source changed while the page was hidden. */ - private boolean stale; - private boolean stopped; - - Following(JComponent page, Runnable update) { - this.page = page; - this.update = update; - } - - void changed() { - if (this.stopped) return; - if (this.page.isShowing()) this.update.run(); - else this.stale = true; - } - - @Override - public void hierarchyChanged(HierarchyEvent event) { - if ((event.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) == 0 || !this.page.isShowing() || !this.stale) return; - this.stale = false; - if (!this.stopped) this.update.run(); - } - } -} diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanel.java index 6448fd1d..172f7f40 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanel.java @@ -223,9 +223,10 @@ public ChangesPanel(PackCatalogService catalog, ChangeRecord record, List(this::prepareLoad, this::show, failure -> setStatus("Could not read the changes: " + failure.getMessage())) - .whenShown(this).follow(this.record.changed()::subscribe).follow(this.catalog.changed()::subscribe); - for (ChangeLabels labels : categories) this.loader.follow(labels::follow); - load(); + .page(this).follows(this.record.changed()).follows(this.catalog.changed()) + // Until the owners notice values put back outside Companion, the labels read the files whenever shown. + .readsWhenShown(this); + for (ChangeLabels labels : categories) labels.follows().forEach(this.loader::follows); } public void load() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigFilePanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigFilePanel.java index f9e6c776..e5f2693a 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigFilePanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigFilePanel.java @@ -49,8 +49,7 @@ public ConfigFilePanel(Path file, ConfigSources.Owner owner, ConfigSettings conf add(bar, BorderLayout.NORTH); add(this.editor.component(), BorderLayout.CENTER); this.loader = new PageLoader<>(() -> this::readFile, this.editor::load, - failure -> setStatus("Could not read " + this.file.getFileName() + ": " + failure.getMessage())).whenShown(this); - load(); + failure -> setStatus("Could not read " + this.file.getFileName() + ": " + failure.getMessage())).page(this).readsWhenShown(this); } public Path file() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanel.java index e1d67e4f..77401bf4 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanel.java @@ -107,15 +107,15 @@ final class ConfigPanel extends JPanel { this.navigator = Objects.requireNonNull(navigator, "navigator"); this.values = new PageLoader<>(this::prepareValues, read -> show(read.file(), read.values(), read.values() == null ? "" : read.values().text(), read.problem()), - failure -> showFailure("Could not read the file: " + failure.getMessage())).whenShown(this); + failure -> showFailure("Could not read the file: " + failure.getMessage())).page(this).readsWhenShown(this); this.sources = new PageLoader<>(this::prepareSources, read -> showSources(read.file(), read.found(), true), failure -> showFailure("Could not list the worlds' copies: " + failure.getMessage())) // A server configuration is shown from the world the game has open first; it follows the game to another // world, unless its text has unsaved changes, which stay with the file they were made in. - .follow(listener -> this.location.playingChanged().subscribe(() -> SwingUtilities.invokeLater(() -> { + .page(this).follows(this.location.playingChanged(), () -> { PackCatalog.ConfigFile file = selectedFile(); - if (file != null && file.type() == PackCatalog.ConfigType.SERVER && !this.textEditor.modified()) listener.run(); - }))); + return file != null && file.type() == PackCatalog.ConfigType.SERVER && !this.textEditor.modified(); + }); configureFiles(); this.content.add(toolbar(), BorderLayout.NORTH); JPanel settings = new JPanel(new BorderLayout()); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ContentPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ContentPanel.java index 03bb097a..9d9f7af5 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ContentPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ContentPanel.java @@ -1,11 +1,11 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; import com.github.minecraft_ta.totalDebugCompanion.catalog.CatalogIndex; import com.github.minecraft_ta.totalDebugCompanion.catalog.PackCatalogService; import com.github.minecraft_ta.totalDebugCompanion.inspection.ItemIconService; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget; import com.github.minecraft_ta.totalDebugCompanion.ui.UiMetrics; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import javax.swing.JLabel; import javax.swing.JPanel; @@ -25,7 +25,7 @@ public final class ContentPanel extends JPanel { private final CatalogIcons icons; private final ContentBrowser browser; private final JLabel message = new JLabel(); - private final Runnable removeCatalogListener; + private final PageLoader redraws; private CatalogIndex index; public ContentPanel(PackCatalogService catalog, ItemIconService icons, Consumer navigator) { @@ -35,7 +35,7 @@ public ContentPanel(PackCatalogService catalog, ItemIconService icons, Consumer< this.browser = new ContentBrowser(this.icons, this::iconOf, Objects.requireNonNull(navigator, "navigator"), entry -> this.index == null ? entry.namespace() : this.index.ownerName(entry.namespace())); this.message.setVerticalAlignment(JLabel.TOP); - this.removeCatalogListener = ShownUpdates.follow(this, catalog.changed()::subscribe, this::load); + this.redraws = PageLoader.redraws(this).updates(catalog.changed(), this::load); load(); } @@ -74,7 +74,7 @@ public JTextComponent filterField() { } public void dispose() { - this.removeCatalogListener.run(); + this.redraws.dispose(); this.icons.dispose(); } } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/DefinitionDetails.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/DefinitionDetails.java index 98a965c7..6dc18b25 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/DefinitionDetails.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/DefinitionDetails.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.catalog.CatalogIndex; import com.github.minecraft_ta.totalDebugCompanion.catalog.ModResources; import com.github.minecraft_ta.totalDebugCompanion.catalog.ModSummary; @@ -16,7 +17,6 @@ import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; import com.github.minecraft_ta.totalDebugCompanion.ui.components.CenteredIcon; import com.github.minecraft_ta.totalDebugCompanion.ui.components.PixelImages; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import com.github.minecraft_ta.totalDebugCompanion.ui.components.subject.ContentKinds; import com.github.minecraft_ta.totalDebugCompanion.ui.components.subject.LinkLabel; import com.github.minecraft_ta.totalDebugCompanion.ui.theme.ThemeColors; @@ -52,7 +52,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; /** @@ -62,11 +61,11 @@ */ public final class DefinitionDetails { /** - * What the details are read from, and where their links lead. {@code resourcesRead} adds a listener for the game - * reading its resources again, as after each reload, which may have changed a mod's files, and returns its removal. + * What the details are read from, and where their links lead. {@code resourcesRead} fires after the game read its + * resources again, as after each reload, which may have changed a mod's files. */ public record Services(PackCatalogService catalog, Supplier sources, ItemIconService icons, - Consumer navigator, Function resourcesRead) { + Consumer navigator, Signal resourcesRead) { public Services { Objects.requireNonNull(catalog, "catalog"); Objects.requireNonNull(sources, "sources"); @@ -79,7 +78,6 @@ public record Services(PackCatalogService catalog, Supplier { }).waitsWhileHidden(page).follow(services.icons().changed()::subscribe); + }, failure -> { }).page(page).follows(services.icons().changed()) + // The catalog is in memory: the definition is looked up again, then its appearance read. + .updates(services.catalog().changed(), this::reload); this.resourceLoader = new PageLoader<>(this::prepareResources, list -> { this.owned = list; this.matched = matching(this.owned, this.subject.namespace(), resourceName()); @@ -117,11 +117,8 @@ public DefinitionDetails(SubjectRef.Definition subject, Services services, JComp this.owned = List.of(); this.matched = List.of(); showExtras(); - }).waitsWhileHidden(page).follow(services.resourcesRead()); - this.removeCatalogListener = ShownUpdates.follow(page, services.catalog().changed()::subscribe, this::reload); + }).page(page).follows(services.resourcesRead()); read(); - loadAppearance(); - loadResources(); } public SubjectRef.Definition subject() { @@ -411,6 +408,5 @@ public void dispose() { this.disposed = true; this.appearanceLoader.dispose(); this.resourceLoader.dispose(); - this.removeCatalogListener.run(); } } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java index 50a2d136..e643d3c6 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java @@ -12,7 +12,6 @@ import com.github.minecraft_ta.totalDebugCompanion.ui.UiMetrics; import com.github.minecraft_ta.totalDebugCompanion.ui.components.Sidebar; import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import com.github.minecraft_ta.totalDebugCompanion.ui.components.BrowserBody; import com.github.minecraft_ta.totalDebugCompanion.ui.presentation.PrimarySecondaryLabel; import com.github.minecraft_ta.totalDebugCompanion.ui.presentation.PrimarySecondaryText; @@ -92,7 +91,6 @@ private record Listed(GameLogs.LogFile file, String primary, String secondary, S private final PackCatalogService catalog; private final Path workspace; - private final Runnable removeCatalogListener; private final Consumer navigator; private final DefaultListModel files = new DefaultListModel<>(); private final JList fileList = new JList<>(this.files); @@ -172,13 +170,12 @@ public void actionPerformed(ActionEvent event) { this.rowLoader = new PageLoader<>(this::prepareRows, this::showRead, failure -> showMessage( (this.rowsOf == null ? "The file" : this.rowsOf.name()) + " could not be read: " + failure.getMessage())); this.loader = new PageLoader>(() -> this::listFiles, this::showFiles, - failure -> showMessage("The logs could not be listed: " + failure.getMessage())).whenShown(this); - // Rows name the mods behind frames and failures as the catalog knows them. - this.removeCatalogListener = ShownUpdates.follow(this, catalog.changed()::subscribe, () -> { - if (this.disposed) return; - this.rowsOf = null; - showRows(); - }); + failure -> showMessage("The logs could not be listed: " + failure.getMessage())).page(this).readsWhenShown(this) + // Rows name the mods behind frames and failures as the catalog knows them. + .updates(catalog.changed(), () -> { + this.rowsOf = null; + showRows(); + }); } private static PrimarySecondaryLabel label(JList list, boolean selected, Icon icon, PrimarySecondaryText text, int indent) { @@ -192,13 +189,21 @@ private static PrimarySecondaryLabel label(JList list, boolean selected, Icon return label; } - /** Selects {@code file} the next time the files are listed; null keeps the selection. */ + /** + * Selects {@code file}, or keeps the selection when null. A file the list does not hold yet, as a crash report written + * since it was read, is looked for by listing the files again. + */ public void select(Path file) { - this.wanted = file == null ? null : file.toAbsolutePath().normalize(); - } - - /** Lists the logs and crash reports again, reading only files that changed, and keeps the selection. */ - public void load() { + if (file == null) return; + Path wanted = file.toAbsolutePath().normalize(); + List listed = new ArrayList<>(); + for (int index = 0; index < this.files.size(); index++) listed.add(this.files.get(index)); + int index = index(listed, wanted); + if (index >= 0) { + this.fileList.setSelectedIndex(index); + return; + } + this.wanted = wanted; this.loader.load(); } @@ -439,6 +444,5 @@ public void dispose() { this.disposed = true; this.loader.dispose(); this.rowLoader.dispose(); - this.removeCatalogListener.run(); } } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ModPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ModPanel.java index 0cd6acb8..d7df9b01 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ModPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ModPanel.java @@ -1,8 +1,8 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigSettings; import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import com.github.minecraft_ta.totalDebugCompanion.ui.components.TabTitles; import com.github.minecraft_ta.totalDebugCompanion.ui.Tooltip; import com.github.minecraft_ta.totalDebugCompanion.ui.components.TypeToFilter; @@ -61,7 +61,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; /** @@ -81,7 +80,6 @@ public final class ModPanel extends JPanel { private final Consumer navigator; private final ItemIconService icons; private final CatalogIcons listIcons; - private final Runnable removeCatalogListener; private final SubjectHeader header = new SubjectHeader(); private final JButton browseCode = new JButton("Browse Code", Icons.JAVA_CLASS); private final JTabbedPane tabs = new JTabbedPane(); @@ -105,7 +103,7 @@ public final class ModPanel extends JPanel { */ public ModPanel(String modId, PackCatalogService catalog, Supplier sources, ItemIconService icons, Path workspace, ConfigSettings configSettings, KeyBindingControl keyControl, - Consumer navigator, Function resourcesRead) { + Consumer navigator, Signal resourcesRead) { super(new BorderLayout()); this.modId = Objects.requireNonNull(modId, "modId"); this.catalog = Objects.requireNonNull(catalog, "catalog"); @@ -122,7 +120,9 @@ public ModPanel(String modId, PackCatalogService catalog, Supplier(this::prepareLoad, loaded -> show(loaded, ""), failure -> show(new Loaded(List.of(), Map.of(), Map.of(), List.of()), "Could not read the configuration files: " + failure.getMessage())) - .whenShown(this).follow(catalog.changed()::subscribe) + // Configuration files are read whenever the page is shown (docs/SYSTEMS.md, section 2). + .page(this).readsWhenShown(this).follows(catalog.changed()) // A server configuration is shown from the copy of the world the game has open, which changes with it. - .follow(this.location.connectionChanged()::subscribe).follow(this.location.playingChanged()::subscribe); - load(); + .follows(this.location.connectionChanged()).follows(this.location.playingChanged()); } /** Reads every file again. */ diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackPanel.java index 8ef525f5..72d64969 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackPanel.java @@ -63,7 +63,7 @@ public PackPanel(Path file, Consumer navigator) { this.header.setSubtitle(List.of()); this.browser.setResources(List.of()); this.browser.setMessage(PackFolders.title(file) + " could not be read: " + failure.getMessage()); - }).whenShown(this); + }).page(this).readsWhenShown(this); } private static Loaded read(Path file) throws IOException { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackResourcesPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackResourcesPanel.java index 5c486733..74388a80 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackResourcesPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackResourcesPanel.java @@ -66,8 +66,8 @@ public PackResourcesPanel(PackCatalogService catalog, ResourceEdits edits, PackS TabTitles.setUncounted(this.tabs, 0, ResourcesTab.FILES.title()); this.browser.setResources(List.of()); this.browser.setMessage("Resources could not be read: " + failure.getMessage()); - }).waitsWhileHidden(this).follow(catalog.changed()::subscribe).follow(edits.packs().changed(ChangeRecord.PackSide.RESOURCES)::subscribe) - .follow(edits.packs().changed(ChangeRecord.PackSide.DATA)::subscribe).follow(edits.edited()::subscribe); + }).page(this).follows(catalog.changed()).follows(edits.packs().changed(ChangeRecord.PackSide.RESOURCES)) + .follows(edits.packs().changed(ChangeRecord.PackSide.DATA)).follows(edits.edited()); this.packLoader = new PageLoader<>(() -> { PackStackPayload stack = this.edits.packs().resourcePacks(); return () -> PackResources.resourcePacks(stack, this.workspace); @@ -79,9 +79,8 @@ public PackResourcesPanel(PackCatalogService catalog, ResourceEdits edits, PackS this.packs.showFailure("The resource packs could not be listed: " + failure.getMessage()); }) // Its count on the tab follows while the page is shown; the folders are read again when the tab is chosen. - .waitsWhileHidden(this).readsWhenShown(this.packs) - .follow(edits.packs().changed(ChangeRecord.PackSide.RESOURCES)::subscribe).follow(catalog.changed()::subscribe); - load(); + .page(this).readsWhenShown(this.packs) + .follows(edits.packs().changed(ChangeRecord.PackSide.RESOURCES)).follows(catalog.changed()); } /** Selects a tab, and on Files a kind of resource. */ diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java index 9018b73a..ee67cd63 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.game.GameLocation; import com.github.minecraft_ta.totalDebugCompanion.catalog.WorldReading; import com.github.minecraft_ta.totalDebugCompanion.Icons; import com.github.minecraft_ta.totalDebugCompanion.catalog.CurrentWorld; @@ -15,7 +16,6 @@ import com.github.minecraft_ta.totalDebugCompanion.ui.components.NoticeLine; import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; import com.github.minecraft_ta.totalDebugCompanion.ui.components.PixelImages; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import com.github.minecraft_ta.totalDebugCompanion.ui.components.TabTitles; import com.github.minecraft_ta.totalDebugCompanion.ui.components.TypeToFilter; import com.github.minecraft_ta.totalDebugCompanion.ui.components.global.EditorTabs; @@ -90,8 +90,8 @@ record ServerWorld(String address, Path world) { private final PackCatalogService catalog; private final ItemIconService icons; private final WorldReading world; + private final GameLocation location; private final PageLoader loader; - private final Runnable removeCatalogListener; private final SubjectHeader header = new SubjectHeader(); /** Why Show in Explorer could not show the world. */ private final NoticeLine notice = new NoticeLine(); @@ -146,10 +146,6 @@ public WorldPanel(PackCatalogService catalog, ItemIconService icons, WorldReadin this.cards.add(page, PAGE_CARD); add(this.cards, BorderLayout.CENTER); - // Only the names of the mods behind datapacks come from the catalog. - this.removeCatalogListener = ShownUpdates.follow(this, catalog.changed()::subscribe, () -> { - if (!this.disposed && (this.saved != null || this.server != null)) this.datapacks.setPacks(this.datapackList, this.catalog.index().orElse(null)); - }); // The world's owner reads it when the game saves it or plays another; the datapacks the connected game names // come with its packs. this.loader = new PageLoader<>(() -> { @@ -157,7 +153,14 @@ public WorldPanel(PackCatalogService catalog, ItemIconService icons, WorldReadin String refusal = edits.packs().worldRefusal(); return () -> read(edits.location().read(), this.world, stack, refusal); }, this::show, failure -> show(Loaded.problem("The world could not be read: " + failure.getMessage()))) - .page(this).follows(world.changed()).follows(edits.packs().changed(ChangeRecord.PackSide.DATA)); + .page(this).follows(world.changed()).follows(edits.packs().changed(ChangeRecord.PackSide.DATA)) + // Only the names of the mods behind datapacks come from the catalog. + .updates(catalog.changed(), () -> { + if (this.saved != null || this.server != null) this.datapacks.setPacks(this.datapackList, this.catalog.index().orElse(null)); + }) + // The tab names the world the owner read, also while the page is hidden. + .retitles(world.changed(), this::refreshTitle).retitles(edits.location().playingChanged(), this::refreshTitle); + this.location = edits.location(); } private static Loaded read(GameState game, WorldReading world, PackStackPayload stack, String refusal) throws IOException { @@ -306,10 +309,10 @@ public WorldTab selectedTab() { return WorldTab.OVERVIEW; } - /** The world's name once it is read, or the address of the server whose world is shown. */ + /** The address of the server the game plays on, or the current world's name as its owner read it last. */ public String title() { - if (this.server != null) return this.server.address(); - return this.saved == null ? "World" : this.saved.name(); + if (this.location.playing() instanceof PlayingPayload.Multiplayer server) return server.address(); + return this.world.publishedName().orElse("World"); } /** Shows a tab with its count, or hides it while it has nothing to show. */ @@ -347,7 +350,6 @@ public int reads() { public void dispose() { this.disposed = true; this.loader.dispose(); - this.removeCatalogListener.run(); } GameRulesPanel rules() { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanel.java index 4f16f8fb..83c80cb5 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanel.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.inspection; +import com.github.minecraft_ta.totalDebugCompanion.ui.components.PageLoader; import com.github.minecraft_ta.totalDebugCompanion.Icons; import com.github.minecraft_ta.totalDebugCompanion.catalog.CatalogIndex; import com.github.minecraft_ta.totalDebugCompanion.catalog.RegistryIds; @@ -19,7 +20,6 @@ import com.github.minecraft_ta.totalDebugCompanion.ui.components.subject.PlateIcon; import com.github.minecraft_ta.totalDebugCompanion.ui.components.subject.SubjectHeader; import com.github.minecraft_ta.totalDebugCompanion.ui.components.values.ScriptResultTree; -import com.github.minecraft_ta.totalDebugCompanion.ui.components.ShownUpdates; import com.github.minecraft_ta.totalDebugCompanion.ui.theme.DynamicMatteBorder; import com.github.minecraft_ta.totalDebugCompanion.ui.theme.ThemeColors; import com.github.minecraft_ta.totaldebug.protocol.Side; @@ -73,7 +73,7 @@ public final class SubjectPanel extends JPanel { private final ItemTabIcon tabIcon; private final JPanel sections = new JPanel(); private final Set collapsed = new HashSet<>(); - private final Runnable removeIconListener; + private final PageLoader redraws; private final Live live; private final FactsPanel.Actions actions = new FactsPanel.Actions() { @Override @@ -138,7 +138,7 @@ private SubjectPanel(SubjectRef.Definition definition, InspectSubjectPayload sub showHeader(); showSections(); // The page draws its items once it is shown; the editor tabs draw the tab's (refreshTabIcon). - this.removeIconListener = ShownUpdates.follow(this, services.icons().changed()::subscribe, this::reloadPageIcons); + this.redraws = PageLoader.redraws(this).updates(services.icons().changed(), this::reloadPageIcons); reloadIcons(); } @@ -324,7 +324,7 @@ public void dispose() { this.disposed = true; if (this.live != null) this.live.dispose(); this.details.dispose(); - this.removeIconListener.run(); + this.redraws.dispose(); } private static JScrollPane scroll(JComponent content) { diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/FileReading.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/FileReading.java index a28c5793..4b71892e 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/FileReading.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/util/FileReading.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.util; +import java.util.Optional; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.file.Path; @@ -82,6 +83,12 @@ public Signal changed() { return this.changed; } + /** The value read last, as published, without reading; empty where none was read yet. */ + public Optional published() { + Read held = this.last; + return held == null ? Optional.empty() : Optional.ofNullable(held.value()); + } + /** * The value read last; where none was read yet, it is read now, on this reading's strand, so it is the one later * changes are told against. Blocking then. diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/SystemsRulesTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/SystemsRulesTest.java index 311193b5..34dd5ba6 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/SystemsRulesTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/SystemsRulesTest.java @@ -100,7 +100,6 @@ private record Allowed(int times, String why) { Map.entry("model/CodeView.java", new Allowed(1, "moves onto Workers in PR 8")), Map.entry("navigation/NavigationService.java", new Allowed(2, "moves onto Workers in PR 8")), Map.entry("script/SnippetExpressionSupport.java", new Allowed(2, "moves onto Workers in PR 8")), - Map.entry("ui/components/PageLoader.java", new Allowed(1, "reads on Workers' file work from PR 7")), Map.entry("ui/components/catalog/ModPanel.java", new Allowed(1, "moves onto Workers in PR 8")), Map.entry("ui/components/editors/PackResourceEditor.java", new Allowed(2, "its saves move onto Workers in PR 8")), Map.entry("ui/components/editors/ResourceTextEditor.java", new Allowed(1, "moves onto Workers in PR 8")), diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReadingTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReadingTest.java index 13c169ca..550582f2 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReadingTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/catalog/WorldReadingTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.io.TempDir; import com.github.minecraft_ta.totalDebugCompanion.pack.GamePacks; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.List; import java.util.stream.Stream; import java.util.Comparator; import java.nio.file.attribute.FileTime; @@ -71,6 +73,24 @@ void theWorldTheGamePlaysIsFollowedAndTheOneBeforeTellsNothing() throws Exceptio } } + @Test + void whenTheGamePlaysAnotherWorldItsFollowersFindItsNamePublished() throws Exception { + Path first = LevelDatFixture.write(this.directory.resolve("saves/First"), LevelDatFixture.world("First")).getParent(); + Path second = LevelDatFixture.write(this.directory.resolve("saves/Second"), LevelDatFixture.world("Second")).getParent(); + GameLocation location = new GameLocation(this.directory); + List named = new CopyOnWriteArrayList<>(); + try (WorldReading reading = new WorldReading(location, new GamePacks(location))) { + location.connected(message -> true); + location.playing(new PlayingPayload.Singleplayer(first.toString())); + await(() -> first.equals(reading.value().directory())); + reading.changed().subscribe(() -> named.add(reading.publishedName().orElse(""))); + + location.playing(new PlayingPayload.Singleplayer(second.toString())); + await(() -> named.contains("Second")); + assertFalse(named.contains(""), "no follower is told before the world it is told of was read: " + named); + } + } + @Test void withoutAWorldThePageIsToldWhy() { try (WorldReading reading = reading()) { diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java index d21caa0b..2ab73ad5 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java @@ -124,6 +124,47 @@ void theWorldPageReadsOnceWhenOpenedAndAgainOnlyAfterTheGameSavedTheWorld() thro } } + @Test + void theWorldTabNamesTheWorldTheGamePlaysWhileItsPageIsHidden() throws Exception { + Path home = Files.createDirectory(this.directory.resolve("home")); + GlobalConfig.getInstance().loadFrom(home); + Path game = Files.createDirectory(this.directory.resolve("game")); + Path first = LevelDatFixture.write(game.resolve("saves/First"), LevelDatFixture.world("First")).getParent(); + Path second = LevelDatFixture.write(game.resolve("saves/Second"), LevelDatFixture.world("Second")).getParent(); + try (CompanionApplication app = new CompanionApplication(new CompanionLaunchConfiguration(home), "test-token")) { + app.openProject(CompanionProfile.forGame(game)).get(10, TimeUnit.SECONDS); + GameLocation location = app.currentScope().location(); + location.connected(message -> true); + location.playing(new PlayingPayload.Singleplayer(first.toString())); + MainWindow window = UiTestScope.onEdt(app::createWindow); + UiTestScope.onEdt(() -> { + window.setSize(1280, 720); + UiTestScope.show(window); + }); + + open(window, new NavigationTarget.World(WorldTab.OVERVIEW)); + WorldView view = UiTestScope.onEdt(() -> assertInstanceOf(WorldView.class, window.getEditorTabs().getSelectedEditor())); + WorldPanel panel = (WorldPanel) view.getComponent(); + UiTestScope.await(() -> panel.reads() == 1); + settle(); + assertEquals("First", tabTitle(window, view)); + + open(window, new NavigationTarget.Changes()); + location.playing(new PlayingPayload.Singleplayer(second.toString())); + UiTestScope.await(() -> "Second".equals(tabTitle(window, view))); + assertEquals(1, (int) UiTestScope.onEdt(panel::reads), "the hidden page does not read for its tab's title"); + } + } + + /** The title the tab strip shows for {@code view}. */ + private static String tabTitle(MainWindow window, WorldView view) { + try { + return UiTestScope.onEdt(() -> window.getEditorTabs().getTitleAt(window.getEditorTabs().indexOfComponent(view.getComponent()))); + } catch (Exception failed) { + throw new AssertionError(failed); + } + } + private static void open(MainWindow window, NavigationTarget target) throws Exception { window.navigation().navigate(target, NavigationService.Activation.KEEP_CURRENT_WINDOW).get(5, TimeUnit.SECONDS); settle(); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoaderTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoaderTest.java index 3a2122b1..d955ebf0 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoaderTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/PageLoaderTest.java @@ -12,14 +12,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import static com.github.minecraft_ta.totalDebugCompanion.testui.UiTestScope.onEdt; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class PageLoaderTest { - private final List source = new CopyOnWriteArrayList<>(); + private final Signal source = new Signal(); private final AtomicInteger prepared = new AtomicInteger(); @Test @@ -90,13 +89,8 @@ void aFailureIsReportedWithItsCauseAndNothingIsShownAfterDisposing() throws Exce assertEquals("level.dat is locked", cause.get().getMessage(), "the read's own exception, not its wrapper"); AtomicInteger shows = new AtomicInteger(); - AtomicInteger listeners = new AtomicInteger(); PageLoader disposed = new PageLoader(() -> () -> "read", value -> shows.incrementAndGet(), failure -> { }) - .follow(listener -> { - listeners.incrementAndGet(); - return listeners::decrementAndGet; - }); - assertEquals(1, listeners.get()); + .follows(this.source); SwingUtilities.invokeAndWait(() -> { disposed.load(); disposed.dispose(); @@ -105,14 +99,15 @@ void aFailureIsReportedWithItsCauseAndNothingIsShownAfterDisposing() throws Exce Thread.sleep(100); SwingUtilities.invokeAndWait(() -> { }); assertEquals(0, shows.get(), "a read that finishes after the page closed shows nothing"); - assertEquals(0, listeners.get(), "disposing removes the listeners on the sources"); + changed(); + assertEquals(1, (int) onEdt(disposed::reads), "a disposed page follows its signals no more"); } @Test void aChangeWhileThePageIsHiddenIsReadOnceItIsShown() throws Exception { ShowablePage page = new ShowablePage(); - PageLoader loader = onEdt(() -> loader().waitsWhileHidden(page).follow(subscribe())); + PageLoader loader = onEdt(() -> loader().page(page).follows(this.source)); changed(); assertEquals(0, this.prepared.get(), "a hidden page does not read"); @@ -130,7 +125,7 @@ void aChangeWhileThePageIsHiddenIsReadOnceItIsShown() throws Exception { @Test void aPageReadWheneverShownWaitsWhileHidden() throws Exception { ShowablePage page = new ShowablePage(); - PageLoader loader = onEdt(() -> loader().whenShown(page).follow(subscribe())); + PageLoader loader = onEdt(() -> loader().page(page).readsWhenShown(page).follows(this.source)); changed(); assertEquals(0, this.prepared.get(), "a hidden page does not read"); @@ -144,14 +139,15 @@ void aPageReadWheneverShownWaitsWhileHidden() throws Exception { void aPartOfThePageReadsWhenItIsChosenAndChangesFollowWhileThePageIsShown() throws Exception { ShowablePage page = new ShowablePage(); ShowablePage tab = new ShowablePage(); - PageLoader loader = onEdt(() -> loader().waitsWhileHidden(page).readsWhenShown(tab).follow(subscribe())); + PageLoader loader = onEdt(() -> loader().page(page).readsWhenShown(tab).follows(this.source)); show(page, loader, true); + assertEquals(1, this.prepared.get(), "the page reads when first shown"); changed(); settle(loader); - assertEquals(1, this.prepared.get(), "a change reads while the page is shown, though its tab is not chosen"); + assertEquals(2, this.prepared.get(), "a change reads while the page is shown, though its tab is not chosen"); show(tab, loader, true); - assertEquals(2, this.prepared.get(), "choosing the tab reads what changed without telling, such as a folder"); + assertEquals(3, this.prepared.get(), "choosing the tab reads what changed without telling, such as a folder"); // Hidden with its tab, a change missed, and shown again with its tab in one event: one read. SwingUtilities.invokeAndWait(() -> { @@ -164,12 +160,12 @@ void aPartOfThePageReadsWhenItIsChosenAndChangesFollowWhileThePageIsShown() thro tab.setShown(true); }); settle(loader); - assertEquals(3, this.prepared.get(), "the page and its tab shown together read once"); + assertEquals(4, this.prepared.get(), "the page and its tab shown together read once"); } @Test void aLoaderWithoutAPageReadsEveryChange() throws Exception { - PageLoader loader = onEdt(() -> loader().follow(subscribe())); + PageLoader loader = onEdt(() -> loader().follows(this.source)); changed(); settle(loader); assertEquals(1, this.prepared.get()); @@ -356,6 +352,50 @@ void manyChangesWhileHiddenAreAskedAboutOncePerSource() throws Exception { assertEquals(1, asked.get(), "a hidden page keeps one question per source, however often it changed"); } + @Test + void anUpdateFromMemoryWaitsWhileThePageIsHiddenAndRunsOnceWhenShown() throws Exception { + AtomicInteger updates = new AtomicInteger(); + ShowablePage page = new ShowablePage(); + PageLoader redraws = onEdt(() -> PageLoader.redraws(page).updates(this.source, updates::incrementAndGet)); + + changed(); + changed(); + assertEquals(0, updates.get(), "a hidden page does not update"); + SwingUtilities.invokeAndWait(() -> page.setShown(true)); + assertEquals(1, updates.get(), "shown, it updates once for the changes it missed"); + SwingUtilities.invokeAndWait(() -> { + page.setShown(false); + page.setShown(true); + }); + assertEquals(1, updates.get(), "shown again without a change, it does not update"); + changed(); + assertEquals(2, updates.get(), "a shown page updates at once"); + + // A change queued before the page closed does not update it. + SwingUtilities.invokeAndWait(() -> { + this.source.fire(); + redraws.dispose(); + }); + SwingUtilities.invokeAndWait(() -> { }); + changed(); + assertEquals(2, updates.get()); + } + + @Test + void aTitleIsRedrawnWhileThePageIsHidden() throws Exception { + AtomicInteger titles = new AtomicInteger(); + ShowablePage page = new ShowablePage(); + PageLoader redraws = onEdt(() -> PageLoader.redraws(page).retitles(this.source, titles::incrementAndGet)); + changed(); + assertEquals(1, titles.get(), "the tab shows its title though the page is hidden"); + onEdt(() -> { + redraws.dispose(); + return null; + }); + changed(); + assertEquals(1, titles.get()); + } + /** Waits until {@code expected} reads were prepared, as a read that a slow machine starts late. */ private void awaitPrepared(int expected, String message) throws Exception { long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); @@ -375,16 +415,8 @@ private PageLoader loader() { }, read -> { }, failure -> { }); } - private Function subscribe() { - return listener -> { - this.source.add(listener); - return () -> this.source.remove(listener); - }; - } - private void changed() throws Exception { - this.source.forEach(Runnable::run); - SwingUtilities.invokeAndWait(() -> { }); + fire(this.source); } /** Shows or hides {@code page}, and waits for a read that showing starts, which runs a Swing step later. */ diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdatesTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdatesTest.java deleted file mode 100644 index b2f12b70..00000000 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/ShownUpdatesTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.minecraft_ta.totalDebugCompanion.ui.components; - -import org.junit.jupiter.api.Test; - -import javax.swing.SwingUtilities; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ShownUpdatesTest { - @Test - void aHiddenPageUpdatesOnceWhenShownAndNotAfterItStopped() throws Exception { - List source = new CopyOnWriteArrayList<>(); - AtomicInteger updates = new AtomicInteger(); - ShowablePage page = new ShowablePage(); - Runnable[] stop = new Runnable[1]; - SwingUtilities.invokeAndWait(() -> stop[0] = ShownUpdates.follow(page, listener -> { - source.add(listener); - return () -> source.remove(listener); - }, updates::incrementAndGet)); - - source.forEach(Runnable::run); - source.forEach(Runnable::run); - SwingUtilities.invokeAndWait(() -> { }); - assertEquals(0, updates.get(), "a hidden page does not update"); - SwingUtilities.invokeAndWait(() -> page.setShown(true)); - assertEquals(1, updates.get(), "shown, it updates once for the changes it missed"); - SwingUtilities.invokeAndWait(() -> { - page.setShown(false); - page.setShown(true); - }); - assertEquals(1, updates.get(), "shown again without a change, it does not update"); - - source.forEach(Runnable::run); - SwingUtilities.invokeAndWait(() -> { }); - assertEquals(2, updates.get(), "a shown page updates at once"); - - // A change queued before it stopped does not update a page that is gone. - SwingUtilities.invokeAndWait(() -> { - source.forEach(Runnable::run); - stop[0].run(); - }); - SwingUtilities.invokeAndWait(() -> { }); - assertEquals(2, updates.get()); - assertEquals(0, source.size(), "it stopped following the source"); - } -} diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/CatalogPanelsTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/CatalogPanelsTest.java index f88bbc19..a8eb04a6 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/CatalogPanelsTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/CatalogPanelsTest.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.testui.UiTestScope; import com.github.minecraft_ta.totalDebugCompanion.testui.UiTest; import org.junit.jupiter.api.AfterEach; @@ -117,7 +118,7 @@ void aModPageListsWhatTheModRegisteredAndOpensDefinitions() throws Exception { try (ItemIconService icons = new ItemIconService()) { onEdt(() -> { ModPanel panel = new ModPanel("testmod", catalog, RuntimeSourceCatalog::empty, icons, this.directory, ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), - new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), opened::add, listener -> () -> { }); + new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), opened::add, new Signal()); try { assertEquals("Test Mod", panel.title()); assertTrue(labels(panel).contains("1.2.3"), labels(panel)::toString); @@ -153,7 +154,7 @@ void aModOverviewLinksInstalledDependencies() throws Exception { try (ItemIconService icons = new ItemIconService()) { onEdt(() -> { ModPanel panel = new ModPanel("testmod", catalog, RuntimeSourceCatalog::empty, icons, this.directory, ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), - new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), target -> { }, listener -> () -> { }); + new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), target -> { }, new Signal()); try { List sections = panel.sections(catalog.index().orElseThrow().mod("testmod").orElseThrow()); assertEquals(List.of("Mod", "Dependencies"), sections.stream().map(FactSection::title).toList()); @@ -174,7 +175,7 @@ void anUnknownModSaysSo() throws Exception { try (ItemIconService icons = new ItemIconService()) { onEdt(() -> { ModPanel panel = new ModPanel("absent", catalog, RuntimeSourceCatalog::empty, icons, this.directory, ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), - new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), target -> { }, listener -> () -> { }); + new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), ChangeRecord.inMemory(), Runnable::run), assignments()), target -> { }, new Signal()); try { assertTrue(labels(panel).contains("absent is not an installed mod"), labels(panel)::toString); assertEquals(1, panel.tabs().getTabCount(), "Only the Overview has something to show"); @@ -191,7 +192,7 @@ void aDefinitionNamesItsClassAndRelatedEntries() throws Exception { PackCatalogService catalog = readyCatalog(); try (ItemIconService icons = new ItemIconService()) { DefinitionDetails.Services services = new DefinitionDetails.Services(catalog, RuntimeSourceCatalog::empty, - icons, target -> { }, listener -> () -> { }); + icons, target -> { }, new Signal()); onEdt(() -> { DefinitionDetails block = new DefinitionDetails( new SubjectRef.Definition(RegistryIds.BLOCK, "testmod:widget_block"), services, new JPanel(), () -> { }); @@ -230,7 +231,7 @@ void aHiddenDefinitionPageNamesItsTabFromTheCatalogCapturedSince() throws Except AtomicInteger reads = new AtomicInteger(); try (ItemIconService icons = new ItemIconService()) { DefinitionDetails.Services services = new DefinitionDetails.Services(catalog, RuntimeSourceCatalog::empty, - icons, target -> { }, listener -> () -> { }); + icons, target -> { }, new Signal()); DefinitionDetails[] details = new DefinitionDetails[1]; onEdt(() -> details[0] = new DefinitionDetails(new SubjectRef.Definition(RegistryIds.BLOCK, "testmod:widget_block"), services, new JPanel(), reads::incrementAndGet)); @@ -298,7 +299,7 @@ void aDefinitionOutsideTheCatalogExplainsWhy() throws Exception { try (ItemIconService icons = new ItemIconService()) { onEdt(() -> { SubjectPanel panel = SubjectPanel.definition(new SubjectRef.Definition(RegistryIds.ITEM, "testmod:widget"), - new DefinitionDetails.Services(empty, RuntimeSourceCatalog::empty, icons, target -> { }, listener -> () -> { })); + new DefinitionDetails.Services(empty, RuntimeSourceCatalog::empty, icons, target -> { }, new Signal())); try { assertTrue(labels(panel).contains(CatalogMessages.unavailable(new PackCatalogService.None())), labels(panel)::toString); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanelTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanelTest.java index 0b77ef6e..96f050ba 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanelTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ChangesPanelTest.java @@ -1,5 +1,7 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTestScope; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTest; import org.junit.jupiter.api.AfterEach; import com.github.minecraft_ta.totalDebugCompanion.catalog.KeyAssignments; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigLabels; @@ -42,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@UiTest class ChangesPanelTest { @TempDir Path directory; @@ -82,6 +85,7 @@ void listsRecordedChangesUntilTheFileHoldsTheOriginalAgain() throws Exception { new KeyBindingLabels(new KeyBindingControl(new ChangePipeline(GameLocations.of(this.directory, false), record, Runnable::run), assignments())), new ResourceLabels(edits), new PackLabels(new PackSelections(edits))); SwingUtilities.invokeAndWait(() -> panel[0] = new ChangesPanel(catalog, record, labels, target -> { })); + SwingUtilities.invokeAndWait(() -> UiTestScope.showPages(panel[0])); try { awaitOnSwing(() -> panel[0].rows("Configuration").size() == 1); SwingUtilities.invokeAndWait(() -> { diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanelTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanelTest.java index d350493b..b65f5b02 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanelTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/ConfigPanelTest.java @@ -1,5 +1,7 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTestScope; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTest; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigLabels; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigSettings; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigSettingsFixture; @@ -42,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +@UiTest class ConfigPanelTest { @TempDir Path directory; @@ -100,6 +103,7 @@ void anEditedValueIsWrittenRefusedOrUndone() throws Exception { SwingUtilities.invokeAndWait(() -> { panel[0] = new ConfigPanel("testmod", ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), target -> { }); panel[0].setFiles(List.of(common)); + UiTestScope.showPages(panel[0]); }); ConfigSettingsTable table = component(panel[0], ConfigSettingsTable.class); awaitOnSwing(() -> table.getRowCount() == 3 && table.row(1).literal() != null); @@ -165,6 +169,7 @@ void anEditedTextIsCheckedSavedRecordedAndUndone() throws Exception { SwingUtilities.invokeAndWait(() -> { panel[0] = new ConfigPanel("testmod", ConfigSettingsFixture.of(GameLocations.of(this.directory, false), record), target -> { }); panel[0].setFiles(List.of(common)); + UiTestScope.showPages(panel[0]); }); ConfigSettingsTable table = component(panel[0], ConfigSettingsTable.class); RSyntaxTextArea area = component(component(panel[0], EditableTextPanel.class), RSyntaxTextArea.class); @@ -206,6 +211,7 @@ void aServerConfigurationFollowsTheGameToAnotherWorld() throws Exception { SwingUtilities.invokeAndWait(() -> { panel[0] = new ConfigPanel("testmod", ConfigSettingsFixture.of(location, ChangeRecord.inMemory()), target -> { }); panel[0].setFiles(List.of(FILE)); + UiTestScope.showPages(panel[0]); }); JComboBox source = component(panel[0], JComboBox.class); awaitOnSwing(() -> source.getSelectedItem() != null && source.getSelectedItem().toString().equals("First")); @@ -231,6 +237,7 @@ void unsavedTextKeepsTheFileItWasEditedFrom() throws Exception { panel[0] = new ConfigPanel("testmod", ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), target -> { }); panel[0].setFiles(List.of(a)); + UiTestScope.showPages(panel[0]); }); ConfigSettingsTable table = component(panel[0], ConfigSettingsTable.class); RSyntaxTextArea area = component(component(panel[0], EditableTextPanel.class), RSyntaxTextArea.class); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackConfigurationPanelTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackConfigurationPanelTest.java index fa5fe15e..b3ba4902 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackConfigurationPanelTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/PackConfigurationPanelTest.java @@ -1,5 +1,7 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.catalog; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTestScope; +import com.github.minecraft_ta.totalDebugCompanion.testui.UiTest; import com.github.minecraft_ta.totalDebugCompanion.catalog.CatalogFixtures; import com.github.minecraft_ta.totalDebugCompanion.catalog.ConfigSettingsFixture; import com.github.minecraft_ta.totalDebugCompanion.catalog.PackCatalogService; @@ -24,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@UiTest class PackConfigurationPanelTest { @TempDir Path directory; @@ -42,6 +45,7 @@ void listsEverySettingUnderItsModFileAndSectionThenTheModifiedOnes() throws Exce catalog.accept(CatalogFixtures.INVENTORY, paths.catalog(), Runnable::run); PackConfigurationPanel[] panel = new PackConfigurationPanel[1]; SwingUtilities.invokeAndWait(() -> panel[0] = new PackConfigurationPanel(catalog, ConfigSettingsFixture.of(GameLocations.of(this.directory, false), ChangeRecord.inMemory()), target -> { })); + SwingUtilities.invokeAndWait(() -> UiTestScope.showPages(panel[0])); ConfigSettingsTable table = table(panel[0]); try { awaitOnSwing(() -> table.getRowCount() == 5 && "mode".equals(table.row(4).name())); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanelTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanelTest.java index 40a5ea21..b0cb2c02 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanelTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/inspection/SubjectPanelTest.java @@ -1,5 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.ui.components.inspection; +import com.github.minecraft_ta.totalDebugCompanion.util.Signal; import com.github.minecraft_ta.totalDebugCompanion.catalog.PackCatalogService; import com.github.minecraft_ta.totalDebugCompanion.catalog.RegistryIds; import com.github.minecraft_ta.totalDebugCompanion.inspection.InspectionSession; @@ -238,7 +239,7 @@ private static void withPanel(InspectSubjectPayload subject, Consumer { throw new IllegalStateException("Tests run no snippets"); }, () -> { throw new IllegalStateException("Tests have no project"); }, - new DefinitionDetails.Services(catalog, RuntimeSourceCatalog::empty, icons, navigator, listener -> () -> { })); + new DefinitionDetails.Services(catalog, RuntimeSourceCatalog::empty, icons, navigator, new Signal())); try { test.accept(panel); } catch (Throwable throwable) { diff --git a/docs/SYSTEMS.md b/docs/SYSTEMS.md index 1b7ae176..de5e2f43 100644 --- a/docs/SYSTEMS.md +++ b/docs/SYSTEMS.md @@ -131,7 +131,7 @@ this.loader = new PageLoader<>(this::read, this::show, this::fail) - **A follow may say whether a change concerns the page**, asked when the page would read: the resource editor follows the whole change record, but only its own file's entry, and not its own save's. A read's preparation may also ask which followed signals led to it (`fired`): the resource editor lets Save wait only for a read after a pack change, which may move it to another pack, not for one after an edit elsewhere. - **A read never changes an owner.** What the Changes page's labels do today with `ChangeRecord.observed` moves to the owners, which notice a value put back outside Companion on their own readings, on the write queue. - A part of a page that reads on its own, such as the resources of a definition, has its own loader with that part as its page. -- Work that only redraws from values in memory, such as a tab's title and icon, uses `loader.updates(runnable)`: the same waiting and merging, with no read. +- Work that only redraws from values in memory, such as icons again after new ones came, uses `loader.updates(signal, redraw)`: the same waiting and merging, with no read. A page with nothing to read has a loader that only redraws (`PageLoader.redraws(page)`). What shows outside the page, such as its tab's title, is redrawn from the owners' published values whenever their signal fires, shown or not (`retitles`). **Navigation never reads files.** It shows a page, and passes a selection to it. A navigation that carries a new request to the game, as inspecting a subject again, asks the owner of that request. `InspectionSession`'s timer, which polls the game for live values, stays until live channels (C2) push them, and is listed in the architecture test. @@ -205,8 +205,8 @@ PRs on 1.21.1, stacked, each reviewed until clean. A shared mechanism comes with | 4 (#115) | The current world as a `FileReading` that follows `playing`, with the World page and the tree | `WorldReadings`, the World page's read whenever shown | | 5 | The owners still telling through their own listeners on signals: `GameLocation`, `ItemIconService`, `RuntimeIndexService` | `GameLocation.Change` and the three listener lists | | 6 | The last watchers onto `FileWatch`: the Project tree's folders, told of entries only, and `ExternalEdits`, whose settle runs on the timer; `FileUtils`' pause for Companion's own moves becomes `FileWatch.pausing` | `FileUtils`, the watchers and schedulers of `ExternalEdits` | -| 7 | All remaining pages, the Project tree and the tab strip, the logs, configuration and resource packs pages reading whenever shown; no file checks on the Swing thread | `ShownUpdates`, `whenShown` and `waitsWhileHidden`, the pages' own subscriptions, the reads in constructors, the navigation refreshes, the `CompanionUi` relays, `ChangeRecord.observed` from page reads | -| 8 | The current project as state; connection numbers for waiting requests; the pipeline owning the write queue; the remaining executors onto `Workers` | `ConfigChanges`' executor, the scope checks, the UI classes' executors | +| 7 | All remaining pages on `page` and `follows`, the logs, configuration and resource packs pages reading whenever shown; the World tab's title from its owner; reads on the file workers | `ShownUpdates`, `whenShown`, `waitsWhileHidden` and `follow`, the pages' own subscriptions, the reads in constructors, the navigation refreshes | +| 8 | The current project as state, which the Project tree and the main window follow; connection numbers for waiting requests; the pipeline owning the write queue, where owners notice values put back outside Companion; no file checks on the Swing thread; the remaining executors onto `Workers` | `ConfigChanges`' executor, the scope checks, the `CompanionUi` relays, `ChangeRecord.observed` from page reads and the Changes page's read whenever shown, the UI classes' executors | After the messages, A4 continues with categories registering their pages and Modpack rows. Splitting `CompanionApplication` (the game connection and the MCP server into their own classes) and the mod's `CompanionAppClient` (launching Companion) is easier then and is decided at that point. From d4e98913e6b92fc9c5722180f2b257d25e8deb3a Mon Sep 17 00:00:00 2001 From: Pelotrio <45769595+Pelotrio@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:35:08 +0200 Subject: [PATCH 2/2] Keep hidden world titles and log navigation current --- .../ui/components/catalog/LogsPanel.java | 2 ++ .../ui/components/catalog/WorldPanel.java | 3 ++- .../totalDebugCompanion/navigation/PageReadsTest.java | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java index e643d3c6..410ef5bc 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/LogsPanel.java @@ -200,6 +200,8 @@ public void select(Path file) { for (int index = 0; index < this.files.size(); index++) listed.add(this.files.get(index)); int index = index(listed, wanted); if (index >= 0) { + // A file asked for before, still being looked for, is not this navigation's. + this.wanted = null; this.fileList.setSelectedIndex(index); return; } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java index ee67cd63..1b35e74b 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/catalog/WorldPanel.java @@ -159,7 +159,8 @@ public WorldPanel(PackCatalogService catalog, ItemIconService icons, WorldReadin if (this.saved != null || this.server != null) this.datapacks.setPacks(this.datapackList, this.catalog.index().orElse(null)); }) // The tab names the world the owner read, also while the page is hidden. - .retitles(world.changed(), this::refreshTitle).retitles(edits.location().playingChanged(), this::refreshTitle); + .retitles(world.changed(), this::refreshTitle).retitles(edits.location().playingChanged(), this::refreshTitle) + .retitles(edits.location().connectionChanged(), this::refreshTitle); this.location = edits.location(); } diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java index 2ab73ad5..c24209ae 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/PageReadsTest.java @@ -153,6 +153,13 @@ void theWorldTabNamesTheWorldTheGamePlaysWhileItsPageIsHidden() throws Exception location.playing(new PlayingPayload.Singleplayer(second.toString())); UiTestScope.await(() -> "Second".equals(tabTitle(window, view))); assertEquals(1, (int) UiTestScope.onEdt(panel::reads), "the hidden page does not read for its tab's title"); + location.playing(new PlayingPayload.Multiplayer("play.example.net", false, true)); + UiTestScope.await(() -> "play.example.net".equals(tabTitle(window, view))); + settle(); + String offlineTitle = app.currentScope().world().publishedName().orElse("World"); + location.disconnected(); + UiTestScope.await(() -> offlineTitle.equals(tabTitle(window, view))); + assertEquals(1, (int) UiTestScope.onEdt(panel::reads), "disconnecting retitles the hidden tab without reading its page"); } }