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.