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..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 @@ -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,23 @@ 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) { + // A file asked for before, still being looked for, is not this navigation's. + this.wanted = null; + this.fileList.setSelectedIndex(index); + return; + } + this.wanted = wanted; this.loader.load(); } @@ -439,6 +446,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..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 @@ -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,15 @@ 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) + .retitles(edits.location().connectionChanged(), this::refreshTitle); + this.location = edits.location(); } private static Loaded read(GameState game, WorldReading world, PackStackPayload stack, String refusal) throws IOException { @@ -306,10 +310,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 +351,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..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 @@ -124,6 +124,54 @@ 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"); + 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"); + } + } + + /** 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.