From 4b9b6821f9c21adf7666239423af0e27b3666ab3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 13:52:00 -0700 Subject: [PATCH 001/110] only blink selection highlighter on one token at a time --- .../enigma/gui/panel/BaseEditorPanel.java | 93 ++++++++++++++----- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 445f647e9..0cc5740b1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -54,8 +54,6 @@ import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; @@ -102,6 +100,9 @@ public class BaseEditorPanel { private final BoxHighlightPainter debugPainter; private final BoxHighlightPainter fallbackPainter; + @Nullable + private SelectionHighlightHandler selectionHighlightHandler; + protected ClassHandler classHandler; private DecompiledClassSource source; private SourceBounds sourceBounds = new DefaultBounds(); @@ -561,26 +562,32 @@ public void navigateToToken(@Nullable Token token) { } // highlight the token momentarily - final Timer timer = new Timer(200, null); - timer.addActionListener(new ActionListener() { - private int counter = 0; - private Object highlight = null; - - @Override - public void actionPerformed(ActionEvent event) { - if (this.counter % 2 == 0) { - this.highlight = BaseEditorPanel.this.addHighlight(boundedToken, SelectionHighlightPainter.INSTANCE); - } else if (this.highlight != null) { - BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); - } - - if (this.counter++ > 6) { - timer.stop(); - } - } - }); - - timer.start(); + // final Timer timer = new Timer(200, null); + // timer.addActionListener(new ActionListener() { + // private int counter = 0; + // private Object highlight = null; + // + // @Override + // public void actionPerformed(ActionEvent event) { + // if (this.counter % 2 == 0) { + // this.highlight = BaseEditorPanel.this.addHighlight(boundedToken, SelectionHighlightPainter.INSTANCE); + // } else if (this.highlight != null) { + // BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); + // } + // + // if (this.counter++ > 6) { + // timer.stop(); + // } + // } + // }); + // + // timer.start(); + + if (this.selectionHighlightHandler != null) { + this.selectionHighlightHandler.finish(); + } + + this.selectionHighlightHandler = this.startHighlighting(boundedToken); } /** @@ -668,6 +675,48 @@ private ClassEntry getDeobfOrObfHandleRef() { return deobfRef == null ? this.classHandler.handle.getRef() : deobfRef; } + private SelectionHighlightHandler startHighlighting(Token token) { + final SelectionHighlightHandler handler = new SelectionHighlightHandler(token, 200); + + handler.start(); + + return handler; + } + + private class SelectionHighlightHandler extends Timer { + int counter = 0; + Object highlight = null; + + SelectionHighlightHandler(Token token, int delay) { + super(delay, null); + this.addActionListener(e -> { + if (this.counter % 2 == 0) { + this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); + } else { + this.removeHighlight(); + } + + if (this.counter++ > 6) { + this.finish(); + } + }); + } + + void removeHighlight() { + if (SelectionHighlightHandler.this.highlight != null) { + BaseEditorPanel.this.editor.getHighlighter().removeHighlight(SelectionHighlightHandler.this.highlight); + } + } + + void finish() { + this.stop(); + this.removeHighlight(); + if (BaseEditorPanel.this.selectionHighlightHandler == this) { + BaseEditorPanel.this.selectionHighlightHandler = null; + } + } + } + public record Snippet(int start, int end) { public Snippet { if (start < 0) { From 456caa71a2e7b0d36de341a879718ce4877d4e6f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 13:55:43 -0700 Subject: [PATCH 002/110] remove initial delay from SelectionHighlightHandler --- .../java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 0cc5740b1..05e54972d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -689,6 +689,9 @@ private class SelectionHighlightHandler extends Timer { SelectionHighlightHandler(Token token, int delay) { super(delay, null); + + this.setInitialDelay(0); + this.addActionListener(e -> { if (this.counter % 2 == 0) { this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); @@ -703,8 +706,8 @@ private class SelectionHighlightHandler extends Timer { } void removeHighlight() { - if (SelectionHighlightHandler.this.highlight != null) { - BaseEditorPanel.this.editor.getHighlighter().removeHighlight(SelectionHighlightHandler.this.highlight); + if (this.highlight != null) { + BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); } } From b9d24a66008609f54b5d79c28ca3db399b703244 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 16:12:27 -0700 Subject: [PATCH 003/110] make selection highlight blink count and blink delay configurable --- .../enigma/gui/config/BoundedNumber.java | 43 +++++++++++++ .../enigma/gui/config/EditorConfig.java | 8 +++ .../gui/config/SelectionHighlightSection.java | 24 +++++++ .../enigma/gui/panel/BaseEditorPanel.java | 63 ++++++++----------- 4 files changed, 102 insertions(+), 36 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java new file mode 100644 index 000000000..4798c9775 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java @@ -0,0 +1,43 @@ +package org.quiltmc.enigma.gui.config; + +import org.quiltmc.config.api.values.ComplexConfigValue; +import org.quiltmc.config.api.values.ConfigSerializableObject; + +public record BoundedNumber>(N value, N min, N max) + implements ConfigSerializableObject { + public BoundedNumber(N value, N min, N max) { + if (min.compareTo(max) >= 0) { + throw new IllegalArgumentException("min must be less than max!"); + } + + this.min = min; + this.max = max; + if (this.min.compareTo(value) > 0) { + this.value = min; + } else if (this.max.compareTo(value) < 0) { + this.value = max; + } else { + this.value = value; + } + } + + @Override + public BoundedNumber convertFrom(N representation) { + return new BoundedNumber<>(representation, this.min, this.max); + } + + @Override + public N getRepresentation() { + return this.value; + } + + @Override + public ComplexConfigValue copy() { + return this; + } + + @Override + public String toString() { + return "%s (min: %s, max: %s)".formatted(this.value, this.min, this.max); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java index eb4acd888..5080f6193 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java @@ -16,4 +16,12 @@ public class EditorConfig extends ReflectiveConfig { @Comment("Settings for editors' entry tooltips.") public final EntryTooltipsSection entryTooltips = new EntryTooltipsSection(); + + @Comment( + """ + Settings for the editor's selection highlighting; used to highlight tokens that have been navigated to. + The color of the highlight is defined per-theme (in themes/) by [syntax_pane_colors] -> selection_highlight.\ + """ + ) + public final SelectionHighlightSection selectionHighlight = new SelectionHighlightSection(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java new file mode 100644 index 000000000..722533665 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java @@ -0,0 +1,24 @@ +package org.quiltmc.enigma.gui.config; + +import org.quiltmc.config.api.ReflectiveConfig; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.annotations.SerializedNameConvention; +import org.quiltmc.config.api.metadata.NamingSchemes; +import org.quiltmc.config.api.values.TrackedValue; + +@SerializedNameConvention(NamingSchemes.SNAKE_CASE) +public class SelectionHighlightSection extends ReflectiveConfig.Section { + @Comment("The number of times the highlighting blinks. Set to 0 to disable highlighting.") + public final TrackedValue> blinks = this.value(new BoundedNumber<>(3, 0, 10)); + + @Comment("The number of milliseconds the highlighting should be on and then off when blinking.") + public final TrackedValue> blinkDelay = this.value(new BoundedNumber<>(200, 10, 5000)); + + public int getBlinks() { + return this.blinks.value().value(); + } + + public int getBlinkDelay() { + return this.blinkDelay.value().value(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 05e54972d..176426f7f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -21,6 +21,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.GuiController; import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.SelectionHighlightSection; import org.quiltmc.enigma.gui.config.theme.ThemeUtil; import org.quiltmc.enigma.gui.config.theme.properties.composite.SyntaxPaneProperties; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; @@ -561,33 +562,23 @@ public void navigateToToken(@Nullable Token token) { return; } - // highlight the token momentarily - // final Timer timer = new Timer(200, null); - // timer.addActionListener(new ActionListener() { - // private int counter = 0; - // private Object highlight = null; - // - // @Override - // public void actionPerformed(ActionEvent event) { - // if (this.counter % 2 == 0) { - // this.highlight = BaseEditorPanel.this.addHighlight(boundedToken, SelectionHighlightPainter.INSTANCE); - // } else if (this.highlight != null) { - // BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); - // } - // - // if (this.counter++ > 6) { - // timer.stop(); - // } - // } - // }); - // - // timer.start(); + this.startHighlightingSelection(boundedToken); + } + private void startHighlightingSelection(Token token) { if (this.selectionHighlightHandler != null) { this.selectionHighlightHandler.finish(); } - this.selectionHighlightHandler = this.startHighlighting(boundedToken); + final SelectionHighlightSection config = Config.editor().selectionHighlight; + final int blinks = config.getBlinks(); + if (blinks > 0) { + final SelectionHighlightHandler handler = new SelectionHighlightHandler(token, config.getBlinkDelay(), blinks); + + handler.start(); + + this.selectionHighlightHandler = handler; + } } /** @@ -675,31 +666,31 @@ private ClassEntry getDeobfOrObfHandleRef() { return deobfRef == null ? this.classHandler.handle.getRef() : deobfRef; } - private SelectionHighlightHandler startHighlighting(Token token) { - final SelectionHighlightHandler handler = new SelectionHighlightHandler(token, 200); - - handler.start(); + private class SelectionHighlightHandler extends Timer { + static final int BLINK_INTERVAL = 2; - return handler; - } + final int counterMax; - private class SelectionHighlightHandler extends Timer { int counter = 0; Object highlight = null; - SelectionHighlightHandler(Token token, int delay) { + SelectionHighlightHandler(Token token, int delay, int blinks) { super(delay, null); + this.counterMax = blinks * BLINK_INTERVAL; + this.setInitialDelay(0); this.addActionListener(e -> { - if (this.counter % 2 == 0) { - this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); - } else { - this.removeHighlight(); - } + if (this.counter < this.counterMax) { + if (this.counter % BLINK_INTERVAL == 0) { + this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); + } else { + this.removeHighlight(); + } - if (this.counter++ > 6) { + this.counter++; + } else { this.finish(); } }); From b6dfe29d37b74f25d7c824404d1980be56e20960 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 17:56:30 -0700 Subject: [PATCH 004/110] on opening new tab, wait for source to be set before highlighting first token --- .../enigma/gui/element/EditorTabbedPane.java | 102 +++++++++++------- .../enigma/gui/panel/BaseEditorPanel.java | 45 ++++---- .../quiltmc/enigma/gui/panel/EditorPanel.java | 7 +- 3 files changed, 93 insertions(+), 61 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorTabbedPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorTabbedPane.java index 3441bc783..36a7fcbc9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorTabbedPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorTabbedPane.java @@ -16,6 +16,7 @@ import java.awt.Component; import java.awt.event.MouseEvent; import java.util.Iterator; +import java.util.concurrent.CompletableFuture; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; @@ -41,52 +42,71 @@ public EditorTabbedPane(Gui gui) { public EditorPanel openClass(ClassEntry entry) { EditorPanel activeEditor = this.getActiveEditor(); - EditorPanel entryEditor = this.editors.computeIfAbsent(entry, editing -> { - ClassHandle classHandle = this.gui.getController().getClassHandleProvider().openClass(editing); - if (classHandle == null) { - return null; - } - - this.navigator = new NavigatorPanel(this.gui); - EditorPanel newEditor = new EditorPanel(this.gui, this.navigator); - newEditor.setClassHandle(classHandle); - this.openFiles.addTab(newEditor.getSimpleClassName(), newEditor.getUi()); - - ClosableTabTitlePane titlePane = new ClosableTabTitlePane(newEditor.getSimpleClassName(), newEditor.getFullClassName(), () -> this.closeEditor(newEditor)); - this.openFiles.setTabComponentAt(this.openFiles.indexOfComponent(newEditor.getUi()), titlePane.getUi()); - titlePane.setTabbedPane(this.openFiles); - - newEditor.addListener(new EditorActionListener() { - @Override - public void onCursorReferenceChanged(EditorPanel editor, EntryReference, Entry> ref) { - if (editor == EditorTabbedPane.this.getActiveEditor()) { - EditorTabbedPane.this.gui.showCursorReference(ref); - } - } - @Override - public void onClassHandleChanged(EditorPanel editor, ClassEntry old, ClassHandle ch) { - EditorTabbedPane.this.editors.remove(old); - EditorTabbedPane.this.editors.put(ch.getRef(), editor); + final EditorPanel entryEditor; + final CompletableFuture entryEditorReady; + { + final EditorPanel existingEditor = this.editors.get(entry); + + if (existingEditor == null) { + ClassHandle classHandle = this.gui.getController().getClassHandleProvider().openClass(entry); + if (classHandle == null) { + entryEditor = null; + entryEditorReady = null; + } else { + this.navigator = new NavigatorPanel(this.gui); + final EditorPanel newEditor = new EditorPanel(this.gui, this.navigator); + entryEditorReady = newEditor.setClassHandle(classHandle); + this.openFiles.addTab(newEditor.getSimpleClassName(), newEditor.getUi()); + + ClosableTabTitlePane titlePane = new ClosableTabTitlePane(newEditor.getSimpleClassName(), newEditor.getFullClassName(), () -> this.closeEditor(newEditor)); + this.openFiles.setTabComponentAt(this.openFiles.indexOfComponent(newEditor.getUi()), titlePane.getUi()); + titlePane.setTabbedPane(this.openFiles); + + newEditor.addListener(new EditorActionListener() { + @Override + public void onCursorReferenceChanged(EditorPanel editor, EntryReference, Entry> ref) { + if (editor == EditorTabbedPane.this.getActiveEditor()) { + EditorTabbedPane.this.gui.showCursorReference(ref); + } + } + + @Override + public void onClassHandleChanged(EditorPanel editor, ClassEntry old, ClassHandle ch) { + EditorTabbedPane.this.editors.remove(old); + EditorTabbedPane.this.editors.put(ch.getRef(), editor); + } + + @Override + public void onTitleChanged(EditorPanel editor, String title) { + titlePane.setText(editor.getSimpleClassName(), editor.getFullClassName()); + } + }); + + putKeyBindAction(KeyBinds.EDITOR_CLOSE_TAB, newEditor.getEditor(), e -> this.closeEditor(newEditor)); + putKeyBindAction(KeyBinds.ENTRY_NAVIGATOR_NEXT, newEditor.getEditor(), e -> newEditor.getNavigatorPanel().navigateDown()); + putKeyBindAction(KeyBinds.ENTRY_NAVIGATOR_LAST, newEditor.getEditor(), e -> newEditor.getNavigatorPanel().navigateUp()); + + this.editors.put(entry, newEditor); + + entryEditor = newEditor; } - - @Override - public void onTitleChanged(EditorPanel editor, String title) { - titlePane.setText(editor.getSimpleClassName(), editor.getFullClassName()); - } - }); - - putKeyBindAction(KeyBinds.EDITOR_CLOSE_TAB, newEditor.getEditor(), e -> this.closeEditor(newEditor)); - putKeyBindAction(KeyBinds.ENTRY_NAVIGATOR_NEXT, newEditor.getEditor(), e -> newEditor.getNavigatorPanel().navigateDown()); - putKeyBindAction(KeyBinds.ENTRY_NAVIGATOR_LAST, newEditor.getEditor(), e -> newEditor.getNavigatorPanel().navigateUp()); - - return newEditor; - }); + } else { + entryEditor = existingEditor; + entryEditorReady = null; + } + } if (entryEditor != null && activeEditor != entryEditor) { - this.openFiles.setSelectedComponent(this.editors.get(entry).getUi()); + this.openFiles.setSelectedComponent(entryEditor.getUi()); this.gui.updateStructure(entryEditor); - this.gui.showCursorReference(entryEditor.getCursorReference()); + + final Runnable showReference = () -> this.gui.showCursorReference(entryEditor.getCursorReference()); + if (entryEditorReady == null) { + showReference.run(); + } else { + entryEditorReady.thenRunAsync(showReference, SwingUtilities::invokeLater); + } } return entryEditor; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 176426f7f..3f8f5a814 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -61,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -156,11 +157,14 @@ protected void installEditorRuler(int lineOffset) { ruler.setFont(this.editor.getFont()); } - public void setClassHandle(ClassHandle handle) { - this.setClassHandle(handle, true, null); + /** + * @return a future whose completion indicates that this editor's class handle and source have been set + */ + public CompletableFuture setClassHandle(ClassHandle handle) { + return this.setClassHandle(handle, true, null); } - protected void setClassHandle( + protected CompletableFuture setClassHandle( ClassHandle handle, boolean closeOldHandle, @Nullable Function snippetFactory ) { @@ -172,10 +176,10 @@ protected void setClassHandle( } } - this.setClassHandleImpl(old, handle, snippetFactory); + return this.setClassHandleImpl(old, handle, snippetFactory); } - protected void setClassHandleImpl( + protected CompletableFuture setClassHandleImpl( ClassEntry old, ClassHandle handle, @Nullable Function snippetFactory ) { @@ -198,10 +202,12 @@ public void onInvalidate(ClassHandle h, InvalidationType t) { } }); - handle.getSource().thenAcceptAsync( - res -> BaseEditorPanel.this.handleDecompilerResult(res, snippetFactory), - SwingUtilities::invokeLater - ); + return handle.getSource() + .thenApplyAsync( + res -> this.handleDecompilerResult(res, snippetFactory), + SwingUtilities::invokeLater + ) + .thenAcceptAsync(CompletableFuture::join); } public void destroy() { @@ -214,19 +220,22 @@ private void redecompileClass() { } } - private void handleDecompilerResult( + private CompletableFuture handleDecompilerResult( Result res, @Nullable Function snippetFactory ) { - SwingUtilities.invokeLater(() -> { - if (res.isOk()) { - this.setSource(res.unwrap(), snippetFactory); - } else { - this.displayError(res.unwrapErr()); - } + return CompletableFuture.runAsync( + () -> { + if (res.isOk()) { + this.setSource(res.unwrap(), snippetFactory); + } else { + this.displayError(res.unwrapErr()); + } - this.nextReference = null; - }); + this.nextReference = null; + }, + SwingUtilities::invokeLater + ); } private void displayError(ClassHandleError t) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 9e89d1382..4d93763ef 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -36,6 +36,7 @@ import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import javax.swing.JComponent; import javax.swing.JPanel; @@ -207,11 +208,11 @@ public NavigatorPanel getNavigatorPanel() { } @Override - protected void setClassHandleImpl( + protected CompletableFuture setClassHandleImpl( ClassEntry old, ClassHandle handle, @Nullable Function snippetFactory ) { - super.setClassHandleImpl(old, handle, snippetFactory); + final CompletableFuture superFuture = super.setClassHandleImpl(old, handle, snippetFactory); handle.addListener(new ClassHandleListener() { @Override @@ -228,6 +229,8 @@ public void onDeleted(ClassHandle h) { }); this.listeners.forEach(l -> l.onClassHandleChanged(this, old, handle)); + + return superFuture; } private void onCaretMove(int pos) { From c48331fdf02a5c3218e69500a5610d3c88113b8d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 14:46:09 -0700 Subject: [PATCH 005/110] use IntegerRange instead of BoundedNumber --- .../enigma/gui/config/BoundedNumber.java | 43 ------------------- .../gui/config/SelectionHighlightSection.java | 15 +++---- .../enigma/gui/panel/BaseEditorPanel.java | 5 ++- 3 files changed, 8 insertions(+), 55 deletions(-) delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java deleted file mode 100644 index 4798c9775..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/BoundedNumber.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.quiltmc.enigma.gui.config; - -import org.quiltmc.config.api.values.ComplexConfigValue; -import org.quiltmc.config.api.values.ConfigSerializableObject; - -public record BoundedNumber>(N value, N min, N max) - implements ConfigSerializableObject { - public BoundedNumber(N value, N min, N max) { - if (min.compareTo(max) >= 0) { - throw new IllegalArgumentException("min must be less than max!"); - } - - this.min = min; - this.max = max; - if (this.min.compareTo(value) > 0) { - this.value = min; - } else if (this.max.compareTo(value) < 0) { - this.value = max; - } else { - this.value = value; - } - } - - @Override - public BoundedNumber convertFrom(N representation) { - return new BoundedNumber<>(representation, this.min, this.max); - } - - @Override - public N getRepresentation() { - return this.value; - } - - @Override - public ComplexConfigValue copy() { - return this; - } - - @Override - public String toString() { - return "%s (min: %s, max: %s)".formatted(this.value, this.min, this.max); - } -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java index 722533665..ad23d3bbc 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java @@ -2,6 +2,7 @@ import org.quiltmc.config.api.ReflectiveConfig; import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.annotations.IntegerRange; import org.quiltmc.config.api.annotations.SerializedNameConvention; import org.quiltmc.config.api.metadata.NamingSchemes; import org.quiltmc.config.api.values.TrackedValue; @@ -9,16 +10,10 @@ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class SelectionHighlightSection extends ReflectiveConfig.Section { @Comment("The number of times the highlighting blinks. Set to 0 to disable highlighting.") - public final TrackedValue> blinks = this.value(new BoundedNumber<>(3, 0, 10)); + @IntegerRange(min = 0, max = 10) + public final TrackedValue blinks = this.value(3); @Comment("The number of milliseconds the highlighting should be on and then off when blinking.") - public final TrackedValue> blinkDelay = this.value(new BoundedNumber<>(200, 10, 5000)); - - public int getBlinks() { - return this.blinks.value().value(); - } - - public int getBlinkDelay() { - return this.blinkDelay.value().value(); - } + @IntegerRange(min = 10, max = 5000) + public final TrackedValue blinkDelay = this.value(200); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 3f8f5a814..3162e0b77 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -580,9 +580,10 @@ private void startHighlightingSelection(Token token) { } final SelectionHighlightSection config = Config.editor().selectionHighlight; - final int blinks = config.getBlinks(); + final int blinks = config.blinks.value(); if (blinks > 0) { - final SelectionHighlightHandler handler = new SelectionHighlightHandler(token, config.getBlinkDelay(), blinks); + final SelectionHighlightHandler handler = + new SelectionHighlightHandler(token, config.blinkDelay.value(), blinks); handler.start(); From a1a514615cd267ff9adec6ea8ebcb6838f96f1d7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 18:22:32 -0700 Subject: [PATCH 006/110] add gui for selection highlight configs --- .../gui/config/SelectionHighlightSection.java | 11 ++- .../gui/element/IntRangeConfigMenuItem.java | 97 +++++++++++++++++++ .../element/menu_bar/AbstractEnigmaMenu.java | 2 +- .../menu_bar/view/SelectionHighlightMenu.java | 42 ++++++++ .../gui/element/menu_bar/view/ViewMenu.java | 4 + enigma/src/main/resources/lang/en_us.json | 11 +++ 6 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/IntRangeConfigMenuItem.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java index ad23d3bbc..ed3aee3d6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/SelectionHighlightSection.java @@ -9,11 +9,16 @@ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class SelectionHighlightSection extends ReflectiveConfig.Section { + public static final int MIN_BLINKS = 0; + public static final int MAX_BLINKS = 10; + public static final int MIN_BLINK_DELAY = 10; + public static final int MAX_BLINK_DELAY = 5000; + @Comment("The number of times the highlighting blinks. Set to 0 to disable highlighting.") - @IntegerRange(min = 0, max = 10) + @IntegerRange(min = MIN_BLINKS, max = MAX_BLINKS) public final TrackedValue blinks = this.value(3); - @Comment("The number of milliseconds the highlighting should be on and then off when blinking.") - @IntegerRange(min = 10, max = 5000) + @Comment("The milliseconds the highlighting should be on and then off when blinking.") + @IntegerRange(min = MIN_BLINK_DELAY, max = MAX_BLINK_DELAY) public final TrackedValue blinkDelay = this.value(200); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/IntRangeConfigMenuItem.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/IntRangeConfigMenuItem.java new file mode 100644 index 000000000..acb908ae8 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/IntRangeConfigMenuItem.java @@ -0,0 +1,97 @@ +package org.quiltmc.enigma.gui.element; + +import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.Utils; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import java.util.Optional; + +public class IntRangeConfigMenuItem extends JMenuItem { + public static final String DIALOG_TITLE_TRANSLATION_KEY_SUFFIX = ".dialog_title"; + public static final String DIALOG_EXPLANATION_TRANSLATION_KEY_SUFFIX = ".dialog_explanation"; + private final TrackedValue config; + + private final String translationKey; + + /** + * Constructs a menu item that, when clicked, prompts the user for an integer between the passed {@code min} and + * {@code max} using a dialog.
+ * The menu item will be kept in sync with the passed {@code config}. + * + * @param gui the gui + * @param config the config value to sync with + * @param min the minimum allowed value; + * this should coincide with any minimum imposed on the passed {@code config} + * @param max the maximum allowed value + * this should coincide with any maximum imposed on the passed {@code config} + * @param rootTranslationKey a translation key for deriving translations as follows: + *
    + *
  • this component's text: the unmodified key + *
  • the title of the dialog: the key with + * {@value #DIALOG_TITLE_TRANSLATION_KEY_SUFFIX} appended + *
  • the explanation of the dialog: the key with + * {@value #DIALOG_EXPLANATION_TRANSLATION_KEY_SUFFIX} appended + *
+ */ + public IntRangeConfigMenuItem(Gui gui, TrackedValue config, int min, int max, String rootTranslationKey) { + this( + gui, config, min, max, rootTranslationKey, + rootTranslationKey + DIALOG_TITLE_TRANSLATION_KEY_SUFFIX, + rootTranslationKey + DIALOG_EXPLANATION_TRANSLATION_KEY_SUFFIX + ); + } + + private IntRangeConfigMenuItem( + Gui gui, TrackedValue config, int min, int max, + String translationKey, String dialogTitleTranslationKey, String dialogExplanationTranslationKey + ) { + this.config = config; + this.translationKey = translationKey; + + this.addActionListener(e -> + getRangedIntInput( + gui, config.value(), min, max, + I18n.translate(dialogTitleTranslationKey), + I18n.translate(dialogExplanationTranslationKey) + ) + .ifPresent(input -> { + if (!input.equals(config.value())) { + config.setValue(input); + } + }) + ); + + config.registerCallback(updated -> { + this.retranslate(); + }); + } + + public void retranslate() { + this.setText(I18n.translateFormatted(this.translationKey, this.config.value())); + } + + private static Optional getRangedIntInput( + Gui gui, int initialValue, int min, int max, String title, String explanation + ) { + final String prompt = I18n.translateFormatted("prompt.input.int_range", min, max); + final String input = (String) JOptionPane.showInputDialog( + gui.getFrame(), + explanation + "\n" + prompt, + title, + JOptionPane.QUESTION_MESSAGE, null, null, initialValue + ); + + if (input != null) { + try { + return Optional.of(Utils.clamp(Integer.parseInt(input), min, max)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java index 472ff03d8..f8b8bf645 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/AbstractEnigmaMenu.java @@ -4,7 +4,7 @@ import javax.swing.JMenu; -public class AbstractEnigmaMenu extends JMenu implements EnigmaMenu { +public abstract class AbstractEnigmaMenu extends JMenu implements EnigmaMenu { protected final Gui gui; protected AbstractEnigmaMenu(Gui gui) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java new file mode 100644 index 000000000..16007e7e7 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java @@ -0,0 +1,42 @@ +package org.quiltmc.enigma.gui.element.menu_bar.view; + +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.SelectionHighlightSection; +import org.quiltmc.enigma.gui.element.IntRangeConfigMenuItem; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.util.I18n; + +public class SelectionHighlightMenu extends AbstractEnigmaMenu { + private final IntRangeConfigMenuItem blinks; + private final IntRangeConfigMenuItem blinkDelay; + + protected SelectionHighlightMenu(Gui gui) { + super(gui); + + final SelectionHighlightSection config = Config.editor().selectionHighlight; + + this.blinks = new IntRangeConfigMenuItem( + gui, config.blinks, + SelectionHighlightSection.MIN_BLINKS, SelectionHighlightSection.MAX_BLINKS, + "menu.view.selection_highlight.blinks" + ); + + this.blinkDelay = new IntRangeConfigMenuItem( + gui, config.blinkDelay, + SelectionHighlightSection.MIN_BLINK_DELAY, SelectionHighlightSection.MAX_BLINK_DELAY, + "menu.view.selection_highlight.blink_delay" + ); + + this.add(this.blinks); + this.add(this.blinkDelay); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.view.selection_highlight")); + + this.blinks.retranslate(); + this.blinkDelay.retranslate(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index a9471b18c..60adf06c9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -15,6 +15,7 @@ public class ViewMenu extends AbstractEnigmaMenu { private final ThemesMenu themes; private final ScaleMenu scale; private final EntryTooltipsMenu entryTooltips; + private final SelectionHighlightMenu selectionHighlight; private final JMenuItem fontItem = new JMenuItem(); @@ -26,8 +27,10 @@ public ViewMenu(Gui gui) { this.themes = new ThemesMenu(gui); this.scale = new ScaleMenu(gui); this.entryTooltips = new EntryTooltipsMenu(gui); + this.selectionHighlight = new SelectionHighlightMenu(gui); this.add(this.themes); + this.add(this.selectionHighlight); this.add(this.languages); this.add(this.notifications); this.add(this.scale); @@ -48,6 +51,7 @@ public void retranslate() { this.scale.retranslate(); this.stats.retranslate(); this.entryTooltips.retranslate(); + this.selectionHighlight.retranslate(); this.fontItem.setText(I18n.translate("menu.view.font")); } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 7fce5ddaa..272ebe259 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -73,6 +73,15 @@ "menu.view.themes.metal": "Metal", "menu.view.themes.system": "System", "menu.view.themes.none": "None (JVM Default)", + "menu.view.selection_highlight": "Selection Highlight", + "menu.view.selection_highlight.blinks": "Blink count (%s)", + "menu.view.selection_highlight.blinks.dialog_title": "Blink Count", + "menu.view.selection_highlight.blinks.dialog_explanation": + "The number of times to blink highlighting that indicates a token that has been navigated to.", + "menu.view.selection_highlight.blink_delay": "Blink delay (%sms)", + "menu.view.selection_highlight.blink_delay.dialog_title": "Blink Delay", + "menu.view.selection_highlight.blink_delay.dialog_explanation": + "The milliseconds to blink on and off highlighting that indicates a token that has been navigated to.", "menu.view.languages": "Languages", "menu.view.scale": "Scale", "menu.view.scale.custom": "Custom...", @@ -281,6 +290,8 @@ "prompt.create_server.confirm": "Start", "prompt.password": "Password:", + "prompt.input.int_range": "Enter a whole number between %s and %s (inclusive):", + "disconnect.disconnected": "Disconnected", "disconnect.server_closed": "Server closed", "disconnect.wrong_jar": "Jar checksums don't match (you have the wrong jar)!", From 807faab833d09ff5d7674a362dc6977c1cc99cfb Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 18:35:45 -0700 Subject: [PATCH 007/110] replace 'token' with 'entry' in strings explaining selection highlight configs --- .../main/java/org/quiltmc/enigma/gui/config/EditorConfig.java | 4 ++-- enigma/src/main/resources/lang/en_us.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java index 5080f6193..2eaca59fe 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java @@ -19,8 +19,8 @@ public class EditorConfig extends ReflectiveConfig { @Comment( """ - Settings for the editor's selection highlighting; used to highlight tokens that have been navigated to. - The color of the highlight is defined per-theme (in themes/) by [syntax_pane_colors] -> selection_highlight.\ + Settings for the editor's selection highlighting; used to highlight entries that have been navigated to. + The color of the highlight is defined per-theme (in themes/) by [syntax_pane_colors] > selection_highlight.\ """ ) public final SelectionHighlightSection selectionHighlight = new SelectionHighlightSection(); diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 272ebe259..dfe843fdb 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -77,11 +77,11 @@ "menu.view.selection_highlight.blinks": "Blink count (%s)", "menu.view.selection_highlight.blinks.dialog_title": "Blink Count", "menu.view.selection_highlight.blinks.dialog_explanation": - "The number of times to blink highlighting that indicates a token that has been navigated to.", + "The number of times to blink highlighting that indicates an entry that has been navigated to.", "menu.view.selection_highlight.blink_delay": "Blink delay (%sms)", "menu.view.selection_highlight.blink_delay.dialog_title": "Blink Delay", "menu.view.selection_highlight.blink_delay.dialog_explanation": - "The milliseconds to blink on and off highlighting that indicates a token that has been navigated to.", + "The milliseconds to blink on and off highlighting that indicates an entry that has been navigated to.", "menu.view.languages": "Languages", "menu.view.scale": "Scale", "menu.view.scale.custom": "Custom...", From d0c9b7b4cc273fd6557be22439227db977d47579 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 2 Nov 2025 12:00:35 -0800 Subject: [PATCH 008/110] replace blink dialog with radio selector --- .../menu_bar/view/SelectionHighlightMenu.java | 24 +++++-- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 68 +++++++++++++++++++ enigma/src/main/resources/lang/en_us.json | 3 - 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java index 16007e7e7..2544e4568 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java @@ -5,10 +5,13 @@ import org.quiltmc.enigma.gui.config.SelectionHighlightSection; import org.quiltmc.enigma.gui.element.IntRangeConfigMenuItem; import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; +import javax.swing.JMenu; + public class SelectionHighlightMenu extends AbstractEnigmaMenu { - private final IntRangeConfigMenuItem blinks; + private final JMenu blinksMenu; private final IntRangeConfigMenuItem blinkDelay; protected SelectionHighlightMenu(Gui gui) { @@ -16,10 +19,10 @@ protected SelectionHighlightMenu(Gui gui) { final SelectionHighlightSection config = Config.editor().selectionHighlight; - this.blinks = new IntRangeConfigMenuItem( - gui, config.blinks, + this.blinksMenu = GuiUtil.createIntConfigRadioMenu( + config.blinks, SelectionHighlightSection.MIN_BLINKS, SelectionHighlightSection.MAX_BLINKS, - "menu.view.selection_highlight.blinks" + this::retranslateBlinksMenu ); this.blinkDelay = new IntRangeConfigMenuItem( @@ -28,15 +31,24 @@ protected SelectionHighlightMenu(Gui gui) { "menu.view.selection_highlight.blink_delay" ); - this.add(this.blinks); + this.add(this.blinksMenu); this.add(this.blinkDelay); + + this.retranslate(); } @Override public void retranslate() { this.setText(I18n.translate("menu.view.selection_highlight")); - this.blinks.retranslate(); + this.retranslateBlinksMenu(); this.blinkDelay.retranslate(); } + + private void retranslateBlinksMenu() { + this.blinksMenu.setText(I18n.translateFormatted( + "menu.view.selection_highlight.blinks", + Config.editor().selectionHighlight.blinks.value()) + ); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index f9e9a973a..4fd1bf829 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -18,13 +18,16 @@ import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.ActionMap; +import javax.swing.ButtonGroup; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JMenu; import javax.swing.JPanel; +import javax.swing.JRadioButtonMenuItem; import javax.swing.JToolTip; import javax.swing.JTree; import javax.swing.KeyStroke; @@ -70,7 +73,10 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; public final class GuiUtil { private GuiUtil() { @@ -472,6 +478,68 @@ private static void syncStateWithConfigImpl( }); } + /** + * Creates a {@link JMenu} containing one {@linkplain JRadioButtonMenuItem radio item} for each value between the + * passed {@code min} and {@code max}, inclusive. + * + *

Listeners are added to keep the selected radio item and the passed {@code config}'s + * {@link TrackedValue#value() value} in sync. + * + * @param config the config value to sync with + * @param min the minimum allowed value + * * this should coincide with any minimum imposed on the passed {@code config} + * @param max the maximum allowed value + * * this should coincide with any maximum imposed on the passed {@code config} + * @param onUpdate a function to run whenever the passed {@code config} changes, whether because a radio item was + * clicked or because another source updated it + * + * @return a newly created menu allowing configuration of the passed {@code config} + */ + public static JMenu createIntConfigRadioMenu( + TrackedValue config, int min, int max, Runnable onUpdate + ) { + final Map radiosByChoice = IntStream.range(min, max + 1) + .boxed() + .collect(Collectors.toMap( + Function.identity(), + choice -> { + final JRadioButtonMenuItem choiceItem = new JRadioButtonMenuItem(); + choiceItem.setText(Integer.toString(choice)); + if (choice.equals(config.value())) { + choiceItem.setSelected(true); + } + + final int finalChoice = choice; + choiceItem.addActionListener(e -> { + if (!choiceItem.isSelected()) { + config.setValue(finalChoice); + onUpdate.run(); + } + }); + + return choiceItem; + } + )); + + final ButtonGroup choicesGroup = new ButtonGroup(); + final JMenu menu = new JMenu(); + for (final JRadioButtonMenuItem radio : radiosByChoice.values()) { + choicesGroup.add(radio); + menu.add(radio); + } + + config.registerCallback(updated -> { + final JRadioButtonMenuItem choiceItem = radiosByChoice.get(updated.value()); + + if (!choiceItem.isSelected()) { + choiceItem.setSelected(true); + onUpdate.run(); + } + }); + + return menu; + } + public enum FocusCondition { /** * @see JComponent#WHEN_IN_FOCUSED_WINDOW diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index dfe843fdb..afc05a5e3 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -75,9 +75,6 @@ "menu.view.themes.none": "None (JVM Default)", "menu.view.selection_highlight": "Selection Highlight", "menu.view.selection_highlight.blinks": "Blink count (%s)", - "menu.view.selection_highlight.blinks.dialog_title": "Blink Count", - "menu.view.selection_highlight.blinks.dialog_explanation": - "The number of times to blink highlighting that indicates an entry that has been navigated to.", "menu.view.selection_highlight.blink_delay": "Blink delay (%sms)", "menu.view.selection_highlight.blink_delay.dialog_title": "Blink Delay", "menu.view.selection_highlight.blink_delay.dialog_explanation": From 7c3ce8a02c1af14887822e947c51303ada068274 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 2 Nov 2025 12:06:47 -0800 Subject: [PATCH 009/110] add elipses to blink delay menu item because it opens a dialog --- enigma/src/main/resources/lang/en_us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index afc05a5e3..2fc9a5b33 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -75,7 +75,7 @@ "menu.view.themes.none": "None (JVM Default)", "menu.view.selection_highlight": "Selection Highlight", "menu.view.selection_highlight.blinks": "Blink count (%s)", - "menu.view.selection_highlight.blink_delay": "Blink delay (%sms)", + "menu.view.selection_highlight.blink_delay": "Blink delay (%sms)...", "menu.view.selection_highlight.blink_delay.dialog_title": "Blink Delay", "menu.view.selection_highlight.blink_delay.dialog_explanation": "The milliseconds to blink on and off highlighting that indicates an entry that has been navigated to.", From 17405786cc3b9c02301da3324448e221a9672e75 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 12:39:44 -0800 Subject: [PATCH 010/110] fix GuiUtil::createIntConfigRadioMenu's action listener --- .../src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index 4fd1bf829..3387db2c3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -509,10 +509,9 @@ public static JMenu createIntConfigRadioMenu( choiceItem.setSelected(true); } - final int finalChoice = choice; choiceItem.addActionListener(e -> { - if (!choiceItem.isSelected()) { - config.setValue(finalChoice); + if (!config.value().equals(choice)) { + config.setValue(choice); onUpdate.run(); } }); From c5c15feebadf88b5e24266d5b4ad49268619380a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 07:05:45 -0700 Subject: [PATCH 011/110] add POC MarkableScrollPane --- .../enigma/gui/panel/BaseEditorPanel.java | 2 +- .../enigma/gui/panel/MarkableScrollPane.java | 358 ++++++++++++++++++ 2 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java index 3162e0b77..13e4bd047 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -73,7 +73,7 @@ public class BaseEditorPanel { protected final JPanel ui = new JPanel(); protected final JEditorPane editor = new JEditorPane(); - protected final JScrollPane editorScrollPane = new JScrollPane(this.editor); + protected final MarkableScrollPane editorScrollPane = new MarkableScrollPane(this.editor); protected final GuiController controller; protected final Gui gui; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java new file mode 100644 index 000000000..51c4fbd34 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -0,0 +1,358 @@ +package org.quiltmc.enigma.gui.panel; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.TreeMultiset; +import org.quiltmc.enigma.gui.util.ScaleUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +// TODO add marker MouseListener and MouseMotionListener support +public class MarkableScrollPane extends JScrollPane { + private static final int DEFAULT_MARKER_WIDTH = 10; + private static final int DEFAULT_MARKER_HEIGHT = 5; + + private static final int DEFAULT_MAX_CONCURRENT_MARKERS = 2; + + private final Multimap markersByPos = Multimaps.newMultimap(new HashMap<>(), TreeMultiset::create); + + private final int markerWidth; + private final int markerHeight; + + private final int maxConcurrentMarkers; + + @Nullable + private PaintState paintState; + + /** + * Constructs a scroll pane with no view, + * {@value DEFAULT_MAX_CONCURRENT_MARKERS} max concurrent markers, + * and {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bar policies. + */ + public MarkableScrollPane() { + this(null); + } + + /** + * Constructs a scroll pane displaying the passed {@code view}, + * {@value DEFAULT_MAX_CONCURRENT_MARKERS} max concurrent markers, + * and {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bar policies. + */ + public MarkableScrollPane(Component view) { + this(view, DEFAULT_MAX_CONCURRENT_MARKERS, ScrollBarPolicy.AS_NEEDED, ScrollBarPolicy.AS_NEEDED); + } + + /** + * @param view the component to display in this scroll pane's view port + * @param maxConcurrentMarkers the maximum number of markers that will be rendered at the same position; + * more markers may be added, but only up to this number of markers + * with the highest priority will be rendered + * @param verticalPolicy the vertical scroll bar policy + * @param horizontalPolicy the horizontal scroll bar policy + */ + public MarkableScrollPane( + @Nullable Component view, int maxConcurrentMarkers, + ScrollBarPolicy verticalPolicy, ScrollBarPolicy horizontalPolicy + ) { + super(view, verticalPolicy.vertical, horizontalPolicy.horizontal); + + { + // DEBUG + final int crowdedPos = 50; + this.addMarker(crowdedPos, Color.BLUE, 0); + this.addMarker(crowdedPos, Color.GREEN, 1); + // not rendered when maxConcurrentMarkers < 3 + this.addMarker(crowdedPos, Color.PINK, -1); + + this.addMarker(100, Color.CYAN, 0); + } + + this.markerWidth = ScaleUtil.scale(DEFAULT_MARKER_WIDTH); + this.markerHeight = ScaleUtil.scale(DEFAULT_MARKER_HEIGHT); + + this.maxConcurrentMarkers = maxConcurrentMarkers; + + this.addComponentListener(new ComponentListener() { + void refreshMarkers() { + MarkableScrollPane.this.clearPaintState(); + MarkableScrollPane.this.repaint(); + } + + @Override + public void componentResized(ComponentEvent e) { + this.refreshMarkers(); + } + + @Override + public void componentMoved(ComponentEvent e) { + this.refreshMarkers(); + } + + @Override + public void componentShown(ComponentEvent e) { + this.refreshMarkers(); + } + + @Override + public void componentHidden(ComponentEvent e) { + this.refreshMarkers(); + } + }); + } + + /** + * Adds a marker with passed {@code color} at the given {@code pos}. + * + * @param pos the vertical center of the marker within the space of this scroll pane's view + * @param color the color of the marker + * @param priority the priority of the marker; if there are multiple markers at the same position, only up to + * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered + * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} + */ + public Object addMarker(int pos, Color color, int priority) { + if (pos < 0) { + throw new IllegalArgumentException("pos must not be negative!"); + } + + final Marker marker = new Marker(color, priority); + this.markersByPos.put(pos, marker); + + if (this.paintState != null) { + this.paintState.pendingMarkerPositions.add(pos); + } + + return marker; + } + + /** + * Removes the passed {@code marker} if it belongs to this scroll pane. + * + * @param marker an object previously returned by {@link #addMarker(int, Color, int)} + */ + public void removeMarker(Object marker) { + if (marker instanceof Marker removing) { + final Iterator> itr = this.markersByPos.entries().iterator(); + + while (itr.hasNext()) { + final Map.Entry entry = itr.next(); + if (entry.getValue() == removing) { + itr.remove(); + if (this.paintState != null) { + this.paintState.pendingMarkerPositions.add(entry.getKey()); + } + + break; + } + } + } + } + + /** + * Removes all markers from this scroll pane. + */ + public void clearMarkers() { + this.markersByPos.clear(); + + if (this.paintState != null) { + this.paintState.clearMarkers(); + } + } + + @Override + public void paint(Graphics graphics) { + super.paint(graphics); + + if (this.paintState == null) { + this.paintState = this.createPaintState(); + } + + this.paintState.paint(graphics); + } + + private void clearPaintState() { + this.paintState = null; + } + + private PaintState createPaintState() { + final Rectangle bounds = this.getBounds(); + final Insets insets = this.getInsets(); + + final int verticalScrollBarWidth = this.verticalScrollBar == null || !this.verticalScrollBar.isVisible() + ? 0 : this.verticalScrollBar.getWidth(); + + final int viewHeight = this.getViewport().getView().getPreferredSize().height; + + final int areaHeight; + if (viewHeight < bounds.height) { + areaHeight = viewHeight; + } else { + final int horizontalScrollBarHeight = + this.horizontalScrollBar == null || !this.horizontalScrollBar.isVisible() + ? 0 : this.horizontalScrollBar.getHeight(); + + areaHeight = bounds.height - horizontalScrollBarHeight - insets.top - insets.bottom; + } + + final int areaX = (int) (bounds.getMaxX() - this.markerWidth - verticalScrollBarWidth - insets.right); + final int areaY = bounds.y + insets.top; + + return new PaintState(areaX, areaY, areaHeight, viewHeight, this.markersByPos.keySet()); + } + + public enum ScrollBarPolicy { + AS_NEEDED(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED), + ALWAYS(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS), + NEVER(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + + private final int horizontal; + private final int vertical; + + ScrollBarPolicy(int horizontal, int vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + } + + private class PaintState { + final int areaX; + final int areaY; + final int areaHeight; + + final int viewHeight; + final Set pendingMarkerPositions; + final Map paintersByPos; + + PaintState(int areaX, int areaY, int areaHeight, int viewHeight, Collection pendingMarkerPositions) { + this.areaX = areaX; + this.areaY = areaY; + this.areaHeight = areaHeight; + this.viewHeight = viewHeight; + this.pendingMarkerPositions = new HashSet<>(pendingMarkerPositions); + // order with greatest position first so lesser positions are rendered later and thus on top + this.paintersByPos = new TreeMap<>(Collections.reverseOrder()); + } + + void paint(Graphics graphics) { + for (final int pos : this.pendingMarkerPositions) { + this.refreshPainter(pos, MarkableScrollPane.this.markersByPos.get(pos)); + } + + this.pendingMarkerPositions.clear(); + + { + // DEBUG + graphics.setColor(new Color(255, 0, 0, 100)); + graphics.fillRect(this.areaX, this.areaY, MarkableScrollPane.this.markerWidth, this.areaHeight); + } + + for (final MarkersPainter painter : this.paintersByPos.values()) { + painter.paint(graphics); + } + } + + void refreshPainter(int pos, Collection markers) { + if (pos < this.viewHeight && !markers.isEmpty()) { + final int scaledPos = this.viewHeight > this.areaHeight + ? pos * this.areaHeight / this.viewHeight + : pos; + + final int markerY = Math.max(scaledPos - MarkableScrollPane.this.markerHeight / 2, 0); + final int markerHeight = Math.min(MarkableScrollPane.this.markerHeight, this.areaHeight - markerY); + + final List posMarkers = markers.stream() + .limit(MarkableScrollPane.this.maxConcurrentMarkers) + .toList(); + + this.paintersByPos.put(pos, new MarkersPainter(posMarkers, this.areaX, markerY, markerHeight)); + } else { + this.paintersByPos.remove(pos); + } + } + + void clearMarkers() { + this.paintersByPos.clear(); + this.pendingMarkerPositions.clear(); + } + } + + private record Marker(Color color, int priority) implements Comparable { + @Override + public int compareTo(@Nonnull Marker other) { + return other.priority - this.priority; + } + + class Span { + final int x; + final int width; + + Span(int x, int width) { + this.x = x; + this.width = width; + } + + Marker getMarker() { + return Marker.this; + } + } + } + + private class MarkersPainter { + final ImmutableList spans; + final int y; + final int height; + + MarkersPainter(List markers, int x, int y, int height) { + final int markerCount = markers.size(); + if (markerCount < 1) { + throw new IllegalArgumentException("no markers!"); + } + + this.y = y; + this.height = height; + + if (markerCount == 1) { + this.spans = ImmutableList.of(markers.get(0).new Span(x, MarkableScrollPane.this.markerWidth)); + } else { + final int spanWidth = MarkableScrollPane.this.markerWidth / markerCount; + // in case of non-evenly divisible width, give the most to the first marker: it has the highest priority + final int firstSpanWidth = MarkableScrollPane.this.markerWidth - spanWidth * (markerCount - 1); + + final ImmutableList.Builder spansBuilder = ImmutableList.builder(); + spansBuilder.add(markers.get(0).new Span(x, firstSpanWidth)); + + for (int i = 1; i < markerCount; i++) { + spansBuilder.add(markers.get(i).new Span(x + firstSpanWidth + spanWidth * (i - 1), spanWidth)); + } + + this.spans = spansBuilder.build(); + } + } + + void paint(Graphics graphics) { + for (final Marker.Span span : this.spans) { + graphics.setColor(span.getMarker().color); + graphics.fillRect(span.x, this.y, span.width, this.height); + } + } + } +} From dba33e4273c166c916854e9f3314e96c93fd8124 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 11:55:20 -0700 Subject: [PATCH 012/110] rename and make abstract BaseEditorPanel -> AbstractEditorPanel make only EditorPanel use MarkablScrollPane --- .../main/java/org/quiltmc/enigma/gui/Gui.java | 4 ++-- ...torPanel.java => AbstractEditorPanel.java} | 24 ++++++++++--------- .../gui/panel/DeclarationSnippetPanel.java | 9 ++++++- .../quiltmc/enigma/gui/panel/EditorPanel.java | 8 ++++++- 4 files changed, 30 insertions(+), 15 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/{BaseEditorPanel.java => AbstractEditorPanel.java} (97%) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java index dc37fd791..ae9700c4d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java @@ -33,7 +33,7 @@ import org.quiltmc.enigma.gui.element.EditorTabbedPane; import org.quiltmc.enigma.gui.element.MainWindow; import org.quiltmc.enigma.gui.element.menu_bar.MenuBar; -import org.quiltmc.enigma.gui.panel.BaseEditorPanel; +import org.quiltmc.enigma.gui.panel.AbstractEditorPanel; import org.quiltmc.enigma.gui.panel.EditorPanel; import org.quiltmc.enigma.gui.panel.IdentifierPanel; import org.quiltmc.enigma.gui.renderer.MessageListCellRenderer; @@ -421,7 +421,7 @@ public void setMappingsFile(Path path) { this.updateUiState(); } - public void showTokens(BaseEditorPanel editor, List tokens) { + public void showTokens(AbstractEditorPanel editor, List tokens) { if (tokens.size() > 1) { this.openDocker(CallsTreeDocker.class); this.controller.setTokenHandle(editor.getClassHandle().copy()); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java similarity index 97% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java index 13e4bd047..3341cf82d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java @@ -70,10 +70,10 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; -public class BaseEditorPanel { +public abstract class AbstractEditorPanel { protected final JPanel ui = new JPanel(); protected final JEditorPane editor = new JEditorPane(); - protected final MarkableScrollPane editorScrollPane = new MarkableScrollPane(this.editor); + protected final S editorScrollPane = this.createEditorScrollPane(this.editor); protected final GuiController controller; protected final Gui gui; @@ -110,7 +110,7 @@ public class BaseEditorPanel { private SourceBounds sourceBounds = new DefaultBounds(); protected boolean settingSource; - public BaseEditorPanel(Gui gui) { + public AbstractEditorPanel(Gui gui) { this.gui = gui; this.controller = gui.getController(); @@ -146,6 +146,8 @@ public BaseEditorPanel(Gui gui) { this.retryButton.addActionListener(e -> this.redecompileClass()); } + protected abstract S createEditorScrollPane(JEditorPane editor); + protected void installEditorRuler(int lineOffset) { final SyntaxPaneProperties.Colors syntaxColors = Config.getCurrentSyntaxPaneColors(); @@ -189,14 +191,14 @@ protected CompletableFuture setClassHandleImpl( this.classHandler = ClassHandler.of(handle, new ClassHandleListener() { @Override public void onMappedSourceChanged(ClassHandle h, Result res) { - BaseEditorPanel.this.handleDecompilerResult(res, snippetFactory); + AbstractEditorPanel.this.handleDecompilerResult(res, snippetFactory); } @Override public void onInvalidate(ClassHandle h, InvalidationType t) { SwingUtilities.invokeLater(() -> { if (t == InvalidationType.FULL) { - BaseEditorPanel.this.setDisplayMode(DisplayMode.IN_PROGRESS); + AbstractEditorPanel.this.setDisplayMode(DisplayMode.IN_PROGRESS); } }); } @@ -636,7 +638,7 @@ protected Token navigateToTokenImpl(@Nullable Token unBoundedToken) { protected Object addHighlight(Token token, HighlightPainter highlightPainter) { try { - return BaseEditorPanel.this.editor.getHighlighter() + return AbstractEditorPanel.this.editor.getHighlighter() .addHighlight(token.start, token.end, highlightPainter); } catch (BadLocationException ex) { return null; @@ -694,7 +696,7 @@ private class SelectionHighlightHandler extends Timer { this.addActionListener(e -> { if (this.counter < this.counterMax) { if (this.counter % BLINK_INTERVAL == 0) { - this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); + this.highlight = AbstractEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); } else { this.removeHighlight(); } @@ -708,15 +710,15 @@ private class SelectionHighlightHandler extends Timer { void removeHighlight() { if (this.highlight != null) { - BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); + AbstractEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); } } void finish() { this.stop(); this.removeHighlight(); - if (BaseEditorPanel.this.selectionHighlightHandler == this) { - BaseEditorPanel.this.selectionHighlightHandler = null; + if (AbstractEditorPanel.this.selectionHighlightHandler == this) { + AbstractEditorPanel.this.selectionHighlightHandler = null; } } } @@ -876,7 +878,7 @@ public int start() { @Override public int end() { - return BaseEditorPanel.this.source.toString().length(); + return AbstractEditorPanel.this.source.toString().length(); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index e9488afa7..faf9bbdf0 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -45,6 +45,8 @@ import org.quiltmc.enigma.util.Result; import org.tinylog.Logger; +import javax.swing.JEditorPane; +import javax.swing.JScrollPane; import java.awt.Color; import java.util.Comparator; import java.util.Optional; @@ -54,7 +56,7 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; import static java.util.Comparator.comparingInt; -public class DeclarationSnippetPanel extends BaseEditorPanel { +public class DeclarationSnippetPanel extends AbstractEditorPanel { private static final String NO_ENTRY_DEFINITION = "no entry definition!"; private static final String NO_TOKEN_RANGE = "no token range!"; // used to compose error messages @@ -91,6 +93,11 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl }); } + @Override + protected JScrollPane createEditorScrollPane(JEditorPane editor) { + return new JScrollPane(editor); + } + private Snippet createSnippet(DecompiledClassSource source, Entry targetEntry) { return this.resolveTarget(source, targetEntry) .map(target -> this.findSnippet(source, target.token, target.entry)) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 4d93763ef..eade5d190 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -39,6 +39,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; import javax.swing.JComponent; +import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -51,7 +52,7 @@ import static javax.swing.SwingUtilities.isDescendingFrom; import static java.awt.event.InputEvent.CTRL_DOWN_MASK; -public class EditorPanel extends BaseEditorPanel { +public class EditorPanel extends AbstractEditorPanel { private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; @@ -156,6 +157,11 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } + @Override + protected MarkableScrollPane createEditorScrollPane(JEditorPane editor) { + return new MarkableScrollPane(editor); + } + public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { From 78af2872d02971f723a859c7e7f9beb23131f461 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 12:51:42 -0700 Subject: [PATCH 013/110] add markers for EditorPanel tokens --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index eade5d190..e61870674 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -6,9 +6,11 @@ import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; +import org.quiltmc.enigma.api.source.TokenStore; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; +import org.quiltmc.enigma.gui.config.theme.properties.composite.SyntaxPaneProperties; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; import org.quiltmc.enigma.gui.element.EditorPopupMenu; import org.quiltmc.enigma.gui.element.NavigatorPanel; @@ -18,7 +20,9 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.syntaxpain.PairsMarker; +import org.tinylog.Logger; +import java.awt.Color; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.KeyboardFocusManager; @@ -44,6 +48,7 @@ import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; +import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import javax.swing.text.TextAction; @@ -53,6 +58,12 @@ import static java.awt.event.InputEvent.CTRL_DOWN_MASK; public class EditorPanel extends AbstractEditorPanel { + private static final int DEOBFUSCATED_PRIORITY = 0; + private static final int PROPOSED_PRIORITY = DEOBFUSCATED_PRIORITY + 1; + private static final int FALLBACK_PRIORITY = PROPOSED_PRIORITY + 1; + private static final int OBFUSCATED_PRIORITY = FALLBACK_PRIORITY + 1; + private static final int DEBUG_PRIORITY = OBFUSCATED_PRIORITY + 1; + private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; @@ -152,6 +163,54 @@ public void keyTyped(KeyEvent event) { if (this.navigatorPanel != null) { this.navigatorPanel.resetEntries(source.getIndex().declarations()); } + + this.editorScrollPane.clearMarkers(); + + final SyntaxPaneProperties.Colors colors = Config.getCurrentSyntaxPaneColors(); + final TokenStore tokenStore = source.getTokenStore(); + tokenStore.getByType().forEach((type, tokens) -> { + final Color nonFallbackColor; + final int nonFallbackPriority; + switch (type) { + case OBFUSCATED -> { + nonFallbackColor = colors.obfuscatedOutline.value(); + nonFallbackPriority = OBFUSCATED_PRIORITY; + } + case DEOBFUSCATED -> { + nonFallbackColor = colors.deobfuscatedOutline.value(); + nonFallbackPriority = DEOBFUSCATED_PRIORITY; + } + case JAR_PROPOSED, DYNAMIC_PROPOSED -> { + nonFallbackColor = colors.proposedOutline.value(); + nonFallbackPriority = PROPOSED_PRIORITY; + } + case DEBUG -> { + nonFallbackColor = colors.debugTokenOutline.value(); + nonFallbackPriority = DEBUG_PRIORITY; + } + default -> throw new AssertionError(); + } + + for (final Token token : tokens) { + final Color color; + final int priority; + if (tokenStore.isFallback(token)) { + color = colors.fallbackOutline.value(); + priority = FALLBACK_PRIORITY; + } else { + color = nonFallbackColor; + priority = nonFallbackPriority; + } + + try { + final int tokenPos = (int) this.editor.modelToView2D(token.start).getCenterY(); + + this.editorScrollPane.addMarker(tokenPos, color, priority); + } catch (BadLocationException e) { + Logger.warn("Tried to add marker for token with bad location: " + token); + } + } + }); }); this.ui.putClientProperty(EditorPanel.class, this); @@ -220,6 +279,8 @@ protected CompletableFuture setClassHandleImpl( ) { final CompletableFuture superFuture = super.setClassHandleImpl(old, handle, snippetFactory); + this.editorScrollPane.clearMarkers(); + handle.addListener(new ClassHandleListener() { @Override public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { From 37d2b510a6cbf6f859326b1dc7c3b6e2be96f4d1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 12:52:20 -0700 Subject: [PATCH 014/110] remove debug code from MarkableScrollPane --- .../enigma/gui/panel/MarkableScrollPane.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 51c4fbd34..e6ecdb230 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -76,17 +76,6 @@ public MarkableScrollPane( ) { super(view, verticalPolicy.vertical, horizontalPolicy.horizontal); - { - // DEBUG - final int crowdedPos = 50; - this.addMarker(crowdedPos, Color.BLUE, 0); - this.addMarker(crowdedPos, Color.GREEN, 1); - // not rendered when maxConcurrentMarkers < 3 - this.addMarker(crowdedPos, Color.PINK, -1); - - this.addMarker(100, Color.CYAN, 0); - } - this.markerWidth = ScaleUtil.scale(DEFAULT_MARKER_WIDTH); this.markerHeight = ScaleUtil.scale(DEFAULT_MARKER_HEIGHT); @@ -259,12 +248,6 @@ void paint(Graphics graphics) { this.pendingMarkerPositions.clear(); - { - // DEBUG - graphics.setColor(new Color(255, 0, 0, 100)); - graphics.fillRect(this.areaX, this.areaY, MarkableScrollPane.this.markerWidth, this.areaHeight); - } - for (final MarkersPainter painter : this.paintersByPos.values()) { painter.paint(graphics); } From 9d480d8a4b2f379a549852fa14ff69f84d88db56 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 14:27:17 -0700 Subject: [PATCH 015/110] make markers for each token type toggleable --- .../enigma/gui/config/EditorConfig.java | 3 + .../gui/config/EntryMarkersSection.java | 25 ++++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 131 ++++++++++++------ 3 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java index 2eaca59fe..a2f39c31f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java @@ -17,6 +17,9 @@ public class EditorConfig extends ReflectiveConfig { @Comment("Settings for editors' entry tooltips.") public final EntryTooltipsSection entryTooltips = new EntryTooltipsSection(); + @Comment("Settings for markers on the right side of the editor indicating where different entry types are.") + public final EntryMarkersSection entryMarkers = new EntryMarkersSection(); + @Comment( """ Settings for the editor's selection highlighting; used to highlight entries that have been navigated to. diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java new file mode 100644 index 000000000..9feb3fb2c --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java @@ -0,0 +1,25 @@ +package org.quiltmc.enigma.gui.config; + +import org.quiltmc.config.api.ReflectiveConfig; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.annotations.SerializedNameConvention; +import org.quiltmc.config.api.metadata.NamingSchemes; +import org.quiltmc.config.api.values.TrackedValue; + +@SerializedNameConvention(NamingSchemes.SNAKE_CASE) +public class EntryMarkersSection extends ReflectiveConfig.Section { + @Comment("Whether markers can be clicked to navigate to their corresponding entries.") + public final TrackedValue interactable = this.value(true); + + @Comment("Whether obfuscated entries should be marked.") + public final TrackedValue markObfuscated = this.value(true); + + @Comment("Whether fallback entries should be marked.") + public final TrackedValue markFallback = this.value(true); + + @Comment("Whether proposed entries should be marked.") + public final TrackedValue markProposed = this.value(false); + + @Comment("Whether deobfuscated entries should be marked.") + public final TrackedValue markDeobfuscated = this.value(false); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index e61870674..ae2dca2c3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -7,8 +7,10 @@ import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.TokenStore; +import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.EntryMarkersSection; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; import org.quiltmc.enigma.gui.config.theme.properties.composite.SyntaxPaneProperties; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; @@ -41,7 +43,9 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.Optional; import java.util.function.Function; +import javax.annotation.Nonnull; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JPanel; @@ -72,6 +76,9 @@ public class EditorPanel extends AbstractEditorPanel { private final List listeners = new ArrayList<>(); + @Nonnull + private MarkablePredicate markablePredicate; + public EditorPanel(Gui gui, NavigatorPanel navigator) { super(gui); @@ -164,56 +171,63 @@ public void keyTyped(KeyEvent event) { this.navigatorPanel.resetEntries(source.getIndex().declarations()); } - this.editorScrollPane.clearMarkers(); + this.refreshMarkers(source); + }); - final SyntaxPaneProperties.Colors colors = Config.getCurrentSyntaxPaneColors(); - final TokenStore tokenStore = source.getTokenStore(); - tokenStore.getByType().forEach((type, tokens) -> { - final Color nonFallbackColor; - final int nonFallbackPriority; - switch (type) { - case OBFUSCATED -> { - nonFallbackColor = colors.obfuscatedOutline.value(); - nonFallbackPriority = OBFUSCATED_PRIORITY; - } - case DEOBFUSCATED -> { - nonFallbackColor = colors.deobfuscatedOutline.value(); - nonFallbackPriority = DEOBFUSCATED_PRIORITY; - } - case JAR_PROPOSED, DYNAMIC_PROPOSED -> { - nonFallbackColor = colors.proposedOutline.value(); - nonFallbackPriority = PROPOSED_PRIORITY; - } - case DEBUG -> { - nonFallbackColor = colors.debugTokenOutline.value(); - nonFallbackPriority = DEBUG_PRIORITY; - } - default -> throw new AssertionError(); - } + this.ui.putClientProperty(EditorPanel.class, this); - for (final Token token : tokens) { - final Color color; - final int priority; - if (tokenStore.isFallback(token)) { - color = colors.fallbackOutline.value(); - priority = FALLBACK_PRIORITY; - } else { - color = nonFallbackColor; - priority = nonFallbackPriority; - } + this.markablePredicate = MarkablePredicate.of(); + final EntryMarkersSection markersConfig = Config.editor().entryMarkers; + markersConfig.markObfuscated.registerCallback(obfuscated -> { + if (obfuscated.value() != this.markablePredicate.obfuscated) { + this.refreshMarkablePredicate(); + } + }); + markersConfig.markFallback.registerCallback(fallback -> { + if (fallback.value() != this.markablePredicate.fallback) { + this.refreshMarkablePredicate(); + } + }); + markersConfig.markProposed.registerCallback(proposed -> { + if (proposed.value() != this.markablePredicate.proposed) { + this.refreshMarkablePredicate(); + } + }); + markersConfig.markDeobfuscated.registerCallback(deobfuscated -> { + if (deobfuscated.value() != this.markablePredicate.deobfuscated) { + this.refreshMarkablePredicate(); + } + }); + } + private void refreshMarkablePredicate() { + this.markablePredicate = MarkablePredicate.of(); + + final DecompiledClassSource source = this.getSource(); + if (source != null) { + this.refreshMarkers(source); + } else { + this.editorScrollPane.clearMarkers(); + } + } + + private void refreshMarkers(DecompiledClassSource source) { + this.editorScrollPane.clearMarkers(); + + final TokenStore tokenStore = source.getTokenStore(); + tokenStore.getByType().forEach((type, tokens) -> { + for (final Token token : tokens) { + this.markablePredicate.getParams(token, type, tokenStore).ifPresent(params -> { try { final int tokenPos = (int) this.editor.modelToView2D(token.start).getCenterY(); - this.editorScrollPane.addMarker(tokenPos, color, priority); + this.editorScrollPane.addMarker(tokenPos, params.color, params.priority); } catch (BadLocationException e) { Logger.warn("Tried to add marker for token with bad location: " + token); } - } - }); + }); + } }); - - this.ui.putClientProperty(EditorPanel.class, this); } @Override @@ -515,4 +529,41 @@ void removeExternalListeners() { Toolkit.getDefaultToolkit().removeAWTEventListener(this.globalKeyListener); } } + + private record MarkablePredicate(boolean obfuscated, boolean fallback, boolean proposed, boolean deobfuscated) { + static MarkablePredicate of() { + final EntryMarkersSection markersConfig = Config.editor().entryMarkers; + return new MarkablePredicate( + markersConfig.markObfuscated.value(), + markersConfig.markFallback.value(), + markersConfig.markProposed.value(), + markersConfig.markDeobfuscated.value() + ); + } + + Optional getParams(Token token, TokenType type, TokenStore tokenStore) { + final SyntaxPaneProperties.Colors colors = Config.getCurrentSyntaxPaneColors(); + if (tokenStore.isFallback(token)) { + return this.fallback + ? Optional.of(new MarkerParams(colors.fallbackOutline.value(), FALLBACK_PRIORITY)) + : Optional.empty(); + } else { + return switch (type) { + case OBFUSCATED -> this.obfuscated + ? Optional.of(new MarkerParams(colors.obfuscatedOutline.value(), OBFUSCATED_PRIORITY)) + : Optional.empty(); + case DEOBFUSCATED -> this.deobfuscated + ? Optional.of(new MarkerParams(colors.deobfuscated.value(), DEOBFUSCATED_PRIORITY)) + : Optional.empty(); + case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposed + ? Optional.of(new MarkerParams(colors.proposedOutline.value(), PROPOSED_PRIORITY)) + : Optional.empty(); + // these only appear if debugTokenHighlights is true, so no need for a separate marker config + case DEBUG -> Optional.of(new MarkerParams(colors.debugTokenOutline.value(), DEBUG_PRIORITY)); + }; + } + } + } + + private record MarkerParams(Color color, int priority) { } } From 0af32ba37f13c94e1070f8ecde32fcb40fb6ce1c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 15:36:39 -0700 Subject: [PATCH 016/110] allow configuring markers via the gui --- .../menu_bar/view/EntryMarkersMenu.java | 75 +++++++++++++++++++ .../gui/element/menu_bar/view/ViewMenu.java | 4 + .../quiltmc/enigma/gui/panel/EditorPanel.java | 2 +- enigma/src/main/resources/lang/en_us.json | 7 ++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java new file mode 100644 index 000000000..b333bd866 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java @@ -0,0 +1,75 @@ +package org.quiltmc.enigma.gui.element.menu_bar.view; + +import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.config.EntryMarkersSection; +import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; + +public class EntryMarkersMenu extends AbstractEnigmaMenu { + private static void syncStateWithConfig(JCheckBoxMenuItem box, TrackedValue config) { + box.setState(config.value()); + + box.addActionListener(e -> { + final boolean checked = box.getState(); + if (checked != config.value()) { + config.setValue(checked); + } + }); + + config.registerCallback(updated -> { + final boolean configured = updated.value(); + if (configured != box.getState()) { + box.setState(configured); + } + }); + } + + private final JCheckBoxMenuItem interactable = new JCheckBoxMenuItem(); + + private final JMenu markMenu = new JMenu(); + private final JCheckBoxMenuItem markObfuscated = new JCheckBoxMenuItem(); + private final JCheckBoxMenuItem markFallback = new JCheckBoxMenuItem(); + private final JCheckBoxMenuItem markProposed = new JCheckBoxMenuItem(); + private final JCheckBoxMenuItem markDeobfuscated = new JCheckBoxMenuItem(); + + public EntryMarkersMenu(Gui gui) { + super(gui); + + this.add(this.interactable); + + this.markMenu.add(this.markObfuscated); + this.markMenu.add(this.markFallback); + this.markMenu.add(this.markProposed); + this.markMenu.add(this.markDeobfuscated); + + this.add(this.markMenu); + + final EntryMarkersSection markerConfig = Config.editor().entryMarkers; + syncStateWithConfig(this.interactable, markerConfig.interactable); + syncStateWithConfig(this.markObfuscated, markerConfig.markObfuscated); + syncStateWithConfig(this.markFallback, markerConfig.markFallback); + syncStateWithConfig(this.markProposed, markerConfig.markProposed); + syncStateWithConfig(this.markDeobfuscated, markerConfig.markDeobfuscated); + + this.retranslate(); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.view.entry_markers")); + + this.interactable.setText(I18n.translate("menu.view.entry_markers.interactable")); + + this.markMenu.setText(I18n.translate("menu.view.entry_markers.mark")); + + this.markObfuscated.setText(I18n.translate("menu.view.entry_markers.mark.obfuscated")); + this.markFallback.setText(I18n.translate("menu.view.entry_markers.mark.fallback")); + this.markProposed.setText(I18n.translate("menu.view.entry_markers.mark.proposed")); + this.markDeobfuscated.setText(I18n.translate("menu.view.entry_markers.mark.deobfuscated")); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java index 60adf06c9..0b0cc26a2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/ViewMenu.java @@ -16,6 +16,7 @@ public class ViewMenu extends AbstractEnigmaMenu { private final ScaleMenu scale; private final EntryTooltipsMenu entryTooltips; private final SelectionHighlightMenu selectionHighlight; + private final EntryMarkersMenu entryMarkers; private final JMenuItem fontItem = new JMenuItem(); @@ -28,6 +29,7 @@ public ViewMenu(Gui gui) { this.scale = new ScaleMenu(gui); this.entryTooltips = new EntryTooltipsMenu(gui); this.selectionHighlight = new SelectionHighlightMenu(gui); + this.entryMarkers = new EntryMarkersMenu(gui); this.add(this.themes); this.add(this.selectionHighlight); @@ -36,6 +38,7 @@ public ViewMenu(Gui gui) { this.add(this.scale); this.add(this.stats); this.add(this.entryTooltips); + this.add(this.entryMarkers); this.add(this.fontItem); this.fontItem.addActionListener(e -> this.onFontClicked(this.gui)); @@ -52,6 +55,7 @@ public void retranslate() { this.stats.retranslate(); this.entryTooltips.retranslate(); this.selectionHighlight.retranslate(); + this.entryMarkers.retranslate(); this.fontItem.setText(I18n.translate("menu.view.font")); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index ae2dca2c3..02b535ac3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -553,7 +553,7 @@ Optional getParams(Token token, TokenType type, TokenStore tokenSt ? Optional.of(new MarkerParams(colors.obfuscatedOutline.value(), OBFUSCATED_PRIORITY)) : Optional.empty(); case DEOBFUSCATED -> this.deobfuscated - ? Optional.of(new MarkerParams(colors.deobfuscated.value(), DEOBFUSCATED_PRIORITY)) + ? Optional.of(new MarkerParams(colors.deobfuscatedOutline.value(), DEOBFUSCATED_PRIORITY)) : Optional.empty(); case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposed ? Optional.of(new MarkerParams(colors.proposedOutline.value(), PROPOSED_PRIORITY)) diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 2fc9a5b33..816a08ae2 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -91,6 +91,13 @@ "menu.view.entry_tooltips": "Entry Tooltips", "menu.view.entry_tooltips.enable": "Enable tooltips", "menu.view.entry_tooltips.interactable": "Allow tooltip interaction", + "menu.view.entry_markers": "Entry Markers", + "menu.view.entry_markers.interactable": "Clickable markers", + "menu.view.entry_markers.mark": "Mark entries", + "menu.view.entry_markers.mark.obfuscated": "Obfuscated", + "menu.view.entry_markers.mark.fallback": "Fallback", + "menu.view.entry_markers.mark.proposed": "Proposed", + "menu.view.entry_markers.mark.deobfuscated": "Deobfuscated", "menu.view.font": "Fonts...", "menu.view.change.title": "Changes", "menu.view.change.summary": "Changes will be applied after the next restart.", From c1c1cdc6c11888050f61b24b8588e9f972266d47 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 26 Oct 2025 21:42:04 -0700 Subject: [PATCH 017/110] navigate to entry on marker click --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 10 +- .../enigma/gui/panel/MarkableScrollPane.java | 93 +++++++++++++++++-- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 02b535ac3..c0e293abd 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -117,7 +117,7 @@ public void focusLost(FocusEvent e) { this.editor.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e1) { - if ((e1.getModifiersEx() & CTRL_DOWN_MASK) != 0 && e1.getButton() == MouseEvent.BUTTON1) { + if (!e1.isConsumed() && (e1.getModifiersEx() & CTRL_DOWN_MASK) != 0 && e1.getButton() == MouseEvent.BUTTON1) { // ctrl + left click EditorPanel.this.navigateToCursorReference(); } @@ -221,7 +221,13 @@ private void refreshMarkers(DecompiledClassSource source) { try { final int tokenPos = (int) this.editor.modelToView2D(token.start).getCenterY(); - this.editorScrollPane.addMarker(tokenPos, params.color, params.priority); + // TODO show/hide tooltip on mouse enter/exit + this.editorScrollPane.addMarker(tokenPos, params.color, params.priority, new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorPanel.this.navigateToToken(token); + } + }); } catch (BadLocationException e) { Logger.warn("Tried to add marker for token with bad location: " + token); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index e6ecdb230..4dee643d9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -4,6 +4,7 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.TreeMultiset; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; import javax.annotation.Nonnull; @@ -14,9 +15,12 @@ import java.awt.Component; import java.awt.Graphics; import java.awt.Insets; +import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,10 +28,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Predicate; -// TODO add marker MouseListener and MouseMotionListener support public class MarkableScrollPane extends JScrollPane { private static final int DEFAULT_MARKER_WIDTH = 10; private static final int DEFAULT_MARKER_HEIGHT = 5; @@ -43,6 +49,7 @@ public class MarkableScrollPane extends JScrollPane { @Nullable private PaintState paintState; + private MouseListener viewMouseListener; /** * Constructs a scroll pane with no view, @@ -109,6 +116,60 @@ public void componentHidden(ComponentEvent e) { }); } + @Override + public void setViewportView(Component view) { + final Component oldView = this.getViewport().getView(); + if (oldView != null) { + oldView.removeMouseListener(this.viewMouseListener); + } + + super.setViewportView(view); + + this.viewMouseListener = new MouseListener() { + private void tryMarkerListeners(MouseEvent e, BiConsumer eventAction) { + if (MarkableScrollPane.this.paintState != null) { + final Point relativePos = GuiUtil + .getRelativePos(MarkableScrollPane.this, e.getXOnScreen(), e.getYOnScreen()); + MarkableScrollPane.this.paintState + .findSpanContaining( + relativePos.x, relativePos.y, + span -> span.getMarker().mouseListener.isPresent() + ) + .map(span -> span.getMarker().mouseListener.orElseThrow()) + .ifPresent(listener -> eventAction.accept(listener, e)); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + this.tryMarkerListeners(e, MouseListener::mouseClicked); + } + + @Override + public void mousePressed(MouseEvent e) { + this.tryMarkerListeners(e, MouseListener::mousePressed); + } + + @Override + public void mouseReleased(MouseEvent e) { + this.tryMarkerListeners(e, MouseListener::mouseReleased); + } + + @Override + public void mouseEntered(MouseEvent e) { + this.tryMarkerListeners(e, MouseListener::mouseEntered); + } + + @Override + public void mouseExited(MouseEvent e) { + this.tryMarkerListeners(e, MouseListener::mouseExited); + } + }; + + // add the listener to the view because this doesn't receive clicks within the view + view.addMouseListener(this.viewMouseListener); + } + /** * Adds a marker with passed {@code color} at the given {@code pos}. * @@ -118,12 +179,12 @@ public void componentHidden(ComponentEvent e) { * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} */ - public Object addMarker(int pos, Color color, int priority) { + public Object addMarker(int pos, Color color, int priority, @Nullable MouseListener mouseListener) { if (pos < 0) { throw new IllegalArgumentException("pos must not be negative!"); } - final Marker marker = new Marker(color, priority); + final Marker marker = new Marker(color, priority, Optional.ofNullable(mouseListener)); this.markersByPos.put(pos, marker); if (this.paintState != null) { @@ -136,7 +197,7 @@ public Object addMarker(int pos, Color color, int priority) { /** * Removes the passed {@code marker} if it belongs to this scroll pane. * - * @param marker an object previously returned by {@link #addMarker(int, Color, int)} + * @param marker an object previously returned by {@link #addMarker(int, Color, int, MouseListener)} */ public void removeMarker(Object marker) { if (marker instanceof Marker removing) { @@ -189,7 +250,8 @@ private PaintState createPaintState() { final int verticalScrollBarWidth = this.verticalScrollBar == null || !this.verticalScrollBar.isVisible() ? 0 : this.verticalScrollBar.getWidth(); - final int viewHeight = this.getViewport().getView().getPreferredSize().height; + final Component view = this.getViewport().getView(); + final int viewHeight = view.getPreferredSize().height; final int areaHeight; if (viewHeight < bounds.height) { @@ -276,9 +338,28 @@ void clearMarkers() { this.paintersByPos.clear(); this.pendingMarkerPositions.clear(); } + + Optional findSpanContaining(int x, int y, Predicate predicate) { + if (this.areaContains(x, y)) { + return this.paintersByPos.values().stream() + .filter(painter -> painter.y <= y && y <= painter.y + painter.height) + .flatMap(painter -> painter.spans.stream()) + .filter(predicate) + .filter(span -> span.x <= x && x <= span.x + span.width) + .findFirst(); + } else { + return Optional.empty(); + } + } + + boolean areaContains(int x, int y) { + return this.areaX <= x && x <= this.areaX + MarkableScrollPane.this.markerWidth + && this.areaY <= y && y <= this.areaY + this.areaHeight; + } } - private record Marker(Color color, int priority) implements Comparable { + private record Marker(Color color, int priority, Optional mouseListener) + implements Comparable { @Override public int compareTo(@Nonnull Marker other) { return other.priority - this.priority; From d13e0e18456e17354b5fa39e00ffda4544639a2a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 15:15:35 -0700 Subject: [PATCH 018/110] tweak paintersByPos order handling, cleanup --- .../enigma/gui/panel/MarkableScrollPane.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 4dee643d9..eef54a7fc 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.panel; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; @@ -83,6 +84,8 @@ public MarkableScrollPane( ) { super(view, verticalPolicy.vertical, horizontalPolicy.horizontal); + Preconditions.checkArgument(maxConcurrentMarkers > 0, "maxConcurrentMarkers must be positive!"); + this.markerWidth = ScaleUtil.scale(DEFAULT_MARKER_WIDTH); this.markerHeight = ScaleUtil.scale(DEFAULT_MARKER_HEIGHT); @@ -285,13 +288,15 @@ public enum ScrollBarPolicy { } private class PaintState { + // order with greatest position first so lesser positions are painted later and thus on top + final TreeMap paintersByPos = new TreeMap<>(Collections.reverseOrder()); + final int areaX; final int areaY; final int areaHeight; final int viewHeight; final Set pendingMarkerPositions; - final Map paintersByPos; PaintState(int areaX, int areaY, int areaHeight, int viewHeight, Collection pendingMarkerPositions) { this.areaX = areaX; @@ -299,8 +304,6 @@ private class PaintState { this.areaHeight = areaHeight; this.viewHeight = viewHeight; this.pendingMarkerPositions = new HashSet<>(pendingMarkerPositions); - // order with greatest position first so lesser positions are rendered later and thus on top - this.paintersByPos = new TreeMap<>(Collections.reverseOrder()); } void paint(Graphics graphics) { @@ -341,7 +344,9 @@ void clearMarkers() { Optional findSpanContaining(int x, int y, Predicate predicate) { if (this.areaContains(x, y)) { - return this.paintersByPos.values().stream() + // default ordering puts greatest positions first so lesser positions are painted on top + // check in reverse order so the lesser positions (on top) are checked first + return this.paintersByPos.descendingMap().values().stream() .filter(painter -> painter.y <= y && y <= painter.y + painter.height) .flatMap(painter -> painter.spans.stream()) .filter(predicate) @@ -387,9 +392,7 @@ private class MarkersPainter { MarkersPainter(List markers, int x, int y, int height) { final int markerCount = markers.size(); - if (markerCount < 1) { - throw new IllegalArgumentException("no markers!"); - } + Preconditions.checkArgument(markerCount > 0, "no markers!"); this.y = y; this.height = height; From 3133dc11a733783b9d6f7e4a6a3974ee7afacf57 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 16:43:10 -0700 Subject: [PATCH 019/110] refactor+rename MarkablePredicate -> MarkerManager --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 193 ++++++++++-------- 1 file changed, 113 insertions(+), 80 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index c0e293abd..df31229f5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -1,6 +1,9 @@ package org.quiltmc.enigma.gui.panel; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import com.google.common.collect.ImmutableMap; +import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; @@ -12,7 +15,7 @@ import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.config.EntryMarkersSection; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; -import org.quiltmc.enigma.gui.config.theme.properties.composite.SyntaxPaneProperties; +import org.quiltmc.enigma.gui.config.theme.properties.ThemeProperties; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; import org.quiltmc.enigma.gui.element.EditorPopupMenu; import org.quiltmc.enigma.gui.element.NavigatorPanel; @@ -42,10 +45,10 @@ import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.Optional; import java.util.function.Function; -import javax.annotation.Nonnull; +import java.util.function.Predicate; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JPanel; @@ -62,12 +65,6 @@ import static java.awt.event.InputEvent.CTRL_DOWN_MASK; public class EditorPanel extends AbstractEditorPanel { - private static final int DEOBFUSCATED_PRIORITY = 0; - private static final int PROPOSED_PRIORITY = DEOBFUSCATED_PRIORITY + 1; - private static final int FALLBACK_PRIORITY = PROPOSED_PRIORITY + 1; - private static final int OBFUSCATED_PRIORITY = FALLBACK_PRIORITY + 1; - private static final int DEBUG_PRIORITY = OBFUSCATED_PRIORITY + 1; - private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; @@ -76,8 +73,8 @@ public class EditorPanel extends AbstractEditorPanel { private final List listeners = new ArrayList<>(); - @Nonnull - private MarkablePredicate markablePredicate; + @NonNull + private MarkerManager markerManager = this.createMarkerManager(); public EditorPanel(Gui gui, NavigatorPanel navigator) { super(gui); @@ -176,39 +173,26 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); - this.markablePredicate = MarkablePredicate.of(); final EntryMarkersSection markersConfig = Config.editor().entryMarkers; - markersConfig.markObfuscated.registerCallback(obfuscated -> { - if (obfuscated.value() != this.markablePredicate.obfuscated) { - this.refreshMarkablePredicate(); - } - }); - markersConfig.markFallback.registerCallback(fallback -> { - if (fallback.value() != this.markablePredicate.fallback) { - this.refreshMarkablePredicate(); - } - }); - markersConfig.markProposed.registerCallback(proposed -> { - if (proposed.value() != this.markablePredicate.proposed) { - this.refreshMarkablePredicate(); - } - }); - markersConfig.markDeobfuscated.registerCallback(deobfuscated -> { - if (deobfuscated.value() != this.markablePredicate.deobfuscated) { - this.refreshMarkablePredicate(); - } - }); + this.registerMarkerRefresher(markersConfig.markObfuscated, MarkerManager::marksObfuscated); + this.registerMarkerRefresher(markersConfig.markFallback, MarkerManager::marksFallback); + this.registerMarkerRefresher(markersConfig.markProposed, MarkerManager::marksProposed); + this.registerMarkerRefresher(markersConfig.markDeobfuscated, MarkerManager::marksDeobfuscated); } - private void refreshMarkablePredicate() { - this.markablePredicate = MarkablePredicate.of(); + private void registerMarkerRefresher(TrackedValue config, Predicate handlerGetter) { + config.registerCallback(updated -> { + if (updated.value() != handlerGetter.test(this.markerManager)) { + this.markerManager = this.createMarkerManager(); - final DecompiledClassSource source = this.getSource(); - if (source != null) { - this.refreshMarkers(source); - } else { - this.editorScrollPane.clearMarkers(); - } + final DecompiledClassSource source = this.getSource(); + if (source != null) { + this.refreshMarkers(source); + } else { + this.editorScrollPane.clearMarkers(); + } + } + }); } private void refreshMarkers(DecompiledClassSource source) { @@ -217,21 +201,7 @@ private void refreshMarkers(DecompiledClassSource source) { final TokenStore tokenStore = source.getTokenStore(); tokenStore.getByType().forEach((type, tokens) -> { for (final Token token : tokens) { - this.markablePredicate.getParams(token, type, tokenStore).ifPresent(params -> { - try { - final int tokenPos = (int) this.editor.modelToView2D(token.start).getCenterY(); - - // TODO show/hide tooltip on mouse enter/exit - this.editorScrollPane.addMarker(tokenPos, params.color, params.priority, new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - EditorPanel.this.navigateToToken(token); - } - }); - } catch (BadLocationException e) { - Logger.warn("Tried to add marker for token with bad location: " + token); - } - }); + this.markerManager.tryMarking(token, type, tokenStore); } }); } @@ -390,6 +360,16 @@ public void actionPerformed(ActionEvent e) { this.popupMenu.getButtonKeyBinds().forEach((key, button) -> putKeyBindAction(key, this.editor, e -> button.doClick())); } + private MarkerManager createMarkerManager() { + final EntryMarkersSection markersConfig = Config.editor().entryMarkers; + return new MarkerManager( + markersConfig.markObfuscated.value(), + markersConfig.markFallback.value(), + markersConfig.markProposed.value(), + markersConfig.markDeobfuscated.value() + ); + } + private class TooltipManager { static final int MOUSE_STOPPED_MOVING_DELAY = 100; @@ -536,40 +516,93 @@ void removeExternalListeners() { } } - private record MarkablePredicate(boolean obfuscated, boolean fallback, boolean proposed, boolean deobfuscated) { - static MarkablePredicate of() { - final EntryMarkersSection markersConfig = Config.editor().entryMarkers; - return new MarkablePredicate( - markersConfig.markObfuscated.value(), - markersConfig.markFallback.value(), - markersConfig.markProposed.value(), - markersConfig.markDeobfuscated.value() + private class MarkerManager { + static final ImmutableMap, Integer> + MARKER_PRIORITIES_BY_COLOR_CONFIG; + + static { + int priority = 0; + MARKER_PRIORITIES_BY_COLOR_CONFIG = ImmutableMap.of( + Config.getCurrentSyntaxPaneColors().deobfuscatedOutline, priority++, + Config.getCurrentSyntaxPaneColors().proposedOutline, priority++, + Config.getCurrentSyntaxPaneColors().fallbackOutline, priority++, + Config.getCurrentSyntaxPaneColors().obfuscatedOutline, priority++, + Config.getCurrentSyntaxPaneColors().debugTokenOutline, priority ); } - Optional getParams(Token token, TokenType type, TokenStore tokenStore) { - final SyntaxPaneProperties.Colors colors = Config.getCurrentSyntaxPaneColors(); + final boolean markObfuscated; + final boolean markFallback; + final boolean markProposed; + final boolean markDeobfuscated; + + MarkerManager(boolean markObfuscated, boolean markFallback, boolean markProposed, boolean markDeobfuscated) { + this.markObfuscated = markObfuscated; + this.markFallback = markFallback; + this.markProposed = markProposed; + this.markDeobfuscated = markDeobfuscated; + } + + void tryMarking(Token token, TokenType type, TokenStore tokenStore) { + @Nullable + final TrackedValue colorConfig = + this.getColorConfig(token, type, tokenStore); + + if (colorConfig != null) { + try { + final int tokenPos = (int) EditorPanel.this.editor.modelToView2D(token.start).getCenterY(); + + final int priority = Objects.requireNonNull(MARKER_PRIORITIES_BY_COLOR_CONFIG.get(colorConfig)); + final Color color = colorConfig.value(); + EditorPanel.this.editorScrollPane.addMarker(tokenPos, color, priority, new MouseAdapter() { + // TODO show/hide tooltip on mouse enter/exit + @Override + public void mouseClicked(MouseEvent e) { + EditorPanel.this.navigateToToken(token); + } + }); + } catch (BadLocationException e) { + Logger.warn("Tried to add marker for token with bad location: " + token); + } + } + } + + private TrackedValue getColorConfig( + Token token, TokenType type, TokenStore tokenStore + ) { if (tokenStore.isFallback(token)) { - return this.fallback - ? Optional.of(new MarkerParams(colors.fallbackOutline.value(), FALLBACK_PRIORITY)) - : Optional.empty(); + return this.markFallback ? Config.getCurrentSyntaxPaneColors().fallbackOutline : null; } else { return switch (type) { - case OBFUSCATED -> this.obfuscated - ? Optional.of(new MarkerParams(colors.obfuscatedOutline.value(), OBFUSCATED_PRIORITY)) - : Optional.empty(); - case DEOBFUSCATED -> this.deobfuscated - ? Optional.of(new MarkerParams(colors.deobfuscatedOutline.value(), DEOBFUSCATED_PRIORITY)) - : Optional.empty(); - case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposed - ? Optional.of(new MarkerParams(colors.proposedOutline.value(), PROPOSED_PRIORITY)) - : Optional.empty(); + case OBFUSCATED -> this.markObfuscated + ? Config.getCurrentSyntaxPaneColors().obfuscatedOutline + : null; + case DEOBFUSCATED -> this.markDeobfuscated + ? Config.getCurrentSyntaxPaneColors().deobfuscatedOutline + : null; + case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.markProposed + ? Config.getCurrentSyntaxPaneColors().proposedOutline + : null; // these only appear if debugTokenHighlights is true, so no need for a separate marker config - case DEBUG -> Optional.of(new MarkerParams(colors.debugTokenOutline.value(), DEBUG_PRIORITY)); + case DEBUG -> Config.getCurrentSyntaxPaneColors().debugTokenOutline; }; } } - } - private record MarkerParams(Color color, int priority) { } + boolean marksObfuscated() { + return this.markObfuscated; + } + + boolean marksFallback() { + return this.markFallback; + } + + boolean marksProposed() { + return this.markProposed; + } + + boolean marksDeobfuscated() { + return this.markDeobfuscated; + } + } } From 0fee955ddfd655154fa646cec38e3b1762b37b7e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 19:27:59 -0700 Subject: [PATCH 020/110] implement MarkableScrollPane.MarkerListener and use it to show entry tooltips for markers --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 47 ++++++- .../enigma/gui/panel/MarkableScrollPane.java | 125 ++++++++++++------ 2 files changed, 126 insertions(+), 46 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index df31229f5..87c35b51f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -554,13 +554,46 @@ void tryMarking(Token token, TokenType type, TokenStore tokenStore) { final int priority = Objects.requireNonNull(MARKER_PRIORITIES_BY_COLOR_CONFIG.get(colorConfig)); final Color color = colorConfig.value(); - EditorPanel.this.editorScrollPane.addMarker(tokenPos, color, priority, new MouseAdapter() { - // TODO show/hide tooltip on mouse enter/exit - @Override - public void mouseClicked(MouseEvent e) { - EditorPanel.this.navigateToToken(token); - } - }); + EditorPanel.this.editorScrollPane.addMarker( + tokenPos, color, priority, + new MarkableScrollPane.MarkerListener() { + @Override + public void mouseClicked() { + EditorPanel.this.navigateToToken(token); + } + + @Override + public void mouseExited() { + if (EditorPanel.this.tooltipManager.lastMouseTargetToken == null) { + EditorPanel.this.tooltipManager.entryTooltip.close(); + } + } + + @Override + public void mouseEntered() { + // dont' resolve the token for markers + final EntryReference, Entry> reference = + EditorPanel.this.getReference(token); + if (reference != null) { + EditorPanel.this.tooltipManager.reset(); + EditorPanel.this.tooltipManager.openTooltip(reference.entry, false); + } + } + + // This is used instead of just exit+enter because closing immediately before opening + // causes the (slightly delayed) window lost focus listener to close the tooltip + // for the new marker immediately after opening it. + @Override + public void mouseTransferred() { + this.mouseEntered(); + } + + @Override + public void mouseMoved() { + EditorPanel.this.tooltipManager.reset(); + } + } + ); } catch (BadLocationException e) { Logger.warn("Tried to add marker for token with bad location: " + token); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index eef54a7fc..390b240eb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -20,8 +20,8 @@ import java.awt.Rectangle; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -32,7 +32,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; -import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Predicate; public class MarkableScrollPane extends JScrollPane { @@ -50,7 +50,7 @@ public class MarkableScrollPane extends JScrollPane { @Nullable private PaintState paintState; - private MouseListener viewMouseListener; + private MouseAdapter viewMouseAdapter; /** * Constructs a scroll pane with no view, @@ -123,71 +123,104 @@ public void componentHidden(ComponentEvent e) { public void setViewportView(Component view) { final Component oldView = this.getViewport().getView(); if (oldView != null) { - oldView.removeMouseListener(this.viewMouseListener); + oldView.removeMouseListener(this.viewMouseAdapter); + oldView.removeMouseMotionListener(this.viewMouseAdapter); } super.setViewportView(view); - this.viewMouseListener = new MouseListener() { - private void tryMarkerListeners(MouseEvent e, BiConsumer eventAction) { - if (MarkableScrollPane.this.paintState != null) { - final Point relativePos = GuiUtil - .getRelativePos(MarkableScrollPane.this, e.getXOnScreen(), e.getYOnScreen()); - MarkableScrollPane.this.paintState - .findSpanContaining( - relativePos.x, relativePos.y, - span -> span.getMarker().mouseListener.isPresent() - ) - .map(span -> span.getMarker().mouseListener.orElseThrow()) - .ifPresent(listener -> eventAction.accept(listener, e)); + this.viewMouseAdapter = new MouseAdapter() { + static MouseEvent withId(MouseEvent e, int id) { + return new MouseEvent( + (Component) e.getSource(), id, e.getWhen(), e.getModifiersEx(), + e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(), + e.getClickCount(), e.isPopupTrigger(), e.getButton() + ); + } + + @Nullable + MarkerListener lastEntered; + + Optional findMarkerListener(MouseEvent e) { + if (MarkableScrollPane.this.paintState == null) { + return Optional.empty(); + } else { + final Point relativePos = + GuiUtil.getRelativePos(MarkableScrollPane.this, e.getXOnScreen(), e.getYOnScreen()); + return MarkableScrollPane.this.paintState + .findSpanContaining( + relativePos.x, relativePos.y, + span -> span.getMarker().listener.isPresent() + ) + .map(span -> span.getMarker().listener.orElseThrow()); } } - @Override - public void mouseClicked(MouseEvent e) { - this.tryMarkerListeners(e, MouseListener::mouseClicked); + void tryMarkerListeners(MouseEvent e, Consumer listen) { + this.findMarkerListener(e).ifPresent(listen); } @Override - public void mousePressed(MouseEvent e) { - this.tryMarkerListeners(e, MouseListener::mousePressed); + public void mouseClicked(MouseEvent e) { + this.tryMarkerListeners(e, MarkerListener::mouseClicked); } @Override - public void mouseReleased(MouseEvent e) { - this.tryMarkerListeners(e, MouseListener::mouseReleased); + public void mouseExited(MouseEvent e) { + this.mouseExitedImpl(); } @Override - public void mouseEntered(MouseEvent e) { - this.tryMarkerListeners(e, MouseListener::mouseEntered); + public void mouseMoved(MouseEvent e) { + this.tryMarkerListeners(e, MarkerListener::mouseMoved); + + this.findMarkerListener(e).ifPresentOrElse( + listener -> { + if (listener != this.lastEntered) { + if (this.lastEntered == null) { + listener.mouseEntered(); + } else { + listener.mouseTransferred(); + } + + this.lastEntered = listener; + } + }, + this::mouseExitedImpl + ); } - @Override - public void mouseExited(MouseEvent e) { - this.tryMarkerListeners(e, MouseListener::mouseExited); + private void mouseExitedImpl() { + if (this.lastEntered != null) { + this.lastEntered.mouseExited(); + this.lastEntered = null; + } } }; // add the listener to the view because this doesn't receive clicks within the view - view.addMouseListener(this.viewMouseListener); + view.addMouseListener(this.viewMouseAdapter); + view.addMouseMotionListener(this.viewMouseAdapter); } /** * Adds a marker with passed {@code color} at the given {@code pos}. * - * @param pos the vertical center of the marker within the space of this scroll pane's view - * @param color the color of the marker - * @param priority the priority of the marker; if there are multiple markers at the same position, only up to - * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered - * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} + * @param pos the vertical center of the marker within the space of this scroll pane's view + * @param color the color of the marker + * @param priority the priority of the marker; if there are multiple markers at the same position, only up to + * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered + * @param listener a listener for events within the marker; may be null + * + * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} */ - public Object addMarker(int pos, Color color, int priority, @Nullable MouseListener mouseListener) { + public Object addMarker(int pos, Color color, int priority, @Nullable MarkerListener listener) { if (pos < 0) { throw new IllegalArgumentException("pos must not be negative!"); } - final Marker marker = new Marker(color, priority, Optional.ofNullable(mouseListener)); + final Marker marker = new Marker(color, priority, Optional.ofNullable(listener)); + this.markersByPos.put(pos, marker); if (this.paintState != null) { @@ -200,7 +233,7 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MouseListe /** * Removes the passed {@code marker} if it belongs to this scroll pane. * - * @param marker an object previously returned by {@link #addMarker(int, Color, int, MouseListener)} + * @param marker an object previously returned by {@link #addMarker(int, Color, int, MarkerListener)} */ public void removeMarker(Object marker) { if (marker instanceof Marker removing) { @@ -363,8 +396,7 @@ boolean areaContains(int x, int y) { } } - private record Marker(Color color, int priority, Optional mouseListener) - implements Comparable { + private record Marker(Color color, int priority, Optional listener) implements Comparable { @Override public int compareTo(@Nonnull Marker other) { return other.priority - this.priority; @@ -422,4 +454,19 @@ void paint(Graphics graphics) { } } } + + public interface MarkerListener { + void mouseClicked(); + + void mouseExited(); + + void mouseEntered(); + + /** + * Called when the mouse moves between two adjacent markers. + */ + void mouseTransferred(); + + void mouseMoved(); + } } From 2f18c90a5b8ff66a9a9828eb4a4708c9ac2231f9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 20:08:20 -0700 Subject: [PATCH 021/110] cleanup and javadoc MarkableScrollPane --- .../enigma/gui/panel/MarkableScrollPane.java | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 390b240eb..6741a2909 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -35,6 +35,23 @@ import java.util.function.Consumer; import java.util.function.Predicate; +/** + * A scroll pane that renders markers in its view along the right edge, to the left of the vertical scroll bar.
+ * Markers support custom {@linkplain Color colors} and {@linkplain MarkerListener listeners}. + * + *

Markers are associated with a vertical position within the vertical space of this scroll pane's view; + * markers with a position greater than the height of the current view are not rendered. + * Multiple markers may be rendered at the same position. Markers with the highest priority (specified when + * {@linkplain #addMarker(int, Color, int, MarkerListener) added}) will be rendered left-most. + * No more than {@link #maxConcurrentMarkers} will be rendered at the same position. If there are excess markers, those + * with lowest priority will be skipped. There's no guarantee which marker will be rendered when priorities are tied. + * When multiple markers are rendered at the same location, each will be narrower so their total width is equal to a + * single marker's. + * + * @see #addMarker(int, Color, int, MarkerListener) + * @see #removeMarker(Object) + * @see MarkerListener + */ public class MarkableScrollPane extends JScrollPane { private static final int DEFAULT_MARKER_WIDTH = 10; private static final int DEFAULT_MARKER_HEIGHT = 5; @@ -72,11 +89,15 @@ public MarkableScrollPane(Component view) { /** * @param view the component to display in this scroll pane's view port - * @param maxConcurrentMarkers the maximum number of markers that will be rendered at the same position; + * @param maxConcurrentMarkers a (positive) number limiting how many markers will be rendered at the same position; * more markers may be added, but only up to this number of markers * with the highest priority will be rendered * @param verticalPolicy the vertical scroll bar policy * @param horizontalPolicy the horizontal scroll bar policy + * + * @throws IllegalArgumentException if {@code maxConcurrentMarkers} is not positive + * + * @see #addMarker(int, Color, int, MarkerListener) */ public MarkableScrollPane( @Nullable Component view, int maxConcurrentMarkers, @@ -130,14 +151,6 @@ public void setViewportView(Component view) { super.setViewportView(view); this.viewMouseAdapter = new MouseAdapter() { - static MouseEvent withId(MouseEvent e, int id) { - return new MouseEvent( - (Component) e.getSource(), id, e.getWhen(), e.getModifiersEx(), - e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(), - e.getClickCount(), e.isPopupTrigger(), e.getButton() - ); - } - @Nullable MarkerListener lastEntered; @@ -206,18 +219,23 @@ private void mouseExitedImpl() { /** * Adds a marker with passed {@code color} at the given {@code pos}. * - * @param pos the vertical center of the marker within the space of this scroll pane's view + * @param pos the vertical center of the marker within the space of this scroll pane's view; + * must not be negative; if greater than the height of the current view, + * the marker will not be rendered * @param color the color of the marker * @param priority the priority of the marker; if there are multiple markers at the same position, only up to * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered - * @param listener a listener for events within the marker; may be null + * @param listener a listener for events within the marker; may be {@code null} * * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} + * + * @throws IllegalArgumentException if {@code pos} is negative + * + * @see #removeMarker(Object) + * @see MarkerListener */ public Object addMarker(int pos, Color color, int priority, @Nullable MarkerListener listener) { - if (pos < 0) { - throw new IllegalArgumentException("pos must not be negative!"); - } + Preconditions.checkArgument(pos >= 0, "pos must not be negative!"); final Marker marker = new Marker(color, priority, Optional.ofNullable(listener)); @@ -234,6 +252,9 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MarkerList * Removes the passed {@code marker} if it belongs to this scroll pane. * * @param marker an object previously returned by {@link #addMarker(int, Color, int, MarkerListener)} + * + * @see #addMarker(int, Color, int, MarkerListener) + * @see #clearMarkers() */ public void removeMarker(Object marker) { if (marker instanceof Marker removing) { @@ -307,8 +328,20 @@ private PaintState createPaintState() { } public enum ScrollBarPolicy { + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_AS_NEEDED + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_AS_NEEDED + */ AS_NEEDED(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED), + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_ALWAYS + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_ALWAYS + */ ALWAYS(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS), + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_NEVER + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_NEVER + */ NEVER(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); private final int horizontal; @@ -455,18 +488,37 @@ void paint(Graphics graphics) { } } + /** + * A listener for marker events. + * + * @see #addMarker(int, Color, int, MarkerListener) + */ public interface MarkerListener { + /** + * Called when the mouse clicks the marker. + */ void mouseClicked(); - void mouseExited(); - + /** + * Called when the mouse enters the marker. + */ void mouseEntered(); /** - * Called when the mouse moves between two adjacent markers. + * Called when the mouse exits the marker. + * + *

Not called when the mouse moves to an adjacent marker; see {@link #mouseTransferred()}. + */ + void mouseExited(); + + /** + * Called when the mouse moves from an adjacent marker to the marker. */ void mouseTransferred(); + /** + * Called when the mouse within the marker. + */ void mouseMoved(); } } From fb2f3cf226157b4ab4de11b3c32ac6e8a46a6f4b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 21:29:08 -0700 Subject: [PATCH 022/110] rename EntryMarkersSection.interactable -> tooltip and implement it add EntryMarkersSection.onlyMarkDeclarations repaint MarkableScrollPane on marker added --- .../gui/config/EntryMarkersSection.java | 7 +- .../menu_bar/view/EntryMarkersMenu.java | 51 +++----- .../enigma/gui/panel/AbstractEditorPanel.java | 20 +-- .../quiltmc/enigma/gui/panel/EditorPanel.java | 123 ++++++++++++------ .../enigma/gui/panel/MarkableScrollPane.java | 1 + enigma/src/main/resources/lang/en_us.json | 3 +- 6 files changed, 122 insertions(+), 83 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java index 9feb3fb2c..d2c4f6190 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java @@ -8,8 +8,11 @@ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class EntryMarkersSection extends ReflectiveConfig.Section { - @Comment("Whether markers can be clicked to navigate to their corresponding entries.") - public final TrackedValue interactable = this.value(true); + @Comment("Whether markers should have tooltips showing their corresponding entries.") + public final TrackedValue tooltip = this.value(true); + + @Comment("Whether only declaration entries should be marked.") + public final TrackedValue onlyMarkDeclarations = this.value(false); @Comment("Whether obfuscated entries should be marked.") public final TrackedValue markObfuscated = this.value(true); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java index b333bd866..907b928fe 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java @@ -1,47 +1,36 @@ package org.quiltmc.enigma.gui.element.menu_bar.view; -import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; -import org.quiltmc.enigma.gui.config.EntryMarkersSection; import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; public class EntryMarkersMenu extends AbstractEnigmaMenu { - private static void syncStateWithConfig(JCheckBoxMenuItem box, TrackedValue config) { - box.setState(config.value()); - - box.addActionListener(e -> { - final boolean checked = box.getState(); - if (checked != config.value()) { - config.setValue(checked); - } - }); - - config.registerCallback(updated -> { - final boolean configured = updated.value(); - if (configured != box.getState()) { - box.setState(configured); - } - }); - } - - private final JCheckBoxMenuItem interactable = new JCheckBoxMenuItem(); + private final JCheckBoxMenuItem tooltip = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.tooltip); private final JMenu markMenu = new JMenu(); - private final JCheckBoxMenuItem markObfuscated = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem markFallback = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem markProposed = new JCheckBoxMenuItem(); - private final JCheckBoxMenuItem markDeobfuscated = new JCheckBoxMenuItem(); + private final JCheckBoxMenuItem onlyMarkDeclarations = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.onlyMarkDeclarations); + private final JCheckBoxMenuItem markObfuscated = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.markObfuscated); + private final JCheckBoxMenuItem markFallback = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.markFallback); + private final JCheckBoxMenuItem markProposed = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.markProposed); + private final JCheckBoxMenuItem markDeobfuscated = GuiUtil + .createSyncedMenuCheckBox(Config.editor().entryMarkers.markDeobfuscated); public EntryMarkersMenu(Gui gui) { super(gui); - this.add(this.interactable); + this.add(this.tooltip); + this.markMenu.add(this.onlyMarkDeclarations); this.markMenu.add(this.markObfuscated); this.markMenu.add(this.markFallback); this.markMenu.add(this.markProposed); @@ -49,13 +38,6 @@ public EntryMarkersMenu(Gui gui) { this.add(this.markMenu); - final EntryMarkersSection markerConfig = Config.editor().entryMarkers; - syncStateWithConfig(this.interactable, markerConfig.interactable); - syncStateWithConfig(this.markObfuscated, markerConfig.markObfuscated); - syncStateWithConfig(this.markFallback, markerConfig.markFallback); - syncStateWithConfig(this.markProposed, markerConfig.markProposed); - syncStateWithConfig(this.markDeobfuscated, markerConfig.markDeobfuscated); - this.retranslate(); } @@ -63,10 +45,11 @@ public EntryMarkersMenu(Gui gui) { public void retranslate() { this.setText(I18n.translate("menu.view.entry_markers")); - this.interactable.setText(I18n.translate("menu.view.entry_markers.interactable")); + this.tooltip.setText(I18n.translate("menu.view.entry_markers.tooltip")); this.markMenu.setText(I18n.translate("menu.view.entry_markers.mark")); + this.onlyMarkDeclarations.setText(I18n.translate("menu.view.entry_markers.mark.only_declarations")); this.markObfuscated.setText(I18n.translate("menu.view.entry_markers.mark.obfuscated")); this.markFallback.setText(I18n.translate("menu.view.entry_markers.mark.fallback")); this.markProposed.setText(I18n.translate("menu.view.entry_markers.mark.proposed")); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java index 3341cf82d..b391b1b5d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java @@ -540,7 +540,18 @@ private void showReferenceImpl(EntryReference, Entry> reference) { return; } - final List tokens = Optional.of(this.controller.getTokensForReference(this.source, reference)) + final List tokens = this.getReferences(reference); + + if (tokens.isEmpty()) { + // DEBUG + Logger.debug("No tokens found for {} in {}", reference, this.classHandler.getHandle().getRef()); + } else { + this.gui.showTokens(this, tokens); + } + } + + protected List getReferences(EntryReference, Entry> reference) { + return Optional.of(this.controller.getTokensForReference(this.source, reference)) .filter(directTokens -> !directTokens.isEmpty()) .or(() -> { // record component getters often don't have a declaration token @@ -553,13 +564,6 @@ private void showReferenceImpl(EntryReference, Entry> reference) { : Optional.empty(); }) .orElse(List.of()); - - if (tokens.isEmpty()) { - // DEBUG - Logger.debug("No tokens found for {} in {}", reference, this.classHandler.getHandle().getRef()); - } else { - this.gui.showTokens(this, tokens); - } } /** diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 87c35b51f..db9c50648 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -174,6 +174,7 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); final EntryMarkersSection markersConfig = Config.editor().entryMarkers; + this.registerMarkerRefresher(markersConfig.onlyMarkDeclarations, MarkerManager::onlyMarksDeclarations); this.registerMarkerRefresher(markersConfig.markObfuscated, MarkerManager::marksObfuscated); this.registerMarkerRefresher(markersConfig.markFallback, MarkerManager::marksFallback); this.registerMarkerRefresher(markersConfig.markProposed, MarkerManager::marksProposed); @@ -363,6 +364,7 @@ public void actionPerformed(ActionEvent e) { private MarkerManager createMarkerManager() { final EntryMarkersSection markersConfig = Config.editor().entryMarkers; return new MarkerManager( + markersConfig.onlyMarkDeclarations.value(), markersConfig.markObfuscated.value(), markersConfig.markFallback.value(), markersConfig.markProposed.value(), @@ -531,12 +533,18 @@ private class MarkerManager { ); } + final boolean onlyMarkDeclarations; + final boolean markObfuscated; final boolean markFallback; final boolean markProposed; final boolean markDeobfuscated; - MarkerManager(boolean markObfuscated, boolean markFallback, boolean markProposed, boolean markDeobfuscated) { + MarkerManager( + boolean onlyMarkDeclarations, + boolean markObfuscated, boolean markFallback, boolean markProposed, boolean markDeobfuscated + ) { + this.onlyMarkDeclarations = onlyMarkDeclarations; this.markObfuscated = markObfuscated; this.markFallback = markFallback; this.markProposed = markProposed; @@ -544,6 +552,26 @@ private class MarkerManager { } void tryMarking(Token token, TokenType type, TokenStore tokenStore) { + if (this.onlyMarkDeclarations) { + final EntryReference, Entry> reference = + EditorPanel.this.getReference(token); + + if (reference != null) { + final Entry resolved = EditorPanel.this.resolveReference(reference); + final EntryReference, Entry> declaration = EntryReference + .declaration(resolved, resolved.getName()); + + if ( + EditorPanel.this.getReferences(declaration).stream() + .findFirst() + .filter(declarationToken -> !declarationToken.equals(token)) + .isPresent() + ) { + return; + } + } + } + @Nullable final TrackedValue colorConfig = this.getColorConfig(token, type, tokenStore); @@ -556,43 +584,7 @@ void tryMarking(Token token, TokenType type, TokenStore tokenStore) { final Color color = colorConfig.value(); EditorPanel.this.editorScrollPane.addMarker( tokenPos, color, priority, - new MarkableScrollPane.MarkerListener() { - @Override - public void mouseClicked() { - EditorPanel.this.navigateToToken(token); - } - - @Override - public void mouseExited() { - if (EditorPanel.this.tooltipManager.lastMouseTargetToken == null) { - EditorPanel.this.tooltipManager.entryTooltip.close(); - } - } - - @Override - public void mouseEntered() { - // dont' resolve the token for markers - final EntryReference, Entry> reference = - EditorPanel.this.getReference(token); - if (reference != null) { - EditorPanel.this.tooltipManager.reset(); - EditorPanel.this.tooltipManager.openTooltip(reference.entry, false); - } - } - - // This is used instead of just exit+enter because closing immediately before opening - // causes the (slightly delayed) window lost focus listener to close the tooltip - // for the new marker immediately after opening it. - @Override - public void mouseTransferred() { - this.mouseEntered(); - } - - @Override - public void mouseMoved() { - EditorPanel.this.tooltipManager.reset(); - } - } + new EntryMarkerListener(token) ); } catch (BadLocationException e) { Logger.warn("Tried to add marker for token with bad location: " + token); @@ -622,6 +614,10 @@ private TrackedValue getColorConfig( } } + boolean onlyMarksDeclarations() { + return this.onlyMarkDeclarations; + } + boolean marksObfuscated() { return this.markObfuscated; } @@ -638,4 +634,55 @@ boolean marksDeobfuscated() { return this.markDeobfuscated; } } + + private class EntryMarkerListener implements MarkableScrollPane.MarkerListener { + private final Token token; + + EntryMarkerListener(Token token) { + this.token = token; + } + + @Override + public void mouseClicked() { + EditorPanel.this.navigateToToken(this.token); + } + + @Override + public void mouseExited() { + if ( + Config.editor().entryMarkers.tooltip.value() + && EditorPanel.this.tooltipManager.lastMouseTargetToken == null + ) { + EditorPanel.this.tooltipManager.entryTooltip.close(); + } + } + + @Override + public void mouseEntered() { + if (Config.editor().entryMarkers.tooltip.value()) { + // dont' resolve the token for markers + final EntryReference, Entry> reference = + EditorPanel.this.getReference(this.token); + if (reference != null) { + EditorPanel.this.tooltipManager.reset(); + EditorPanel.this.tooltipManager.openTooltip(reference.entry, false); + } + } + } + + // This is used instead of just exit+enter because closing immediately before opening + // causes the (slightly delayed) window lost focus listener to close the tooltip + // for the new marker immediately after opening it. + @Override + public void mouseTransferred() { + this.mouseEntered(); + } + + @Override + public void mouseMoved() { + if (Config.editor().entryMarkers.tooltip.value()) { + EditorPanel.this.tooltipManager.reset(); + } + } + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 6741a2909..91f389c02 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -243,6 +243,7 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MarkerList if (this.paintState != null) { this.paintState.pendingMarkerPositions.add(pos); + this.repaint(); } return marker; diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 816a08ae2..8e8a2b3e5 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -92,8 +92,9 @@ "menu.view.entry_tooltips.enable": "Enable tooltips", "menu.view.entry_tooltips.interactable": "Allow tooltip interaction", "menu.view.entry_markers": "Entry Markers", - "menu.view.entry_markers.interactable": "Clickable markers", + "menu.view.entry_markers.tooltip": "Marker tooltips", "menu.view.entry_markers.mark": "Mark entries", + "menu.view.entry_markers.mark.only_declarations": "Only declarations", "menu.view.entry_markers.mark.obfuscated": "Obfuscated", "menu.view.entry_markers.mark.fallback": "Fallback", "menu.view.entry_markers.mark.proposed": "Proposed", From e872de2802836566e807020e0c87006aef0292ec Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 21:57:13 -0700 Subject: [PATCH 023/110] don't mark constructors when only marking declarations --- .../main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index db9c50648..4de5a7100 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -11,6 +11,7 @@ import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.TokenStore; import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.config.EntryMarkersSection; @@ -557,6 +558,10 @@ void tryMarking(Token token, TokenType type, TokenStore tokenStore) { EditorPanel.this.getReference(token); if (reference != null) { + if (reference.entry instanceof MethodEntry method && method.isConstructor()) { + return; + } + final Entry resolved = EditorPanel.this.resolveReference(reference); final EntryReference, Entry> declaration = EntryReference .declaration(resolved, resolved.getName()); From 260db10f2924f3f1a72cf05c8851fb6fbe178064 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 30 Oct 2025 11:33:02 -0700 Subject: [PATCH 024/110] add separator between onlyMarkDeclarations and token type toggles --- .../enigma/gui/element/menu_bar/view/EntryMarkersMenu.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java index 907b928fe..207277bee 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java @@ -8,6 +8,7 @@ import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; +import javax.swing.JToolBar; public class EntryMarkersMenu extends AbstractEnigmaMenu { private final JCheckBoxMenuItem tooltip = GuiUtil @@ -31,6 +32,7 @@ public EntryMarkersMenu(Gui gui) { this.add(this.tooltip); this.markMenu.add(this.onlyMarkDeclarations); + this.markMenu.add(new JToolBar.Separator()); this.markMenu.add(this.markObfuscated); this.markMenu.add(this.markFallback); this.markMenu.add(this.markProposed); From 936b8ac1d656d95c4291a6b977b25df20d36816e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 30 Oct 2025 11:49:01 -0700 Subject: [PATCH 025/110] add new test input class --- .../input/tooltip/ConvergentInheritance.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/ConvergentInheritance.java diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/ConvergentInheritance.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/ConvergentInheritance.java new file mode 100644 index 000000000..8ce9c6217 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/ConvergentInheritance.java @@ -0,0 +1,20 @@ +package org.quiltmc.enigma.input.tooltip; + +public class ConvergentInheritance { + abstract static class Named { + public abstract void setName(String name); + } + + interface Nameable { + void setName(String name); + } + + static class Implementer extends Named implements Nameable { + private String name; + + @Override + public void setName(String name) { + this.name = name; + } + } +} From 1ece814cb3102f82c48187c64223bc2f795a7ec3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 31 Oct 2025 14:56:27 -0700 Subject: [PATCH 026/110] make max markers per line configurable --- .../gui/config/EntryMarkersSection.java | 10 +- .../menu_bar/view/EntryMarkersMenu.java | 70 +++++++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 6 +- .../enigma/gui/panel/MarkableScrollPane.java | 196 +++++++++--------- enigma/src/main/resources/lang/en_us.json | 1 + 5 files changed, 187 insertions(+), 96 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java index d2c4f6190..4894c457c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryMarkersSection.java @@ -2,17 +2,25 @@ import org.quiltmc.config.api.ReflectiveConfig; import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.annotations.IntegerRange; import org.quiltmc.config.api.annotations.SerializedNameConvention; import org.quiltmc.config.api.metadata.NamingSchemes; import org.quiltmc.config.api.values.TrackedValue; @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class EntryMarkersSection extends ReflectiveConfig.Section { + public static final int MIN_MAX_MARKERS_PER_LINE = 0; + public static final int MAX_MAX_MARKERS_PER_LINE = 3; + @Comment("Whether markers should have tooltips showing their corresponding entries.") public final TrackedValue tooltip = this.value(true); + @Comment("The maximum number of markers to show for a single line. Set to 0 to disable markers.") + @IntegerRange(min = MIN_MAX_MARKERS_PER_LINE, max = MAX_MAX_MARKERS_PER_LINE) + public final TrackedValue maxMarkersPerLine = this.value(2); + @Comment("Whether only declaration entries should be marked.") - public final TrackedValue onlyMarkDeclarations = this.value(false); + public final TrackedValue onlyMarkDeclarations = this.value(true); @Comment("Whether obfuscated entries should be marked.") public final TrackedValue markObfuscated = this.value(true); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java index 207277bee..e625eda5b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java @@ -1,19 +1,79 @@ package org.quiltmc.enigma.gui.element.menu_bar.view; +import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; +import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; +import javax.swing.JRadioButtonMenuItem; import javax.swing.JToolBar; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.quiltmc.enigma.gui.config.EntryMarkersSection.MAX_MAX_MARKERS_PER_LINE; +import static org.quiltmc.enigma.gui.config.EntryMarkersSection.MIN_MAX_MARKERS_PER_LINE; public class EntryMarkersMenu extends AbstractEnigmaMenu { + @SuppressWarnings("SameParameterValue") + private static JMenu createIntConfigRadioMenu( + TrackedValue config, int min, int max, Runnable onUpdate + ) { + final Map radiosByChoice = IntStream.range(min, max + 1) + .boxed() + .collect(Collectors.toMap( + Function.identity(), + choice -> { + final JRadioButtonMenuItem choiceItem = new JRadioButtonMenuItem(); + choiceItem.setText(Integer.toString(choice)); + if (choice.equals(config.value())) { + choiceItem.setSelected(true); + } + + final int finalChoice = choice; + choiceItem.addActionListener(e -> { + config.setValue(finalChoice); + onUpdate.run(); + }); + + return choiceItem; + } + )); + + final ButtonGroup choicesGroup = new ButtonGroup(); + final JMenu menu = new JMenu(); + for (final JRadioButtonMenuItem radio : radiosByChoice.values()) { + choicesGroup.add(radio); + menu.add(radio); + } + + config.registerCallback(updated -> { + final JRadioButtonMenuItem choiceItem = radiosByChoice.get(updated.value()); + + if (!choiceItem.isSelected()) { + choiceItem.setSelected(true); + onUpdate.run(); + } + }); + + return menu; + } + private final JCheckBoxMenuItem tooltip = GuiUtil .createSyncedMenuCheckBox(Config.editor().entryMarkers.tooltip); + private final JMenu maxMarkersPerLineMenu = createIntConfigRadioMenu( + Config.editor().entryMarkers.maxMarkersPerLine, + MIN_MAX_MARKERS_PER_LINE, MAX_MAX_MARKERS_PER_LINE, + this::translateMarkersPerLineMenu + ); + private final JMenu markMenu = new JMenu(); private final JCheckBoxMenuItem onlyMarkDeclarations = GuiUtil .createSyncedMenuCheckBox(Config.editor().entryMarkers.onlyMarkDeclarations); @@ -31,6 +91,8 @@ public EntryMarkersMenu(Gui gui) { this.add(this.tooltip); + this.add(this.maxMarkersPerLineMenu); + this.markMenu.add(this.onlyMarkDeclarations); this.markMenu.add(new JToolBar.Separator()); this.markMenu.add(this.markObfuscated); @@ -49,6 +111,7 @@ public void retranslate() { this.tooltip.setText(I18n.translate("menu.view.entry_markers.tooltip")); + this.translateMarkersPerLineMenu(); this.markMenu.setText(I18n.translate("menu.view.entry_markers.mark")); this.onlyMarkDeclarations.setText(I18n.translate("menu.view.entry_markers.mark.only_declarations")); @@ -57,4 +120,11 @@ public void retranslate() { this.markProposed.setText(I18n.translate("menu.view.entry_markers.mark.proposed")); this.markDeobfuscated.setText(I18n.translate("menu.view.entry_markers.mark.deobfuscated")); } + + private void translateMarkersPerLineMenu() { + this.maxMarkersPerLineMenu.setText(I18n.translateFormatted( + "menu.view.entry_markers.max_markers_per_line", + Config.editor().entryMarkers.maxMarkersPerLine.value()) + ); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 4de5a7100..dd66fbd84 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -180,6 +180,10 @@ public void keyTyped(KeyEvent event) { this.registerMarkerRefresher(markersConfig.markFallback, MarkerManager::marksFallback); this.registerMarkerRefresher(markersConfig.markProposed, MarkerManager::marksProposed); this.registerMarkerRefresher(markersConfig.markDeobfuscated, MarkerManager::marksDeobfuscated); + + markersConfig.maxMarkersPerLine.registerCallback(updated -> { + this.editorScrollPane.setMaxConcurrentMarkers(updated.value()); + }); } private void registerMarkerRefresher(TrackedValue config, Predicate handlerGetter) { @@ -210,7 +214,7 @@ private void refreshMarkers(DecompiledClassSource source) { @Override protected MarkableScrollPane createEditorScrollPane(JEditorPane editor) { - return new MarkableScrollPane(editor); + return new MarkableScrollPane(editor, Config.editor().entryMarkers.maxMarkersPerLine.value()); } public void onRename(boolean isNewMapping) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 91f389c02..86f8df07a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -53,49 +53,41 @@ * @see MarkerListener */ public class MarkableScrollPane extends JScrollPane { + private static void requireNonNegative(int value, String name) { + Preconditions.checkArgument(value >= 0, "%s (%s) must not be negative!".formatted(value, name)); + } + private static final int DEFAULT_MARKER_WIDTH = 10; private static final int DEFAULT_MARKER_HEIGHT = 5; - private static final int DEFAULT_MAX_CONCURRENT_MARKERS = 2; - private final Multimap markersByPos = Multimaps.newMultimap(new HashMap<>(), TreeMultiset::create); private final int markerWidth; private final int markerHeight; - private final int maxConcurrentMarkers; + private int maxConcurrentMarkers; @Nullable private PaintState paintState; private MouseAdapter viewMouseAdapter; /** - * Constructs a scroll pane with no view, - * {@value DEFAULT_MAX_CONCURRENT_MARKERS} max concurrent markers, - * and {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bar policies. - */ - public MarkableScrollPane() { - this(null); - } - - /** - * Constructs a scroll pane displaying the passed {@code view}, - * {@value DEFAULT_MAX_CONCURRENT_MARKERS} max concurrent markers, + * Constructs a scroll pane displaying the passed {@code view} and {@code maxConcurrentMarkers}, * and {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bar policies. + * + * @see #MarkableScrollPane(Component, int, ScrollBarPolicy, ScrollBarPolicy) */ - public MarkableScrollPane(Component view) { - this(view, DEFAULT_MAX_CONCURRENT_MARKERS, ScrollBarPolicy.AS_NEEDED, ScrollBarPolicy.AS_NEEDED); + public MarkableScrollPane(@Nullable Component view, int maxConcurrentMarkers) { + this(view, maxConcurrentMarkers, ScrollBarPolicy.AS_NEEDED, ScrollBarPolicy.AS_NEEDED); } /** * @param view the component to display in this scroll pane's view port - * @param maxConcurrentMarkers a (positive) number limiting how many markers will be rendered at the same position; - * more markers may be added, but only up to this number of markers - * with the highest priority will be rendered + * @param maxConcurrentMarkers see {@link #setMaxConcurrentMarkers(int)} * @param verticalPolicy the vertical scroll bar policy * @param horizontalPolicy the horizontal scroll bar policy * - * @throws IllegalArgumentException if {@code maxConcurrentMarkers} is not positive + * @throws IllegalArgumentException if {@code maxConcurrentMarkers} is negative * * @see #addMarker(int, Color, int, MarkerListener) */ @@ -105,13 +97,11 @@ public MarkableScrollPane( ) { super(view, verticalPolicy.vertical, horizontalPolicy.horizontal); - Preconditions.checkArgument(maxConcurrentMarkers > 0, "maxConcurrentMarkers must be positive!"); + this.setMaxConcurrentMarkers(maxConcurrentMarkers); this.markerWidth = ScaleUtil.scale(DEFAULT_MARKER_WIDTH); this.markerHeight = ScaleUtil.scale(DEFAULT_MARKER_HEIGHT); - this.maxConcurrentMarkers = maxConcurrentMarkers; - this.addComponentListener(new ComponentListener() { void refreshMarkers() { MarkableScrollPane.this.clearPaintState(); @@ -140,6 +130,94 @@ public void componentHidden(ComponentEvent e) { }); } + /** + * Adds a marker with passed {@code color} at the given {@code pos}. + * + * @param pos the vertical center of the marker within the space of this scroll pane's view; + * must not be negative; if greater than the height of the current view, + * the marker will not be rendered + * @param color the color of the marker + * @param priority the priority of the marker; if there are multiple markers at the same position, only up to + * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered + * @param listener a listener for events within the marker; may be {@code null} + * + * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} + * + * @throws IllegalArgumentException if {@code pos} is negative + * + * @see #removeMarker(Object) + * @see MarkerListener + */ + public Object addMarker(int pos, Color color, int priority, @Nullable MarkerListener listener) { + requireNonNegative(pos, "pos"); + + final Marker marker = new Marker(color, priority, Optional.ofNullable(listener)); + + this.markersByPos.put(pos, marker); + + if (this.paintState != null) { + this.paintState.pendingMarkerPositions.add(pos); + this.repaint(); + } + + return marker; + } + + /** + * Removes the passed {@code marker} if it belongs to this scroll pane. + * + * @param marker an object previously returned by {@link #addMarker(int, Color, int, MarkerListener)} + * + * @see #addMarker(int, Color, int, MarkerListener) + * @see #clearMarkers() + */ + public void removeMarker(Object marker) { + if (marker instanceof Marker removing) { + final Iterator> itr = this.markersByPos.entries().iterator(); + + while (itr.hasNext()) { + final Map.Entry entry = itr.next(); + if (entry.getValue() == removing) { + itr.remove(); + if (this.paintState != null) { + this.paintState.pendingMarkerPositions.add(entry.getKey()); + } + + break; + } + } + } + } + + /** + * Removes all markers from this scroll pane. + */ + public void clearMarkers() { + this.markersByPos.clear(); + + if (this.paintState != null) { + this.paintState.clearMarkers(); + } + } + + /** + * @param maxConcurrentMarkers a (non-negative) number limiting how many markers will be rendered at the same position; + * more markers may be added, but only up to this number of markers with the highest priority will be + * rendered + * + * @throws IllegalArgumentException if {@code maxConcurrentMarkers} is negative + */ + public void setMaxConcurrentMarkers(int maxConcurrentMarkers) { + requireNonNegative(maxConcurrentMarkers, "maxConcurrentMarkers"); + + if (maxConcurrentMarkers != this.maxConcurrentMarkers) { + this.maxConcurrentMarkers = maxConcurrentMarkers; + + this.clearPaintState(); + this.repaint(); + } + } + @Override public void setViewportView(Component view) { final Component oldView = this.getViewport().getView(); @@ -216,76 +294,6 @@ private void mouseExitedImpl() { view.addMouseMotionListener(this.viewMouseAdapter); } - /** - * Adds a marker with passed {@code color} at the given {@code pos}. - * - * @param pos the vertical center of the marker within the space of this scroll pane's view; - * must not be negative; if greater than the height of the current view, - * the marker will not be rendered - * @param color the color of the marker - * @param priority the priority of the marker; if there are multiple markers at the same position, only up to - * {@link #maxConcurrentMarkers} of the highest priority markers will be rendered - * @param listener a listener for events within the marker; may be {@code null} - * - * @return an object which may be used to remove the marker by passing it to {@link #removeMarker(Object)} - * - * @throws IllegalArgumentException if {@code pos} is negative - * - * @see #removeMarker(Object) - * @see MarkerListener - */ - public Object addMarker(int pos, Color color, int priority, @Nullable MarkerListener listener) { - Preconditions.checkArgument(pos >= 0, "pos must not be negative!"); - - final Marker marker = new Marker(color, priority, Optional.ofNullable(listener)); - - this.markersByPos.put(pos, marker); - - if (this.paintState != null) { - this.paintState.pendingMarkerPositions.add(pos); - this.repaint(); - } - - return marker; - } - - /** - * Removes the passed {@code marker} if it belongs to this scroll pane. - * - * @param marker an object previously returned by {@link #addMarker(int, Color, int, MarkerListener)} - * - * @see #addMarker(int, Color, int, MarkerListener) - * @see #clearMarkers() - */ - public void removeMarker(Object marker) { - if (marker instanceof Marker removing) { - final Iterator> itr = this.markersByPos.entries().iterator(); - - while (itr.hasNext()) { - final Map.Entry entry = itr.next(); - if (entry.getValue() == removing) { - itr.remove(); - if (this.paintState != null) { - this.paintState.pendingMarkerPositions.add(entry.getKey()); - } - - break; - } - } - } - } - - /** - * Removes all markers from this scroll pane. - */ - public void clearMarkers() { - this.markersByPos.clear(); - - if (this.paintState != null) { - this.paintState.clearMarkers(); - } - } - @Override public void paint(Graphics graphics) { super.paint(graphics); @@ -386,7 +394,7 @@ void paint(Graphics graphics) { } void refreshPainter(int pos, Collection markers) { - if (pos < this.viewHeight && !markers.isEmpty()) { + if (pos < this.viewHeight && !markers.isEmpty() && MarkableScrollPane.this.maxConcurrentMarkers > 0) { final int scaledPos = this.viewHeight > this.areaHeight ? pos * this.areaHeight / this.viewHeight : pos; diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 8e8a2b3e5..637e50e9d 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -93,6 +93,7 @@ "menu.view.entry_tooltips.interactable": "Allow tooltip interaction", "menu.view.entry_markers": "Entry Markers", "menu.view.entry_markers.tooltip": "Marker tooltips", + "menu.view.entry_markers.max_markers_per_line": "Max markers per line (%s)", "menu.view.entry_markers.mark": "Mark entries", "menu.view.entry_markers.mark.only_declarations": "Only declarations", "menu.view.entry_markers.mark.obfuscated": "Obfuscated", From 3ec53af37756c9dda48ff85a8a251d35b4814b43 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 31 Oct 2025 18:26:51 -0700 Subject: [PATCH 027/110] eliminate marker overlap --- .../enigma/gui/panel/MarkableScrollPane.java | 193 +++++++++++++----- 1 file changed, 143 insertions(+), 50 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 86f8df07a..8048b9a96 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; +import com.google.common.collect.Multiset; import com.google.common.collect.TreeMultiset; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; @@ -23,12 +24,12 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -336,6 +337,30 @@ private PaintState createPaintState() { return new PaintState(areaX, areaY, areaHeight, viewHeight, this.markersByPos.keySet()); } + private MarkersPainter markersPainterOf(List markers, int x, int y, int height) { + final int markerCount = markers.size(); + Preconditions.checkArgument(markerCount > 0, "no markers!"); + + if (markerCount == 1) { + final ImmutableList spans = ImmutableList + .of(markers.get(0).new Span(x, MarkableScrollPane.this.markerWidth)); + return new MarkersPainter(spans, y, height); + } else { + final int spanWidth = MarkableScrollPane.this.markerWidth / markerCount; + // in case of non-evenly divisible width, give the most to the first marker: it has the highest priority + final int firstSpanWidth = MarkableScrollPane.this.markerWidth - spanWidth * (markerCount - 1); + + final ImmutableList.Builder spansBuilder = ImmutableList.builder(); + spansBuilder.add(markers.get(0).new Span(x, firstSpanWidth)); + + for (int i = 1; i < markerCount; i++) { + spansBuilder.add(markers.get(i).new Span(x + firstSpanWidth + spanWidth * (i - 1), spanWidth)); + } + + return new MarkersPainter(spansBuilder.build(), y, height); + } + } + public enum ScrollBarPolicy { /** * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_AS_NEEDED @@ -363,8 +388,7 @@ public enum ScrollBarPolicy { } private class PaintState { - // order with greatest position first so lesser positions are painted later and thus on top - final TreeMap paintersByPos = new TreeMap<>(Collections.reverseOrder()); + final NavigableMap paintersByPos = new TreeMap<>(); final int areaX; final int areaY; @@ -382,33 +406,68 @@ private class PaintState { } void paint(Graphics graphics) { + final Map builderByScaledPos = new TreeMap<>(); + for (final int pos : this.pendingMarkerPositions) { - this.refreshPainter(pos, MarkableScrollPane.this.markersByPos.get(pos)); + final Collection markers = MarkableScrollPane.this.markersByPos.get(pos); + if (pos < this.viewHeight && !markers.isEmpty() && MarkableScrollPane.this.maxConcurrentMarkers > 0) { + final int scaledPos = this.viewHeight > this.areaHeight + ? pos * this.areaHeight / this.viewHeight + : pos; + + this.paintersByPos.remove(pos); + + builderByScaledPos + .computeIfAbsent(scaledPos, builderPos -> { + final int markerTop = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, 0); + final int markerBottom = Math.min(markerTop + MarkableScrollPane.this.markerHeight, this.areaHeight); + + return new MarkersPainterBuilder(pos, markerTop, markerBottom); + }) + .addMarkers(markers); + } else { + this.paintersByPos.remove(pos); + } } - this.pendingMarkerPositions.clear(); + if (!builderByScaledPos.isEmpty()) { + final Iterator buildersItr = builderByScaledPos.values().iterator(); + MarkersPainterBuilder currentBuilder = buildersItr.next(); + while (true) { + final Map.Entry above = this.paintersByPos.lowerEntry(currentBuilder.pos); + if (above != null) { + final MarkersPainter aboveReplacement = currentBuilder.eliminateOverlap(above.getValue()); + if (aboveReplacement != null) { + above.setValue(aboveReplacement); + } + } - for (final MarkersPainter painter : this.paintersByPos.values()) { - painter.paint(graphics); - } - } + final Map.Entry below = this.paintersByPos.higherEntry(currentBuilder.pos); + if (below != null) { + final MarkersPainter belowReplacement = currentBuilder.eliminateOverlap(below.getValue()); + if (belowReplacement != null) { + below.setValue(belowReplacement); + } + } - void refreshPainter(int pos, Collection markers) { - if (pos < this.viewHeight && !markers.isEmpty() && MarkableScrollPane.this.maxConcurrentMarkers > 0) { - final int scaledPos = this.viewHeight > this.areaHeight - ? pos * this.areaHeight / this.viewHeight - : pos; + if (buildersItr.hasNext()) { + final MarkersPainterBuilder nextBuilder = buildersItr.next(); + currentBuilder.eliminateOverlap(nextBuilder); + currentBuilder = nextBuilder; + } else { + break; + } + } - final int markerY = Math.max(scaledPos - MarkableScrollPane.this.markerHeight / 2, 0); - final int markerHeight = Math.min(MarkableScrollPane.this.markerHeight, this.areaHeight - markerY); + for (final MarkersPainterBuilder builder : builderByScaledPos.values()) { + this.paintersByPos.put(builder.pos, builder.build(this.areaX)); + } + } - final List posMarkers = markers.stream() - .limit(MarkableScrollPane.this.maxConcurrentMarkers) - .toList(); + this.pendingMarkerPositions.clear(); - this.paintersByPos.put(pos, new MarkersPainter(posMarkers, this.areaX, markerY, markerHeight)); - } else { - this.paintersByPos.remove(pos); + for (final MarkersPainter painter : this.paintersByPos.values()) { + painter.paint(graphics); } } @@ -419,9 +478,7 @@ void clearMarkers() { Optional findSpanContaining(int x, int y, Predicate predicate) { if (this.areaContains(x, y)) { - // default ordering puts greatest positions first so lesser positions are painted on top - // check in reverse order so the lesser positions (on top) are checked first - return this.paintersByPos.descendingMap().values().stream() + return this.paintersByPos.values().stream() .filter(painter -> painter.y <= y && y <= painter.y + painter.height) .flatMap(painter -> painter.spans.stream()) .filter(predicate) @@ -459,41 +516,77 @@ Marker getMarker() { } } - private class MarkersPainter { - final ImmutableList spans; - final int y; - final int height; + private record MarkersPainter(ImmutableList spans, int y, int height) { + void paint(Graphics graphics) { + for (final Marker.Span span : this.spans) { + graphics.setColor(span.getMarker().color); + graphics.fillRect(span.x, this.y, span.width, this.height); + } + } - MarkersPainter(List markers, int x, int y, int height) { - final int markerCount = markers.size(); - Preconditions.checkArgument(markerCount > 0, "no markers!"); + MarkersPainter withMovedTop(int amount) { + return new MarkersPainter(this.spans, this.y + amount, this.height - amount); + } - this.y = y; - this.height = height; + MarkersPainter withMovedBottom(int amount) { + return new MarkersPainter(this.spans, this.y, this.height + amount); + } + } - if (markerCount == 1) { - this.spans = ImmutableList.of(markers.get(0).new Span(x, MarkableScrollPane.this.markerWidth)); - } else { - final int spanWidth = MarkableScrollPane.this.markerWidth / markerCount; - // in case of non-evenly divisible width, give the most to the first marker: it has the highest priority - final int firstSpanWidth = MarkableScrollPane.this.markerWidth - spanWidth * (markerCount - 1); + private class MarkersPainterBuilder { + final int pos; + int top; + int bottom; + final Multiset markers = TreeMultiset.create(); - final ImmutableList.Builder spansBuilder = ImmutableList.builder(); - spansBuilder.add(markers.get(0).new Span(x, firstSpanWidth)); + MarkersPainterBuilder(int pos, int top, int bottom) { + this.pos = pos; + this.top = top; + this.bottom = bottom; + } - for (int i = 1; i < markerCount; i++) { - spansBuilder.add(markers.get(i).new Span(x + firstSpanWidth + spanWidth * (i - 1), spanWidth)); - } + void addMarkers(Collection markers) { + this.markers.addAll(markers); + } + + void eliminateOverlap(MarkersPainterBuilder lower) { + final int spaceBelow = lower.top - this.bottom; + if (spaceBelow < 0) { + final int thisAdjustment = spaceBelow / 2; + final int otherAdjustment = spaceBelow - thisAdjustment; - this.spans = spansBuilder.build(); + this.bottom += thisAdjustment; + lower.top -= otherAdjustment; } } - void paint(Graphics graphics) { - for (final Marker.Span span : this.spans) { - graphics.setColor(span.getMarker().color); - graphics.fillRect(span.x, this.y, span.width, this.height); + MarkersPainter eliminateOverlap(MarkersPainter painter) { + final int spaceAbove = painter.y + painter.height - this.top; + if (spaceAbove < 0) { + final int painterAdjustment = spaceAbove / 2; + final int thisAdjustment = spaceAbove - painterAdjustment; + + this.top -= thisAdjustment; + return painter.withMovedBottom(painterAdjustment); + } else { + final int spaceBelow = painter.y - this.bottom; + if (spaceBelow < 0) { + final int thisAdjustment = spaceBelow / 2; + final int painterAdjustment = spaceBelow - thisAdjustment; + + this.bottom += thisAdjustment; + return painter.withMovedTop(painterAdjustment); + } } + + return null; + } + + MarkersPainter build(int x) { + final List markers = this.markers.stream() + .limit(MarkableScrollPane.this.maxConcurrentMarkers) + .toList(); + return MarkableScrollPane.this.markersPainterOf(markers, x, this.top, this.bottom - this.top); } } From a6bffe69ab38f490e4284b231402d8664e3449c9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 31 Oct 2025 19:01:56 -0700 Subject: [PATCH 028/110] account for existing markers with differing positions but equal scaled positions when updating markers --- .../enigma/gui/panel/MarkableScrollPane.java | 64 +++++++++++++++---- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 8048b9a96..94274880e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -406,25 +406,61 @@ private class PaintState { } void paint(Graphics graphics) { + this.update(); + + for (final MarkersPainter painter : this.paintersByPos.values()) { + painter.paint(graphics); + } + } + + void update() { + if (this.pendingMarkerPositions.isEmpty()) { + return; + } + final Map builderByScaledPos = new TreeMap<>(); for (final int pos : this.pendingMarkerPositions) { final Collection markers = MarkableScrollPane.this.markersByPos.get(pos); if (pos < this.viewHeight && !markers.isEmpty() && MarkableScrollPane.this.maxConcurrentMarkers > 0) { - final int scaledPos = this.viewHeight > this.areaHeight - ? pos * this.areaHeight / this.viewHeight - : pos; + final int scaledPos = this.scalePos(pos); this.paintersByPos.remove(pos); - builderByScaledPos - .computeIfAbsent(scaledPos, builderPos -> { - final int markerTop = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, 0); - final int markerBottom = Math.min(markerTop + MarkableScrollPane.this.markerHeight, this.areaHeight); + final MarkersPainterBuilder builder = builderByScaledPos.computeIfAbsent(scaledPos, builderPos -> { + final int top = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, 0); + final int bottom = Math.min(top + MarkableScrollPane.this.markerHeight, this.areaHeight); + + return new MarkersPainterBuilder(pos, top, bottom); + }); + + builder.addMarkers(markers); + + Integer abovePos = this.paintersByPos.lowerKey(pos); + while (abovePos != null) { + final int aboveScaled = this.scalePos(abovePos); + if (aboveScaled == scaledPos) { + this.paintersByPos.remove(abovePos); + builder.addMarkers(MarkableScrollPane.this.markersByPos.get(abovePos)); - return new MarkersPainterBuilder(pos, markerTop, markerBottom); - }) - .addMarkers(markers); + abovePos = this.paintersByPos.lowerKey(abovePos); + } else { + break; + } + } + + Integer belowPos = this.paintersByPos.higherKey(pos); + while (belowPos != null) { + final int belowScaled = this.scalePos(belowPos); + if (belowScaled == scaledPos) { + this.paintersByPos.remove(belowPos); + builder.addMarkers(MarkableScrollPane.this.markersByPos.get(belowPos)); + + belowPos = this.paintersByPos.higherKey(belowPos); + } else { + break; + } + } } else { this.paintersByPos.remove(pos); } @@ -465,10 +501,12 @@ void paint(Graphics graphics) { } this.pendingMarkerPositions.clear(); + } - for (final MarkersPainter painter : this.paintersByPos.values()) { - painter.paint(graphics); - } + private int scalePos(int pos) { + return this.viewHeight > this.areaHeight + ? pos * this.areaHeight / this.viewHeight + : pos; } void clearMarkers() { From 6b6d4a82968044b3a48f120ab127d253b2ec7786 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 31 Oct 2025 21:35:02 -0700 Subject: [PATCH 029/110] add WIP EditorPanel.MarkerTooltip --- .../enigma/gui/panel/AbstractEditorPanel.java | 1 + .../gui/panel/DeclarationSnippetPanel.java | 4 +- .../quiltmc/enigma/gui/panel/EditorPanel.java | 148 +++++++++++++----- .../enigma/gui/panel/SimpleSnippetPanel.java | 43 +++++ 4 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java index b391b1b5d..a16473b4a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/AbstractEditorPanel.java @@ -172,6 +172,7 @@ protected CompletableFuture setClassHandle( ) { ClassEntry old = null; if (this.classHandler != null) { + this.classHandler.removeListener(); old = this.classHandler.getHandle().getRef(); if (closeOldHandle) { this.classHandler.getHandle().close(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index faf9bbdf0..e0dde6d5a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -71,8 +71,6 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.editor.setCaretColor(new Color(0, 0, 0, 0)); this.editor.getCaret().setSelectionVisible(true); - this.setClassHandle(targetTopClassHandle, false, source -> this.createSnippet(source, target)); - this.addSourceSetListener(source -> { if (!this.isBounded()) { // the source isn't very useful if it couldn't be trimmed @@ -91,6 +89,8 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl ))); } }); + + this.setClassHandle(targetTopClassHandle, false, source -> this.createSnippet(source, target)); } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index dd66fbd84..551808bf7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -26,13 +26,18 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.syntaxpain.PairsMarker; +import org.quiltmc.enigma.util.LineIndexer; import org.tinylog.Logger; +import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.KeyboardFocusManager; +import java.awt.MouseInfo; +import java.awt.Point; import java.awt.Toolkit; +import java.awt.Window; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; @@ -53,6 +58,7 @@ import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JPanel; +import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; @@ -380,7 +386,6 @@ private MarkerManager createMarkerManager() { private class TooltipManager { static final int MOUSE_STOPPED_MOVING_DELAY = 100; - // DIY tooltip because JToolTip can't be moved or resized final EntryTooltip entryTooltip = new EntryTooltip(EditorPanel.this.gui); final WindowAdapter guiFocusListener = new WindowAdapter() { @@ -538,6 +543,8 @@ private class MarkerManager { ); } + final MarkerTooltip markerTooltip = new MarkerTooltip(); + final boolean onlyMarkDeclarations; final boolean markObfuscated; @@ -593,7 +600,7 @@ void tryMarking(Token token, TokenType type, TokenStore tokenStore) { final Color color = colorConfig.value(); EditorPanel.this.editorScrollPane.addMarker( tokenPos, color, priority, - new EntryMarkerListener(token) + new MarkerListener(token) ); } catch (BadLocationException e) { Logger.warn("Tried to add marker for token with bad location: " + token); @@ -642,56 +649,121 @@ boolean marksProposed() { boolean marksDeobfuscated() { return this.markDeobfuscated; } - } - private class EntryMarkerListener implements MarkableScrollPane.MarkerListener { - private final Token token; + private class MarkerListener implements MarkableScrollPane.MarkerListener { + private final Token token; - EntryMarkerListener(Token token) { - this.token = token; - } + MarkerListener(Token token) { + this.token = token; + } - @Override - public void mouseClicked() { - EditorPanel.this.navigateToToken(this.token); - } + @Override + public void mouseClicked() { + EditorPanel.this.navigateToToken(this.token); + } - @Override - public void mouseExited() { - if ( - Config.editor().entryMarkers.tooltip.value() - && EditorPanel.this.tooltipManager.lastMouseTargetToken == null - ) { - EditorPanel.this.tooltipManager.entryTooltip.close(); + @Override + public void mouseExited() { + if ( + Config.editor().entryMarkers.tooltip.value() + && EditorPanel.this.tooltipManager.lastMouseTargetToken == null + ) { + MarkerManager.this.markerTooltip.close(); + } } - } - @Override - public void mouseEntered() { - if (Config.editor().entryMarkers.tooltip.value()) { - // dont' resolve the token for markers - final EntryReference, Entry> reference = - EditorPanel.this.getReference(this.token); - if (reference != null) { + @Override + public void mouseEntered() { + if (Config.editor().entryMarkers.tooltip.value()) { + // dont' resolve the token for markers + final EntryReference, Entry> reference = + EditorPanel.this.getReference(this.token); + if (reference != null) { + EditorPanel.this.tooltipManager.reset(); + // TODO pass marker middle-left edge pos to listeners and then pass it here so the tooltip + // can position itself relative to that instead of the mouse + MarkerManager.this.markerTooltip.open(this.token); + } + } + } + + // TODO comment is probably obsolete + // This is used instead of just exit+enter because closing immediately before opening + // causes the (slightly delayed) window lost focus listener to close the tooltip + // for the new marker immediately after opening it. + @Override + public void mouseTransferred() { + this.mouseEntered(); + } + + @Override + public void mouseMoved() { + if (Config.editor().entryMarkers.tooltip.value()) { EditorPanel.this.tooltipManager.reset(); - EditorPanel.this.tooltipManager.openTooltip(reference.entry, false); } } } + } + + private class MarkerTooltip extends JWindow { + final JPanel content = new JPanel(); + + MarkerTooltip() { + this.setContentPane(this.content); - // This is used instead of just exit+enter because closing immediately before opening - // causes the (slightly delayed) window lost focus listener to close the tooltip - // for the new marker immediately after opening it. - @Override - public void mouseTransferred() { - this.mouseEntered(); + this.setAlwaysOnTop(true); + this.setType(Window.Type.POPUP); + this.setLayout(new BorderLayout()); } - @Override - public void mouseMoved() { - if (Config.editor().entryMarkers.tooltip.value()) { - EditorPanel.this.tooltipManager.reset(); + void open(Token target) { + this.content.removeAll(); + + if (EditorPanel.this.classHandler == null) { + return; } + + final SimpleSnippetPanel snippet = new SimpleSnippetPanel(EditorPanel.this.gui, target); + + this.content.add(snippet.ui); + + snippet.setSource(EditorPanel.this.getSource(), source -> { + // TODO attach a lineIndexer to DecompiledClassSource + final String sourceString = source.toString(); + final LineIndexer lineIndexer = new LineIndexer(sourceString); + final int line = lineIndexer.getLine(target.start); + int lineStart = lineIndexer.getStartIndex(line); + int lineEnd = lineIndexer.getStartIndex(line + 1); + + if (lineEnd < 0) { + lineEnd = sourceString.length(); + } + + while (lineStart < lineEnd && Character.isWhitespace(sourceString.charAt(lineStart))) { + lineStart++; + } + + while (lineEnd > lineStart && Character.isWhitespace(sourceString.charAt(lineEnd - 1))) { + lineEnd--; + } + + return new Snippet(lineStart, lineEnd); + }); + + this.pack(); + + final Point mousePos = MouseInfo.getPointerInfo().getLocation(); + final int x = mousePos.x - this.getWidth(); + final int y = mousePos.y - this.getHeight() / 2; + + this.setLocation(x, y); + + this.setVisible(true); + } + + void close() { + this.setVisible(false); + this.content.removeAll(); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java new file mode 100644 index 000000000..4022f8d64 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java @@ -0,0 +1,43 @@ +package org.quiltmc.enigma.gui.panel; + +import org.quiltmc.enigma.api.source.Token; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; +import org.quiltmc.enigma.util.LineIndexer; + +import javax.annotation.Nullable; +import javax.swing.JEditorPane; +import javax.swing.JScrollPane; +import java.awt.Color; + +public class SimpleSnippetPanel extends AbstractEditorPanel { + public SimpleSnippetPanel(Gui gui, @Nullable Token target) { + super(gui); + + this.addSourceSetListener(source -> { + if (this.isBounded()) { + this.installEditorRuler(new LineIndexer(source.toString()).getLine(this.getSourceBounds().start())); + + if (target != null) { + final Token boundedTarget = this.navigateToTokenImpl(target); + if (boundedTarget != null) { + this.addHighlight(boundedTarget, BoxHighlightPainter.create( + new Color(0, 0, 0, 0), + Config.getCurrentSyntaxPaneColors().selectionHighlight.value() + )); + } + } + } + }); + } + + @Override + protected JScrollPane createEditorScrollPane(JEditorPane editor) { + return new JScrollPane( + editor, + JScrollPane.VERTICAL_SCROLLBAR_NEVER, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + ); + } +} From 4db9659f21ab5254a0b3559619040932fa134db3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 11:15:54 -0700 Subject: [PATCH 030/110] fix marker max top + min bottom --- .../java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 94274880e..4c764eb6c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -428,8 +428,8 @@ void update() { this.paintersByPos.remove(pos); final MarkersPainterBuilder builder = builderByScaledPos.computeIfAbsent(scaledPos, builderPos -> { - final int top = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, 0); - final int bottom = Math.min(top + MarkableScrollPane.this.markerHeight, this.areaHeight); + final int top = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, this.areaY); + final int bottom = Math.min(top + MarkableScrollPane.this.markerHeight, this.areaY + this.areaHeight); return new MarkersPainterBuilder(pos, top, bottom); }); From 6575a3850afc3cb027a12a062b84839e97655c3e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 13:56:12 -0700 Subject: [PATCH 031/110] refine position and size of marker tooltip ensure entry tooltip doesn't show when marker tooltip is showing --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 118 ++++++++------ .../enigma/gui/panel/MarkableScrollPane.java | 153 ++++++++++-------- .../enigma/gui/panel/SimpleSnippetPanel.java | 2 +- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 7 + 4 files changed, 162 insertions(+), 118 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 551808bf7..0b7283964 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -26,16 +26,16 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.syntaxpain.PairsMarker; +import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.LineIndexer; import org.tinylog.Logger; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.KeyboardFocusManager; -import java.awt.MouseInfo; -import java.awt.Point; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; @@ -76,7 +76,7 @@ public class EditorPanel extends AbstractEditorPanel { private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; - private final TooltipManager tooltipManager = new TooltipManager(); + private final EntryTooltipManager entryTooltipManager = new EntryTooltipManager(); private final List listeners = new ArrayList<>(); @@ -267,7 +267,7 @@ public static EditorPanel byUi(Component ui) { @Override public void destroy() { super.destroy(); - this.tooltipManager.removeExternalListeners(); + this.entryTooltipManager.removeExternalListeners(); } public NavigatorPanel getNavigatorPanel() { @@ -328,13 +328,13 @@ protected void setCursorReference(EntryReference, Entry> ref) { @Override public void offsetEditorZoom(int zoomAmount) { super.offsetEditorZoom(zoomAmount); - this.tooltipManager.entryTooltip.setZoom(zoomAmount); + this.entryTooltipManager.tooltip.setZoom(zoomAmount); } @Override public void resetEditorZoom() { super.resetEditorZoom(); - this.tooltipManager.entryTooltip.resetZoom(); + this.entryTooltipManager.tooltip.resetZoom(); } public void addListener(EditorActionListener listener) { @@ -383,16 +383,16 @@ private MarkerManager createMarkerManager() { ); } - private class TooltipManager { + private class EntryTooltipManager { static final int MOUSE_STOPPED_MOVING_DELAY = 100; - final EntryTooltip entryTooltip = new EntryTooltip(EditorPanel.this.gui); + final EntryTooltip tooltip = new EntryTooltip(EditorPanel.this.gui); final WindowAdapter guiFocusListener = new WindowAdapter() { @Override public void windowLostFocus(WindowEvent e) { - if (e.getOppositeWindow() != TooltipManager.this.entryTooltip) { - TooltipManager.this.entryTooltip.close(); + if (e.getOppositeWindow() != EntryTooltipManager.this.tooltip) { + EntryTooltipManager.this.tooltip.close(); } } }; @@ -407,11 +407,14 @@ public void windowLostFocus(WindowEvent e) { // This also reduces the chances of accidentally updating the tooltip with // a new entry's content as you move your mouse to the tooltip. final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - if (Config.editor().entryTooltips.enable.value()) { + if ( + Config.editor().entryTooltips.enable.value() + && !EditorPanel.this.markerManager.markerTooltip.isShowing() + ) { EditorPanel.this.consumeEditorMouseTarget( (token, entry, resolvedParent) -> { this.hideTimer.stop(); - if (this.entryTooltip.isVisible()) { + if (this.tooltip.isVisible()) { this.showTimer.stop(); if (!token.equals(this.lastMouseTargetToken)) { @@ -424,7 +427,7 @@ public void windowLostFocus(WindowEvent e) { } }, () -> consumeMousePositionIn( - this.entryTooltip.getContentPane(), + this.tooltip.getContentPane(), (absolute, relative) -> this.hideTimer.stop(), absolute -> { this.lastMouseTargetToken = null; @@ -440,7 +443,7 @@ public void windowLostFocus(WindowEvent e) { ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { EditorPanel.this.consumeEditorMouseTarget((token, entry, resolvedParent) -> { if (token.equals(this.lastMouseTargetToken)) { - this.entryTooltip.setVisible(true); + this.tooltip.setVisible(true); this.openTooltip(entry, resolvedParent); } }); @@ -449,55 +452,55 @@ public void windowLostFocus(WindowEvent e) { final Timer hideTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, - e -> this.entryTooltip.close() + e -> this.tooltip.close() ); @Nullable Token lastMouseTargetToken; - TooltipManager() { + EntryTooltipManager() { this.mouseStoppedMovingTimer.setRepeats(false); this.showTimer.setRepeats(false); this.hideTimer.setRepeats(false); - this.entryTooltip.setVisible(false); + this.tooltip.setVisible(false); - this.entryTooltip.addMouseMotionListener(new MouseAdapter() { + this.tooltip.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { if (Config.editor().entryTooltips.interactable.value()) { - TooltipManager.this.mouseStoppedMovingTimer.stop(); - TooltipManager.this.hideTimer.stop(); + EntryTooltipManager.this.mouseStoppedMovingTimer.stop(); + EntryTooltipManager.this.hideTimer.stop(); } } }); - this.entryTooltip.addCloseListener(TooltipManager.this::reset); + this.tooltip.addCloseListener(EntryTooltipManager.this::reset); EditorPanel.this.editor.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { - TooltipManager.this.reset(); + EntryTooltipManager.this.reset(); } }); EditorPanel.this.editor.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) { - TooltipManager.this.entryTooltip.close(); + EntryTooltipManager.this.tooltip.close(); } }); EditorPanel.this.editor.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { - if (!TooltipManager.this.entryTooltip.hasRepopulated()) { - TooltipManager.this.mouseStoppedMovingTimer.restart(); + if (!EntryTooltipManager.this.tooltip.hasRepopulated()) { + EntryTooltipManager.this.mouseStoppedMovingTimer.restart(); } } }); - EditorPanel.this.editorScrollPane.getViewport().addChangeListener(e -> this.entryTooltip.close()); + EditorPanel.this.editorScrollPane.getViewport().addChangeListener(e -> this.tooltip.close()); this.addExternalListeners(); } @@ -514,7 +517,7 @@ void openTooltip(Entry target, boolean inherited) { final Component eventReceiver = focusOwner != null && isDescendingFrom(focusOwner, EditorPanel.this.gui.getFrame()) ? focusOwner : null; - this.entryTooltip.open(target, inherited, eventReceiver); + this.tooltip.open(target, inherited, eventReceiver); } void addExternalListeners() { @@ -658,56 +661,50 @@ private class MarkerListener implements MarkableScrollPane.MarkerListener { } @Override - public void mouseClicked() { + public void mouseClicked(int x, int y) { EditorPanel.this.navigateToToken(this.token); } @Override - public void mouseExited() { + public void mouseExited(int x, int y) { if ( Config.editor().entryMarkers.tooltip.value() - && EditorPanel.this.tooltipManager.lastMouseTargetToken == null + && EditorPanel.this.entryTooltipManager.lastMouseTargetToken == null ) { MarkerManager.this.markerTooltip.close(); } } @Override - public void mouseEntered() { + public void mouseEntered(int x, int y) { if (Config.editor().entryMarkers.tooltip.value()) { - // dont' resolve the token for markers - final EntryReference, Entry> reference = - EditorPanel.this.getReference(this.token); - if (reference != null) { - EditorPanel.this.tooltipManager.reset(); - // TODO pass marker middle-left edge pos to listeners and then pass it here so the tooltip - // can position itself relative to that instead of the mouse - MarkerManager.this.markerTooltip.open(this.token); - } + EditorPanel.this.entryTooltipManager.tooltip.close(); + MarkerManager.this.markerTooltip.open(this.token, x, y); } } - // TODO comment is probably obsolete - // This is used instead of just exit+enter because closing immediately before opening - // causes the (slightly delayed) window lost focus listener to close the tooltip - // for the new marker immediately after opening it. @Override - public void mouseTransferred() { - this.mouseEntered(); + public void mouseTransferred(int x, int y) { + this.mouseEntered(x, y); } @Override - public void mouseMoved() { + public void mouseMoved(int x, int y) { if (Config.editor().entryMarkers.tooltip.value()) { - EditorPanel.this.tooltipManager.reset(); + EditorPanel.this.entryTooltipManager.tooltip.close(); } } } } private class MarkerTooltip extends JWindow { + public static final int DEFAULT_MARKER_PAD = 5; final JPanel content = new JPanel(); + // HACK to make getPreferredSize aware of its (future) position + // negative values indicate it's un-set and should be ignored + int right = -1; + MarkerTooltip() { this.setContentPane(this.content); @@ -716,7 +713,7 @@ private class MarkerTooltip extends JWindow { this.setLayout(new BorderLayout()); } - void open(Token target) { + void open(Token target, int markerX, int markerY) { this.content.removeAll(); if (EditorPanel.this.classHandler == null) { @@ -750,13 +747,13 @@ void open(Token target) { return new Snippet(lineStart, lineEnd); }); + this.right = markerX - ScaleUtil.scale(DEFAULT_MARKER_PAD); + this.pack(); - final Point mousePos = MouseInfo.getPointerInfo().getLocation(); - final int x = mousePos.x - this.getWidth(); - final int y = mousePos.y - this.getHeight() / 2; + this.setLocation(this.right - this.getWidth(), markerY - this.getHeight() / 2); - this.setLocation(x, y); + this.right = -1; this.setVisible(true); } @@ -765,5 +762,20 @@ void close() { this.setVisible(false); this.content.removeAll(); } + + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); + + if (this.right >= 0) { + final int left = this.right - size.width; + if (left < 0) { + // don't extend off the left side of the screen + size.width += left; + } + } + + return size; + } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 4c764eb6c..410c61246 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -33,8 +33,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; -import java.util.function.Consumer; -import java.util.function.Predicate; /** * A scroll pane that renders markers in its view along the right edge, to the left of the vertical scroll bar.
@@ -46,8 +44,8 @@ * {@linkplain #addMarker(int, Color, int, MarkerListener) added}) will be rendered left-most. * No more than {@link #maxConcurrentMarkers} will be rendered at the same position. If there are excess markers, those * with lowest priority will be skipped. There's no guarantee which marker will be rendered when priorities are tied. - * When multiple markers are rendered at the same location, each will be narrower so their total width is equal to a - * single marker's. + * When multiple markers are rendered at the same location, each will be narrower so the total width remains constant + * regardless of the number of markers at a position. * * @see #addMarker(int, Color, int, MarkerListener) * @see #removeMarker(Object) @@ -55,7 +53,7 @@ */ public class MarkableScrollPane extends JScrollPane { private static void requireNonNegative(int value, String name) { - Preconditions.checkArgument(value >= 0, "%s (%s) must not be negative!".formatted(value, name)); + Preconditions.checkArgument(value >= 0, "%s (%s) must not be negative!".formatted(name, value)); } private static final int DEFAULT_MARKER_WIDTH = 10; @@ -152,7 +150,7 @@ public void componentHidden(ComponentEvent e) { public Object addMarker(int pos, Color color, int priority, @Nullable MarkerListener listener) { requireNonNegative(pos, "pos"); - final Marker marker = new Marker(color, priority, Optional.ofNullable(listener)); + final Marker marker = new Marker(color, priority, pos, Optional.ofNullable(listener)); this.markersByPos.put(pos, marker); @@ -174,17 +172,12 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MarkerList */ public void removeMarker(Object marker) { if (marker instanceof Marker removing) { - final Iterator> itr = this.markersByPos.entries().iterator(); - - while (itr.hasNext()) { - final Map.Entry entry = itr.next(); - if (entry.getValue() == removing) { - itr.remove(); - if (this.paintState != null) { - this.paintState.pendingMarkerPositions.add(entry.getKey()); - } - - break; + final boolean removed = this.markersByPos.remove(removing.pos, removing); + if (removed) { + if (this.paintState != null) { + this.paintState.pendingMarkerPositions.add(removing.pos); + // TODO try the repaint method that takes a rectangle + this.repaint(); } } } @@ -231,25 +224,20 @@ public void setViewportView(Component view) { this.viewMouseAdapter = new MouseAdapter() { @Nullable - MarkerListener lastEntered; + ListenerPos lastListenerPos; - Optional findMarkerListener(MouseEvent e) { + Optional findListenerPos(MouseEvent e) { if (MarkableScrollPane.this.paintState == null) { return Optional.empty(); } else { - final Point relativePos = - GuiUtil.getRelativePos(MarkableScrollPane.this, e.getXOnScreen(), e.getYOnScreen()); - return MarkableScrollPane.this.paintState - .findSpanContaining( - relativePos.x, relativePos.y, - span -> span.getMarker().listener.isPresent() - ) - .map(span -> span.getMarker().listener.orElseThrow()); + final Point relativePos = GuiUtil + .getRelativePos(MarkableScrollPane.this, e.getXOnScreen(), e.getYOnScreen()); + return MarkableScrollPane.this.paintState.findListenerPos(relativePos.x, relativePos.y); } } - void tryMarkerListeners(MouseEvent e, Consumer listen) { - this.findMarkerListener(e).ifPresent(listen); + void tryMarkerListeners(MouseEvent e, ListenerMethod method) { + this.findListenerPos(e).ifPresent(listenerPos -> listenerPos.invoke(method)); } @Override @@ -266,16 +254,16 @@ public void mouseExited(MouseEvent e) { public void mouseMoved(MouseEvent e) { this.tryMarkerListeners(e, MarkerListener::mouseMoved); - this.findMarkerListener(e).ifPresentOrElse( - listener -> { - if (listener != this.lastEntered) { - if (this.lastEntered == null) { - listener.mouseEntered(); + this.findListenerPos(e).ifPresentOrElse( + listenerPos -> { + if (!listenerPos.equals(this.lastListenerPos)) { + if (this.lastListenerPos == null) { + listenerPos.invoke(MarkerListener::mouseEntered); } else { - listener.mouseTransferred(); + listenerPos.invoke(MarkerListener::mouseTransferred); } - this.lastEntered = listener; + this.lastListenerPos = listenerPos; } }, this::mouseExitedImpl @@ -283,9 +271,9 @@ public void mouseMoved(MouseEvent e) { } private void mouseExitedImpl() { - if (this.lastEntered != null) { - this.lastEntered.mouseExited(); - this.lastEntered = null; + if (this.lastListenerPos != null) { + this.lastListenerPos.invoke(MarkerListener::mouseExited); + this.lastListenerPos = null; } } }; @@ -337,14 +325,14 @@ private PaintState createPaintState() { return new PaintState(areaX, areaY, areaHeight, viewHeight, this.markersByPos.keySet()); } - private MarkersPainter markersPainterOf(List markers, int x, int y, int height) { + private MarkersPainter markersPainterOf(List markers, int scaledPos, int x, int y, int height) { final int markerCount = markers.size(); Preconditions.checkArgument(markerCount > 0, "no markers!"); if (markerCount == 1) { final ImmutableList spans = ImmutableList .of(markers.get(0).new Span(x, MarkableScrollPane.this.markerWidth)); - return new MarkersPainter(spans, y, height); + return new MarkersPainter(spans, scaledPos, y, height); } else { final int spanWidth = MarkableScrollPane.this.markerWidth / markerCount; // in case of non-evenly divisible width, give the most to the first marker: it has the highest priority @@ -357,7 +345,7 @@ private MarkersPainter markersPainterOf(List markers, int x, int y, int spansBuilder.add(markers.get(i).new Span(x + firstSpanWidth + spanWidth * (i - 1), spanWidth)); } - return new MarkersPainter(spansBuilder.build(), y, height); + return new MarkersPainter(spansBuilder.build(), scaledPos, y, height); } } @@ -431,7 +419,7 @@ void update() { final int top = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, this.areaY); final int bottom = Math.min(top + MarkableScrollPane.this.markerHeight, this.areaY + this.areaHeight); - return new MarkersPainterBuilder(pos, top, bottom); + return new MarkersPainterBuilder(pos, scaledPos, top, bottom); }); builder.addMarkers(markers); @@ -514,13 +502,23 @@ void clearMarkers() { this.pendingMarkerPositions.clear(); } - Optional findSpanContaining(int x, int y, Predicate predicate) { + Optional findListenerPos(int x, int y) { if (this.areaContains(x, y)) { return this.paintersByPos.values().stream() .filter(painter -> painter.y <= y && y <= painter.y + painter.height) - .flatMap(painter -> painter.spans.stream()) - .filter(predicate) - .filter(span -> span.x <= x && x <= span.x + span.width) + .flatMap(painter -> painter + .spans.stream() + .filter(span -> span.getMarker().listener.isPresent()) + .filter(span -> span.x <= x && x <= span.x + span.width) + .findFirst() + .map(span -> { + final Point absolutePos = GuiUtil + .getAbsolutePos(MarkableScrollPane.this, this.areaX, painter.scaledPos); + + return new ListenerPos(span.getMarker().listener.orElseThrow(), absolutePos.x, absolutePos.y); + }) + .stream() + ) .findFirst(); } else { return Optional.empty(); @@ -533,7 +531,8 @@ boolean areaContains(int x, int y) { } } - private record Marker(Color color, int priority, Optional listener) implements Comparable { + private record Marker(Color color, int priority, int pos, Optional listener) + implements Comparable { @Override public int compareTo(@Nonnull Marker other) { return other.priority - this.priority; @@ -554,7 +553,7 @@ Marker getMarker() { } } - private record MarkersPainter(ImmutableList spans, int y, int height) { + private record MarkersPainter(ImmutableList spans, int scaledPos, int y, int height) { void paint(Graphics graphics) { for (final Marker.Span span : this.spans) { graphics.setColor(span.getMarker().color); @@ -562,23 +561,28 @@ void paint(Graphics graphics) { } } - MarkersPainter withMovedTop(int amount) { - return new MarkersPainter(this.spans, this.y + amount, this.height - amount); + MarkersPainter withTopMoved(int amount) { + return new MarkersPainter(this.spans, this.scaledPos, this.y + amount, this.height - amount); } - MarkersPainter withMovedBottom(int amount) { - return new MarkersPainter(this.spans, this.y, this.height + amount); + MarkersPainter withBottomMoved(int amount) { + return new MarkersPainter(this.spans, this.scaledPos, this.y, this.height + amount); } } private class MarkersPainterBuilder { final int pos; + final int scaledPos; + + final Multiset markers = TreeMultiset.create(); + int top; int bottom; - final Multiset markers = TreeMultiset.create(); - MarkersPainterBuilder(int pos, int top, int bottom) { + MarkersPainterBuilder(int pos, int scaledPos, int top, int bottom) { this.pos = pos; + this.scaledPos = scaledPos; + this.top = top; this.bottom = bottom; } @@ -598,6 +602,7 @@ void eliminateOverlap(MarkersPainterBuilder lower) { } } + @Nullable MarkersPainter eliminateOverlap(MarkersPainter painter) { final int spaceAbove = painter.y + painter.height - this.top; if (spaceAbove < 0) { @@ -605,7 +610,7 @@ MarkersPainter eliminateOverlap(MarkersPainter painter) { final int thisAdjustment = spaceAbove - painterAdjustment; this.top -= thisAdjustment; - return painter.withMovedBottom(painterAdjustment); + return painter.withBottomMoved(painterAdjustment); } else { final int spaceBelow = painter.y - this.bottom; if (spaceBelow < 0) { @@ -613,7 +618,7 @@ MarkersPainter eliminateOverlap(MarkersPainter painter) { final int painterAdjustment = spaceBelow - thisAdjustment; this.bottom += thisAdjustment; - return painter.withMovedTop(painterAdjustment); + return painter.withTopMoved(painterAdjustment); } } @@ -624,41 +629,61 @@ MarkersPainter build(int x) { final List markers = this.markers.stream() .limit(MarkableScrollPane.this.maxConcurrentMarkers) .toList(); - return MarkableScrollPane.this.markersPainterOf(markers, x, this.top, this.bottom - this.top); + + return MarkableScrollPane.this.markersPainterOf(markers, this.scaledPos, x, this.top, this.bottom - this.top); } } /** * A listener for marker events. * + *

Listener methods receive the absolute position of the marker's left edge on the screen. + * * @see #addMarker(int, Color, int, MarkerListener) */ public interface MarkerListener { /** * Called when the mouse clicks the marker. */ - void mouseClicked(); + void mouseClicked(int x, int y); /** * Called when the mouse enters the marker. */ - void mouseEntered(); + void mouseEntered(int x, int y); /** * Called when the mouse exits the marker. * - *

Not called when the mouse moves to an adjacent marker; see {@link #mouseTransferred()}. + *

Not called when the mouse moves to an adjacent marker; + * see {@link #mouseTransferred}. */ - void mouseExited(); + void mouseExited(int x, int y); /** * Called when the mouse moves from an adjacent marker to the marker. */ - void mouseTransferred(); + void mouseTransferred(int x, int y); /** * Called when the mouse within the marker. */ - void mouseMoved(); + void mouseMoved(int x, int y); + } + + private record ListenerPos(MarkerListener listener, int x, int y) { + void invoke(ListenerMethod method) { + method.listen(this.listener, this.x, this.y); + } + + @Override + public boolean equals(Object o) { + return o instanceof ListenerPos other && other.listener == this.listener; + } + } + + @FunctionalInterface + private interface ListenerMethod { + void listen(MarkerListener listener, int x, int pos); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java index 4022f8d64..b1293968a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java @@ -37,7 +37,7 @@ protected JScrollPane createEditorScrollPane(JEditorPane editor) { return new JScrollPane( editor, JScrollPane.VERTICAL_SCROLLBAR_NEVER, - JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index 3387db2c3..9f077e3f9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -402,6 +402,13 @@ public static Point getRelativePos(Component component, int absoluteX, int absol return componentPos; } + public static Point getAbsolutePos(Component component, int relativeX, int relativeY) { + final Point componentPos = component.getLocationOnScreen(); + componentPos.translate(relativeX, relativeY); + + return componentPos; + } + public static Optional getRecordIndexingService(Gui gui) { return gui.getController() .getProject() From 708af1630977e77873228e5711533c126fafb933 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 14:45:02 -0700 Subject: [PATCH 032/110] pass marker area to repaint method --- .../enigma/gui/panel/MarkableScrollPane.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 410c61246..15c146e1f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -103,8 +103,9 @@ public MarkableScrollPane( this.addComponentListener(new ComponentListener() { void refreshMarkers() { - MarkableScrollPane.this.clearPaintState(); - MarkableScrollPane.this.repaint(); + MarkableScrollPane.this.paintState = MarkableScrollPane.this.createPaintState(); + + MarkableScrollPane.this.repaint(MarkableScrollPane.this.paintState.createArea()); } @Override @@ -124,7 +125,7 @@ public void componentShown(ComponentEvent e) { @Override public void componentHidden(ComponentEvent e) { - this.refreshMarkers(); + MarkableScrollPane.this.paintState = null; } }); } @@ -154,11 +155,13 @@ public Object addMarker(int pos, Color color, int priority, @Nullable MarkerList this.markersByPos.put(pos, marker); - if (this.paintState != null) { - this.paintState.pendingMarkerPositions.add(pos); - this.repaint(); + if (this.paintState == null) { + this.paintState = this.createPaintState(); } + this.paintState.pendingMarkerPositions.add(pos); + this.repaint(this.paintState.createArea()); + return marker; } @@ -174,11 +177,12 @@ public void removeMarker(Object marker) { if (marker instanceof Marker removing) { final boolean removed = this.markersByPos.remove(removing.pos, removing); if (removed) { - if (this.paintState != null) { - this.paintState.pendingMarkerPositions.add(removing.pos); - // TODO try the repaint method that takes a rectangle - this.repaint(); + if (this.paintState == null) { + this.paintState = this.createPaintState(); } + + this.paintState.pendingMarkerPositions.add(removing.pos); + this.repaint(this.paintState.createArea()); } } } @@ -207,8 +211,8 @@ public void setMaxConcurrentMarkers(int maxConcurrentMarkers) { if (maxConcurrentMarkers != this.maxConcurrentMarkers) { this.maxConcurrentMarkers = maxConcurrentMarkers; - this.clearPaintState(); - this.repaint(); + this.paintState = this.createPaintState(); + this.repaint(this.paintState.createArea()); } } @@ -294,10 +298,6 @@ public void paint(Graphics graphics) { this.paintState.paint(graphics); } - private void clearPaintState() { - this.paintState = null; - } - private PaintState createPaintState() { final Rectangle bounds = this.getBounds(); final Insets insets = this.getInsets(); @@ -529,6 +529,10 @@ boolean areaContains(int x, int y) { return this.areaX <= x && x <= this.areaX + MarkableScrollPane.this.markerWidth && this.areaY <= y && y <= this.areaY + this.areaHeight; } + + Rectangle createArea() { + return new Rectangle(this.areaX, this.areaY, MarkableScrollPane.this.markerWidth, this.areaHeight); + } } private record Marker(Color color, int priority, int pos, Optional listener) From 535f16b397f1731fe68fc9f2c1db9b424b6f08a7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 15:21:37 -0700 Subject: [PATCH 033/110] fully repaint in component listener --- .../org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 15c146e1f..4d79d55ca 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -105,7 +105,10 @@ public MarkableScrollPane( void refreshMarkers() { MarkableScrollPane.this.paintState = MarkableScrollPane.this.createPaintState(); - MarkableScrollPane.this.repaint(MarkableScrollPane.this.paintState.createArea()); + // I tried repainting only the old and new paintState areas here, but it sometimes left artifacts of + // previously painted markers when quickly resizing a right-side docker. + // Doing a full repaint avoids that artifacting. + MarkableScrollPane.this.repaint(); } @Override From 8b1117a5ff1a9b1763acd7a083017c68e3d4b57a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 15:23:29 -0700 Subject: [PATCH 034/110] attach a LineIndexer to DecompiledClassSource --- .../gui/panel/DeclarationSnippetPanel.java | 48 ++++++++----------- .../quiltmc/enigma/gui/panel/EditorPanel.java | 3 +- .../api/source/DecompiledClassSource.java | 8 ++++ 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index e0dde6d5a..c7bbb1557 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -78,7 +78,7 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.editor.setText("// " + I18n.translate("editor.snippet.message.no_declaration_found")); this.editor.getHighlighter().removeAllHighlights(); } else { - this.installEditorRuler(new LineIndexer(source.toString()).getLine(this.getSourceBounds().start())); + this.installEditorRuler(source.getLineIndexer().getLine(this.getSourceBounds().start())); this.resolveTarget(source, target) .map(Target::token) @@ -151,7 +151,7 @@ private Result getVariableSnippet( private Result findLambdaVariable( DecompiledClassSource source, Token target, LocalVariableEntry targetEntry, MethodEntry parent ) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); + final LineIndexer lineIndexer = source.getLineIndexer(); final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); return Optional.ofNullable(entryIndex.getDefinition(parent)) @@ -204,9 +204,8 @@ private Result findClassSnippet( DecompiledClassSource source, Token target, ClassEntry targetEntry ) { return this.getNodeType(targetEntry).andThen(nodeType -> { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, target, nodeType, lineIndexer) - .andThen(declaration -> findTypeDeclarationSnippet(declaration, lineIndexer)); + return findDeclaration(source, target, nodeType) + .andThen(declaration -> findTypeDeclarationSnippet(declaration, source.getLineIndexer())); }); } @@ -252,8 +251,6 @@ private Result>, String> getNodeType(ClassEnt private Result findMethodSnippet( DecompiledClassSource source, Token target, MethodEntry targetEntry ) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - final Class> nodeType; final Function, Optional> bodyGetter; if (targetEntry.isConstructor()) { @@ -264,7 +261,7 @@ private Result findMethodSnippet( bodyGetter = declaration -> ((MethodDeclaration) declaration).getBody(); } - return findDeclaration(source, target, nodeType, lineIndexer).andThen(declaration -> { + return findDeclaration(source, target, nodeType).andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); return bodyGetter.apply(declaration) @@ -272,10 +269,10 @@ private Result findMethodSnippet( .getRange() .>map(Result::ok) .orElseGet(() -> Result.err("no method body range!")) - .map(bodyRange -> toSnippet(lineIndexer, range.begin, bodyRange.begin)) + .map(bodyRange -> toSnippet(source.getLineIndexer(), range.begin, bodyRange.begin)) ) // no body: abstract - .orElseGet(() -> Result.ok(toSnippet(lineIndexer, range))); + .orElseGet(() -> Result.ok(toSnippet(source.getLineIndexer(), range))); }); } @@ -303,8 +300,7 @@ private Result findFieldSnippet( private Result findComponentParent(DecompiledClassSource source, ClassDefEntry parent) { final Token parentToken = source.getIndex().getDeclarationToken(parent); - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, parentToken, RecordDeclaration.class, lineIndexer) + return findDeclaration(source, parentToken, RecordDeclaration.class) .andThen(parentDeclaration -> parentDeclaration .getImplementedTypes() .getFirst() @@ -316,7 +312,7 @@ private Result findComponentParent(DecompiledClassSource source .getRange() .map(implRange -> implRange.begin.right(-1)) .map(beforeImpl -> toSnippet( - lineIndexer, + source.getLineIndexer(), parentDeclaration.getBegin().orElseThrow(), beforeImpl )) @@ -328,26 +324,24 @@ private Result findComponentParent(DecompiledClassSource source .orElseGet(() -> Result.err("no parent record token range!")) ) // no implemented types - .orElseGet(() -> findTypeDeclarationSnippet(parentDeclaration, lineIndexer)) + .orElseGet(() -> findTypeDeclarationSnippet(parentDeclaration, source.getLineIndexer())) ); } private static Result findEnumConstantSnippet(DecompiledClassSource source, Token target) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, target, EnumConstantDeclaration.class, lineIndexer) - .andThen(declaration -> Result.ok(toSnippet(lineIndexer, declaration.getRange().orElseThrow()))); + return findDeclaration(source, target, EnumConstantDeclaration.class) + .andThen(declaration -> Result.ok(toSnippet(source.getLineIndexer(), declaration.getRange().orElseThrow()))); } private static Result findRegularFieldSnippet(DecompiledClassSource source, Token target) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, target, FieldDeclaration.class, lineIndexer).andThen(declaration -> declaration + return findDeclaration(source, target, FieldDeclaration.class).andThen(declaration -> declaration .getTokenRange() .map(tokenRange -> { final Range range = declaration.getRange().orElseThrow(); return declaration.getVariables().stream() - .filter(variable -> rangeContains(lineIndexer, variable, target)) + .filter(variable -> rangeContains(source.getLineIndexer(), variable, target)) .findFirst() - .map(variable -> toDeclaratorSnippet(range, variable, lineIndexer)) + .map(variable -> toDeclaratorSnippet(range, variable, source.getLineIndexer())) .orElseGet(() -> Result.err("no matching field declarator!")); }) .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) @@ -357,14 +351,12 @@ private static Result findRegularFieldSnippet(DecompiledClassSo private Result findLocalSnippet( DecompiledClassSource source, Token parentToken, Token targetToken ) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - - return findDeclaration(source, parentToken, MethodDeclaration.class, lineIndexer) + return findDeclaration(source, parentToken, MethodDeclaration.class) .andThen(declaration -> declaration .getBody() .>map(Result::ok) .orElseGet(() -> Result.err("no method body!")) - .andThen(parentBody -> findLocalSnippet(targetToken, parentBody, lineIndexer, METHOD)) + .andThen(parentBody -> findLocalSnippet(targetToken, parentBody, source.getLineIndexer(), METHOD)) ); } @@ -405,12 +397,12 @@ private static Result findVariableExpressionSnippet( * found declarations always {@linkplain TypeDeclaration#hasRange() have a range} */ private static > Result findDeclaration( - DecompiledClassSource source, Token target, Class nodeType, LineIndexer lineIndexer + DecompiledClassSource source, Token target, Class nodeType ) { return parse(source).andThen(unit -> unit - .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) + .findAll(nodeType, declaration -> rangeContains(source.getLineIndexer(), declaration, target)) .stream() - .max(depthComparatorOf(lineIndexer)) + .max(depthComparatorOf(source.getLineIndexer())) .>map(Result::ok) .orElseGet(() -> Result.err("not found in parsed source!")) ); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 0b7283964..758021822 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -725,9 +725,8 @@ void open(Token target, int markerX, int markerY) { this.content.add(snippet.ui); snippet.setSource(EditorPanel.this.getSource(), source -> { - // TODO attach a lineIndexer to DecompiledClassSource final String sourceString = source.toString(); - final LineIndexer lineIndexer = new LineIndexer(sourceString); + final LineIndexer lineIndexer = source.getLineIndexer(); final int line = lineIndexer.getLine(target.start); int lineStart = lineIndexer.getStartIndex(line); int lineEnd = lineIndexer.getStartIndex(line + 1); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java index e6a49631f..fbbf1b574 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java @@ -11,6 +11,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableDefEntry; import org.quiltmc.enigma.impl.translation.LocalNameGenerator; +import org.quiltmc.enigma.util.LineIndexer; import java.util.Collection; import java.util.Iterator; @@ -27,11 +28,14 @@ public class DecompiledClassSource { private final TokenStore highlightedTokens; + private final LineIndexer lineIndexer; + private DecompiledClassSource(ClassEntry classEntry, SourceIndex obfuscatedIndex, SourceIndex remappedIndex, TokenStore highlightedTokens) { this.classEntry = classEntry; this.obfuscatedIndex = obfuscatedIndex; this.remappedIndex = remappedIndex; this.highlightedTokens = highlightedTokens; + this.lineIndexer = new LineIndexer(remappedIndex.getSource()); } public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) { @@ -128,6 +132,10 @@ private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fro return fromOffset + relativeOffset; } + public LineIndexer getLineIndexer() { + return this.lineIndexer; + } + @Override public String toString() { return this.remappedIndex.getSource(); From 236548ad824c40c23ebcc2231dc1f2893547dba9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 15:54:11 -0700 Subject: [PATCH 035/110] remove isBounded check from SimpleSnippetPanel --- .../enigma/gui/panel/SimpleSnippetPanel.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java index b1293968a..178ae7e18 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java @@ -16,17 +16,15 @@ public SimpleSnippetPanel(Gui gui, @Nullable Token target) { super(gui); this.addSourceSetListener(source -> { - if (this.isBounded()) { - this.installEditorRuler(new LineIndexer(source.toString()).getLine(this.getSourceBounds().start())); + this.installEditorRuler(new LineIndexer(source.toString()).getLine(this.getSourceBounds().start())); - if (target != null) { - final Token boundedTarget = this.navigateToTokenImpl(target); - if (boundedTarget != null) { - this.addHighlight(boundedTarget, BoxHighlightPainter.create( - new Color(0, 0, 0, 0), - Config.getCurrentSyntaxPaneColors().selectionHighlight.value() - )); - } + if (target != null) { + final Token boundedTarget = this.navigateToTokenImpl(target); + if (boundedTarget != null) { + this.addHighlight(boundedTarget, BoxHighlightPainter.create( + new Color(0, 0, 0, 0), + Config.getCurrentSyntaxPaneColors().selectionHighlight.value() + )); } } }); From 553f5c72a51537becf8c63b6158fd67e7d29fe2e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 1 Nov 2025 17:17:14 -0700 Subject: [PATCH 036/110] don't refresh markers on componentShown (causes unecessary repaint) --- .../java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 4d79d55ca..a46b54340 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -122,9 +122,7 @@ public void componentMoved(ComponentEvent e) { } @Override - public void componentShown(ComponentEvent e) { - this.refreshMarkers(); - } + public void componentShown(ComponentEvent e) { } @Override public void componentHidden(ComponentEvent e) { From 00edab99f6e51ff2fed3a8ae3c1ba29236f38cf1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 2 Nov 2025 12:03:56 -0800 Subject: [PATCH 037/110] use GuiUtil's createIntConfigRadioMenu in EntryMarkersMenu --- .../menu_bar/view/EntryMarkersMenu.java | 53 +------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java index e625eda5b..7038e9d1a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryMarkersMenu.java @@ -1,74 +1,23 @@ package org.quiltmc.enigma.gui.element.menu_bar.view; -import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.element.menu_bar.AbstractEnigmaMenu; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; -import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; -import javax.swing.JRadioButtonMenuItem; import javax.swing.JToolBar; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import static org.quiltmc.enigma.gui.config.EntryMarkersSection.MAX_MAX_MARKERS_PER_LINE; import static org.quiltmc.enigma.gui.config.EntryMarkersSection.MIN_MAX_MARKERS_PER_LINE; public class EntryMarkersMenu extends AbstractEnigmaMenu { - @SuppressWarnings("SameParameterValue") - private static JMenu createIntConfigRadioMenu( - TrackedValue config, int min, int max, Runnable onUpdate - ) { - final Map radiosByChoice = IntStream.range(min, max + 1) - .boxed() - .collect(Collectors.toMap( - Function.identity(), - choice -> { - final JRadioButtonMenuItem choiceItem = new JRadioButtonMenuItem(); - choiceItem.setText(Integer.toString(choice)); - if (choice.equals(config.value())) { - choiceItem.setSelected(true); - } - - final int finalChoice = choice; - choiceItem.addActionListener(e -> { - config.setValue(finalChoice); - onUpdate.run(); - }); - - return choiceItem; - } - )); - - final ButtonGroup choicesGroup = new ButtonGroup(); - final JMenu menu = new JMenu(); - for (final JRadioButtonMenuItem radio : radiosByChoice.values()) { - choicesGroup.add(radio); - menu.add(radio); - } - - config.registerCallback(updated -> { - final JRadioButtonMenuItem choiceItem = radiosByChoice.get(updated.value()); - - if (!choiceItem.isSelected()) { - choiceItem.setSelected(true); - onUpdate.run(); - } - }); - - return menu; - } - private final JCheckBoxMenuItem tooltip = GuiUtil .createSyncedMenuCheckBox(Config.editor().entryMarkers.tooltip); - private final JMenu maxMarkersPerLineMenu = createIntConfigRadioMenu( + private final JMenu maxMarkersPerLineMenu = GuiUtil.createIntConfigRadioMenu( Config.editor().entryMarkers.maxMarkersPerLine, MIN_MAX_MARKERS_PER_LINE, MAX_MAX_MARKERS_PER_LINE, this::translateMarkersPerLineMenu From 947301ba11848bbd6962179faead2013657a7dba Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 8 Nov 2025 18:07:49 -0800 Subject: [PATCH 038/110] shift scaledPos down by this.areaY --- .../java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index a46b54340..fe76fe6d9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -412,7 +412,7 @@ void update() { for (final int pos : this.pendingMarkerPositions) { final Collection markers = MarkableScrollPane.this.markersByPos.get(pos); if (pos < this.viewHeight && !markers.isEmpty() && MarkableScrollPane.this.maxConcurrentMarkers > 0) { - final int scaledPos = this.scalePos(pos); + final int scaledPos = this.scalePos(pos) + this.areaY; this.paintersByPos.remove(pos); From 87a406be211683d71c8dc100a48925346d41ad30 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 13:58:05 -0800 Subject: [PATCH 039/110] use jspecify annotations in new classes --- .../org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 6 +++--- .../org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index fe76fe6d9..e3bce1ade 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -6,11 +6,11 @@ import com.google.common.collect.Multimaps; import com.google.common.collect.Multiset; import com.google.common.collect.TreeMultiset; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import java.awt.Color; @@ -539,7 +539,7 @@ Rectangle createArea() { private record Marker(Color color, int priority, int pos, Optional listener) implements Comparable { @Override - public int compareTo(@Nonnull Marker other) { + public int compareTo(@NonNull Marker other) { return other.priority - this.priority; } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java index 178ae7e18..6ff73a39c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java @@ -1,12 +1,12 @@ package org.quiltmc.enigma.gui.panel; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; import org.quiltmc.enigma.util.LineIndexer; -import javax.annotation.Nullable; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import java.awt.Color; From e69af0eda345a5070a452790068ebc61001a2288 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 14:00:44 -0800 Subject: [PATCH 040/110] checkstyle --- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 12 ++++++------ .../quiltmc/enigma/gui/panel/MarkableScrollPane.java | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 758021822..3cf51563f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -619,14 +619,14 @@ private TrackedValue getColorConfig( } else { return switch (type) { case OBFUSCATED -> this.markObfuscated - ? Config.getCurrentSyntaxPaneColors().obfuscatedOutline - : null; + ? Config.getCurrentSyntaxPaneColors().obfuscatedOutline + : null; case DEOBFUSCATED -> this.markDeobfuscated - ? Config.getCurrentSyntaxPaneColors().deobfuscatedOutline - : null; + ? Config.getCurrentSyntaxPaneColors().deobfuscatedOutline + : null; case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.markProposed - ? Config.getCurrentSyntaxPaneColors().proposedOutline - : null; + ? Config.getCurrentSyntaxPaneColors().proposedOutline + : null; // these only appear if debugTokenHighlights is true, so no need for a separate marker config case DEBUG -> Config.getCurrentSyntaxPaneColors().debugTokenOutline; }; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index e3bce1ade..24447f1c7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -494,8 +494,8 @@ void update() { private int scalePos(int pos) { return this.viewHeight > this.areaHeight - ? pos * this.areaHeight / this.viewHeight - : pos; + ? pos * this.areaHeight / this.viewHeight + : pos; } void clearMarkers() { @@ -528,7 +528,7 @@ Optional findListenerPos(int x, int y) { boolean areaContains(int x, int y) { return this.areaX <= x && x <= this.areaX + MarkableScrollPane.this.markerWidth - && this.areaY <= y && y <= this.areaY + this.areaHeight; + && this.areaY <= y && y <= this.areaY + this.areaHeight; } Rectangle createArea() { From c8359185898a0dda9e9a0135730731ecbc341759 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 4 Dec 2025 12:35:53 -0800 Subject: [PATCH 041/110] copy and dispose graphics in MarkableScrollPane::paint --- .../java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index 24447f1c7..ca2359967 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -296,7 +296,9 @@ public void paint(Graphics graphics) { this.paintState = this.createPaintState(); } - this.paintState.paint(graphics); + final Graphics disposableGraphics = graphics.create(); + this.paintState.paint(disposableGraphics); + disposableGraphics.dispose(); } private PaintState createPaintState() { From bf021ffaeaf9bf508ea0d2a0ee27e736174955c8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 4 Dec 2025 12:43:46 -0800 Subject: [PATCH 042/110] extract GuiUtil.TRANSPARENT hide SimpleSnippetPanel's caret --- .../enigma/gui/panel/DeclarationSnippetPanel.java | 3 ++- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 10 +++------- .../quiltmc/enigma/gui/panel/SimpleSnippetPanel.java | 3 +++ .../main/java/org/quiltmc/enigma/gui/util/GuiUtil.java | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index c7bbb1557..1fcb8bd84 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -40,6 +40,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; @@ -68,7 +69,7 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.getEditor().setEditable(false); - this.editor.setCaretColor(new Color(0, 0, 0, 0)); + this.editor.setCaretColor(GuiUtil.TRANSPARENT); this.editor.getCaret().setSelectionVisible(true); this.addSourceSetListener(source -> { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index dd19f4843..208346b4f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -21,6 +21,7 @@ import org.quiltmc.enigma.gui.docker.Docker; import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.Utils; @@ -36,7 +37,6 @@ import javax.swing.tree.TreePath; import java.awt.AWTEvent; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; @@ -608,8 +608,8 @@ private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopI text.setWrapStyleWord(true); text.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); text.setFont(font); - text.setBackground(invisibleColorOf()); - text.setCaretColor(invisibleColorOf()); + text.setBackground(GuiUtil.TRANSPARENT); + text.setCaretColor(GuiUtil.TRANSPARENT); text.getCaret().setSelectionVisible(true); text.setBorder(createEmptyBorder()); @@ -620,10 +620,6 @@ private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopI return text; } - private static Color invisibleColorOf() { - return new Color(0, 0, 0, 0); - } - private ImmutableList paramJavadocsOf( Entry target, Font nameFont, Font javadocFont, MouseAdapter stopInteraction ) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java index 6ff73a39c..6f001528a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SimpleSnippetPanel.java @@ -5,6 +5,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; +import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.util.LineIndexer; import javax.swing.JEditorPane; @@ -15,6 +16,8 @@ public class SimpleSnippetPanel extends AbstractEditorPanel { public SimpleSnippetPanel(Gui gui, @Nullable Token target) { super(gui); + this.editor.setCaretColor(GuiUtil.TRANSPARENT); + this.addSourceSetListener(source -> { this.installEditorRuler(new LineIndexer(source.toString()).getLine(this.getSourceBounds().start())); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index 9f077e3f9..427e4717c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -79,6 +79,8 @@ import java.util.stream.IntStream; public final class GuiUtil { + public static final Color TRANSPARENT = new Color(0, true); + private GuiUtil() { throw new UnsupportedOperationException(); } From 683b9943c13acd59287598a2c20d441dd6211acd Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 23 Nov 2025 19:33:42 -0800 Subject: [PATCH 043/110] start FlexGridLayout with Constraints --- .../enigma/gui/util/FlexGridLayout.java | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java new file mode 100644 index 000000000..9526a4ce8 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -0,0 +1,247 @@ +package org.quiltmc.enigma.gui.util; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager2; +import java.util.HashMap; +import java.util.Map; + +public class FlexGridLayout implements LayoutManager2 { + private final Map> grid = new HashMap<>(); + private final Map posByComponent = new HashMap<>(); + + @Override + public void addLayoutComponent(Component comp, Object constraints) throws ClassCastException { + this.addLayoutComponent(comp, (Constraints) constraints); + } + + public void addLayoutComponent(Component component, Constraints constraints) { + final Constrained constrained = new Constrained(component, constraints); + final Pos oldPos = this.posByComponent.replace(component, new Pos(constraints.x, constraints.y)); + if (oldPos != null) { + this.grid.get(oldPos.x).remove(oldPos.y, constrained); + } + + this.grid.computeIfAbsent(constraints.x, ignored -> HashMultimap.create(8, 1)) + .put(constraints.y, constrained); + } + + @Override + public Dimension maximumLayoutSize(Container target) { } + + @Override + public float getLayoutAlignmentX(Container target) { + return 0; + } + + @Override + public float getLayoutAlignmentY(Container target) { + return 0; + } + + @Override + public void invalidateLayout(Container target) { + // TODO + } + + @Override + public void addLayoutComponent(String name, Component comp) { + null + } + + @Override + public void removeLayoutComponent(Component comp) { + null + } + + @Override + public Dimension preferredLayoutSize(Container parent) { } + + @Override + public Dimension minimumLayoutSize(Container parent) { } + + @Override + public void layoutContainer(Container parent) { + null + } + + public static final class Constraints { + private static final int DEFAULT_X = 0; + private static final int DEFAULT_Y = 0; + private static final int DEFAULT_PRIORITY = 0; + private static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; + private static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; + private static final boolean DEFAULT_FILL_X = false; + private static final boolean DEFAULT_FILL_Y = false; + + public static Constraints of() { + return new Constraints( + DEFAULT_X, DEFAULT_Y, + DEFAULT_PRIORITY, + DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, + DEFAULT_FILL_X, DEFAULT_FILL_Y + ); + } + + private int x; + private int y; + + private int priority; + + private Alignment xAlignment; + private Alignment yAlignment; + + private boolean fillX; + private boolean fillY; + + private Constraints( + int x, int y, + int priority, + Alignment xAlignment, Alignment yAlignment, + boolean fillX, boolean fillY + ) { + this.x = x; + this.y = y; + + this.priority = priority; + + this.xAlignment = xAlignment; + this.yAlignment = yAlignment; + + this.fillX = fillX; + this.fillY = fillY; + } + + public Constraints x(int x) { + Preconditions.checkArgument(x >= 0, "x must not be negative!"); + this.x = x; + return this; + } + + public Constraints nextRow() { + this.x++; + this.y = 0; + return this; + } + + public Constraints y(int y) { + Preconditions.checkArgument(y >= 0, "y must not be negative!"); + this.y = y; + return this; + } + + public Constraints nextColumn() { + this.y++; + return this; + } + + public Constraints pos(int x, int y) { + this.x(x); + this.y(y); + return this; + } + + public Constraints priority(int priority) { + this.priority = priority; + return this; + } + + public Constraints xAlignment(Alignment alignment) { + this.xAlignment = alignment; + return this; + } + + public Constraints alignLeft() { + return this.xAlignment(Alignment.BEGIN); + } + + public Constraints alignRight() { + return this.xAlignment(Alignment.END); + } + + public Constraints yAlignment(Alignment alignment) { + this.yAlignment = alignment; + return this; + } + + public Constraints alignTop() { + return this.yAlignment(Alignment.BEGIN); + } + + public Constraints alignBottom() { + return this.yAlignment(Alignment.END); + } + + public Constraints align(Alignment x, Alignment y) { + this.xAlignment(x); + this.yAlignment(y); + return this; + } + + public Constraints alignCenter() { + return this.align(Alignment.CENTER, Alignment.CENTER); + } + + public Constraints fillX(boolean fill) { + this.fillX = fill; + return this; + } + + public Constraints fillX() { + return this.fillX(true); + } + + public Constraints fillY(boolean fill) { + this.fillY = fill; + return this; + } + + public Constraints fillY() { + return this.fillY(true); + } + + public Constraints fill(boolean x, boolean y) { + this.fillX(x); + this.fillY(y); + return this; + } + + public Constraints fillBoth() { + return this.fill(true, true); + } + + public Constraints copy() { + return new Constraints( + this.x, this.y, + this.priority, + this.xAlignment, this.yAlignment, + this.fillX, this.fillY + ); + } + + public enum Alignment { + BEGIN, CENTER, END + } + } + + private record Constrained( + Component component, int priority, + Constraints.Alignment xAlignment, Constraints.Alignment yAlignment, + boolean fillX, boolean fillY + ) { + Constrained(Component component, Constraints constraints) { + this( + component, constraints.priority, + constraints.xAlignment, constraints.yAlignment, + constraints.fillX, constraints.fillY + ); + } + } + + private record Pos(int x, int y) { } +} From 9cf8273a3b41fc4176630c13b50d2db228948093 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 24 Nov 2025 14:12:25 -0800 Subject: [PATCH 044/110] extract ConstrainedGrid from FlexGridLayout add Relative Constraints --- .../enigma/gui/util/ConstrainedGrid.java | 87 ++++ .../enigma/gui/util/FlexGridLayout.java | 417 ++++++++++++------ 2 files changed, 373 insertions(+), 131 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java new file mode 100644 index 000000000..a0e57815e --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java @@ -0,0 +1,87 @@ +package org.quiltmc.enigma.gui.util; + +import org.quiltmc.enigma.gui.util.FlexGridLayout.Constrained; + +import java.awt.Component; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Stream; + +/** + * A map of cartesian coordinates to {@link Constrained} values.
+ * Only designed for use with {@link FlexGridLayout}. + * + *

Multiple values can be associated with the same coordinates, + * but a value may only be associated with one coordinate pair at a time. + */ +class ConstrainedGrid { + private static Set createValueSet(Integer ignored) { + return new HashSet<>(); + } + + private final Map>> grid = new HashMap<>(); + private final Map valuePositions = new HashMap<>(); + private final SortedMap> valuesByMaxX = new TreeMap<>(); + private final SortedMap> valuesByMaxY = new TreeMap<>(); + + public void put(int x, int y, Constrained value) { + final Component component = value.component(); + final Position oldPos = this.valuePositions.replace(component, new Position(x, y)); + if (oldPos != null) { + final Map> column = this.grid.get(oldPos.x); + final Constrained oldValue = column.get(oldPos.y).get(component); + column.get(oldPos.y).remove(component, value); + this.valuesByMaxX.get(oldPos.x + oldValue.getXExcess()).remove(component); + this.valuesByMaxY.get(oldPos.y + oldValue.getYExcess()).remove(component); + } + + this.grid + .computeIfAbsent(x, ignored -> new HashMap<>()) + .computeIfAbsent(y, ignored -> new HashMap<>(1)) + .put(component, value); + this.valuesByMaxX.computeIfAbsent(x + value.getXExcess(), ConstrainedGrid::createValueSet).add(component); + this.valuesByMaxY.computeIfAbsent(y + value.getYExcess(), ConstrainedGrid::createValueSet).add(component); + } + + public Stream get(int x, int y) { + return this.grid.getOrDefault(x, Map.of()) + .getOrDefault(y, Map.of()) + .values() + .stream(); + } + + public boolean remove(Component value) { + final Position pos = this.valuePositions.remove(value); + if (pos != null) { + final Constrained removed = this.grid.get(pos.x).get(pos.y).remove(value); + this.valuesByMaxX.get(pos.x + removed.getXExcess()).remove(value); + this.valuesByMaxY.get(pos.y + removed.getYExcess()).remove(value); + + return true; + } else { + return false; + } + } + + public int getMaxXOrThrow() { + return this.valuesByMaxX.lastKey(); + } + + public int getMaxYOrThrow() { + return this.valuesByMaxY.lastKey(); + } + + public int getSize() { + return this.valuesByMaxX.size(); + } + + public boolean isEmpty() { + return this.valuesByMaxX.isEmpty(); + } + + private record Position(int x, int y) { } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 9526a4ce8..2d7d7f59a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -1,34 +1,69 @@ package org.quiltmc.enigma.gui.util; import com.google.common.base.Preconditions; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; +import org.jspecify.annotations.Nullable; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.LayoutManager2; -import java.util.HashMap; -import java.util.Map; +import java.util.Objects; public class FlexGridLayout implements LayoutManager2 { - private final Map> grid = new HashMap<>(); - private final Map posByComponent = new HashMap<>(); + private final ConstrainedGrid grid = new ConstrainedGrid(); @Override - public void addLayoutComponent(Component comp, Object constraints) throws ClassCastException { - this.addLayoutComponent(comp, (Constraints) constraints); + public void addLayoutComponent(Component component, @Nullable Object constraints) throws IllegalArgumentException { + if (constraints == null) { + this.addDefaultConstrainedLayoutComponent(component); + } else if (constraints instanceof Constraints typedConstraints) { + this.addLayoutComponent(component, typedConstraints); + } else { + throw new IllegalArgumentException( + "constraints type must be %s, but was %s!" + .formatted(Constraints.class.getName(), constraints.getClass().getName()) + ); + } } - public void addLayoutComponent(Component component, Constraints constraints) { - final Constrained constrained = new Constrained(component, constraints); - final Pos oldPos = this.posByComponent.replace(component, new Pos(constraints.x, constraints.y)); - if (oldPos != null) { - this.grid.get(oldPos.x).remove(oldPos.y, constrained); + public void addLayoutComponent(Component component, @Nullable Constraints constraints) { + if (constraints == null) { + this.addDefaultConstrainedLayoutComponent(component); + } else { + final int x; + final int y; + if (constraints instanceof Constraints.Absolute absolute) { + x = absolute.x; + y = absolute.y; + } else { + x = this.getRelativeX(); + y = this.getRelativeY(); + } + + this.grid.put(x, y, new Constrained(component, constraints)); } + } + + @Override + public void addLayoutComponent(String ignored, Component component) { + this.addDefaultConstrainedLayoutComponent(component); + } + + private void addDefaultConstrainedLayoutComponent(Component component) { + this.grid.put(this.getRelativeX(), this.getRelativeY(), Constrained.defaultOf(component)); + } + + private int getRelativeX() { + return this.grid.isEmpty() ? Constraints.Absolute.DEFAULT_X : this.grid.getMaxXOrThrow() + 1; + } - this.grid.computeIfAbsent(constraints.x, ignored -> HashMultimap.create(8, 1)) - .put(constraints.y, constrained); + private int getRelativeY() { + return this.grid.isEmpty() ? Constraints.Absolute.DEFAULT_Y : this.grid.getMaxYOrThrow(); + } + + @Override + public void removeLayoutComponent(Component component) { + this.grid.remove(component); } @Override @@ -36,12 +71,12 @@ public Dimension maximumLayoutSize(Container target) { } @Override public float getLayoutAlignmentX(Container target) { - return 0; + return 0.5f; } @Override public float getLayoutAlignmentY(Container target) { - return 0; + return 0.5f; } @Override @@ -49,16 +84,6 @@ public void invalidateLayout(Container target) { // TODO } - @Override - public void addLayoutComponent(String name, Component comp) { - null - } - - @Override - public void removeLayoutComponent(Component comp) { - null - } - @Override public Dimension preferredLayoutSize(Container parent) { } @@ -70,178 +95,308 @@ public void layoutContainer(Container parent) { null } - public static final class Constraints { - private static final int DEFAULT_X = 0; - private static final int DEFAULT_Y = 0; + public enum Alignment { + BEGIN, CENTER, END + } + + public static sealed abstract class Constraints> { private static final int DEFAULT_PRIORITY = 0; - private static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; - private static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; + private static final int DEFAULT_WIDTH = 1; + private static final int DEFAULT_HEIGHT = 1; private static final boolean DEFAULT_FILL_X = false; private static final boolean DEFAULT_FILL_Y = false; + private static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; + private static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; - public static Constraints of() { - return new Constraints( - DEFAULT_X, DEFAULT_Y, - DEFAULT_PRIORITY, - DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, - DEFAULT_FILL_X, DEFAULT_FILL_Y - ); + public static Relative createRelative() { + return Relative.of(); } - private int x; - private int y; + public static Absolute createAbsolute() { + return Absolute.of(); + } + + private static Alignment requireNonNullAlignment(Alignment alignment) { + return Objects.requireNonNull(alignment, "alignment must not be null!"); + } - private int priority; + int width; + int height; - private Alignment xAlignment; - private Alignment yAlignment; + boolean fillX; + boolean fillY; - private boolean fillX; - private boolean fillY; + Alignment xAlignment; + Alignment yAlignment; + + int priority; private Constraints( - int x, int y, - int priority, + int width, int height, + boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, - boolean fillX, boolean fillY + int priority ) { - this.x = x; - this.y = y; + this.width = width; + this.height = height; - this.priority = priority; + this.fillX = fillX; + this.fillY = fillY; this.xAlignment = xAlignment; this.yAlignment = yAlignment; - this.fillX = fillX; - this.fillY = fillY; + this.priority = priority; } - public Constraints x(int x) { - Preconditions.checkArgument(x >= 0, "x must not be negative!"); - this.x = x; - return this; + public C width(int width) { + Preconditions.checkArgument(width > 0, "width must be positive!"); + this.width = width; + return this.getSelf(); } - public Constraints nextRow() { - this.x++; - this.y = 0; - return this; + public C height(int height) { + Preconditions.checkArgument(height > 0, "height must be positive!"); + this.height = height; + return this.getSelf(); } - public Constraints y(int y) { - Preconditions.checkArgument(y >= 0, "y must not be negative!"); - this.y = y; - return this; + public C fillX(boolean fill) { + this.fillX = fill; + return this.getSelf(); } - public Constraints nextColumn() { - this.y++; - return this; + public C fillX() { + return this.fillX(true); } - public Constraints pos(int x, int y) { - this.x(x); - this.y(y); - return this; + public C fillY(boolean fill) { + this.fillY = fill; + return this.getSelf(); } - public Constraints priority(int priority) { - this.priority = priority; - return this; + public C fillY() { + return this.fillY(true); + } + + public C fill(boolean x, boolean y) { + this.fillX(x); + this.fillY(y); + return this.getSelf(); } - public Constraints xAlignment(Alignment alignment) { - this.xAlignment = alignment; - return this; + public C fillBoth() { + return this.fill(true, true); } - public Constraints alignLeft() { + public C xAlignment(Alignment alignment) { + this.xAlignment = requireNonNullAlignment(alignment); + return this.getSelf(); + } + + public C alignLeft() { return this.xAlignment(Alignment.BEGIN); } - public Constraints alignRight() { + public C alignRight() { return this.xAlignment(Alignment.END); } - public Constraints yAlignment(Alignment alignment) { - this.yAlignment = alignment; - return this; + public C yAlignment(Alignment alignment) { + this.yAlignment = requireNonNullAlignment(alignment); + return this.getSelf(); } - public Constraints alignTop() { + public C alignTop() { return this.yAlignment(Alignment.BEGIN); } - public Constraints alignBottom() { + public C alignBottom() { return this.yAlignment(Alignment.END); } - public Constraints align(Alignment x, Alignment y) { + public C align(Alignment x, Alignment y) { this.xAlignment(x); this.yAlignment(y); - return this; + return this.getSelf(); } - public Constraints alignCenter() { + public C alignCenter() { return this.align(Alignment.CENTER, Alignment.CENTER); } - public Constraints fillX(boolean fill) { - this.fillX = fill; - return this; - } - - public Constraints fillX() { - return this.fillX(true); - } - - public Constraints fillY(boolean fill) { - this.fillY = fill; - return this; + public C priority(int priority) { + this.priority = priority; + return this.getSelf(); } - public Constraints fillY() { - return this.fillY(true); + public abstract C copy(); + + protected abstract C getSelf(); + + public static final class Relative extends Constraints { + public static Relative of() { + return new Relative( + DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_FILL_X, DEFAULT_FILL_Y, + DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, + DEFAULT_PRIORITY + ); + } + + private Relative( + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority + ) { + super(width, height, fillX, fillY, xAlignment, yAlignment, priority); + } + + @Override + public Relative copy() { + return new Relative( + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Absolute toAbsolute() { + return new Absolute( + Absolute.DEFAULT_X, Absolute.DEFAULT_Y, + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Absolute toAbsolute(int x, int y) { + return this.toAbsolute().pos(x, y); + } + + @Override + protected Relative getSelf() { + return this; + } } - public Constraints fill(boolean x, boolean y) { - this.fillX(x); - this.fillY(y); - return this; + public static final class Absolute extends Constraints { + private static final int DEFAULT_X = 0; + private static final int DEFAULT_Y = 0; + + public static Absolute of() { + return new Absolute( + DEFAULT_X, DEFAULT_Y, + DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_FILL_X, DEFAULT_FILL_Y, + DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, + DEFAULT_PRIORITY + ); + } + + private int x; + private int y; + + private Absolute( + int x, int y, + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority + ) { + super(width, height, fillX, fillY, xAlignment, yAlignment, priority); + + this.x = x; + this.y = y; + } + + public Absolute x(int x) { + this.x = x; + return this; + } + + public Absolute nextRow() { + this.x++; + this.y = 0; + return this; + } + + public Absolute y(int y) { + this.y = y; + return this; + } + + public Absolute nextColumn() { + this.y++; + return this; + } + + public Absolute pos(int x, int y) { + this.x(x); + this.y(y); + return this; + } + + @Override + public Absolute copy() { + return new Absolute( + this.x, this.y, + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Relative toRelative() { + return new Relative( + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + @Override + protected Absolute getSelf() { + return this; + } } + } - public Constraints fillBoth() { - return this.fill(true, true); + record Constrained( + Component component, int width, int height, boolean fillX, boolean fillY, Alignment xAlignment, + Alignment yAlignment, int priority + ) { + static Constrained defaultOf(Component component) { + return new Constrained( + component, + Constraints.DEFAULT_WIDTH, Constraints.DEFAULT_HEIGHT, + Constraints.DEFAULT_FILL_X, Constraints.DEFAULT_FILL_Y, + Constraints.DEFAULT_X_ALIGNMENT, Constraints.DEFAULT_Y_ALIGNMENT, + Constraints.DEFAULT_PRIORITY + ); } - public Constraints copy() { - return new Constraints( - this.x, this.y, - this.priority, - this.xAlignment, this.yAlignment, - this.fillX, this.fillY + Constrained(Component component, Constraints constraints) { + this( + component, + constraints.width, constraints.height, + constraints.fillX, constraints.fillY, + constraints.xAlignment, constraints.yAlignment, + constraints.priority ); } - public enum Alignment { - BEGIN, CENTER, END + int getXExcess() { + return this.width - 1; } - } - private record Constrained( - Component component, int priority, - Constraints.Alignment xAlignment, Constraints.Alignment yAlignment, - boolean fillX, boolean fillY - ) { - Constrained(Component component, Constraints constraints) { - this( - component, constraints.priority, - constraints.xAlignment, constraints.yAlignment, - constraints.fillX, constraints.fillY - ); + int getYExcess() { + return this.height - 1; } } - - private record Pos(int x, int y) { } } From bd6467d52c04db0159ab041cd6e6d7e05e0f7f13 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 24 Nov 2025 14:25:28 -0800 Subject: [PATCH 045/110] promote FlexGridLayout.Constraints -> FlexGridConstraints --- .../enigma/gui/util/FlexGridConstraints.java | 273 ++++++++++++++++ .../enigma/gui/util/FlexGridLayout.java | 298 +----------------- 2 files changed, 287 insertions(+), 284 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java new file mode 100644 index 000000000..fbaf9257d --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -0,0 +1,273 @@ +package org.quiltmc.enigma.gui.util; + +import com.google.common.base.Preconditions; + +import java.util.Objects; + +public abstract sealed class FlexGridConstraints> { + static final int DEFAULT_PRIORITY = 0; + static final int DEFAULT_WIDTH = 1; + static final int DEFAULT_HEIGHT = 1; + static final boolean DEFAULT_FILL_X = false; + static final boolean DEFAULT_FILL_Y = false; + static final FlexGridLayout.Alignment DEFAULT_X_ALIGNMENT = FlexGridLayout.Alignment.BEGIN; + static final FlexGridLayout.Alignment DEFAULT_Y_ALIGNMENT = FlexGridLayout.Alignment.CENTER; + + public static Relative createRelative() { + return Relative.of(); + } + + public static Absolute createAbsolute() { + return Absolute.of(); + } + + private static FlexGridLayout.Alignment requireNonNullAlignment(FlexGridLayout.Alignment alignment) { + return Objects.requireNonNull(alignment, "alignment must not be null!"); + } + + int width; + int height; + + boolean fillX; + boolean fillY; + + FlexGridLayout.Alignment xAlignment; + FlexGridLayout.Alignment yAlignment; + + int priority; + + private FlexGridConstraints( + int width, int height, + boolean fillX, boolean fillY, + FlexGridLayout.Alignment xAlignment, FlexGridLayout.Alignment yAlignment, + int priority + ) { + this.width = width; + this.height = height; + + this.fillX = fillX; + this.fillY = fillY; + + this.xAlignment = xAlignment; + this.yAlignment = yAlignment; + + this.priority = priority; + } + + public C width(int width) { + Preconditions.checkArgument(width > 0, "width must be positive!"); + this.width = width; + return this.getSelf(); + } + + public C height(int height) { + Preconditions.checkArgument(height > 0, "height must be positive!"); + this.height = height; + return this.getSelf(); + } + + public C fillX(boolean fill) { + this.fillX = fill; + return this.getSelf(); + } + + public C fillX() { + return this.fillX(true); + } + + public C fillY(boolean fill) { + this.fillY = fill; + return this.getSelf(); + } + + public C fillY() { + return this.fillY(true); + } + + public C fill(boolean x, boolean y) { + this.fillX(x); + this.fillY(y); + return this.getSelf(); + } + + public C fillBoth() { + return this.fill(true, true); + } + + public C xAlignment(FlexGridLayout.Alignment alignment) { + this.xAlignment = requireNonNullAlignment(alignment); + return this.getSelf(); + } + + public C alignLeft() { + return this.xAlignment(FlexGridLayout.Alignment.BEGIN); + } + + public C alignRight() { + return this.xAlignment(FlexGridLayout.Alignment.END); + } + + public C yAlignment(FlexGridLayout.Alignment alignment) { + this.yAlignment = requireNonNullAlignment(alignment); + return this.getSelf(); + } + + public C alignTop() { + return this.yAlignment(FlexGridLayout.Alignment.BEGIN); + } + + public C alignBottom() { + return this.yAlignment(FlexGridLayout.Alignment.END); + } + + public C align(FlexGridLayout.Alignment x, FlexGridLayout.Alignment y) { + this.xAlignment(x); + this.yAlignment(y); + return this.getSelf(); + } + + public C alignCenter() { + return this.align(FlexGridLayout.Alignment.CENTER, FlexGridLayout.Alignment.CENTER); + } + + public C priority(int priority) { + this.priority = priority; + return this.getSelf(); + } + + public abstract C copy(); + + protected abstract C getSelf(); + + public static final class Relative extends FlexGridConstraints { + public static Relative of() { + return new Relative( + DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_FILL_X, DEFAULT_FILL_Y, + DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, + DEFAULT_PRIORITY + ); + } + + private Relative( + int width, int height, + boolean fillX, boolean fillY, + FlexGridLayout.Alignment xAlignment, FlexGridLayout.Alignment yAlignment, + int priority + ) { + super(width, height, fillX, fillY, xAlignment, yAlignment, priority); + } + + @Override + public Relative copy() { + return new Relative( + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Absolute toAbsolute() { + return new Absolute( + Absolute.DEFAULT_X, Absolute.DEFAULT_Y, + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Absolute toAbsolute(int x, int y) { + return this.toAbsolute().pos(x, y); + } + + @Override + protected Relative getSelf() { + return this; + } + } + + public static final class Absolute extends FlexGridConstraints { + static final int DEFAULT_X = 0; + static final int DEFAULT_Y = 0; + + public static Absolute of() { + return new Absolute( + DEFAULT_X, DEFAULT_Y, + DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_FILL_X, DEFAULT_FILL_Y, + DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, + DEFAULT_PRIORITY + ); + } + + int x; + int y; + + private Absolute( + int x, int y, + int width, int height, + boolean fillX, boolean fillY, + FlexGridLayout.Alignment xAlignment, FlexGridLayout.Alignment yAlignment, + int priority + ) { + super(width, height, fillX, fillY, xAlignment, yAlignment, priority); + + this.x = x; + this.y = y; + } + + public Absolute x(int x) { + this.x = x; + return this; + } + + public Absolute nextRow() { + this.x++; + this.y = 0; + return this; + } + + public Absolute y(int y) { + this.y = y; + return this; + } + + public Absolute nextColumn() { + this.y++; + return this; + } + + public Absolute pos(int x, int y) { + this.x(x); + this.y(y); + return this; + } + + @Override + public Absolute copy() { + return new Absolute( + this.x, this.y, + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + public Relative toRelative() { + return new Relative( + this.width, this.height, + this.fillX, this.fillY, + this.xAlignment, this.yAlignment, + this.priority + ); + } + + @Override + protected Absolute getSelf() { + return this; + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 2d7d7f59a..31e0abdf9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -1,13 +1,11 @@ package org.quiltmc.enigma.gui.util; -import com.google.common.base.Preconditions; import org.jspecify.annotations.Nullable; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.LayoutManager2; -import java.util.Objects; public class FlexGridLayout implements LayoutManager2 { private final ConstrainedGrid grid = new ConstrainedGrid(); @@ -16,23 +14,23 @@ public class FlexGridLayout implements LayoutManager2 { public void addLayoutComponent(Component component, @Nullable Object constraints) throws IllegalArgumentException { if (constraints == null) { this.addDefaultConstrainedLayoutComponent(component); - } else if (constraints instanceof Constraints typedConstraints) { + } else if (constraints instanceof FlexGridConstraints typedConstraints) { this.addLayoutComponent(component, typedConstraints); } else { throw new IllegalArgumentException( "constraints type must be %s, but was %s!" - .formatted(Constraints.class.getName(), constraints.getClass().getName()) + .formatted(FlexGridConstraints.class.getName(), constraints.getClass().getName()) ); } } - public void addLayoutComponent(Component component, @Nullable Constraints constraints) { + public void addLayoutComponent(Component component, @Nullable FlexGridConstraints constraints) { if (constraints == null) { this.addDefaultConstrainedLayoutComponent(component); } else { final int x; final int y; - if (constraints instanceof Constraints.Absolute absolute) { + if (constraints instanceof FlexGridConstraints.Absolute absolute) { x = absolute.x; y = absolute.y; } else { @@ -54,11 +52,11 @@ private void addDefaultConstrainedLayoutComponent(Component component) { } private int getRelativeX() { - return this.grid.isEmpty() ? Constraints.Absolute.DEFAULT_X : this.grid.getMaxXOrThrow() + 1; + return this.grid.isEmpty() ? FlexGridConstraints.Absolute.DEFAULT_X : this.grid.getMaxXOrThrow() + 1; } private int getRelativeY() { - return this.grid.isEmpty() ? Constraints.Absolute.DEFAULT_Y : this.grid.getMaxYOrThrow(); + return this.grid.isEmpty() ? FlexGridConstraints.Absolute.DEFAULT_Y : this.grid.getMaxYOrThrow(); } @Override @@ -66,9 +64,6 @@ public void removeLayoutComponent(Component component) { this.grid.remove(component); } - @Override - public Dimension maximumLayoutSize(Container target) { } - @Override public float getLayoutAlignmentX(Container target) { return 0.5f; @@ -84,6 +79,9 @@ public void invalidateLayout(Container target) { // TODO } + @Override + public Dimension maximumLayoutSize(Container target) { } + @Override public Dimension preferredLayoutSize(Container parent) { } @@ -99,274 +97,6 @@ public enum Alignment { BEGIN, CENTER, END } - public static sealed abstract class Constraints> { - private static final int DEFAULT_PRIORITY = 0; - private static final int DEFAULT_WIDTH = 1; - private static final int DEFAULT_HEIGHT = 1; - private static final boolean DEFAULT_FILL_X = false; - private static final boolean DEFAULT_FILL_Y = false; - private static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; - private static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; - - public static Relative createRelative() { - return Relative.of(); - } - - public static Absolute createAbsolute() { - return Absolute.of(); - } - - private static Alignment requireNonNullAlignment(Alignment alignment) { - return Objects.requireNonNull(alignment, "alignment must not be null!"); - } - - int width; - int height; - - boolean fillX; - boolean fillY; - - Alignment xAlignment; - Alignment yAlignment; - - int priority; - - private Constraints( - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority - ) { - this.width = width; - this.height = height; - - this.fillX = fillX; - this.fillY = fillY; - - this.xAlignment = xAlignment; - this.yAlignment = yAlignment; - - this.priority = priority; - } - - public C width(int width) { - Preconditions.checkArgument(width > 0, "width must be positive!"); - this.width = width; - return this.getSelf(); - } - - public C height(int height) { - Preconditions.checkArgument(height > 0, "height must be positive!"); - this.height = height; - return this.getSelf(); - } - - public C fillX(boolean fill) { - this.fillX = fill; - return this.getSelf(); - } - - public C fillX() { - return this.fillX(true); - } - - public C fillY(boolean fill) { - this.fillY = fill; - return this.getSelf(); - } - - public C fillY() { - return this.fillY(true); - } - - public C fill(boolean x, boolean y) { - this.fillX(x); - this.fillY(y); - return this.getSelf(); - } - - public C fillBoth() { - return this.fill(true, true); - } - - public C xAlignment(Alignment alignment) { - this.xAlignment = requireNonNullAlignment(alignment); - return this.getSelf(); - } - - public C alignLeft() { - return this.xAlignment(Alignment.BEGIN); - } - - public C alignRight() { - return this.xAlignment(Alignment.END); - } - - public C yAlignment(Alignment alignment) { - this.yAlignment = requireNonNullAlignment(alignment); - return this.getSelf(); - } - - public C alignTop() { - return this.yAlignment(Alignment.BEGIN); - } - - public C alignBottom() { - return this.yAlignment(Alignment.END); - } - - public C align(Alignment x, Alignment y) { - this.xAlignment(x); - this.yAlignment(y); - return this.getSelf(); - } - - public C alignCenter() { - return this.align(Alignment.CENTER, Alignment.CENTER); - } - - public C priority(int priority) { - this.priority = priority; - return this.getSelf(); - } - - public abstract C copy(); - - protected abstract C getSelf(); - - public static final class Relative extends Constraints { - public static Relative of() { - return new Relative( - DEFAULT_WIDTH, DEFAULT_HEIGHT, - DEFAULT_FILL_X, DEFAULT_FILL_Y, - DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, - DEFAULT_PRIORITY - ); - } - - private Relative( - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority - ) { - super(width, height, fillX, fillY, xAlignment, yAlignment, priority); - } - - @Override - public Relative copy() { - return new Relative( - this.width, this.height, - this.fillX, this.fillY, - this.xAlignment, this.yAlignment, - this.priority - ); - } - - public Absolute toAbsolute() { - return new Absolute( - Absolute.DEFAULT_X, Absolute.DEFAULT_Y, - this.width, this.height, - this.fillX, this.fillY, - this.xAlignment, this.yAlignment, - this.priority - ); - } - - public Absolute toAbsolute(int x, int y) { - return this.toAbsolute().pos(x, y); - } - - @Override - protected Relative getSelf() { - return this; - } - } - - public static final class Absolute extends Constraints { - private static final int DEFAULT_X = 0; - private static final int DEFAULT_Y = 0; - - public static Absolute of() { - return new Absolute( - DEFAULT_X, DEFAULT_Y, - DEFAULT_WIDTH, DEFAULT_HEIGHT, - DEFAULT_FILL_X, DEFAULT_FILL_Y, - DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, - DEFAULT_PRIORITY - ); - } - - private int x; - private int y; - - private Absolute( - int x, int y, - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority - ) { - super(width, height, fillX, fillY, xAlignment, yAlignment, priority); - - this.x = x; - this.y = y; - } - - public Absolute x(int x) { - this.x = x; - return this; - } - - public Absolute nextRow() { - this.x++; - this.y = 0; - return this; - } - - public Absolute y(int y) { - this.y = y; - return this; - } - - public Absolute nextColumn() { - this.y++; - return this; - } - - public Absolute pos(int x, int y) { - this.x(x); - this.y(y); - return this; - } - - @Override - public Absolute copy() { - return new Absolute( - this.x, this.y, - this.width, this.height, - this.fillX, this.fillY, - this.xAlignment, this.yAlignment, - this.priority - ); - } - - public Relative toRelative() { - return new Relative( - this.width, this.height, - this.fillX, this.fillY, - this.xAlignment, this.yAlignment, - this.priority - ); - } - - @Override - protected Absolute getSelf() { - return this; - } - } - } - record Constrained( Component component, int width, int height, boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, int priority @@ -374,14 +104,14 @@ record Constrained( static Constrained defaultOf(Component component) { return new Constrained( component, - Constraints.DEFAULT_WIDTH, Constraints.DEFAULT_HEIGHT, - Constraints.DEFAULT_FILL_X, Constraints.DEFAULT_FILL_Y, - Constraints.DEFAULT_X_ALIGNMENT, Constraints.DEFAULT_Y_ALIGNMENT, - Constraints.DEFAULT_PRIORITY + FlexGridConstraints.DEFAULT_WIDTH, FlexGridConstraints.DEFAULT_HEIGHT, + FlexGridConstraints.DEFAULT_FILL_X, FlexGridConstraints.DEFAULT_FILL_Y, + FlexGridConstraints.DEFAULT_X_ALIGNMENT, FlexGridConstraints.DEFAULT_Y_ALIGNMENT, + FlexGridConstraints.DEFAULT_PRIORITY ); } - Constrained(Component component, Constraints constraints) { + Constrained(Component component, FlexGridConstraints constraints) { this( component, constraints.width, constraints.height, From 804877d9cb650a802bf880d7574321dbdc37c649 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 24 Nov 2025 14:28:45 -0800 Subject: [PATCH 046/110] move Alignment from FlexGridLayout to FlexGridConstraints --- .../enigma/gui/util/FlexGridConstraints.java | 56 ++++++++++--------- .../enigma/gui/util/FlexGridLayout.java | 8 +-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java index fbaf9257d..1cd787142 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -10,8 +10,8 @@ public abstract sealed class FlexGridConstraints { public static Relative of() { return new Relative( @@ -150,10 +154,10 @@ public static Relative of() { } private Relative( - int width, int height, - boolean fillX, boolean fillY, - FlexGridLayout.Alignment xAlignment, FlexGridLayout.Alignment yAlignment, - int priority + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority ) { super(width, height, fillX, fillY, xAlignment, yAlignment, priority); } @@ -206,11 +210,11 @@ public static Absolute of() { int y; private Absolute( - int x, int y, - int width, int height, - boolean fillX, boolean fillY, - FlexGridLayout.Alignment xAlignment, FlexGridLayout.Alignment yAlignment, - int priority + int x, int y, + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority ) { super(width, height, fillX, fillY, xAlignment, yAlignment, priority); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 31e0abdf9..7280c25c4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -93,13 +93,9 @@ public void layoutContainer(Container parent) { null } - public enum Alignment { - BEGIN, CENTER, END - } - record Constrained( - Component component, int width, int height, boolean fillX, boolean fillY, Alignment xAlignment, - Alignment yAlignment, int priority + Component component, int width, int height, boolean fillX, boolean fillY, FlexGridConstraints.Alignment xAlignment, + FlexGridConstraints.Alignment yAlignment, int priority ) { static Constrained defaultOf(Component component) { return new Constrained( From c95f1782989e881ec326e1c2b1ee74c946c104b3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 24 Nov 2025 15:59:54 -0800 Subject: [PATCH 047/110] implement ...LayouSize methods and size caching --- .../enigma/gui/util/ConstrainedGrid.java | 27 +++-- .../enigma/gui/util/FlexGridLayout.java | 104 +++++++++++++++++- .../java/org/quiltmc/enigma/util/Utils.java | 8 ++ 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java index a0e57815e..15900defe 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java @@ -28,7 +28,7 @@ private static Set createValueSet(Integer ignored) { private final SortedMap> valuesByMaxX = new TreeMap<>(); private final SortedMap> valuesByMaxY = new TreeMap<>(); - public void put(int x, int y, Constrained value) { + void put(int x, int y, Constrained value) { final Component component = value.component(); final Position oldPos = this.valuePositions.replace(component, new Position(x, y)); if (oldPos != null) { @@ -47,14 +47,14 @@ public void put(int x, int y, Constrained value) { this.valuesByMaxY.computeIfAbsent(y + value.getYExcess(), ConstrainedGrid::createValueSet).add(component); } - public Stream get(int x, int y) { + Stream get(int x, int y) { return this.grid.getOrDefault(x, Map.of()) .getOrDefault(y, Map.of()) .values() .stream(); } - public boolean remove(Component value) { + boolean remove(Component value) { final Position pos = this.valuePositions.remove(value); if (pos != null) { final Constrained removed = this.grid.get(pos.x).get(pos.y).remove(value); @@ -67,21 +67,34 @@ public boolean remove(Component value) { } } - public int getMaxXOrThrow() { + int getMaxXOrThrow() { return this.valuesByMaxX.lastKey(); } - public int getMaxYOrThrow() { + int getMaxYOrThrow() { return this.valuesByMaxY.lastKey(); } - public int getSize() { + int getSize() { return this.valuesByMaxX.size(); } - public boolean isEmpty() { + boolean isEmpty() { return this.valuesByMaxX.isEmpty(); } + void forEach(EntriesConsumer action) { + this.grid.forEach((x, column) -> { + column.forEach((y, constrainedByComponent) -> { + action.accept(x, y, constrainedByComponent.values().stream()); + }); + }); + } + private record Position(int x, int y) { } + + @FunctionalInterface + interface EntriesConsumer { + void accept(int x, int y, Stream values); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 7280c25c4..eeaabca40 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -1,15 +1,24 @@ package org.quiltmc.enigma.gui.util; +import com.google.common.collect.ImmutableMap; import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.util.Utils; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.LayoutManager2; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; public class FlexGridLayout implements LayoutManager2 { private final ConstrainedGrid grid = new ConstrainedGrid(); + private @Nullable Sizes preferredSizes; + private @Nullable Sizes minSizes; + private @Nullable Sizes maxSizes; + @Override public void addLayoutComponent(Component component, @Nullable Object constraints) throws IllegalArgumentException { if (constraints == null) { @@ -76,17 +85,49 @@ public float getLayoutAlignmentY(Container target) { @Override public void invalidateLayout(Container target) { - // TODO + this.preferredSizes = null; + this.minSizes = null; + this.maxSizes = null; } @Override - public Dimension maximumLayoutSize(Container target) { } + public Dimension preferredLayoutSize(Container parent) { + return this.getPreferredSizes().createTotalDimension(); + } + + private Sizes getPreferredSizes() { + if (this.preferredSizes == null) { + this.preferredSizes = Sizes.calculate(this.grid, Component::getPreferredSize); + } + + return this.preferredSizes; + } @Override - public Dimension preferredLayoutSize(Container parent) { } + public Dimension minimumLayoutSize(Container parent) { + return this.getMinSizes().createTotalDimension(); + } + + private Sizes getMinSizes() { + if (this.minSizes == null) { + this.minSizes = Sizes.calculate(this.grid, Component::getMinimumSize); + } + + return this.minSizes; + } @Override - public Dimension minimumLayoutSize(Container parent) { } + public Dimension maximumLayoutSize(Container target) { + return this.getMaxSizes().createTotalDimension(); + } + + private Sizes getMaxSizes() { + if (this.maxSizes == null) { + this.maxSizes = Sizes.calculate(this.grid, Component::getMaximumSize); + } + + return this.maxSizes; + } @Override public void layoutContainer(Container parent) { @@ -94,8 +135,11 @@ public void layoutContainer(Container parent) { } record Constrained( - Component component, int width, int height, boolean fillX, boolean fillY, FlexGridConstraints.Alignment xAlignment, - FlexGridConstraints.Alignment yAlignment, int priority + Component component, + int width, int height, + boolean fillX, boolean fillY, + FlexGridConstraints.Alignment xAlignment, FlexGridConstraints.Alignment yAlignment, + int priority ) { static Constrained defaultOf(Component component) { return new Constrained( @@ -125,4 +169,52 @@ int getYExcess() { return this.height - 1; } } + + private record Sizes( + int totalWidth, int totalHeight, + ImmutableMap cellWidths, ImmutableMap cellHeights, + ImmutableMap componentSizes + ) { + static Sizes calculate(ConstrainedGrid grid, Function getSize) { + final Map componentSizesBuilder = new HashMap<>(); + + final Map cellWidthsBuilder = new HashMap<>(); + final Map cellHeightsBuilder = new HashMap<>(); + + grid.forEach((x, y, values) -> { + values.forEach(constrained -> { + final Dimension size = componentSizesBuilder + .computeIfAbsent(constrained.component, getSize); + + final int componentCellWidth = Utils.ceilDiv(size.width, constrained.width); + for (int offset = 0; offset < constrained.width; offset++) { + cellWidthsBuilder.compute(x + offset, (ignored, width) -> { + return width == null ? componentCellWidth : width + componentCellWidth; + }); + } + + final int componentCellHeight = Utils.ceilDiv(size.height, constrained.height); + for (int offset = 0; offset < constrained.height; offset++) { + cellHeightsBuilder.compute(y + offset, (ignored, height) -> { + return height == null ? componentCellHeight : height + componentCellHeight; + }); + } + }); + }); + + final ImmutableMap cellWidths = ImmutableMap.copyOf(cellWidthsBuilder); + final ImmutableMap cellHeights = ImmutableMap.copyOf(cellHeightsBuilder); + + return new Sizes( + cellWidths.values().stream().mapToInt(Integer::intValue).sum(), + cellHeights.values().stream().mapToInt(Integer::intValue).sum(), + cellWidths, cellHeights, + ImmutableMap.copyOf(componentSizesBuilder) + ); + } + + Dimension createTotalDimension() { + return new Dimension(this.totalWidth, this.totalHeight); + } + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index d68d1b15b..2bc8181d2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -196,4 +196,12 @@ public static float clamp(double value, float min, float max) { public static double clamp(double value, double min, double max) { return Math.min(max, Math.max(value, min)); } + + public static int ceilDiv(int dividend, int divisor) { + return -Math.floorDiv(-dividend, divisor); + } + + public static long ceilDiv(long dividend, long divisor) { + return -Math.floorDiv(-dividend, divisor); + } } From ab8ffbb7f05e794cedb92f0aae152b186b9ff533 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 25 Nov 2025 16:15:29 -0800 Subject: [PATCH 048/110] first complete pass at alignment; fill/alignment not yet respected --- .../enigma/gui/util/ConstrainedGrid.java | 10 +- .../enigma/gui/util/FlexGridConstraints.java | 27 ++- .../enigma/gui/util/FlexGridLayout.java | 229 +++++++++++++++--- 3 files changed, 208 insertions(+), 58 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java index 15900defe..388d2b207 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java @@ -23,7 +23,7 @@ private static Set createValueSet(Integer ignored) { return new HashSet<>(); } - private final Map>> grid = new HashMap<>(); + private final Map>> grid = new TreeMap<>(); private final Map valuePositions = new HashMap<>(); private final SortedMap> valuesByMaxX = new TreeMap<>(); private final SortedMap> valuesByMaxY = new TreeMap<>(); @@ -40,7 +40,7 @@ void put(int x, int y, Constrained value) { } this.grid - .computeIfAbsent(x, ignored -> new HashMap<>()) + .computeIfAbsent(x, ignored -> new TreeMap<>()) .computeIfAbsent(y, ignored -> new HashMap<>(1)) .put(component, value); this.valuesByMaxX.computeIfAbsent(x + value.getXExcess(), ConstrainedGrid::createValueSet).add(component); @@ -54,16 +54,12 @@ Stream get(int x, int y) { .stream(); } - boolean remove(Component value) { + void remove(Component value) { final Position pos = this.valuePositions.remove(value); if (pos != null) { final Constrained removed = this.grid.get(pos.x).get(pos.y).remove(value); this.valuesByMaxX.get(pos.x + removed.getXExcess()).remove(value); this.valuesByMaxY.get(pos.y + removed.getYExcess()).remove(value); - - return true; - } else { - return false; } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java index 1cd787142..820db56fb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -28,6 +28,7 @@ private static Alignment requireNonNullAlignment(Alignment alignment) { int width; int height; + // TODO merge fill+alignment boolean fillX; boolean fillY; @@ -37,10 +38,10 @@ private static Alignment requireNonNullAlignment(Alignment alignment) { int priority; private FlexGridConstraints( - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority ) { this.width = width; this.height = height; @@ -154,10 +155,10 @@ public static Relative of() { } private Relative( - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority ) { super(width, height, fillX, fillY, xAlignment, yAlignment, priority); } @@ -210,11 +211,11 @@ public static Absolute of() { int y; private Absolute( - int x, int y, - int width, int height, - boolean fillX, boolean fillY, - Alignment xAlignment, Alignment yAlignment, - int priority + int x, int y, + int width, int height, + boolean fillX, boolean fillY, + Alignment xAlignment, Alignment yAlignment, + int priority ) { super(width, height, fillX, fillY, xAlignment, yAlignment, priority); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index eeaabca40..9ccb7911d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -1,15 +1,21 @@ package org.quiltmc.enigma.gui.util; import com.google.common.collect.ImmutableMap; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.util.Utils; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; +import java.awt.Insets; import java.awt.LayoutManager2; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.BiFunction; import java.util.function.Function; public class FlexGridLayout implements LayoutManager2 { @@ -91,8 +97,8 @@ public void invalidateLayout(Container target) { } @Override - public Dimension preferredLayoutSize(Container parent) { - return this.getPreferredSizes().createTotalDimension(); + public Dimension preferredLayoutSize(Container container) { + return this.getPreferredSizes().createTotalDimension(container.getInsets()); } private Sizes getPreferredSizes() { @@ -104,8 +110,8 @@ private Sizes getPreferredSizes() { } @Override - public Dimension minimumLayoutSize(Container parent) { - return this.getMinSizes().createTotalDimension(); + public Dimension minimumLayoutSize(Container container) { + return this.getMinSizes().createTotalDimension(container.getInsets()); } private Sizes getMinSizes() { @@ -117,8 +123,8 @@ private Sizes getMinSizes() { } @Override - public Dimension maximumLayoutSize(Container target) { - return this.getMaxSizes().createTotalDimension(); + public Dimension maximumLayoutSize(Container container) { + return this.getMaxSizes().createTotalDimension(container.getInsets()); } private Sizes getMaxSizes() { @@ -131,7 +137,119 @@ private Sizes getMaxSizes() { @Override public void layoutContainer(Container parent) { - null + this.layoutAxis(parent, true); + this.layoutAxis(parent, false); + } + + private void layoutAxis(Container parent, boolean xAxis) { + final Insets insets = parent.getInsets(); + final int leadingInset = xAxis ? insets.left : insets.top; + + final int availableSpace = xAxis + ? parent.getWidth() - insets.left - insets.right + : parent.getHeight() - insets.top - insets.bottom; + + final Sizes preferredSizes = this.getPreferredSizes(); + + final int extraSpace = availableSpace - (xAxis ? preferredSizes.totalWidth : preferredSizes.totalHeight); + if (extraSpace >= 0) { + this.layoutFixedAxis(preferredSizes, leadingInset + extraSpace / 2, xAxis); + } else { + final Sizes minSizes = this.getMinSizes(); + + final int extraMinSpace = availableSpace - (xAxis ? minSizes.totalWidth : minSizes.totalHeight); + if (extraMinSpace <= 0) { + this.layoutFixedAxis(minSizes, leadingInset, xAxis); + } else { + final Map cellSpans = this.allocateCellSpace(xAxis, extraMinSpace); + + this.layoutAxisImpl(leadingInset, xAxis, cellSpans, (component, coord) -> { + final Dimension preferredSize = preferredSizes.componentSizes.get(component); + assert preferredSize != null; + return Math.min(xAxis ? preferredSize.width : preferredSize.height, cellSpans.get(coord)); + }); + } + } + } + + private Map allocateCellSpace(boolean xAxis, int remainingSpace) { + final SortedSet prioritizedConstrained = new TreeSet<>(); + this.grid.forEach((x, y, values) -> { + values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(xAxis ? x : y))); + }); + + final ImmutableMap preferredComponentSizes = this.getPreferredSizes().componentSizes; + final Map cellSpans = new HashMap<>(this.getMinSizes().columnWidths); + for (final Constrained.At at : prioritizedConstrained) { + final int currentSpan = cellSpans.get(at.coord); + final Dimension preferredSize = preferredComponentSizes.get(at.constrained().component); + assert preferredSize != null; + final int preferredSpan = xAxis ? preferredSize.width : preferredSize.height; + final int preferredDiff = preferredSpan - currentSpan; + if (preferredDiff > 0) { + if (preferredDiff <= remainingSpace) { + cellSpans.put(at.coord, preferredSpan); + + if (remainingSpace == preferredDiff) { + break; + } else { + remainingSpace -= preferredDiff; + } + } else { + final int lastOfSpan = remainingSpace; + cellSpans.compute(at.coord, (ignored, span) -> { + assert span != null; + return span + lastOfSpan; + }); + + break; + } + } + } + + return cellSpans; + } + + @SuppressWarnings("DataFlowIssue") + private void layoutFixedAxis(Sizes sizes, int startPos, boolean xAxis) { + this.layoutAxisImpl( + startPos, xAxis, xAxis ? sizes.columnWidths : sizes.rowHeights, + (component, coord) -> { + final Dimension size = sizes.componentSizes.get(component); + return xAxis ? size.width : size.height; + } + ); + } + + // TODO respect fill/alignment + private void layoutAxisImpl( + int startPos, boolean xAxis, + Map cellSpans, + BiFunction getComponentSpan + ) { + final Map positions = new HashMap<>(); + + this.grid.forEach((x, y, values) -> { + final int coord = xAxis ? x : y; + final int pos = positions.computeIfAbsent(coord, ignored -> startPos); + + values.forEach(constrained -> { + final int span = getComponentSpan.apply(constrained.component, coord); + if (xAxis) { + constrained.component.setBounds( + pos, constrained.component.getY(), + span, constrained.component.getHeight() + ); + } else { + constrained.component.setBounds( + constrained.component.getX(), pos, + constrained.component.getWidth(), span + ); + } + }); + + positions.put(coord, pos + cellSpans.get(coord)); + }); } record Constrained( @@ -153,11 +271,11 @@ static Constrained defaultOf(Component component) { Constrained(Component component, FlexGridConstraints constraints) { this( - component, - constraints.width, constraints.height, - constraints.fillX, constraints.fillY, - constraints.xAlignment, constraints.yAlignment, - constraints.priority + component, + constraints.width, constraints.height, + constraints.fillX, constraints.fillY, + constraints.xAlignment, constraints.yAlignment, + constraints.priority ); } @@ -168,53 +286,88 @@ int getXExcess() { int getYExcess() { return this.height - 1; } + + private class At implements Comparable { + static final Comparator PRIORITY_COMPARATOR = (left, right) -> { + return right.constrained().priority - left.constrained().priority; + }; + + static final Comparator COORD_COMPARATOR = Comparator.comparingInt(At::getCoord); + + static final Comparator COMPARATOR = PRIORITY_COMPARATOR.thenComparing(COORD_COMPARATOR); + + final int coord; + + At(int coord) { + this.coord = coord; + } + + int getCoord() { + return this.coord; + } + + Constrained constrained() { + return Constrained.this; + } + + @Override + public int compareTo(@NonNull At other) { + return COMPARATOR.compare(this, other); + } + } } private record Sizes( - int totalWidth, int totalHeight, - ImmutableMap cellWidths, ImmutableMap cellHeights, - ImmutableMap componentSizes + int totalWidth, int totalHeight, + ImmutableMap rowHeights, ImmutableMap columnWidths, + ImmutableMap componentSizes ) { static Sizes calculate(ConstrainedGrid grid, Function getSize) { - final Map componentSizesBuilder = new HashMap<>(); + final Map componentSizes = new HashMap<>(); - final Map cellWidthsBuilder = new HashMap<>(); - final Map cellHeightsBuilder = new HashMap<>(); + final Map> cellSizes = new HashMap<>(); grid.forEach((x, y, values) -> { values.forEach(constrained -> { - final Dimension size = componentSizesBuilder - .computeIfAbsent(constrained.component, getSize); + final Dimension size = componentSizes.computeIfAbsent(constrained.component, getSize); final int componentCellWidth = Utils.ceilDiv(size.width, constrained.width); - for (int offset = 0; offset < constrained.width; offset++) { - cellWidthsBuilder.compute(x + offset, (ignored, width) -> { - return width == null ? componentCellWidth : width + componentCellWidth; - }); - } - final int componentCellHeight = Utils.ceilDiv(size.height, constrained.height); - for (int offset = 0; offset < constrained.height; offset++) { - cellHeightsBuilder.compute(y + offset, (ignored, height) -> { - return height == null ? componentCellHeight : height + componentCellHeight; - }); + for (int xOffset = 0; xOffset < constrained.width; xOffset++) { + for (int yOffset = 0; yOffset < constrained.width; yOffset++) { + final Dimension cellSize = cellSizes + .computeIfAbsent(x + xOffset, ignored -> new HashMap<>()) + .computeIfAbsent(y +yOffset, ignored -> new Dimension()); + + cellSize.width = Math.max(cellSize.width, componentCellWidth); + cellSize.height = Math.max(cellSize.height, componentCellHeight); + } } }); }); - final ImmutableMap cellWidths = ImmutableMap.copyOf(cellWidthsBuilder); - final ImmutableMap cellHeights = ImmutableMap.copyOf(cellHeightsBuilder); + final Map rowHeights = new HashMap<>(); + final Map columnWidths = new HashMap<>(); + cellSizes.forEach((x, column) -> { + column.forEach((y, size) -> { + rowHeights.compute(y, (ignored, height) -> height == null ? size.height : Math.max(height, size.height)); + columnWidths.compute(x, (ignored, width) -> width == null ? size.width : Math.max(width, size.width)); + }); + }); return new Sizes( - cellWidths.values().stream().mapToInt(Integer::intValue).sum(), - cellHeights.values().stream().mapToInt(Integer::intValue).sum(), - cellWidths, cellHeights, - ImmutableMap.copyOf(componentSizesBuilder) + columnWidths.values().stream().mapToInt(Integer::intValue).sum(), + rowHeights.values().stream().mapToInt(Integer::intValue).sum(), + ImmutableMap.copyOf(rowHeights), ImmutableMap.copyOf(columnWidths), + ImmutableMap.copyOf(componentSizes) ); } - Dimension createTotalDimension() { - return new Dimension(this.totalWidth, this.totalHeight); + Dimension createTotalDimension(Insets insets) { + return new Dimension( + this.totalWidth + insets.left + insets.right, + this.totalHeight + insets.top + insets.bottom + ); } } } From f398a543ea8541178efac34b8ebeef18356132da Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 25 Nov 2025 18:25:30 -0800 Subject: [PATCH 049/110] extract FlexGridLayout.AxisOperation to replace `boolean xAxis` spaghetti fix swapped rowHeights/columnWidths --- .../enigma/gui/util/FlexGridLayout.java | 155 +++++++++++++----- 1 file changed, 117 insertions(+), 38 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 9ccb7911d..1338cc008 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -137,45 +137,43 @@ private Sizes getMaxSizes() { @Override public void layoutContainer(Container parent) { - this.layoutAxis(parent, true); - this.layoutAxis(parent, false); + this.layoutAxis(parent, AxisOperations.X); + this.layoutAxis(parent, AxisOperations.Y); } - private void layoutAxis(Container parent, boolean xAxis) { + private void layoutAxis(Container parent, AxisOperations ops) { final Insets insets = parent.getInsets(); - final int leadingInset = xAxis ? insets.left : insets.top; + final int leadingInset = ops.getLeadingInset(insets); - final int availableSpace = xAxis - ? parent.getWidth() - insets.left - insets.right - : parent.getHeight() - insets.top - insets.bottom; + final int availableSpace = ops.getParentSpace(parent) - leadingInset - ops.getTrailingInset(insets); final Sizes preferredSizes = this.getPreferredSizes(); - final int extraSpace = availableSpace - (xAxis ? preferredSizes.totalWidth : preferredSizes.totalHeight); + final int extraSpace = availableSpace - ops.getTotalSpace(preferredSizes); if (extraSpace >= 0) { - this.layoutFixedAxis(preferredSizes, leadingInset + extraSpace / 2, xAxis); + this.layoutFixedAxis(preferredSizes, leadingInset + extraSpace / 2, ops); } else { final Sizes minSizes = this.getMinSizes(); - final int extraMinSpace = availableSpace - (xAxis ? minSizes.totalWidth : minSizes.totalHeight); + final int extraMinSpace = availableSpace - ops.getTotalSpace(minSizes); if (extraMinSpace <= 0) { - this.layoutFixedAxis(minSizes, leadingInset, xAxis); + this.layoutFixedAxis(minSizes, leadingInset, ops); } else { - final Map cellSpans = this.allocateCellSpace(xAxis, extraMinSpace); + final Map cellSpans = this.allocateCellSpace(ops, extraMinSpace); - this.layoutAxisImpl(leadingInset, xAxis, cellSpans, (component, coord) -> { + this.layoutAxisImpl(leadingInset, ops, cellSpans, (component, coord) -> { final Dimension preferredSize = preferredSizes.componentSizes.get(component); assert preferredSize != null; - return Math.min(xAxis ? preferredSize.width : preferredSize.height, cellSpans.get(coord)); + return Math.min(ops.getSpan(preferredSize), cellSpans.get(coord)); }); } } } - private Map allocateCellSpace(boolean xAxis, int remainingSpace) { + private Map allocateCellSpace(AxisOperations ops, int remainingSpace) { final SortedSet prioritizedConstrained = new TreeSet<>(); this.grid.forEach((x, y, values) -> { - values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(xAxis ? x : y))); + values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(ops.chooseCoord(x, y)))); }); final ImmutableMap preferredComponentSizes = this.getPreferredSizes().componentSizes; @@ -184,7 +182,7 @@ private Map allocateCellSpace(boolean xAxis, int remainingSpac final int currentSpan = cellSpans.get(at.coord); final Dimension preferredSize = preferredComponentSizes.get(at.constrained().component); assert preferredSize != null; - final int preferredSpan = xAxis ? preferredSize.width : preferredSize.height; + final int preferredSpan = ops.getSpan(preferredSize); final int preferredDiff = preferredSpan - currentSpan; if (preferredDiff > 0) { if (preferredDiff <= remainingSpace) { @@ -210,42 +208,28 @@ private Map allocateCellSpace(boolean xAxis, int remainingSpac return cellSpans; } - @SuppressWarnings("DataFlowIssue") - private void layoutFixedAxis(Sizes sizes, int startPos, boolean xAxis) { + private void layoutFixedAxis(Sizes sizes, int startPos, AxisOperations ops) { this.layoutAxisImpl( - startPos, xAxis, xAxis ? sizes.columnWidths : sizes.rowHeights, - (component, coord) -> { - final Dimension size = sizes.componentSizes.get(component); - return xAxis ? size.width : size.height; - } + startPos, ops, ops.getCellSpans(sizes), + (component, coord) -> ops.getSpan(sizes.componentSizes.get(component)) ); } // TODO respect fill/alignment private void layoutAxisImpl( - int startPos, boolean xAxis, + int startPos, AxisOperations ops, Map cellSpans, BiFunction getComponentSpan ) { final Map positions = new HashMap<>(); this.grid.forEach((x, y, values) -> { - final int coord = xAxis ? x : y; + final int coord = ops.chooseCoord(x, y); final int pos = positions.computeIfAbsent(coord, ignored -> startPos); values.forEach(constrained -> { final int span = getComponentSpan.apply(constrained.component, coord); - if (xAxis) { - constrained.component.setBounds( - pos, constrained.component.getY(), - span, constrained.component.getHeight() - ); - } else { - constrained.component.setBounds( - constrained.component.getX(), pos, - constrained.component.getWidth(), span - ); - } + ops.setBounds(constrained.component, pos, span); }); positions.put(coord, pos + cellSpans.get(coord)); @@ -337,7 +321,7 @@ static Sizes calculate(ConstrainedGrid grid, Function getS for (int yOffset = 0; yOffset < constrained.width; yOffset++) { final Dimension cellSize = cellSizes .computeIfAbsent(x + xOffset, ignored -> new HashMap<>()) - .computeIfAbsent(y +yOffset, ignored -> new Dimension()); + .computeIfAbsent(y + yOffset, ignored -> new Dimension()); cellSize.width = Math.max(cellSize.width, componentCellWidth); cellSize.height = Math.max(cellSize.height, componentCellHeight); @@ -370,4 +354,99 @@ Dimension createTotalDimension(Insets insets) { ); } } + + private interface AxisOperations { + AxisOperations X = new AxisOperations() { + @Override + public int getLeadingInset(Insets insets) { + return insets.left; + } + + @Override + public int getTrailingInset(Insets insets) { + return insets.right; + } + + @Override + public int getParentSpace(Container parent) { + return parent.getWidth(); + } + + @Override + public int getTotalSpace(Sizes sizes) { + return sizes.totalWidth; + } + + @Override + public ImmutableMap getCellSpans(Sizes sizes) { + return sizes.columnWidths; + } + + @Override + public int getSpan(Dimension size) { + return size.width; + } + + @Override + public int chooseCoord(int x, int y) { + return x; + } + + @Override + public void setBounds(Component component, int x, int width) { + component.setBounds(x, component.getY(), width, component.getHeight()); + } + }; + + AxisOperations Y = new AxisOperations() { + @Override + public int getLeadingInset(Insets insets) { + return insets.top; + } + + @Override + public int getTrailingInset(Insets insets) { + return insets.bottom; + } + + @Override + public int getParentSpace(Container parent) { + return parent.getHeight(); + } + + @Override + public int getTotalSpace(Sizes sizes) { + return sizes.totalHeight; + } + + @Override + public ImmutableMap getCellSpans(Sizes sizes) { + return sizes.rowHeights; + } + + @Override + public int getSpan(Dimension size) { + return size.height; + } + + @Override + public int chooseCoord(int x, int y) { + return y; + } + + @Override + public void setBounds(Component component, int y, int height) { + component.setBounds(component.getX(), y, component.getWidth(), height); + } + }; + + int getLeadingInset(Insets insets); + int getTrailingInset(Insets insets); + int getParentSpace(Container parent); + int getTotalSpace(Sizes sizes); + ImmutableMap getCellSpans(Sizes sizes); + int getSpan(Dimension size); + int chooseCoord(int x, int y); + void setBounds(Component component, int pos, int span); + } } From edc8428d7fb1ccfc08ca1261ca9f7e96e2c1c933 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 25 Nov 2025 19:12:40 -0800 Subject: [PATCH 050/110] setup gui visualization sourceSet+task to open a window --- enigma-swing/build.gradle | 25 +++++++++++++++ .../internal/gui/visualization/Main.java | 32 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle index dda6ada24..06fa86a50 100644 --- a/enigma-swing/build.gradle +++ b/enigma-swing/build.gradle @@ -20,6 +20,15 @@ plugins { alias(libs.plugins.shadow) } +sourceSets { + guiVisualization +} + +configurations { + guiVisualizationRuntimeClasspath.extendsFrom runtimeClasspath + guiVisualizationCompileClasspath.extendsFrom compileClasspath +} + dependencies { implementation project(':enigma') implementation project(':enigma-server') @@ -32,7 +41,9 @@ dependencies { implementation libs.swing.dpi implementation libs.fontchooser implementation libs.javaparser + testImplementation(testFixtures(project(':enigma'))) + guiVisualizationImplementation(project) } application { @@ -97,6 +108,20 @@ project(":enigma").file("src/test/java/org/quiltmc/enigma/input").listFiles().ea registerTestTask("complete") +final guiVisualizationJar = tasks.register('guiVisualizationJar', Jar.class) { + from(sourceSets.guiVisualization.output) + + archiveFileName = 'gui-visualization.jar' + destinationDirectory = layout.buildDirectory.map { it.dir('gui-visualization') } +} + +tasks.register('visualizeGui', JavaExec) { + dependsOn(guiVisualizationJar) + + classpath = sourceSets.guiVisualization.runtimeClasspath + mainClass = 'org.quilt.internal.gui.visualization.Main' +} + void registerPrintColorKeyGroupsMapCode(String lafsName, LookAndFeel... lookAndFeels) { tasks.register("print" + lafsName + "ColorKeyGroupsMapCode", PrintColorKeyGroupsMapCodeTask) { task -> task.lookAndFeels = lookAndFeels as List diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java new file mode 100644 index 000000000..a9b8a04ed --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -0,0 +1,32 @@ +package org.quilt.internal.gui.visualization; + +import javax.swing.JFrame; +import javax.swing.WindowConstants; +import java.awt.Dimension; +import java.awt.Toolkit; + +public final class Main { + private static final double WINDOW_TO_SCREEN_RATIO = 2d/3d; + + private static final JFrame WINDOW = new JFrame(); + + static { + WINDOW.setTitle("Gui Visualization"); + WINDOW.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + } + + public static void main(String[] args) { + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final int width = (int) (screenSize.width * WINDOW_TO_SCREEN_RATIO); + final int height = (int) (screenSize.height * WINDOW_TO_SCREEN_RATIO); + final int x = (screenSize.width - width) / 2; + final int y = (screenSize.height - height) / 2; + + WINDOW.setBounds(x, y, width, height); + WINDOW.setVisible(true); + } + + private Main() { + throw new UnsupportedOperationException(); + } +} From 92fedee776bb731d5ad8e3d5161666fbfdeea6c4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 25 Nov 2025 19:23:45 -0800 Subject: [PATCH 051/110] minor tweaks --- .../org/quilt/internal/gui/visualization/Main.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index a9b8a04ed..906dff8ee 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -6,19 +6,15 @@ import java.awt.Toolkit; public final class Main { - private static final double WINDOW_TO_SCREEN_RATIO = 2d/3d; - private static final JFrame WINDOW = new JFrame(); - static { + public static void main(String[] args) { WINDOW.setTitle("Gui Visualization"); WINDOW.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - } - public static void main(String[] args) { final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - final int width = (int) (screenSize.width * WINDOW_TO_SCREEN_RATIO); - final int height = (int) (screenSize.height * WINDOW_TO_SCREEN_RATIO); + final int width = screenSize.width * 2 / 3; + final int height = screenSize.height * 2 / 3; final int x = (screenSize.width - width) / 2; final int y = (screenSize.height - height) / 2; From 34d540fa38196cdc9826afc0ef804ec50e39b213 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 26 Nov 2025 10:50:43 -0800 Subject: [PATCH 052/110] move unsupported constructor to top --- .../java/org/quilt/internal/gui/visualization/Main.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 906dff8ee..0b23a9a5e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -6,6 +6,10 @@ import java.awt.Toolkit; public final class Main { + private Main() { + throw new UnsupportedOperationException(); + } + private static final JFrame WINDOW = new JFrame(); public static void main(String[] args) { @@ -21,8 +25,4 @@ public static void main(String[] args) { WINDOW.setBounds(x, y, width, height); WINDOW.setVisible(true); } - - private Main() { - throw new UnsupportedOperationException(); - } } From ee10a1956cc8dab8b504365b0b3d3b3b2a104427 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 20:41:19 -0800 Subject: [PATCH 053/110] add FlexGridRelativeRowVisualiser fix several bugs in FlexGridLayout and ConstrainedGrid --- .../FlexGridRelativeRowVisualiser.java | 22 ++++++++++ .../internal/gui/visualization/Main.java | 41 ++++++++++++++++++- .../enigma/gui/util/ConstrainedGrid.java | 7 ++-- .../enigma/gui/util/FlexGridLayout.java | 19 +++++++-- 4 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java new file mode 100644 index 000000000..250d22017 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java @@ -0,0 +1,22 @@ +package org.quilt.internal.gui.visualization; + +import org.quiltmc.enigma.gui.util.FlexGridLayout; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +public class FlexGridRelativeRowVisualiser extends JFrame { + public static final String TITLE = "Flex Grid Relative Row"; + + public FlexGridRelativeRowVisualiser() { + super(TITLE); + + this.setLayout(new FlexGridLayout()); + + this.add(new JLabel("Label 1")); + this.add(new JLabel("Label 2")); + this.add(new JLabel("Label 3")); + + this.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 0b23a9a5e..b2422e07f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -1,21 +1,38 @@ package org.quilt.internal.gui.visualization; +import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.WindowConstants; import java.awt.Dimension; +import java.awt.FlowLayout; import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.function.Supplier; public final class Main { private Main() { throw new UnsupportedOperationException(); } - private static final JFrame WINDOW = new JFrame(); + private static final JFrame WINDOW = new JFrame("Gui Visualization"); + + private static void position(Window window) { + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final int x = (screenSize.width - window.getWidth()) / 2; + final int y = (screenSize.height - window.getHeight()) / 2; + + window.setLocation(x, y); + } public static void main(String[] args) { - WINDOW.setTitle("Gui Visualization"); WINDOW.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + WINDOW.setLayout(new FlowLayout()); + + addVisualizer(FlexGridRelativeRowVisualiser.TITLE, FlexGridRelativeRowVisualiser::new); + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); final int width = screenSize.width * 2 / 3; final int height = screenSize.height * 2 / 3; @@ -25,4 +42,24 @@ public static void main(String[] args) { WINDOW.setBounds(x, y, width, height); WINDOW.setVisible(true); } + + private static void addVisualizer(String title, Supplier factory) { + final JButton button = new JButton(title); + button.addActionListener(e -> { + final JFrame window = factory.get(); + + window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + window.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + WINDOW.requestFocus(); + } + }); + + position(window); + window.setVisible(true); + }); + + WINDOW.add(button); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java index 388d2b207..071dc0dfa 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.util; +import com.google.common.collect.ImmutableSortedMap; import org.quiltmc.enigma.gui.util.FlexGridLayout.Constrained; import java.awt.Component; @@ -23,14 +24,14 @@ private static Set createValueSet(Integer ignored) { return new HashSet<>(); } - private final Map>> grid = new TreeMap<>(); + private final SortedMap>> grid = new TreeMap<>(); private final Map valuePositions = new HashMap<>(); private final SortedMap> valuesByMaxX = new TreeMap<>(); private final SortedMap> valuesByMaxY = new TreeMap<>(); void put(int x, int y, Constrained value) { final Component component = value.component(); - final Position oldPos = this.valuePositions.replace(component, new Position(x, y)); + final Position oldPos = this.valuePositions.put(component, new Position(x, y)); if (oldPos != null) { final Map> column = this.grid.get(oldPos.x); final Constrained oldValue = column.get(oldPos.y).get(component); @@ -48,7 +49,7 @@ void put(int x, int y, Constrained value) { } Stream get(int x, int y) { - return this.grid.getOrDefault(x, Map.of()) + return this.grid.getOrDefault(x, ImmutableSortedMap.of()) .getOrDefault(y, Map.of()) .values() .stream(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 1338cc008..f1c6688b1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -224,15 +224,15 @@ private void layoutAxisImpl( final Map positions = new HashMap<>(); this.grid.forEach((x, y, values) -> { - final int coord = ops.chooseCoord(x, y); - final int pos = positions.computeIfAbsent(coord, ignored -> startPos); + final int otherCoord = ops.chooseOtherCoord(x, y); + final int pos = positions.computeIfAbsent(otherCoord, ignored -> startPos); values.forEach(constrained -> { - final int span = getComponentSpan.apply(constrained.component, coord); + final int span = getComponentSpan.apply(constrained.component, otherCoord); ops.setBounds(constrained.component, pos, span); }); - positions.put(coord, pos + cellSpans.get(coord)); + positions.put(otherCoord, pos + cellSpans.get(ops.chooseCoord(x, y))); }); } @@ -392,6 +392,11 @@ public int chooseCoord(int x, int y) { return x; } + @Override + public int chooseOtherCoord(int x, int y) { + return y; + } + @Override public void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); @@ -434,6 +439,11 @@ public int chooseCoord(int x, int y) { return y; } + @Override + public int chooseOtherCoord(int x, int y) { + return x; + } + @Override public void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); @@ -447,6 +457,7 @@ public void setBounds(Component component, int y, int height) { ImmutableMap getCellSpans(Sizes sizes); int getSpan(Dimension size); int chooseCoord(int x, int y); + int chooseOtherCoord(int x, int y); void setBounds(Component component, int pos, int span); } } From acd65ca2f7484925a2d948f4f15bbb5d90d92e80 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 20:49:25 -0800 Subject: [PATCH 054/110] add FlexGridColumnVisualiser fix FlexGridConstraints nextRow and nextColumn --- .../FlexGridColumnVisualiser.java | 24 +++++++++++++++++++ .../internal/gui/visualization/Main.java | 8 +++++-- .../enigma/gui/util/FlexGridConstraints.java | 6 ++--- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java new file mode 100644 index 000000000..303b16c29 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java @@ -0,0 +1,24 @@ +package org.quilt.internal.gui.visualization; + +import org.quiltmc.enigma.gui.util.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.FlexGridLayout; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +public class FlexGridColumnVisualiser extends JFrame { + public static final String TITLE = "Flex Grid Column"; + + public FlexGridColumnVisualiser() { + super(TITLE); + + this.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + this.add(new JLabel("Label 1"), constraints); + this.add(new JLabel("Label 2"), constraints.nextRow()); + this.add(new JLabel("Label 3"), constraints.nextRow()); + + this.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index b2422e07f..17253ea43 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -18,6 +18,12 @@ private Main() { private static final JFrame WINDOW = new JFrame("Gui Visualization"); + // bootstrap + static { + addVisualizer(FlexGridRelativeRowVisualiser.TITLE, FlexGridRelativeRowVisualiser::new); + addVisualizer(FlexGridColumnVisualiser.TITLE, FlexGridColumnVisualiser::new); + } + private static void position(Window window) { final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); final int x = (screenSize.width - window.getWidth()) / 2; @@ -31,8 +37,6 @@ public static void main(String[] args) { WINDOW.setLayout(new FlowLayout()); - addVisualizer(FlexGridRelativeRowVisualiser.TITLE, FlexGridRelativeRowVisualiser::new); - final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); final int width = screenSize.width * 2 / 3; final int height = screenSize.height * 2 / 3; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java index 820db56fb..65484ea3b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -229,8 +229,8 @@ public Absolute x(int x) { } public Absolute nextRow() { - this.x++; - this.y = 0; + this.x = 0; + this.y++; return this; } @@ -240,7 +240,7 @@ public Absolute y(int y) { } public Absolute nextColumn() { - this.y++; + this.x++; return this; } From 825d2b7bdd4a70f2ae1f3390ac76472b24669f04 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 21:03:26 -0800 Subject: [PATCH 055/110] use Visualizer interface instead of making visualizers extend JFrame add FlexGridGridVisualiser --- .../FlexGridColumnVisualiser.java | 22 ++++++------ .../visualization/FlexGridGridVisualiser.java | 34 +++++++++++++++++++ .../FlexGridRelativeRowVisualiser.java | 22 ++++++------ .../internal/gui/visualization/Main.java | 13 +++---- .../gui/visualization/Visualizer.java | 9 +++++ 5 files changed, 74 insertions(+), 26 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java index 303b16c29..9e9b6a5d3 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java @@ -6,19 +6,21 @@ import javax.swing.JFrame; import javax.swing.JLabel; -public class FlexGridColumnVisualiser extends JFrame { - public static final String TITLE = "Flex Grid Column"; - - public FlexGridColumnVisualiser() { - super(TITLE); +public class FlexGridColumnVisualiser implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Column"; + } - this.setLayout(new FlexGridLayout()); + @Override + public void visualizeWindow(JFrame window) { + window.setLayout(new FlexGridLayout()); final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - this.add(new JLabel("Label 1"), constraints); - this.add(new JLabel("Label 2"), constraints.nextRow()); - this.add(new JLabel("Label 3"), constraints.nextRow()); + window.add(new JLabel("Label 1"), constraints); + window.add(new JLabel("Label 2"), constraints.nextRow()); + window.add(new JLabel("Label 3"), constraints.nextRow()); - this.pack(); + window.pack(); } } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java new file mode 100644 index 000000000..e439c2eba --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java @@ -0,0 +1,34 @@ +package org.quilt.internal.gui.visualization; + +import org.quiltmc.enigma.gui.util.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.FlexGridLayout; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +public class FlexGridGridVisualiser implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Grid"; + } + + @Override + public void visualizeWindow(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(new JLabel("[0, 0]"), constraints); + window.add(new JLabel("[1, 0]"), constraints.nextColumn()); + window.add(new JLabel("[2, 0]"), constraints.nextColumn()); + + window.add(new JLabel("[0, 1]"), constraints.nextRow()); + window.add(new JLabel("[1, 1]"), constraints.nextColumn()); + window.add(new JLabel("[2, 1]"), constraints.nextColumn()); + + window.add(new JLabel("[0, 2]"), constraints.nextRow()); + window.add(new JLabel("[1, 2]"), constraints.nextColumn()); + window.add(new JLabel("[2, 2]"), constraints.nextColumn()); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java index 250d22017..36de8fcf1 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java @@ -5,18 +5,20 @@ import javax.swing.JFrame; import javax.swing.JLabel; -public class FlexGridRelativeRowVisualiser extends JFrame { - public static final String TITLE = "Flex Grid Relative Row"; - - public FlexGridRelativeRowVisualiser() { - super(TITLE); +public class FlexGridRelativeRowVisualiser implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Relative Row"; + } - this.setLayout(new FlexGridLayout()); + @Override + public void visualizeWindow(JFrame window) { + window.setLayout(new FlexGridLayout()); - this.add(new JLabel("Label 1")); - this.add(new JLabel("Label 2")); - this.add(new JLabel("Label 3")); + window.add(new JLabel("Label 1")); + window.add(new JLabel("Label 2")); + window.add(new JLabel("Label 3")); - this.pack(); + window.pack(); } } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 17253ea43..e5b9b3f1e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -9,7 +9,6 @@ import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.util.function.Supplier; public final class Main { private Main() { @@ -20,8 +19,9 @@ private Main() { // bootstrap static { - addVisualizer(FlexGridRelativeRowVisualiser.TITLE, FlexGridRelativeRowVisualiser::new); - addVisualizer(FlexGridColumnVisualiser.TITLE, FlexGridColumnVisualiser::new); + registerVisualizer(new FlexGridRelativeRowVisualiser()); + registerVisualizer(new FlexGridColumnVisualiser()); + registerVisualizer(new FlexGridGridVisualiser()); } private static void position(Window window) { @@ -47,10 +47,11 @@ public static void main(String[] args) { WINDOW.setVisible(true); } - private static void addVisualizer(String title, Supplier factory) { - final JButton button = new JButton(title); + private static void registerVisualizer(Visualizer visualizer) { + final JButton button = new JButton(visualizer.getTitle()); button.addActionListener(e -> { - final JFrame window = factory.get(); + final JFrame window = new JFrame(visualizer.getTitle()); + visualizer.visualizeWindow(window); window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); window.addWindowListener(new WindowAdapter() { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java new file mode 100644 index 000000000..6a21a90d4 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java @@ -0,0 +1,9 @@ +package org.quilt.internal.gui.visualization; + +import javax.swing.JFrame; + +public interface Visualizer { + String getTitle(); + + void visualizeWindow(JFrame window); +} From 8ff32456a9f8d4233e11b4c352d56ef42f26a13b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 22:04:32 -0800 Subject: [PATCH 056/110] add FlexGridOverlapVisualiser --- .../FlexGridOverlapVisualiser.java | 37 ++++++++++++++ .../internal/gui/visualization/Main.java | 1 + .../gui/visualization/util/VisualBox.java | 51 +++++++++++++++++++ .../enigma/gui/util/FlexGridConstraints.java | 24 +++++++-- 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java new file mode 100644 index 000000000..6fc00477c --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java @@ -0,0 +1,37 @@ +package org.quilt.internal.gui.visualization; + +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.FlexGridLayout; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridOverlapVisualiser implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Overlap"; + } + + /** + *

+	 * -------------------
+	 * | RGB | RB  |  R  |
+	 * -------------------
+	 * | GB  |  B  |
+	 * -------------
+	 * |  G  |
+	 * 
+ */ + @Override + public void visualizeWindow(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(VisualBox.of(Color.RED, 300, 100), constraints.size(3, 1)); + window.add(VisualBox.of(Color.GREEN, 100, 300), constraints.size(1, 3)); + window.add(VisualBox.of(Color.BLUE, 200, 200), constraints.size(2, 2)); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index e5b9b3f1e..4ca20248f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -22,6 +22,7 @@ private Main() { registerVisualizer(new FlexGridRelativeRowVisualiser()); registerVisualizer(new FlexGridColumnVisualiser()); registerVisualizer(new FlexGridGridVisualiser()); + registerVisualizer(new FlexGridOverlapVisualiser()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java new file mode 100644 index 000000000..7a218210e --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -0,0 +1,51 @@ +package org.quilt.internal.gui.visualization.util; + +import org.jspecify.annotations.Nullable; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; + +public class VisualBox extends JPanel { + public static VisualBox of(@Nullable Color color, int width, int height) { + return new VisualBox(null, color, width, height); + } + + private final int width; + private final int height; + + public VisualBox(@Nullable String name, @Nullable Color color, int width, int height) { + this.width = width; + this.height = height; + + this.setBackground(new Color(0, true)); + + if (color != null) { + this.setForeground(color); + } + + final Color foreground = this.getForeground(); + + this.setLayout(new BorderLayout()); + + if (name != null) { + final JLabel nameLabel = new JLabel(name); + nameLabel.setForeground(foreground); + this.add(nameLabel, BorderLayout.NORTH); + } + + final JLabel dimensions = new JLabel("%s x %s".formatted(this.width, this.height)); + dimensions.setForeground(foreground); + this.add(dimensions, BorderLayout.EAST); + + this.setBorder(BorderFactory.createDashedBorder(foreground, 2, 2, 4, false)); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(this.width, this.height); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java index 65484ea3b..c3fa8539d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -67,6 +67,12 @@ public C height(int height) { return this.getSelf(); } + public C size(int width, int height) { + this.width(width); + this.height(height); + return this.getSelf(); + } + public C fillX(boolean fill) { this.fillX = fill; return this.getSelf(); @@ -228,10 +234,14 @@ public Absolute x(int x) { return this; } - public Absolute nextRow() { + public Absolute advanceRows(int count) { this.x = 0; - this.y++; - return this; + this.y += count; + return this.getSelf(); + } + + public Absolute nextRow() { + return this.advanceRows(1); } public Absolute y(int y) { @@ -239,9 +249,13 @@ public Absolute y(int y) { return this; } + public Absolute advanceColumns(int count) { + this.x += count; + return this.getSelf(); + } + public Absolute nextColumn() { - this.x++; - return this; + return this.advanceColumns(1); } public Absolute pos(int x, int y) { From cc1296bc3ea3669cf58679db56fc5f60c546bafe Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 23:09:58 -0800 Subject: [PATCH 057/110] quiltify FlexGridGridVisualiser configure min/max size of VisualBox center text in VisualBox only open one instance of a visualizer at a time --- .../visualization/FlexGridGridVisualiser.java | 34 ---------- .../FlexGridQuiltVisualiser.java | 46 ++++++++++++++ .../internal/gui/visualization/Main.java | 21 ++++++- .../gui/visualization/util/VisualBox.java | 62 +++++++++++++++---- .../enigma/gui/util/FlexGridLayout.java | 1 + 5 files changed, 116 insertions(+), 48 deletions(-) delete mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java deleted file mode 100644 index e439c2eba..000000000 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridGridVisualiser.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.quilt.internal.gui.visualization; - -import org.quiltmc.enigma.gui.util.FlexGridConstraints; -import org.quiltmc.enigma.gui.util.FlexGridLayout; - -import javax.swing.JFrame; -import javax.swing.JLabel; - -public class FlexGridGridVisualiser implements Visualizer { - @Override - public String getTitle() { - return "Flex Grid Grid"; - } - - @Override - public void visualizeWindow(JFrame window) { - window.setLayout(new FlexGridLayout()); - - final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - window.add(new JLabel("[0, 0]"), constraints); - window.add(new JLabel("[1, 0]"), constraints.nextColumn()); - window.add(new JLabel("[2, 0]"), constraints.nextColumn()); - - window.add(new JLabel("[0, 1]"), constraints.nextRow()); - window.add(new JLabel("[1, 1]"), constraints.nextColumn()); - window.add(new JLabel("[2, 1]"), constraints.nextColumn()); - - window.add(new JLabel("[0, 2]"), constraints.nextRow()); - window.add(new JLabel("[1, 2]"), constraints.nextColumn()); - window.add(new JLabel("[2, 2]"), constraints.nextColumn()); - - window.pack(); - } -} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java new file mode 100644 index 000000000..5c84cd7bb --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java @@ -0,0 +1,46 @@ +package org.quilt.internal.gui.visualization; + +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.FlexGridLayout; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridQuiltVisualiser implements Visualizer { + private static final int PATCH_SIZE = 100; + + private static final Color PURPLE = new Color(151, 34, 255); + private static final Color MAGENTA = new Color(220, 41, 221); + private static final Color CYAN = new Color(39, 162, 253); + private static final Color BLUE = new Color(51, 68, 255); + + private static VisualBox patchOf(String name, Color color) { + return VisualBox.of(name, color, PATCH_SIZE, PATCH_SIZE); + } + + @Override + public String getTitle() { + return "Flex Grid Quilt"; + } + + @Override + public void visualizeWindow(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(patchOf("[0, 0]", PURPLE), constraints); + window.add(patchOf("[1, 0]", MAGENTA), constraints.nextColumn()); + window.add(patchOf("[2, 0]", CYAN), constraints.nextColumn()); + + window.add(patchOf("[0, 1]", MAGENTA), constraints.nextRow()); + window.add(patchOf("[1, 1]", CYAN), constraints.nextColumn()); + window.add(patchOf("[2, 1]", BLUE), constraints.nextColumn()); + + window.add(patchOf("[0, 2]", PURPLE), constraints.nextRow()); + window.add(patchOf("[1, 2]", BLUE), constraints.nextColumn()); + window.add(patchOf("[2, 2]", PURPLE), constraints.nextColumn()); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 4ca20248f..e595d771e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -9,6 +9,7 @@ import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.util.concurrent.atomic.AtomicReference; public final class Main { private Main() { @@ -21,7 +22,7 @@ private Main() { static { registerVisualizer(new FlexGridRelativeRowVisualiser()); registerVisualizer(new FlexGridColumnVisualiser()); - registerVisualizer(new FlexGridGridVisualiser()); + registerVisualizer(new FlexGridQuiltVisualiser()); registerVisualizer(new FlexGridOverlapVisualiser()); } @@ -44,21 +45,35 @@ public static void main(String[] args) { final int x = (screenSize.width - width) / 2; final int y = (screenSize.height - height) / 2; + System.out.printf("size: %s x %s%n", width, height); + WINDOW.setBounds(x, y, width, height); WINDOW.setVisible(true); } private static void registerVisualizer(Visualizer visualizer) { final JButton button = new JButton(visualizer.getTitle()); + final AtomicReference currentWindow = new AtomicReference<>(); + button.addActionListener(e -> { - final JFrame window = new JFrame(visualizer.getTitle()); + final JFrame window = currentWindow.updateAndGet(old -> { + if (old != null) { + old.dispose(); + } + + return new JFrame(visualizer.getTitle()); + }); + visualizer.visualizeWindow(window); window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); window.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { - WINDOW.requestFocus(); + final JFrame window = currentWindow.get(); + if (window == null || !window.isDisplayable()) { + WINDOW.requestFocus(); + } } }); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index 7a218210e..e8982a75f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.util; import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.gui.docker.component.VerticalFlowLayout; import javax.swing.BorderFactory; import javax.swing.JLabel; @@ -8,18 +9,41 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; public class VisualBox extends JPanel { public static VisualBox of(@Nullable Color color, int width, int height) { - return new VisualBox(null, color, width, height); + return of(null, color, width, height); } - private final int width; - private final int height; + public static VisualBox of(@Nullable String name, @Nullable Color color, int width, int height) { + return new VisualBox(name, color, width, height, width / 2, height / 2, width * 2, height * 2); + } + + private final int preferredWidth; + private final int preferredHeight; + + private final int minWidth; + private final int minHeight; + + private final int maxWidth; + private final int maxHeight; - public VisualBox(@Nullable String name, @Nullable Color color, int width, int height) { - this.width = width; - this.height = height; + protected VisualBox( + @Nullable String name, @Nullable Color color, + int preferredWidth, int preferredHeight, + int minWidth, int minHeight, + int maxWidth, int maxHeight + ) { + this.preferredWidth = preferredWidth; + this.preferredHeight = preferredHeight; + + this.minWidth = minWidth; + this.minHeight = minHeight; + + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; this.setBackground(new Color(0, true)); @@ -29,23 +53,39 @@ public VisualBox(@Nullable String name, @Nullable Color color, int width, int he final Color foreground = this.getForeground(); - this.setLayout(new BorderLayout()); + this.setLayout(new GridBagLayout()); + // this.setLayout(new BorderLayout()); + + final var center = new JPanel(new VerticalFlowLayout(5)); + center.setBackground(this.getBackground()); if (name != null) { final JLabel nameLabel = new JLabel(name); nameLabel.setForeground(foreground); - this.add(nameLabel, BorderLayout.NORTH); + center.add(nameLabel, BorderLayout.WEST); } - final JLabel dimensions = new JLabel("%s x %s".formatted(this.width, this.height)); + final JLabel dimensions = new JLabel("%s x %s".formatted(this.preferredWidth, this.preferredHeight)); dimensions.setForeground(foreground); - this.add(dimensions, BorderLayout.EAST); + center.add(dimensions, BorderLayout.EAST); + + this.add(center, new GridBagConstraints()); this.setBorder(BorderFactory.createDashedBorder(foreground, 2, 2, 4, false)); } @Override public Dimension getPreferredSize() { - return new Dimension(this.width, this.height); + return new Dimension(this.preferredWidth, this.preferredHeight); + } + + @Override + public Dimension getMinimumSize() { + return new Dimension(this.minWidth, this.minHeight); + } + + @Override + public Dimension getMaximumSize() { + return new Dimension(this.maxWidth, this.maxHeight); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index f1c6688b1..717d1078b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -170,6 +170,7 @@ private void layoutAxis(Container parent, AxisOperations ops) { } } + // TODO fix erroneous overlap and inconsistent sizes private Map allocateCellSpace(AxisOperations ops, int remainingSpace) { final SortedSet prioritizedConstrained = new TreeSet<>(); this.grid.forEach((x, y, values) -> { From b9a1b0de04817912763520837bd92b7dcf065666 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 27 Nov 2025 23:17:12 -0800 Subject: [PATCH 058/110] remove print --- .../java/org/quilt/internal/gui/visualization/Main.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index e595d771e..46a48fafc 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -45,8 +45,6 @@ public static void main(String[] args) { final int x = (screenSize.width - width) / 2; final int y = (screenSize.height - height) / 2; - System.out.printf("size: %s x %s%n", width, height); - WINDOW.setBounds(x, y, width, height); WINDOW.setVisible(true); } From d1f71e4a10e5b5e88322bce6d657ae68a162a406 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 09:04:34 -0800 Subject: [PATCH 059/110] fix passing an incorrect coord in layoutAxisImpl minor tweaks --- .../FlexGridColumnVisualiser.java | 2 +- ...java => FlexGridDefaultRowVisualiser.java} | 6 +-- .../FlexGridOverlapVisualiser.java | 3 +- .../FlexGridQuiltVisualiser.java | 2 +- .../internal/gui/visualization/Main.java | 4 +- .../gui/visualization/Visualizer.java | 2 +- .../enigma/gui/util/FlexGridLayout.java | 48 ++++++++++--------- 7 files changed, 35 insertions(+), 32 deletions(-) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{FlexGridRelativeRowVisualiser.java => FlexGridDefaultRowVisualiser.java} (72%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java index 9e9b6a5d3..cb525e3fd 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java @@ -13,7 +13,7 @@ public String getTitle() { } @Override - public void visualizeWindow(JFrame window) { + public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java similarity index 72% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java index 36de8fcf1..e25af3ee0 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridRelativeRowVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java @@ -5,14 +5,14 @@ import javax.swing.JFrame; import javax.swing.JLabel; -public class FlexGridRelativeRowVisualiser implements Visualizer { +public class FlexGridDefaultRowVisualiser implements Visualizer { @Override public String getTitle() { - return "Flex Grid Relative Row"; + return "Flex Grid Default Row"; } @Override - public void visualizeWindow(JFrame window) { + public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); window.add(new JLabel("Label 1")); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java index 6fc00477c..a6a994b65 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java @@ -21,10 +21,11 @@ public String getTitle() { * | GB | B | * ------------- * | G | + * ------- * */ @Override - public void visualizeWindow(JFrame window) { + public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java index 5c84cd7bb..90d100144 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java @@ -25,7 +25,7 @@ public String getTitle() { } @Override - public void visualizeWindow(JFrame window) { + public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 46a48fafc..dc8d99e01 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -20,7 +20,7 @@ private Main() { // bootstrap static { - registerVisualizer(new FlexGridRelativeRowVisualiser()); + registerVisualizer(new FlexGridDefaultRowVisualiser()); registerVisualizer(new FlexGridColumnVisualiser()); registerVisualizer(new FlexGridQuiltVisualiser()); registerVisualizer(new FlexGridOverlapVisualiser()); @@ -62,7 +62,7 @@ private static void registerVisualizer(Visualizer visualizer) { return new JFrame(visualizer.getTitle()); }); - visualizer.visualizeWindow(window); + visualizer.visualize(window); window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); window.addWindowListener(new WindowAdapter() { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java index 6a21a90d4..b9f4e6bc8 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java @@ -5,5 +5,5 @@ public interface Visualizer { String getTitle(); - void visualizeWindow(JFrame window); + void visualize(JFrame window); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java index 717d1078b..3726b17eb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java @@ -137,11 +137,11 @@ private Sizes getMaxSizes() { @Override public void layoutContainer(Container parent) { - this.layoutAxis(parent, AxisOperations.X); - this.layoutAxis(parent, AxisOperations.Y); + this.layoutAxis(parent, CartesianOperations.X); + this.layoutAxis(parent, CartesianOperations.Y); } - private void layoutAxis(Container parent, AxisOperations ops) { + private void layoutAxis(Container parent, CartesianOperations ops) { final Insets insets = parent.getInsets(); final int leadingInset = ops.getLeadingInset(insets); @@ -170,8 +170,7 @@ private void layoutAxis(Container parent, AxisOperations ops) { } } - // TODO fix erroneous overlap and inconsistent sizes - private Map allocateCellSpace(AxisOperations ops, int remainingSpace) { + private Map allocateCellSpace(CartesianOperations ops, int remainingSpace) { final SortedSet prioritizedConstrained = new TreeSet<>(); this.grid.forEach((x, y, values) -> { values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(ops.chooseCoord(x, y)))); @@ -209,7 +208,7 @@ private Map allocateCellSpace(AxisOperations ops, int remainin return cellSpans; } - private void layoutFixedAxis(Sizes sizes, int startPos, AxisOperations ops) { + private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) { this.layoutAxisImpl( startPos, ops, ops.getCellSpans(sizes), (component, coord) -> ops.getSpan(sizes.componentSizes.get(component)) @@ -218,22 +217,24 @@ private void layoutFixedAxis(Sizes sizes, int startPos, AxisOperations ops) { // TODO respect fill/alignment private void layoutAxisImpl( - int startPos, AxisOperations ops, + int startPos, CartesianOperations ops, Map cellSpans, BiFunction getComponentSpan ) { final Map positions = new HashMap<>(); this.grid.forEach((x, y, values) -> { - final int otherCoord = ops.chooseOtherCoord(x, y); - final int pos = positions.computeIfAbsent(otherCoord, ignored -> startPos); + final int oppositeCoord = ops.opposite().chooseCoord(x, y); + final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); + + final int coord = ops.chooseCoord(x, y); values.forEach(constrained -> { - final int span = getComponentSpan.apply(constrained.component, otherCoord); + final int span = getComponentSpan.apply(constrained.component, coord); ops.setBounds(constrained.component, pos, span); }); - positions.put(otherCoord, pos + cellSpans.get(ops.chooseCoord(x, y))); + positions.put(oppositeCoord, pos + cellSpans.get(coord)); }); } @@ -356,8 +357,8 @@ Dimension createTotalDimension(Insets insets) { } } - private interface AxisOperations { - AxisOperations X = new AxisOperations() { + private interface CartesianOperations { + CartesianOperations X = new CartesianOperations() { @Override public int getLeadingInset(Insets insets) { return insets.left; @@ -394,17 +395,17 @@ public int chooseCoord(int x, int y) { } @Override - public int chooseOtherCoord(int x, int y) { - return y; + public void setBounds(Component component, int x, int width) { + component.setBounds(x, component.getY(), width, component.getHeight()); } @Override - public void setBounds(Component component, int x, int width) { - component.setBounds(x, component.getY(), width, component.getHeight()); + public CartesianOperations opposite() { + return Y; } }; - AxisOperations Y = new AxisOperations() { + CartesianOperations Y = new CartesianOperations() { @Override public int getLeadingInset(Insets insets) { return insets.top; @@ -441,13 +442,13 @@ public int chooseCoord(int x, int y) { } @Override - public int chooseOtherCoord(int x, int y) { - return x; + public void setBounds(Component component, int y, int height) { + component.setBounds(component.getX(), y, component.getWidth(), height); } @Override - public void setBounds(Component component, int y, int height) { - component.setBounds(component.getX(), y, component.getWidth(), height); + public CartesianOperations opposite() { + return X; } }; @@ -458,7 +459,8 @@ public void setBounds(Component component, int y, int height) { ImmutableMap getCellSpans(Sizes sizes); int getSpan(Dimension size); int chooseCoord(int x, int y); - int chooseOtherCoord(int x, int y); void setBounds(Component component, int pos, int span); + + CartesianOperations opposite(); } } From 845a3d3f3c5e7026884bc28a7034bc4d2c423768 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 09:46:36 -0800 Subject: [PATCH 060/110] reject TODO --- .../java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java | 1 - 1 file changed, 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java index c3fa8539d..6b2de0c5b 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java @@ -28,7 +28,6 @@ private static Alignment requireNonNullAlignment(Alignment alignment) { int width; int height; - // TODO merge fill+alignment boolean fillX; boolean fillY; From 8fdfd43ff7af2ad9b123a89b9b8045e9ad51739d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 10:12:20 -0800 Subject: [PATCH 061/110] package flex grid --- .../FlexGridColumnVisualiser.java | 4 +- .../FlexGridDefaultRowVisualiser.java | 2 +- .../FlexGridOverlapVisualiser.java | 4 +- .../FlexGridQuiltVisualiser.java | 4 +- .../flex_grid}/ConstrainedGrid.java | 7 ++- .../flex_grid}/FlexGridLayout.java | 19 ++++--- .../constraints}/FlexGridConstraints.java | 56 +++++++++++++++---- 7 files changed, 67 insertions(+), 29 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/{ => layout/flex_grid}/ConstrainedGrid.java (94%) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/{ => layout/flex_grid}/FlexGridLayout.java (97%) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/{ => layout/flex_grid/constraints}/FlexGridConstraints.java (84%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java index cb525e3fd..7239ac26e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java @@ -1,7 +1,7 @@ package org.quilt.internal.gui.visualization; -import org.quiltmc.enigma.gui.util.FlexGridConstraints; -import org.quiltmc.enigma.gui.util.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import javax.swing.JFrame; import javax.swing.JLabel; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java index e25af3ee0..cf9d32e04 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java @@ -1,6 +1,6 @@ package org.quilt.internal.gui.visualization; -import org.quiltmc.enigma.gui.util.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import javax.swing.JFrame; import javax.swing.JLabel; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java index a6a994b65..7080996f0 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java @@ -1,8 +1,8 @@ package org.quilt.internal.gui.visualization; import org.quilt.internal.gui.visualization.util.VisualBox; -import org.quiltmc.enigma.gui.util.FlexGridConstraints; -import org.quiltmc.enigma.gui.util.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import javax.swing.JFrame; import java.awt.Color; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java index 90d100144..82b80c724 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java @@ -1,8 +1,8 @@ package org.quilt.internal.gui.visualization; import org.quilt.internal.gui.visualization.util.VisualBox; -import org.quiltmc.enigma.gui.util.FlexGridConstraints; -import org.quiltmc.enigma.gui.util.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import javax.swing.JFrame; import java.awt.Color; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java similarity index 94% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 071dc0dfa..9fe6c4a59 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -1,7 +1,7 @@ -package org.quiltmc.enigma.gui.util; +package org.quiltmc.enigma.gui.util.layout.flex_grid; import com.google.common.collect.ImmutableSortedMap; -import org.quiltmc.enigma.gui.util.FlexGridLayout.Constrained; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.Constrained; import java.awt.Component; import java.util.HashMap; @@ -14,7 +14,7 @@ /** * A map of cartesian coordinates to {@link Constrained} values.
- * Only designed for use with {@link FlexGridLayout}. + * Only designed for use in {@link FlexGridLayout}. * *

Multiple values can be associated with the same coordinates, * but a value may only be associated with one coordinate pair at a time. @@ -31,6 +31,7 @@ private static Set createValueSet(Integer ignored) { void put(int x, int y, Constrained value) { final Component component = value.component(); + final Position oldPos = this.valuePositions.put(component, new Position(x, y)); if (oldPos != null) { final Map> column = this.grid.get(oldPos.x); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java similarity index 97% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 3726b17eb..f2d0c7a98 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -1,8 +1,9 @@ -package org.quiltmc.enigma.gui.util; +package org.quiltmc.enigma.gui.util.layout.flex_grid; import com.google.common.collect.ImmutableMap; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.util.Utils; import java.awt.Component; @@ -46,8 +47,8 @@ public void addLayoutComponent(Component component, @Nullable FlexGridConstraint final int x; final int y; if (constraints instanceof FlexGridConstraints.Absolute absolute) { - x = absolute.x; - y = absolute.y; + x = absolute.getX(); + y = absolute.getY(); } else { x = this.getRelativeX(); y = this.getRelativeY(); @@ -224,10 +225,10 @@ private void layoutAxisImpl( final Map positions = new HashMap<>(); this.grid.forEach((x, y, values) -> { + final int coord = ops.chooseCoord(x, y); final int oppositeCoord = ops.opposite().chooseCoord(x, y); - final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); - final int coord = ops.chooseCoord(x, y); + final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); values.forEach(constrained -> { final int span = getComponentSpan.apply(constrained.component, coord); @@ -258,10 +259,10 @@ static Constrained defaultOf(Component component) { Constrained(Component component, FlexGridConstraints constraints) { this( component, - constraints.width, constraints.height, - constraints.fillX, constraints.fillY, - constraints.xAlignment, constraints.yAlignment, - constraints.priority + constraints.getWidth(), constraints.getHeight(), + constraints.fillsX(), constraints.fillsY(), + constraints.getXAlignment(), constraints.getYAlignment(), + constraints.getPriority() ); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java similarity index 84% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 6b2de0c5b..2513f1536 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -1,17 +1,17 @@ -package org.quiltmc.enigma.gui.util; +package org.quiltmc.enigma.gui.util.layout.flex_grid.constraints; import com.google.common.base.Preconditions; import java.util.Objects; public abstract sealed class FlexGridConstraints> { - static final int DEFAULT_PRIORITY = 0; - static final int DEFAULT_WIDTH = 1; - static final int DEFAULT_HEIGHT = 1; - static final boolean DEFAULT_FILL_X = false; - static final boolean DEFAULT_FILL_Y = false; - static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; - static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; + public static final int DEFAULT_PRIORITY = 0; + public static final int DEFAULT_WIDTH = 1; + public static final int DEFAULT_HEIGHT = 1; + public static final boolean DEFAULT_FILL_X = false; + public static final boolean DEFAULT_FILL_Y = false; + public static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; + public static final Alignment DEFAULT_Y_ALIGNMENT = Alignment.CENTER; public static Relative createRelative() { return Relative.of(); @@ -54,6 +54,34 @@ private FlexGridConstraints( this.priority = priority; } + public int getWidth() { + return this.width; + } + + public int getHeight() { + return this.height; + } + + public boolean fillsX() { + return this.fillX; + } + + public boolean fillsY() { + return this.fillY; + } + + public Alignment getXAlignment() { + return this.xAlignment; + } + + public Alignment getYAlignment() { + return this.yAlignment; + } + + public int getPriority() { + return this.priority; + } + public C width(int width) { Preconditions.checkArgument(width > 0, "width must be positive!"); this.width = width; @@ -199,8 +227,8 @@ protected Relative getSelf() { } public static final class Absolute extends FlexGridConstraints { - static final int DEFAULT_X = 0; - static final int DEFAULT_Y = 0; + public static final int DEFAULT_X = 0; + public static final int DEFAULT_Y = 0; public static Absolute of() { return new Absolute( @@ -228,6 +256,14 @@ private Absolute( this.y = y; } + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + public Absolute x(int x) { this.x = x; return this; From daaac2a270868005f48fc60c59460fe91926ee2e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 11:41:30 -0800 Subject: [PATCH 062/110] implement filling --- .../layout/flex_grid/ConstrainedGrid.java | 64 ++++++++--- .../util/layout/flex_grid/FlexGridLayout.java | 107 +++++++++++++----- 2 files changed, 127 insertions(+), 44 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 9fe6c4a59..16ea56c87 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -25,28 +25,32 @@ private static Set createValueSet(Integer ignored) { } private final SortedMap>> grid = new TreeMap<>(); - private final Map valuePositions = new HashMap<>(); + private final Map componentPositions = new HashMap<>(); private final SortedMap> valuesByMaxX = new TreeMap<>(); private final SortedMap> valuesByMaxY = new TreeMap<>(); + private final Set xFillers = new HashSet<>(); + private final Set yFillers = new HashSet<>(); void put(int x, int y, Constrained value) { final Component component = value.component(); - final Position oldPos = this.valuePositions.put(component, new Position(x, y)); - if (oldPos != null) { - final Map> column = this.grid.get(oldPos.x); - final Constrained oldValue = column.get(oldPos.y).get(component); - column.get(oldPos.y).remove(component, value); - this.valuesByMaxX.get(oldPos.x + oldValue.getXExcess()).remove(component); - this.valuesByMaxY.get(oldPos.y + oldValue.getYExcess()).remove(component); - } + this.remove(value.component()); this.grid .computeIfAbsent(x, ignored -> new TreeMap<>()) .computeIfAbsent(y, ignored -> new HashMap<>(1)) .put(component, value); + this.valuesByMaxX.computeIfAbsent(x + value.getXExcess(), ConstrainedGrid::createValueSet).add(component); this.valuesByMaxY.computeIfAbsent(y + value.getYExcess(), ConstrainedGrid::createValueSet).add(component); + + if (value.fillX()) { + this.xFillers.add(component); + } + + if (value.fillY()) { + this.yFillers.add(component); + } } Stream get(int x, int y) { @@ -56,12 +60,36 @@ Stream get(int x, int y) { .stream(); } - void remove(Component value) { - final Position pos = this.valuePositions.remove(value); + void remove(Component component) { + final Position pos = this.componentPositions.remove(component); if (pos != null) { - final Constrained removed = this.grid.get(pos.x).get(pos.y).remove(value); - this.valuesByMaxX.get(pos.x + removed.getXExcess()).remove(value); - this.valuesByMaxY.get(pos.y + removed.getYExcess()).remove(value); + final SortedMap> column = this.grid.get(pos.x); + final Map values = column.get(pos.y); + final Constrained removed = values.remove(component); + if (values.isEmpty()) { + column.remove(pos.y); + + if (column.isEmpty()) { + this.grid.remove(pos.x); + } + } + + final int maxX = pos.x + removed.getXExcess(); + final Set maxXValues = this.valuesByMaxX.get(maxX); + maxXValues.remove(component); + if (maxXValues.isEmpty()) { + this.valuesByMaxX.remove(maxX); + } + + final int maxY = pos.y + removed.getYExcess(); + final Set maxYValues = this.valuesByMaxY.get(maxY); + maxYValues.remove(component); + if (maxYValues.isEmpty()) { + this.valuesByMaxY.remove(maxY); + } + + this.xFillers.remove(component); + this.yFillers.remove(component); } } @@ -73,6 +101,14 @@ int getMaxYOrThrow() { return this.valuesByMaxY.lastKey(); } + boolean noneFillX() { + return this.xFillers.isEmpty(); + } + + boolean noneFillY() { + return this.yFillers.isEmpty(); + } + int getSize() { return this.valuesByMaxX.size(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index f2d0c7a98..dbbfa485e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -148,22 +148,35 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final int availableSpace = ops.getParentSpace(parent) - leadingInset - ops.getTrailingInset(insets); - final Sizes preferredSizes = this.getPreferredSizes(); + final Sizes preferred = this.getPreferredSizes(); - final int extraSpace = availableSpace - ops.getTotalSpace(preferredSizes); + final int extraSpace = availableSpace - ops.getTotalSpace(preferred); if (extraSpace >= 0) { - this.layoutFixedAxis(preferredSizes, leadingInset + extraSpace / 2, ops); + if (extraSpace == 0 || ops.noneFill(this.grid)) { + this.layoutFixedAxis(preferred, leadingInset + extraSpace / 2, ops); + } else { + final Map cellSpans = this.allocateCellSpace(ops, extraSpace, true); + + final Sizes max = this.getMaxSizes(); + this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { + final Sizes targets = ops.fills(constrained) ? max : preferred; + final Dimension targetSize = targets.componentSizes.get(constrained.component); + assert targetSize != null; + + return Math.min(ops.getSpan(targetSize), cellSpans.get(coord)); + }); + } } else { - final Sizes minSizes = this.getMinSizes(); + final Sizes min = this.getMinSizes(); - final int extraMinSpace = availableSpace - ops.getTotalSpace(minSizes); + final int extraMinSpace = availableSpace - ops.getTotalSpace(min); if (extraMinSpace <= 0) { - this.layoutFixedAxis(minSizes, leadingInset, ops); + this.layoutFixedAxis(min, leadingInset, ops); } else { - final Map cellSpans = this.allocateCellSpace(ops, extraMinSpace); + final Map cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); - this.layoutAxisImpl(leadingInset, ops, cellSpans, (component, coord) -> { - final Dimension preferredSize = preferredSizes.componentSizes.get(component); + this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { + final Dimension preferredSize = preferred.componentSizes.get(constrained.component); assert preferredSize != null; return Math.min(ops.getSpan(preferredSize), cellSpans.get(coord)); }); @@ -171,28 +184,40 @@ private void layoutAxis(Container parent, CartesianOperations ops) { } } - private Map allocateCellSpace(CartesianOperations ops, int remainingSpace) { + private Map allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { + final Sizes sizes; + final Map cellSpans; + if (fill) { + sizes = this.getMaxSizes(); + cellSpans = new HashMap<>(this.getPreferredSizes().columnWidths); + } else { + sizes = this.getPreferredSizes(); + cellSpans = new HashMap<>(this.getMinSizes().columnWidths); + } + final SortedSet prioritizedConstrained = new TreeSet<>(); this.grid.forEach((x, y, values) -> { values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(ops.chooseCoord(x, y)))); }); - final ImmutableMap preferredComponentSizes = this.getPreferredSizes().componentSizes; - final Map cellSpans = new HashMap<>(this.getMinSizes().columnWidths); for (final Constrained.At at : prioritizedConstrained) { + if (fill && !ops.fills(at.constrained())) { + continue; + } + final int currentSpan = cellSpans.get(at.coord); - final Dimension preferredSize = preferredComponentSizes.get(at.constrained().component); - assert preferredSize != null; - final int preferredSpan = ops.getSpan(preferredSize); - final int preferredDiff = preferredSpan - currentSpan; - if (preferredDiff > 0) { - if (preferredDiff <= remainingSpace) { - cellSpans.put(at.coord, preferredSpan); - - if (remainingSpace == preferredDiff) { + final Dimension targetSize = sizes.componentSizes.get(at.constrained().component); + assert targetSize != null; + final int targetSpan = ops.getSpan(targetSize); + final int targetDiff = targetSpan - currentSpan; + if (targetDiff > 0) { + if (targetDiff <= remainingSpace) { + cellSpans.put(at.coord, targetSpan); + + if (remainingSpace == targetDiff) { break; } else { - remainingSpace -= preferredDiff; + remainingSpace -= targetDiff; } } else { final int lastOfSpan = remainingSpace; @@ -212,7 +237,7 @@ private Map allocateCellSpace(CartesianOperations ops, int rem private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) { this.layoutAxisImpl( startPos, ops, ops.getCellSpans(sizes), - (component, coord) -> ops.getSpan(sizes.componentSizes.get(component)) + (constrained, coord) -> ops.getSpan(sizes.componentSizes.get(constrained.component)) ); } @@ -220,7 +245,7 @@ private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) private void layoutAxisImpl( int startPos, CartesianOperations ops, Map cellSpans, - BiFunction getComponentSpan + BiFunction getComponentSpan ) { final Map positions = new HashMap<>(); @@ -231,7 +256,7 @@ private void layoutAxisImpl( final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); values.forEach(constrained -> { - final int span = getComponentSpan.apply(constrained.component, coord); + final int span = getComponentSpan.apply(constrained, coord); ops.setBounds(constrained.component, pos, span); }); @@ -360,6 +385,11 @@ Dimension createTotalDimension(Insets insets) { private interface CartesianOperations { CartesianOperations X = new CartesianOperations() { + @Override + public int chooseCoord(int x, int y) { + return x; + } + @Override public int getLeadingInset(Insets insets) { return insets.left; @@ -391,8 +421,13 @@ public int getSpan(Dimension size) { } @Override - public int chooseCoord(int x, int y) { - return x; + public boolean fills(Constrained constrained) { + return constrained.fillY; + } + + @Override + public boolean noneFill(ConstrainedGrid grid) { + return grid.noneFillX(); } @Override @@ -407,6 +442,11 @@ public CartesianOperations opposite() { }; CartesianOperations Y = new CartesianOperations() { + @Override + public int chooseCoord(int x, int y) { + return y; + } + @Override public int getLeadingInset(Insets insets) { return insets.top; @@ -438,8 +478,13 @@ public int getSpan(Dimension size) { } @Override - public int chooseCoord(int x, int y) { - return y; + public boolean fills(Constrained constrained) { + return constrained.fillY; + } + + @Override + public boolean noneFill(ConstrainedGrid grid) { + return grid.noneFillY(); } @Override @@ -453,13 +498,15 @@ public CartesianOperations opposite() { } }; + int chooseCoord(int x, int y); int getLeadingInset(Insets insets); int getTrailingInset(Insets insets); int getParentSpace(Container parent); int getTotalSpace(Sizes sizes); ImmutableMap getCellSpans(Sizes sizes); int getSpan(Dimension size); - int chooseCoord(int x, int y); + boolean fills(Constrained constrained); + boolean noneFill(ConstrainedGrid grid); void setBounds(Component component, int pos, int span); CartesianOperations opposite(); From 7f4453da9b9b73e54e60f11cd03fc7dbe5b67382 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 14:40:43 -0800 Subject: [PATCH 063/110] fix prioritization in allocateCellSpace generalize patches and quilts add many VisualBox factories add FlexGridFillVisualizer and FlexGridAlignmentVisualizer (alignment not yet implemented) --- .../FlexGridAlignmentVisualizer.java | 49 +++++++++ .../visualization/FlexGridFillVisualizer.java | 24 ++++ .../FlexGridQuiltVisualiser.java | 78 +++++++++---- .../internal/gui/visualization/Main.java | 2 + .../gui/visualization/util/VisualBox.java | 103 ++++++++++++++++++ .../util/layout/flex_grid/FlexGridLayout.java | 47 ++++---- .../constraints/FlexGridConstraints.java | 12 ++ 7 files changed, 272 insertions(+), 43 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java new file mode 100644 index 000000000..7de6c5eef --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java @@ -0,0 +1,49 @@ +package org.quilt.internal.gui.visualization; + +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; + +import javax.swing.JFrame; + +import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.BEGIN; +import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.CENTER; +import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.END; + +public class FlexGridAlignmentVisualizer implements Visualizer { + private static final int SPACER_SIZE = (int) (VisualBox.DEFAULT_SIZE * 1.2); + + private static VisualBox createSpacer() { + return VisualBox.ofFixed(SPACER_SIZE); + } + + @Override + public String getTitle() { + return "Flex Grid Alignment"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + c -> c.align(BEGIN, BEGIN), c -> c.align(CENTER, BEGIN), c -> c.align(END, BEGIN), + c -> c.align(BEGIN, CENTER), c -> c.align(CENTER, CENTER), c -> c.align(END, CENTER), + c -> c.align(BEGIN, END), c -> c.align(CENTER, END), c -> c.align(END, END) + ); + + final Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(createSpacer(), constraints); + window.add(createSpacer(), constraints.nextColumn()); + window.add(createSpacer(), constraints.nextColumn()); + + window.add(createSpacer(), constraints.nextRow()); + window.add(createSpacer(), constraints.nextColumn()); + window.add(createSpacer(), constraints.nextColumn()); + + window.add(createSpacer(), constraints.nextRow()); + window.add(createSpacer(), constraints.nextColumn()); + window.add(createSpacer(), constraints.nextColumn()); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java new file mode 100644 index 000000000..8d8493a07 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java @@ -0,0 +1,24 @@ +package org.quilt.internal.gui.visualization; + +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; + +public class FlexGridFillVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Fill"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + FlexGridConstraints::fillNone, FlexGridConstraints::fillOnlyY, FlexGridConstraints::fillNone, + FlexGridConstraints::fillOnlyX, FlexGridConstraints::fillBoth, FlexGridConstraints::fillOnlyX, + FlexGridConstraints::fillNone, FlexGridConstraints::fillOnlyY, FlexGridConstraints::fillNone + ); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java index 82b80c724..3cca8bfb0 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java @@ -3,20 +3,62 @@ import org.quilt.internal.gui.visualization.util.VisualBox; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; import javax.swing.JFrame; -import java.awt.Color; +import java.util.function.UnaryOperator; public class FlexGridQuiltVisualiser implements Visualizer { - private static final int PATCH_SIZE = 100; + public static void visualizeQuilt( + JFrame window, + UnaryOperator constrainer1, + UnaryOperator constrainer2, + UnaryOperator constrainer3, - private static final Color PURPLE = new Color(151, 34, 255); - private static final Color MAGENTA = new Color(220, 41, 221); - private static final Color CYAN = new Color(39, 162, 253); - private static final Color BLUE = new Color(51, 68, 255); + UnaryOperator constrainer4, + UnaryOperator constrainer5, + UnaryOperator constrainer6, - private static VisualBox patchOf(String name, Color color) { - return VisualBox.of(name, color, PATCH_SIZE, PATCH_SIZE); + UnaryOperator constrainer7, + UnaryOperator constrainer8, + UnaryOperator constrainer9 + ) { + visualizeQuilt( + window, + "[0, 0]", constrainer1, "[1, 0]", constrainer2, "[2, 0]", constrainer3, + "[0, 1]", constrainer4, "[1, 1]", constrainer5, "[2, 1]", constrainer6, + "[0, 2]", constrainer7, "[1, 2]", constrainer8, "[2, 2]", constrainer9 + ); + } + + public static void visualizeQuilt( + JFrame window, + String name1, UnaryOperator constrainer1, + String name2, UnaryOperator constrainer2, + String name3, UnaryOperator constrainer3, + + String name4, UnaryOperator constrainer4, + String name5, UnaryOperator constrainer5, + String name6, UnaryOperator constrainer6, + + String name7, UnaryOperator constrainer7, + String name8, UnaryOperator constrainer8, + String name9, UnaryOperator constrainer9 + ) { + window.setLayout(new FlexGridLayout()); + + final Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(VisualBox.purplePatchOf(name1), constrainer1.apply(constraints)); + window.add(VisualBox.magentaPatchOf(name2), constrainer2.apply(constraints.nextColumn())); + window.add(VisualBox.cyanPatchOf(name3), constrainer3.apply(constraints.nextColumn())); + + window.add(VisualBox.magentaPatchOf(name4), constrainer4.apply(constraints.nextRow())); + window.add(VisualBox.cyanPatchOf(name5), constrainer5.apply(constraints.nextColumn())); + window.add(VisualBox.bluePatchOf(name6), constrainer6.apply(constraints.nextColumn())); + + window.add(VisualBox.purplePatchOf(name7), constrainer7.apply(constraints.nextRow())); + window.add(VisualBox.bluePatchOf(name8), constrainer8.apply(constraints.nextColumn())); + window.add(VisualBox.purplePatchOf(name9), constrainer9.apply(constraints.nextColumn())); } @Override @@ -26,20 +68,12 @@ public String getTitle() { @Override public void visualize(JFrame window) { - window.setLayout(new FlexGridLayout()); - - final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - window.add(patchOf("[0, 0]", PURPLE), constraints); - window.add(patchOf("[1, 0]", MAGENTA), constraints.nextColumn()); - window.add(patchOf("[2, 0]", CYAN), constraints.nextColumn()); - - window.add(patchOf("[0, 1]", MAGENTA), constraints.nextRow()); - window.add(patchOf("[1, 1]", CYAN), constraints.nextColumn()); - window.add(patchOf("[2, 1]", BLUE), constraints.nextColumn()); - - window.add(patchOf("[0, 2]", PURPLE), constraints.nextRow()); - window.add(patchOf("[1, 2]", BLUE), constraints.nextColumn()); - window.add(patchOf("[2, 2]", PURPLE), constraints.nextColumn()); + visualizeQuilt( + window, + UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), + UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), + UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity() + ); window.pack(); } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index dc8d99e01..019de0e40 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -24,6 +24,8 @@ private Main() { registerVisualizer(new FlexGridColumnVisualiser()); registerVisualizer(new FlexGridQuiltVisualiser()); registerVisualizer(new FlexGridOverlapVisualiser()); + registerVisualizer(new FlexGridFillVisualizer()); + registerVisualizer(new FlexGridAlignmentVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index e8982a75f..191f08657 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -13,6 +13,41 @@ import java.awt.GridBagLayout; public class VisualBox extends JPanel { + public static final int DEFAULT_SIZE = 100; + + public static final Color PATCH_PURPLE = new Color(151, 34, 255); + public static final Color PATCH_MAGENTA = new Color(220, 41, 221); + public static final Color PATCH_CYAN = new Color(39, 162, 253); + public static final Color PATCH_BLUE = new Color(51, 68, 255); + + public static VisualBox of() { + return of(null); + } + + public static VisualBox of(@Nullable Color color) { + return of(null, color); + } + + public static VisualBox of(@Nullable String name, @Nullable Color color) { + return of(name, color, DEFAULT_SIZE); + } + + public static VisualBox of(int size) { + return of (null, size); + } + + public static VisualBox of(@Nullable Color color, int size) { + return of(null, color, size); + } + + public static VisualBox of(@Nullable String name, @Nullable Color color, int size) { + return of(name, color, size, size); + } + + public static VisualBox of(int width, int height) { + return of(null, width, height); + } + public static VisualBox of(@Nullable Color color, int width, int height) { return of(null, color, width, height); } @@ -21,6 +56,74 @@ public static VisualBox of(@Nullable String name, @Nullable Color color, int wid return new VisualBox(name, color, width, height, width / 2, height / 2, width * 2, height * 2); } + public static VisualBox ofFixed() { + return ofFixed(null); + } + + public static VisualBox ofFixed(@Nullable Color color) { + return ofFixed(null, color); + } + + public static VisualBox ofFixed(@Nullable String name, @Nullable Color color) { + return ofFixed(name, color, DEFAULT_SIZE); + } + + public static VisualBox ofFixed(int size) { + return ofFixed(null, size); + } + + public static VisualBox ofFixed(@Nullable Color color, int size) { + return ofFixed(null, color, size); + } + + public static VisualBox ofFixed(@Nullable String name, @Nullable Color color, int size) { + return ofFixed(name, color, size, size); + } + + public static VisualBox ofFixed(int width, int height) { + return ofFixed(null, width, height); + } + + public static VisualBox ofFixed(@Nullable Color color, int width, int height) { + return ofFixed(null, color, width, height); + } + + public static VisualBox ofFixed(@Nullable String name, @Nullable Color color, int width, int height) { + return new VisualBox(name, color, width, height, width, height, width, height); + } + + public static VisualBox purplePatchOf() { + return purplePatchOf(null); + } + + public static VisualBox purplePatchOf(@Nullable String name) { + return of(name, PATCH_PURPLE); + } + + public static VisualBox magentaPatchOf() { + return purplePatchOf(null); + } + + public static VisualBox magentaPatchOf(@Nullable String name) { + return of(name, PATCH_MAGENTA); + } + + public static VisualBox cyanPatchOf() { + return cyanPatchOf(null); + } + + public static VisualBox cyanPatchOf(@Nullable String name) { + return of(name, PATCH_CYAN); + } + + public static VisualBox bluePatchOf() { + return bluePatchOf(null); + } + + public static VisualBox bluePatchOf(@Nullable String name) { + return of(name, PATCH_BLUE); + } + private final int preferredWidth; private final int preferredHeight; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index dbbfa485e..c043281d7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -14,8 +14,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.PriorityQueue; import java.util.function.BiFunction; import java.util.function.Function; @@ -30,12 +29,12 @@ public class FlexGridLayout implements LayoutManager2 { public void addLayoutComponent(Component component, @Nullable Object constraints) throws IllegalArgumentException { if (constraints == null) { this.addDefaultConstrainedLayoutComponent(component); - } else if (constraints instanceof FlexGridConstraints typedConstraints) { - this.addLayoutComponent(component, typedConstraints); + } else if (constraints instanceof FlexGridConstraints gridConstraints) { + this.addLayoutComponent(component, gridConstraints); } else { throw new IllegalArgumentException( - "constraints type must be %s, but was %s!" - .formatted(FlexGridConstraints.class.getName(), constraints.getClass().getName()) + "constraints type %s does not extend %s!" + .formatted(constraints.getClass().getName(), FlexGridConstraints.class.getName()) ); } } @@ -185,28 +184,34 @@ private void layoutAxis(Container parent, CartesianOperations ops) { } private Map allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { - final Sizes sizes; + final Sizes large; + final Sizes small; final Map cellSpans; if (fill) { - sizes = this.getMaxSizes(); - cellSpans = new HashMap<>(this.getPreferredSizes().columnWidths); + large = this.getMaxSizes(); + small = this.getPreferredSizes(); } else { - sizes = this.getPreferredSizes(); - cellSpans = new HashMap<>(this.getMinSizes().columnWidths); + large = this.getPreferredSizes(); + small = this.getMinSizes(); } - final SortedSet prioritizedConstrained = new TreeSet<>(); - this.grid.forEach((x, y, values) -> { - values.forEach(constrained -> prioritizedConstrained.add(constrained.new At(ops.chooseCoord(x, y)))); - }); + cellSpans = new HashMap<>(ops.getCellSpans(small)); - for (final Constrained.At at : prioritizedConstrained) { - if (fill && !ops.fills(at.constrained())) { - continue; + final PriorityQueue prioritized = new PriorityQueue<>(this.grid.getSize()); + this.grid.forEach((x, y, values) -> { + if (fill) { + values = values.filter(ops::fills); } + values.forEach(constrained -> { + prioritized.add(constrained.new At(ops.chooseCoord(x, y))); + }); + }); + + for (final Constrained.At at : prioritized) { final int currentSpan = cellSpans.get(at.coord); - final Dimension targetSize = sizes.componentSizes.get(at.constrained().component); + + final Dimension targetSize = large.componentSizes.get(at.constrained().component); assert targetSize != null; final int targetSpan = ops.getSpan(targetSize); final int targetDiff = targetSpan - currentSpan; @@ -241,7 +246,7 @@ private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) ); } - // TODO respect fill/alignment + // TODO respect alignment private void layoutAxisImpl( int startPos, CartesianOperations ops, Map cellSpans, @@ -422,7 +427,7 @@ public int getSpan(Dimension size) { @Override public boolean fills(Constrained constrained) { - return constrained.fillY; + return constrained.fillX; } @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 2513f1536..687a4898c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -109,6 +109,10 @@ public C fillX() { return this.fillX(true); } + public C fillOnlyX() { + return this.fill(true, false); + } + public C fillY(boolean fill) { this.fillY = fill; return this.getSelf(); @@ -118,6 +122,10 @@ public C fillY() { return this.fillY(true); } + public C fillOnlyY() { + return this.fill(false, true); + } + public C fill(boolean x, boolean y) { this.fillX(x); this.fillY(y); @@ -128,6 +136,10 @@ public C fillBoth() { return this.fill(true, true); } + public C fillNone() { + return this.fill(false, false); + } + public C xAlignment(Alignment alignment) { this.xAlignment = requireNonNullAlignment(alignment); return this.getSelf(); From 7019ac8220ae29fb8112c12148abab95fb22aab8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 15:42:03 -0800 Subject: [PATCH 064/110] implement alignment add setters for alignment combinations add FlexGridAlignAndFillVisualizer --- .../FlexGridAlignAndFillVisualizer.java | 30 ++++++++++ .../FlexGridAlignmentVisualizer.java | 10 +--- .../internal/gui/visualization/Main.java | 1 + .../util/layout/flex_grid/FlexGridLayout.java | 27 +++++++-- .../constraints/FlexGridConstraints.java | 56 ++++++++++++++++--- 5 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java new file mode 100644 index 000000000..8c975f058 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java @@ -0,0 +1,30 @@ +package org.quilt.internal.gui.visualization; + +import javax.swing.JFrame; + +public class FlexGridAlignAndFillVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Align & Fill"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + c -> c.fillNone().alignTopLeft(), + c -> c.fillOnlyY().alignTopCenter(), + c -> c.fillNone().alignTopRight(), + + c -> c.fillOnlyX().alightCenterLeft(), + c -> c.fillBoth().alignCenter(), + c -> c.fillOnlyX().alignCenterRight(), + + c -> c.fillNone().alignBottomLeft(), + c -> c.fillOnlyY().alignBottomCenter(), + c -> c.fillNone().alignBottomRight() + ); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java index 7de6c5eef..1bc43e0b6 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java @@ -6,10 +6,6 @@ import javax.swing.JFrame; -import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.BEGIN; -import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.CENTER; -import static org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment.END; - public class FlexGridAlignmentVisualizer implements Visualizer { private static final int SPACER_SIZE = (int) (VisualBox.DEFAULT_SIZE * 1.2); @@ -26,9 +22,9 @@ public String getTitle() { public void visualize(JFrame window) { FlexGridQuiltVisualiser.visualizeQuilt( window, - c -> c.align(BEGIN, BEGIN), c -> c.align(CENTER, BEGIN), c -> c.align(END, BEGIN), - c -> c.align(BEGIN, CENTER), c -> c.align(CENTER, CENTER), c -> c.align(END, CENTER), - c -> c.align(BEGIN, END), c -> c.align(CENTER, END), c -> c.align(END, END) + Absolute::alignTopLeft, Absolute::alignTopCenter, Absolute::alignTopRight, + Absolute::alightCenterLeft, Absolute::alignCenter, Absolute::alignCenterRight, + Absolute::alignBottomLeft, Absolute::alignBottomCenter, Absolute::alignBottomRight ); final Absolute constraints = FlexGridConstraints.createAbsolute(); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 019de0e40..5925f1720 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -26,6 +26,7 @@ private Main() { registerVisualizer(new FlexGridOverlapVisualiser()); registerVisualizer(new FlexGridFillVisualizer()); registerVisualizer(new FlexGridAlignmentVisualizer()); + registerVisualizer(new FlexGridAlignAndFillVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index c043281d7..ef8fa7916 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -4,6 +4,7 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment; import org.quiltmc.enigma.util.Utils; import java.awt.Component; @@ -246,7 +247,6 @@ private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) ); } - // TODO respect alignment private void layoutAxisImpl( int startPos, CartesianOperations ops, Map cellSpans, @@ -259,13 +259,21 @@ private void layoutAxisImpl( final int oppositeCoord = ops.opposite().chooseCoord(x, y); final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); + final Integer cellSpan = cellSpans.get(coord); values.forEach(constrained -> { final int span = getComponentSpan.apply(constrained, coord); - ops.setBounds(constrained.component, pos, span); + + final int constrainedPos = span == cellSpan ? pos : switch (ops.getAlignment(constrained)) { + case BEGIN -> pos; + case CENTER -> pos + (cellSpan - span) / 2; + case END -> pos + cellSpan - span; + }; + + ops.setBounds(constrained.component, constrainedPos, span); }); - positions.put(oppositeCoord, pos + cellSpans.get(coord)); + positions.put(oppositeCoord, pos + cellSpan); }); } @@ -273,7 +281,7 @@ record Constrained( Component component, int width, int height, boolean fillX, boolean fillY, - FlexGridConstraints.Alignment xAlignment, FlexGridConstraints.Alignment yAlignment, + Alignment xAlignment, Alignment yAlignment, int priority ) { static Constrained defaultOf(Component component) { @@ -435,6 +443,11 @@ public boolean noneFill(ConstrainedGrid grid) { return grid.noneFillX(); } + @Override + public Alignment getAlignment(Constrained constrained) { + return constrained.xAlignment; + } + @Override public void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); @@ -492,6 +505,11 @@ public boolean noneFill(ConstrainedGrid grid) { return grid.noneFillY(); } + @Override + public Alignment getAlignment(Constrained constrained) { + return constrained.yAlignment; + } + @Override public void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); @@ -512,6 +530,7 @@ public CartesianOperations opposite() { int getSpan(Dimension size); boolean fills(Constrained constrained); boolean noneFill(ConstrainedGrid grid); + Alignment getAlignment(Constrained constrained); void setBounds(Component component, int pos, int span); CartesianOperations opposite(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 687a4898c..6f4c08c71 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -140,42 +140,82 @@ public C fillNone() { return this.fill(false, false); } - public C xAlignment(Alignment alignment) { + public C alignX(Alignment alignment) { this.xAlignment = requireNonNullAlignment(alignment); return this.getSelf(); } public C alignLeft() { - return this.xAlignment(Alignment.BEGIN); + return this.alignX(Alignment.BEGIN); + } + + public C alignCenterX() { + return this.alignX(Alignment.CENTER); } public C alignRight() { - return this.xAlignment(Alignment.END); + return this.alignX(Alignment.END); } - public C yAlignment(Alignment alignment) { + public C alignY(Alignment alignment) { this.yAlignment = requireNonNullAlignment(alignment); return this.getSelf(); } public C alignTop() { - return this.yAlignment(Alignment.BEGIN); + return this.alignY(Alignment.BEGIN); + } + + public C alignCenterY() { + return this.alignY(Alignment.CENTER); } public C alignBottom() { - return this.yAlignment(Alignment.END); + return this.alignY(Alignment.END); } public C align(Alignment x, Alignment y) { - this.xAlignment(x); - this.yAlignment(y); + this.alignX(x); + this.alignY(y); return this.getSelf(); } + public C alignTopLeft() { + return this.align(Alignment.BEGIN, Alignment.BEGIN); + } + + public C alignTopCenter() { + return this.align(Alignment.CENTER, Alignment.BEGIN); + } + + public C alignTopRight() { + return this.align(Alignment.END, Alignment.BEGIN); + } + + public C alightCenterLeft() { + return this.align(Alignment.BEGIN, Alignment.CENTER); + } + public C alignCenter() { return this.align(Alignment.CENTER, Alignment.CENTER); } + public C alignCenterRight() { + return this.align(Alignment.END, Alignment.CENTER); + } + + public C alignBottomLeft() { + return this.align(Alignment.BEGIN, Alignment.END); + } + + public C alignBottomCenter() { + return this.align(Alignment.CENTER, Alignment.END); + } + + public C alignBottomRight() { + return this.align(Alignment.END, Alignment.END); + } + public C priority(int priority) { this.priority = priority; return this.getSelf(); From 437a4288475daed0c534c8e5a8d3e0dcc6ac6713 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 16:14:50 -0800 Subject: [PATCH 065/110] make getSelf package-private --- .../layout/flex_grid/constraints/FlexGridConstraints.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 6f4c08c71..5b88466fa 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -223,7 +223,7 @@ public C priority(int priority) { public abstract C copy(); - protected abstract C getSelf(); + abstract C getSelf(); public enum Alignment { BEGIN, CENTER, END @@ -273,7 +273,7 @@ public Absolute toAbsolute(int x, int y) { } @Override - protected Relative getSelf() { + Relative getSelf() { return this; } } @@ -372,7 +372,7 @@ public Relative toRelative() { } @Override - protected Absolute getSelf() { + Absolute getSelf() { return this; } } From 814c904384cf5d6906bca40637fd24585566c9c6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 16:59:23 -0800 Subject: [PATCH 066/110] ignore unused in FlexGridConstraints --- .../util/layout/flex_grid/constraints/FlexGridConstraints.java | 1 + 1 file changed, 1 insertion(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 5b88466fa..9a66cbd2a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -4,6 +4,7 @@ import java.util.Objects; +@SuppressWarnings("unused") public abstract sealed class FlexGridConstraints> { public static final int DEFAULT_PRIORITY = 0; public static final int DEFAULT_WIDTH = 1; From 7745223e7d5f88448c6ddd3190646dfea3a4c89b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 17:01:39 -0800 Subject: [PATCH 067/110] package flex grid visualizers --- .../java/org/quilt/internal/gui/visualization/Main.java | 8 ++++++++ .../{ => flex_grid}/FlexGridAlignAndFillVisualizer.java | 4 +++- .../{ => flex_grid}/FlexGridAlignmentVisualizer.java | 3 ++- .../{ => flex_grid}/FlexGridColumnVisualiser.java | 3 ++- .../{ => flex_grid}/FlexGridDefaultRowVisualiser.java | 3 ++- .../{ => flex_grid}/FlexGridFillVisualizer.java | 3 ++- .../{ => flex_grid}/FlexGridOverlapVisualiser.java | 3 ++- .../{ => flex_grid}/FlexGridQuiltVisualiser.java | 3 ++- 8 files changed, 23 insertions(+), 7 deletions(-) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridAlignAndFillVisualizer.java (85%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridAlignmentVisualizer.java (93%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridColumnVisualiser.java (86%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridDefaultRowVisualiser.java (81%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridFillVisualizer.java (85%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridOverlapVisualiser.java (89%) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/{ => flex_grid}/FlexGridQuiltVisualiser.java (96%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 5925f1720..5934ad472 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -1,5 +1,13 @@ package org.quilt.internal.gui.visualization; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridAlignAndFillVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridAlignmentVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridColumnVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridOverlapVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; + import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.WindowConstants; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java similarity index 85% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java index 8c975f058..089d90fb2 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignAndFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java @@ -1,4 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; import javax.swing.JFrame; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java similarity index 93% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java index 1bc43e0b6..8c5b0729e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridAlignmentVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridColumnVisualiser.java similarity index 86% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridColumnVisualiser.java index 7239ac26e..b7eb64aa5 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridColumnVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridColumnVisualiser.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDefaultRowVisualiser.java similarity index 81% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDefaultRowVisualiser.java index cf9d32e04..9422c85ae 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridDefaultRowVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDefaultRowVisualiser.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import javax.swing.JFrame; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java similarity index 85% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java index 8d8493a07..754fb4df4 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import javax.swing.JFrame; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java similarity index 89% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java index 7080996f0..e0d06048d 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java similarity index 96% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java index 3cca8bfb0..80be7fa1d 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java @@ -1,5 +1,6 @@ -package org.quilt.internal.gui.visualization; +package org.quilt.internal.gui.visualization.flex_grid; +import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; From 17d030b803fbb5b081a37875a6d2aecd4c31efcd Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 17:22:44 -0800 Subject: [PATCH 068/110] add more FlexGridConstraints priority setters add FlexGridPriorityVisualizer --- .../internal/gui/visualization/Main.java | 2 ++ .../flex_grid/FlexGridPriorityVisualizer.java | 26 +++++++++++++++++++ .../constraints/FlexGridConstraints.java | 16 ++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 5934ad472..bbf4041e5 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -6,6 +6,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridOverlapVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import javax.swing.JButton; @@ -35,6 +36,7 @@ private Main() { registerVisualizer(new FlexGridFillVisualizer()); registerVisualizer(new FlexGridAlignmentVisualizer()); registerVisualizer(new FlexGridAlignAndFillVisualizer()); + registerVisualizer(new FlexGridPriorityVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java new file mode 100644 index 000000000..1b4cb68f7 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java @@ -0,0 +1,26 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; + +import javax.swing.JFrame; +import java.util.function.UnaryOperator; + +public class FlexGridPriorityVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Priority"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + UnaryOperator.identity(), Absolute::incrementPriority, Absolute::incrementPriority, + Absolute::incrementPriority, Absolute::incrementPriority, Absolute::incrementPriority, + Absolute::incrementPriority, Absolute::incrementPriority, Absolute::incrementPriority + ); + + window.pack(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 9a66cbd2a..50bc16a47 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -222,6 +222,22 @@ public C priority(int priority) { return this.getSelf(); } + public C defaultPriority() { + return this.priority(DEFAULT_PRIORITY); + } + + public C addPriority(int amount) { + return this.priority(this.priority + amount); + } + + public C incrementPriority() { + return this.addPriority(1); + } + + public C decrementPriority() { + return this.addPriority(-1); + } + public abstract C copy(); abstract C getSelf(); From 49d315f0d43ad9a20a8ebfbeaab87cef6a375ba2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 17:29:42 -0800 Subject: [PATCH 069/110] convert CartesianOperations to enum --- .../util/layout/flex_grid/FlexGridLayout.java | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index ef8fa7916..40df55ddf 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -396,143 +396,146 @@ Dimension createTotalDimension(Insets insets) { } } - private interface CartesianOperations { - CartesianOperations X = new CartesianOperations() { + private enum CartesianOperations { + X() { @Override - public int chooseCoord(int x, int y) { + int chooseCoord(int x, int y) { return x; } @Override - public int getLeadingInset(Insets insets) { + int getLeadingInset(Insets insets) { return insets.left; } @Override - public int getTrailingInset(Insets insets) { + int getTrailingInset(Insets insets) { return insets.right; } @Override - public int getParentSpace(Container parent) { + int getParentSpace(Container parent) { return parent.getWidth(); } @Override - public int getTotalSpace(Sizes sizes) { + int getTotalSpace(Sizes sizes) { return sizes.totalWidth; } @Override - public ImmutableMap getCellSpans(Sizes sizes) { + ImmutableMap getCellSpans(Sizes sizes) { return sizes.columnWidths; } @Override - public int getSpan(Dimension size) { + int getSpan(Dimension size) { return size.width; } @Override - public boolean fills(Constrained constrained) { + boolean fills(Constrained constrained) { return constrained.fillX; } @Override - public boolean noneFill(ConstrainedGrid grid) { + boolean noneFill(ConstrainedGrid grid) { return grid.noneFillX(); } @Override - public Alignment getAlignment(Constrained constrained) { + Alignment getAlignment(Constrained constrained) { return constrained.xAlignment; } @Override - public void setBounds(Component component, int x, int width) { + void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); } @Override - public CartesianOperations opposite() { + CartesianOperations opposite() { return Y; } - }; - - CartesianOperations Y = new CartesianOperations() { + }, + Y() { @Override - public int chooseCoord(int x, int y) { + int chooseCoord(int x, int y) { return y; } @Override - public int getLeadingInset(Insets insets) { + int getLeadingInset(Insets insets) { return insets.top; } @Override - public int getTrailingInset(Insets insets) { + int getTrailingInset(Insets insets) { return insets.bottom; } @Override - public int getParentSpace(Container parent) { + int getParentSpace(Container parent) { return parent.getHeight(); } @Override - public int getTotalSpace(Sizes sizes) { + int getTotalSpace(Sizes sizes) { return sizes.totalHeight; } @Override - public ImmutableMap getCellSpans(Sizes sizes) { + ImmutableMap getCellSpans(Sizes sizes) { return sizes.rowHeights; } @Override - public int getSpan(Dimension size) { + int getSpan(Dimension size) { return size.height; } @Override - public boolean fills(Constrained constrained) { + boolean fills(Constrained constrained) { return constrained.fillY; } @Override - public boolean noneFill(ConstrainedGrid grid) { + boolean noneFill(ConstrainedGrid grid) { return grid.noneFillY(); } @Override - public Alignment getAlignment(Constrained constrained) { + Alignment getAlignment(Constrained constrained) { return constrained.yAlignment; } @Override - public void setBounds(Component component, int y, int height) { + void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); } @Override - public CartesianOperations opposite() { + CartesianOperations opposite() { return X; } }; - int chooseCoord(int x, int y); - int getLeadingInset(Insets insets); - int getTrailingInset(Insets insets); - int getParentSpace(Container parent); - int getTotalSpace(Sizes sizes); - ImmutableMap getCellSpans(Sizes sizes); - int getSpan(Dimension size); - boolean fills(Constrained constrained); - boolean noneFill(ConstrainedGrid grid); - Alignment getAlignment(Constrained constrained); - void setBounds(Component component, int pos, int span); - - CartesianOperations opposite(); + abstract int chooseCoord(int x, int y); + + abstract int getLeadingInset(Insets insets); + abstract int getTrailingInset(Insets insets); + abstract int getParentSpace(Container parent); + + abstract int getTotalSpace(Sizes sizes); + abstract ImmutableMap getCellSpans(Sizes sizes); + abstract int getSpan(Dimension size); + + abstract boolean fills(Constrained constrained); + abstract boolean noneFill(ConstrainedGrid grid); + abstract Alignment getAlignment(Constrained constrained); + + abstract void setBounds(Component component, int pos, int span); + + abstract CartesianOperations opposite(); } } From fa59ba720515a8df873540f50017f5d682c1544f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 17:45:38 -0800 Subject: [PATCH 070/110] add some javadocs to visualizer stuff --- .../org/quilt/internal/gui/visualization/Main.java | 5 +++++ .../internal/gui/visualization/Visualizer.java | 13 +++++++++++++ .../internal/gui/visualization/util/VisualBox.java | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index bbf4041e5..28632d1bc 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -20,6 +20,11 @@ import java.awt.event.WindowEvent; import java.util.concurrent.atomic.AtomicReference; +/** + * The main entrypoint for {@link Visualizer}s. + * + *

Opens a window full of buttons that open visualizer windows. + */ public final class Main { private Main() { throw new UnsupportedOperationException(); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java index b9f4e6bc8..601705e9f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Visualizer.java @@ -2,8 +2,21 @@ import javax.swing.JFrame; +/** + * A visualizer populates a window with content to help visualize what GUI code does. + * + *

Visualizers are registered in {@link Main}. + */ public interface Visualizer { + /** + * The title of the visualizer. + * + *

Used by {@link Main} for the visualizer's button text and window titles. + */ String getTitle(); + /** + * Populates the passed {@code window} with content for visualization. + */ void visualize(JFrame window); } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index 191f08657..6c66d1f93 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -12,6 +12,13 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +/** + * A simple box with customizable sizes, color, and name. + * + *

Boxes have a dashed outline and a transparent background.
+ * There are numerous factories for creating boxes with as much specificity as needed. + */ +@SuppressWarnings("unused") public class VisualBox extends JPanel { public static final int DEFAULT_SIZE = 100; From 2fbb692091eb289b2e738f04a4c517f8671e6540 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 17:57:04 -0800 Subject: [PATCH 071/110] javadoc visualizeQuilt --- .../flex_grid/FlexGridQuiltVisualiser.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java index 80be7fa1d..4bbb8f27d 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java @@ -10,6 +10,14 @@ import java.util.function.UnaryOperator; public class FlexGridQuiltVisualiser implements Visualizer { + /** + * Visualizes Quilt's logo, giving each patch a name indicating its coordinates. + * + * @see #visualizeQuilt(JFrame, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator) + */ public static void visualizeQuilt( JFrame window, UnaryOperator constrainer1, @@ -32,6 +40,18 @@ public static void visualizeQuilt( ); } + /** + * Gives the passed {@code window} a {@link FlexGridLayout} and forms Quilt's logo out of a 3 x 3 grid of + * {@link VisualBox} patches. + * + *

The patches are given the passed names and their constraints are adjusted using the passed constrainers.
+ * The same {@link FlexGridConstraints} instance is passed to each constrainer, and its x and y coordinates are + * updated for each patch before passing it. + * + * @see #visualizeQuilt(JFrame, + * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, + * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator) + */ public static void visualizeQuilt( JFrame window, String name1, UnaryOperator constrainer1, From 7e1078fd42bca884af5c3fa74436e246a6fa58e2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 28 Nov 2025 19:35:07 -0800 Subject: [PATCH 072/110] javadoc FlexGridLayout (class only), start javadocing FlexGridConstraints --- .../util/layout/flex_grid/FlexGridLayout.java | 69 +++++++++++++++++++ .../constraints/FlexGridConstraints.java | 15 ++++ 2 files changed, 84 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 40df55ddf..58e2e15de 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -10,6 +10,8 @@ import java.awt.Component; import java.awt.Container; import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.LayoutManager2; import java.util.Comparator; @@ -19,6 +21,73 @@ import java.util.function.BiFunction; import java.util.function.Function; +/** + * A layout manager that lays out components in a grid and allocates space according to priority. + * + *

Flex grids are similar to {@link GridBagLayout}s, with some key differences: + *

    + *
  • flex grids don't {@linkplain GridBagConstraints#weightx weight} their components; instead, space is + * allocated according to {@linkplain FlexGridConstraints#priority(int) priority} and position + *
  • flex grids respect each component's {@linkplain Component#getMaximumSize() maximum size} + *
  • flex grids support negative coordinates + *
+ * + *

Constraints

+ * + * Flex grids are configured by passing {@link FlexGridConstraints} when + * {@linkplain Container#add(Component, Object) adding} {@link Component}s.
+ * If no constraints are specified, + * a default set of {@linkplain FlexGridConstraints#createRelative() relative} constraints are used.
+ * If non-{@linkplain FlexGridConstraints flex grid} constraints are passed, + * an {@link IllegalArgumentException} is thrown. + * + *

Space allocation

+ * + *

Space is allocated to components per-axis, with high-{@linkplain FlexGridConstraints#priority(int) priority} + * components getting space first. In ties, components with the least position on the axis get priority.
+ * Components never get less space in an axis than their {@linkplain Component#getMinimumSize() minimum sizes} + * allow, and they never get more space than their {@linkplain Component#getMaximumSize() maximum sizes} allow. + * Components only ever get more space in an axis than their + * {@linkplain Component#getPreferredSize() preferred sizes} request if they're set to + * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} that axis. + * + *

Space allocation behavior depends on the parent container's available space and + * child components' total required space (as before, per-axis): + * + * + * + * + * + * + * + * + * + * + * + * + * + *
less than minimum spaceeach component gets its minimum size (excess is clipped)
between minimum and preferred space + * each component gets at least its minimum size; components get additional space + * - up to their preferred size - according to priority + *
more than preferred space + * each component gets at least is preferred size; components that + * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} the axis get additional space + * - up to their max size - according to priority + *
+ * + *

Grid specifics

+ * + *
    + *
  • a component can occupy multiple grid cells when its {@linkplain FlexGridConstraints#width(int) width} + * or {@linkplain FlexGridConstraints#height(int) height} exceeds one; it occupies cells starting + * from its coordinates and extending in the positive direction of each axis + *
  • any number of components can share grid cells, resulting in overlap + *
  • only the relative values of coordinates matter; components with the least x coordinate are left-most + * whether the coordinate is {@code -1000}, {@code 0}, or {@code 1000} + *
  • vacant rows and columns are ignored; if two components are at x {@code -10} and {@code 10} and + * no other component has an x coordinate in that range, the two components are adjacent (in terms of x) + *
+ */ public class FlexGridLayout implements LayoutManager2 { private final ConstrainedGrid grid = new ConstrainedGrid(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 50bc16a47..89296579e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -1,9 +1,24 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid.constraints; import com.google.common.base.Preconditions; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.util.Objects; +/** + * Constraints for components added to a {@link Container} with a {@link FlexGridLayout} using + * {@link Container#add(Component, Object)}. + * + *

{@link FlexGridConstraints} is to {@link GridBagConstraints} as + * {@link FlexGridLayout} is to {@link GridBagLayout}.
+ * TODO differences, chaining, copying + * + * @param the type of these constraints; usually not relevant to users + */ @SuppressWarnings("unused") public abstract sealed class FlexGridConstraints> { public static final int DEFAULT_PRIORITY = 0; From d3d2289ae9638561e37af6525c8764a993db9d19 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 29 Nov 2025 11:52:14 -0800 Subject: [PATCH 073/110] dequeue from prioritized instead of iterating because iteration order isn't guaranteed --- .../enigma/gui/util/layout/flex_grid/FlexGridLayout.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 58e2e15de..ff8eb7cb2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -278,7 +278,9 @@ private Map allocateCellSpace(CartesianOperations ops, int rem }); }); - for (final Constrained.At at : prioritized) { + while (!prioritized.isEmpty()) { + final Constrained.At at = prioritized.remove(); + final int currentSpan = cellSpans.get(at.coord); final Dimension targetSize = large.componentSizes.get(at.constrained().component); From 59b079c14229e5aebededc60c6af8e0ef276aeb1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 2 Dec 2025 13:16:09 -0800 Subject: [PATCH 074/110] javadoc FlexGridConstraints rename FlexGridConstraints' width/height -> xExtent/yExtent --- .../flex_grid/FlexGridOverlapVisualiser.java | 6 +- .../util/layout/flex_grid/FlexGridLayout.java | 53 +- .../constraints/FlexGridConstraints.java | 695 +++++++++++++++--- 3 files changed, 646 insertions(+), 108 deletions(-) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java index e0d06048d..e200e5b50 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java @@ -30,9 +30,9 @@ public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - window.add(VisualBox.of(Color.RED, 300, 100), constraints.size(3, 1)); - window.add(VisualBox.of(Color.GREEN, 100, 300), constraints.size(1, 3)); - window.add(VisualBox.of(Color.BLUE, 200, 200), constraints.size(2, 2)); + window.add(VisualBox.of(Color.RED, 300, 100), constraints.extent(3, 1)); + window.add(VisualBox.of(Color.GREEN, 100, 300), constraints.extent(1, 3)); + window.add(VisualBox.of(Color.BLUE, 200, 200), constraints.extent(2, 2)); window.pack(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index ff8eb7cb2..fe3188f39 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -78,9 +78,14 @@ *

Grid specifics

* *
    - *
  • a component can occupy multiple grid cells when its {@linkplain FlexGridConstraints#width(int) width} - * or {@linkplain FlexGridConstraints#height(int) height} exceeds one; it occupies cells starting - * from its coordinates and extending in the positive direction of each axis + *
  • components with {@linkplain FlexGridConstraints#createRelative() relative constraints} (or no constraints) + * are added to the end of the bottom row,
    + * or ({@value FlexGridConstraints.Absolute#DEFAULT_X}, {@value FlexGridConstraints.Absolute#DEFAULT_Y}) + * if no other components have been added + *
  • a component can occupy multiple grid cells when its constraint + * {@linkplain FlexGridConstraints#xExtent(int) xExtent} or + * {@linkplain FlexGridConstraints#yExtent(int) yExtent} exceeds {@code 1}; + * it occupies cells starting from its coordinates and extending in the positive direction of each axis *
  • any number of components can share grid cells, resulting in overlap *
  • only the relative values of coordinates matter; components with the least x coordinate are left-most * whether the coordinate is {@code -1000}, {@code 0}, or {@code 1000} @@ -91,8 +96,25 @@ public class FlexGridLayout implements LayoutManager2 { private final ConstrainedGrid grid = new ConstrainedGrid(); + /** + * Lazily populated cache. + * + * @see #getPreferredSizes() + */ private @Nullable Sizes preferredSizes; + + /** + * Lazily populated cache. + * + * @see #getMinSizes() + */ private @Nullable Sizes minSizes; + + /** + * Lazily populated cache. + * + * @see #getMaxSizes() + */ private @Nullable Sizes maxSizes; @Override @@ -350,7 +372,7 @@ private void layoutAxisImpl( record Constrained( Component component, - int width, int height, + int xExtent, int yExtent, boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, int priority @@ -358,7 +380,7 @@ record Constrained( static Constrained defaultOf(Component component) { return new Constrained( component, - FlexGridConstraints.DEFAULT_WIDTH, FlexGridConstraints.DEFAULT_HEIGHT, + FlexGridConstraints.DEFAULT_X_EXTENT, FlexGridConstraints.DEFAULT_Y_EXTENT, FlexGridConstraints.DEFAULT_FILL_X, FlexGridConstraints.DEFAULT_FILL_Y, FlexGridConstraints.DEFAULT_X_ALIGNMENT, FlexGridConstraints.DEFAULT_Y_ALIGNMENT, FlexGridConstraints.DEFAULT_PRIORITY @@ -368,7 +390,7 @@ static Constrained defaultOf(Component component) { Constrained(Component component, FlexGridConstraints constraints) { this( component, - constraints.getWidth(), constraints.getHeight(), + constraints.getXExtent(), constraints.getYExtent(), constraints.fillsX(), constraints.fillsY(), constraints.getXAlignment(), constraints.getYAlignment(), constraints.getPriority() @@ -376,11 +398,11 @@ static Constrained defaultOf(Component component) { } int getXExcess() { - return this.width - 1; + return this.xExtent - 1; } int getYExcess() { - return this.height - 1; + return this.yExtent - 1; } private class At implements Comparable { @@ -413,6 +435,10 @@ public int compareTo(@NonNull At other) { } } + /** + * A collection of sizes and size metrics use for calculating min/max/preferred container size and + * for laying out the container. + */ private record Sizes( int totalWidth, int totalHeight, ImmutableMap rowHeights, ImmutableMap columnWidths, @@ -427,10 +453,10 @@ static Sizes calculate(ConstrainedGrid grid, Function getS values.forEach(constrained -> { final Dimension size = componentSizes.computeIfAbsent(constrained.component, getSize); - final int componentCellWidth = Utils.ceilDiv(size.width, constrained.width); - final int componentCellHeight = Utils.ceilDiv(size.height, constrained.height); - for (int xOffset = 0; xOffset < constrained.width; xOffset++) { - for (int yOffset = 0; yOffset < constrained.width; yOffset++) { + final int componentCellWidth = Utils.ceilDiv(size.width, constrained.xExtent); + final int componentCellHeight = Utils.ceilDiv(size.height, constrained.yExtent); + for (int xOffset = 0; xOffset < constrained.xExtent; xOffset++) { + for (int yOffset = 0; yOffset < constrained.xExtent; yOffset++) { final Dimension cellSize = cellSizes .computeIfAbsent(x + xOffset, ignored -> new HashMap<>()) .computeIfAbsent(y + yOffset, ignored -> new Dimension()); @@ -467,6 +493,9 @@ Dimension createTotalDimension(Insets insets) { } } + /** + * Sets of operations for the {@link #X} and {@link #Y} axes of a cartesian plane. + */ private enum CartesianOperations { X() { @Override diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 89296579e..76a203d2c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -11,19 +11,40 @@ /** * Constraints for components added to a {@link Container} with a {@link FlexGridLayout} using - * {@link Container#add(Component, Object)}. + * {@link Container#add(Component, Object)}.
    + * {@link FlexGridConstraints} is to {@link GridBagConstraints} as + * {@link FlexGridLayout} is to {@link GridBagLayout}. * - *

    {@link FlexGridConstraints} is to {@link GridBagConstraints} as - * {@link FlexGridLayout} is to {@link GridBagLayout}.
    - * TODO differences, chaining, copying + *

    Differences from {@link GridBagConstraints}

    + *
      + *
    • flex constraints have separate {@link Relative Relative} and {@link Absolute Absolute} + * types; {@link Absolute#toRelative() toRelative()} and {@link Relative#toAbsolute() toAbsolute()} + * convert between them + *
    • flex constraints support negative {@linkplain Absolute#pos(int, int) coordinates} + *
    • flex constraints don't use magic constants + *
    + * + *

    Convenience

    + *
      + *
    • constraints use the builder pattern; they're designed for method chaining + *
    • constraints are mutable, but their values are copied when + * {@linkplain Container#add(Component, Object) adding} to a container + *
    • they have numerous method variations for common use cases, including: + *
        + *
      • {@link Absolute#nextRow() nextRow()} and {@link Absolute#nextColumn() nextColumn()} + *
      • {@link #incrementPriority()} and {@link #decrementPriority()} + *
      • a method for each combination of vertical and horizontal alignments + *
      • {@link #copy()} + *
      + *
    * * @param the type of these constraints; usually not relevant to users */ @SuppressWarnings("unused") public abstract sealed class FlexGridConstraints> { public static final int DEFAULT_PRIORITY = 0; - public static final int DEFAULT_WIDTH = 1; - public static final int DEFAULT_HEIGHT = 1; + public static final int DEFAULT_X_EXTENT = 1; + public static final int DEFAULT_Y_EXTENT = 1; public static final boolean DEFAULT_FILL_X = false; public static final boolean DEFAULT_FILL_Y = false; public static final Alignment DEFAULT_X_ALIGNMENT = Alignment.BEGIN; @@ -41,25 +62,48 @@ private static Alignment requireNonNullAlignment(Alignment alignment) { return Objects.requireNonNull(alignment, "alignment must not be null!"); } - int width; - int height; + /** + * Defaults to {@value #DEFAULT_X_EXTENT}.
    + * Always positive. + */ + int xExtent; + + /** + * Defaults to {@value #DEFAULT_Y_EXTENT}.
    + * Always positive. + */ + int yExtent; + /** + * Defaults to {@value #DEFAULT_FILL_X}. + */ boolean fillX; + + /** + * Defaults to {@value #DEFAULT_FILL_Y}. + */ boolean fillY; + /** + * Defaults to {@link #DEFAULT_X_ALIGNMENT}. + */ Alignment xAlignment; + + /** + * Defaults to {@link #DEFAULT_Y_ALIGNMENT}. + */ Alignment yAlignment; int priority; private FlexGridConstraints( - int width, int height, + int xExtent, int yExtent, boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, int priority ) { - this.width = width; - this.height = height; + this.xExtent = xExtent; + this.yExtent = yExtent; this.fillX = fillX; this.fillY = fillY; @@ -70,12 +114,12 @@ private FlexGridConstraints( this.priority = priority; } - public int getWidth() { - return this.width; + public int getXExtent() { + return this.xExtent; } - public int getHeight() { - return this.height; + public int getYExtent() { + return this.yExtent; } public boolean fillsX() { @@ -98,157 +142,514 @@ public int getPriority() { return this.priority; } - public C width(int width) { - Preconditions.checkArgument(width > 0, "width must be positive!"); - this.width = width; - return this.getSelf(); - } - - public C height(int height) { - Preconditions.checkArgument(height > 0, "height must be positive!"); - this.height = height; + /** + * Sets {@link #xExtent} to the passed value. + * + *

    The default values is {@value #DEFAULT_X_EXTENT}. + * + *

    {@link #xExtent} controls the number of grid cells the constrained component occupies; it extends rightward. + * + * @throws IllegalArgumentException if the passed {@code extent} is not positive + * + * @see #yExtent(int) + * @see #extent(int, int) + */ + public C xExtent(int extent) { + Preconditions.checkArgument(extent > 0, "extent must be positive!"); + this.xExtent = extent; return this.getSelf(); } - public C size(int width, int height) { - this.width(width); - this.height(height); + /** + * Sets {@link #yExtent} to the passed value. + * + *

    The default values is {@value #DEFAULT_Y_EXTENT}. + * + *

    {@link #yExtent} controls the number of grid cells the constrained component occupies; it extends downward. + * + * @throws IllegalArgumentException if the passed {@code extent} is not positive + * + * @see #xExtent(int) + * @see #extent(int, int) + */ + public C yExtent(int extent) { + Preconditions.checkArgument(extent > 0, "extent must be positive!"); + this.yExtent = extent; return this.getSelf(); } + /** + * Sets {@link #xExtent} and {@link #yExtent} to the passed values. + * + *

    The default values are {@value #DEFAULT_X_EXTENT} and {@value #DEFAULT_Y_EXTENT}, respectively. + * + *

    {@link #xExtent} and {@link #yExtent} control the number of grid cells the constrained component occupies. + * + * @throws IllegalArgumentException if the passed {@code x} and {@code y} are not both positive + * + * @see #xExtent(int) + * @see #yExtent(int) + */ + public C extent(int x, int y) { + return this.xExtent(x).yExtent(y); + } + + /** + * Sets {@link #fillX} to the passed value. + * + *

    The default value is {@value #DEFAULT_FILL_X}. + * + * @see #fillX() + * @see #fillOnlyX() + * @see #fillY(boolean) + * @see #fill(boolean, boolean) + */ public C fillX(boolean fill) { this.fillX = fill; return this.getSelf(); } + /** + * Sets {@link #fillX} to {@code true}. + * + *

    The default value is {@value #DEFAULT_FILL_X}. + * + * @see #fillX(boolean) + * @see #fillOnlyX() + * @see #fillY() + * @see #fill(boolean, boolean) + */ public C fillX() { return this.fillX(true); } + /** + * Sets {@link #fillX} to {@code true} and {@link #fillY} to {@code false}. + * + *

    The default values are {@value #DEFAULT_FILL_X} and {@value #DEFAULT_FILL_Y}, respectively. + * + * @see #fillX() + * @see #fillX(boolean) + * @see #fillOnlyY() + * @see #fillBoth() + * @see #fillNone() + * @see #fill(boolean, boolean) + */ public C fillOnlyX() { - return this.fill(true, false); - } - + return this.fillX().fillY(false); + } + + /** + * Sets {@link #fillY} to the passed value. + * + *

    The default value is {@value #DEFAULT_FILL_Y}. + * + * @see #fillY() + * @see #fillOnlyY() + * @see #fillX(boolean) + * @see #fill(boolean, boolean) + */ public C fillY(boolean fill) { this.fillY = fill; return this.getSelf(); } + /** + * Sets {@link #fillY} to {@code true}. + * + *

    The default value is {@value #DEFAULT_FILL_Y}. + * + * @see #fillY(boolean) + * @see #fillOnlyY() + * @see #fillX() + * @see #fill(boolean, boolean) + */ public C fillY() { return this.fillY(true); } + /** + * Sets {@link #fillY} to {@code true} and {@link #fillX} to {@code false}. + * + *

    The default values are {@value #DEFAULT_FILL_Y} and {@value #DEFAULT_FILL_X}, respectively. + * + * @see #fillY() + * @see #fillY(boolean) + * @see #fillOnlyX() + * @see #fillBoth() + * @see #fillNone() + * @see #fill(boolean, boolean) + */ public C fillOnlyY() { - return this.fill(false, true); - } - + return this.fillY().fillX(false); + } + + /** + * Sets {@link #fillX} and {@link #fillY} to the passed values. + * + *

    The default values are {@value #DEFAULT_FILL_X} and {@value #DEFAULT_FILL_Y}, respectively. + * + * @see #fillX() + * @see #fillX(boolean) + * @see #fillOnlyX() + * @see #fillY() + * @see #fillY(boolean) + * @see #fillOnlyY() + * @see #fillBoth() + * @see #fillNone() + */ public C fill(boolean x, boolean y) { - this.fillX(x); - this.fillY(y); - return this.getSelf(); - } - + return this.fillX(x).fillY(y); + } + + /** + * Sets {@link #fillX} and {@link #fillY} to {@code true}. + * + *

    The default values are {@value #DEFAULT_FILL_X} and {@value #DEFAULT_FILL_Y}, respectively. + * + * @see #fillNone() + * @see #fillOnlyX() + * @see #fillOnlyY() + * @see #fill(boolean, boolean) + */ public C fillBoth() { - return this.fill(true, true); - } - + return this.fillX().fillY(); + } + + /** + * Sets {@link #fillX} and {@link #fillY} to {@code false}. + * + *

    The default values are {@value #DEFAULT_FILL_X} and {@value #DEFAULT_FILL_Y}, respectively. + * + * @see #fillBoth() + * @see #fillOnlyX() + * @see #fillOnlyY() + * @see #fill(boolean, boolean) + */ public C fillNone() { return this.fill(false, false); } + /** + * Sets {@link #xAlignment} to the passed {@code alignment}. + * + *

    The default value is {@link #DEFAULT_X_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignLeft() + * @see #alignCenterX() + * @see #alignRight() + * @see #align(Alignment, Alignment) + */ public C alignX(Alignment alignment) { this.xAlignment = requireNonNullAlignment(alignment); return this.getSelf(); } + /** + * Sets {@link #xAlignment} to {@link Alignment#BEGIN BEGIN}. + * + *

    The default value is {@link #DEFAULT_X_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignCenterX() + * @see #alignRight() + * @see #alignX(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignLeft() { return this.alignX(Alignment.BEGIN); } + /** + * Sets {@link #xAlignment} to {@link Alignment#CENTER CENTER}. + * + *

    The default value is {@link #DEFAULT_X_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignLeft() + * @see #alignRight() + * @see #alignX(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignCenterX() { return this.alignX(Alignment.CENTER); } + /** + * Sets {@link #xAlignment} to {@link Alignment#END END}. + * + *

    The default value is {@link #DEFAULT_X_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignLeft() + * @see #alignCenterX() + * @see #alignX(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignRight() { return this.alignX(Alignment.END); } + /** + * Sets {@link #yAlignment} to the passed {@code alignment}. + * + *

    The default value is {@link #DEFAULT_Y_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignTop() + * @see #alignCenterY() + * @see #alignBottom() + * @see #align(Alignment, Alignment) + */ public C alignY(Alignment alignment) { this.yAlignment = requireNonNullAlignment(alignment); return this.getSelf(); } + /** + * Sets {@link #yAlignment} to {@link Alignment#BEGIN BEGIN}. + * + *

    The default value is {@link #DEFAULT_Y_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignCenterY() + * @see #alignBottom() + * @see #alignY(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignTop() { return this.alignY(Alignment.BEGIN); } + /** + * Sets {@link #yAlignment} to {@link Alignment#CENTER CENTER}. + * + *

    The default value is {@link #DEFAULT_Y_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignTop() + * @see #alignBottom() + * @see #alignY(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignCenterY() { return this.alignY(Alignment.CENTER); } + /** + * Sets {@link #yAlignment} to {@link Alignment#END END}. + * + *

    The default value is {@link #DEFAULT_Y_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignTop() + * @see #alignCenterY() + * @see #alignY(Alignment) + * @see #align(Alignment, Alignment) + */ public C alignBottom() { return this.alignY(Alignment.END); } + /** + * Sets the {@link #xAlignment} and {@link #yAlignment} to the passed values. + * + *

    The default values are {@link #DEFAULT_X_ALIGNMENT} and {@link #DEFAULT_Y_ALIGNMENT}. + * + *

    Also see the methods {@linkplain #alignTopLeft() below} for setting each combination of alignments. + * + * @see #alignX(Alignment) + * @see #alignY(Alignment) + */ public C align(Alignment x, Alignment y) { - this.alignX(x); - this.alignY(y); - return this.getSelf(); + return this.alignX(x).alignY(y); } + /** + * Sets {@link #yAlignment} and {@link #xAlignment} to {@link Alignment#BEGIN BEGIN}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #align(Alignment, Alignment) + */ public C alignTopLeft() { - return this.align(Alignment.BEGIN, Alignment.BEGIN); - } - + return this.alignTop().alignLeft(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#BEGIN BEGIN} and {@link #xAlignment} to + * {@link Alignment#CENTER CENTER}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignTop() + * @see #alignCenterX() + * @see #align(Alignment, Alignment) + */ public C alignTopCenter() { - return this.align(Alignment.CENTER, Alignment.BEGIN); - } - + return this.alignTop().alignCenterX(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#BEGIN BEGIN} and {@link #xAlignment} to {@link Alignment#END END}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignTop() + * @see #alignRight() + * @see #align(Alignment, Alignment) + */ public C alignTopRight() { - return this.align(Alignment.END, Alignment.BEGIN); - } - + return this.alignTop().alignRight(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#CENTER CENTER} and {@link #xAlignment} to + * {@link Alignment#BEGIN BEGIN}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignCenterY() + * @see #alignLeft() + * @see #align(Alignment, Alignment) + */ public C alightCenterLeft() { - return this.align(Alignment.BEGIN, Alignment.CENTER); - } - + return this.alignCenterY().alignLeft(); + } + + /** + * Sets {@link #yAlignment} and {@link #xAlignment} to {@link Alignment#CENTER CENTER}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignCenterY() + * @see #alignCenterX() + * @see #align(Alignment, Alignment) + */ public C alignCenter() { - return this.align(Alignment.CENTER, Alignment.CENTER); - } - + return this.alignCenterY().alignCenterX(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#CENTER CENTER} and {@link #xAlignment} to {@link Alignment#END END}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignCenterY() + * @see #alignRight() + * @see #align(Alignment, Alignment) + */ public C alignCenterRight() { - return this.align(Alignment.END, Alignment.CENTER); - } - + return this.alignCenterY().alignRight(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#END END} and {@link #xAlignment} to {@link Alignment#BEGIN BEGIN}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignBottom() + * @see #alignLeft() + * @see #align(Alignment, Alignment) + */ public C alignBottomLeft() { - return this.align(Alignment.BEGIN, Alignment.END); - } - + return this.alignBottom().alignLeft(); + } + + /** + * Sets {@link #yAlignment} to {@link Alignment#END END} and {@link #xAlignment} to {@link Alignment#CENTER CENTER}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignBottom() + * @see #alignCenterX() + * @see #align(Alignment, Alignment) + */ public C alignBottomCenter() { - return this.align(Alignment.CENTER, Alignment.END); - } - + return this.alignBottom().alignCenterX(); + } + + /** + * Sets {@link #yAlignment} and {@link #xAlignment} to {@link Alignment#END END}. + * + *

    The default values are {@link #DEFAULT_Y_ALIGNMENT} and {@link #DEFAULT_X_ALIGNMENT}. + * + * @see #alignBottom() + * @see #alignRight() + * @see #align(Alignment, Alignment) + */ public C alignBottomRight() { - return this.align(Alignment.END, Alignment.END); - } - + return this.alignBottom().alignRight(); + } + + /** + * Sets {@link #priority}. + * + *

    The default value is {@value #DEFAULT_PRIORITY}. + * + * @see #defaultPriority() + * @see #addPriority(int) + * @see #incrementPriority() + * @see #decrementPriority() + */ public C priority(int priority) { this.priority = priority; return this.getSelf(); } + /** + * Sets the {@link #priority} to its default value: {@value #DEFAULT_PRIORITY}. + * + * @see #priority(int) + */ public C defaultPriority() { return this.priority(DEFAULT_PRIORITY); } + /** + * Adds the passed {@code amount} to {@link #priority}. + * + *

    The default value is {@value #DEFAULT_PRIORITY}. + * + * @see #incrementPriority() + * @see #decrementPriority() + * @see #priority(int) + */ public C addPriority(int amount) { return this.priority(this.priority + amount); } + /** + * Adds {@code 1} to {@link #priority}. + * + *

    The default value is {@value #DEFAULT_PRIORITY}. + * + * @see #decrementPriority() + * @see #addPriority(int) + * @see #priority(int) + */ public C incrementPriority() { return this.addPriority(1); } + /** + * Subtracts {@code 1} from {@link #priority}. + * + *

    The default value is {@value #DEFAULT_PRIORITY}. + * + * @see #incrementPriority() + * @see #addPriority(int) + * @see #priority(int) + */ public C decrementPriority() { return this.addPriority(-1); } @@ -261,10 +662,22 @@ public enum Alignment { BEGIN, CENTER, END } + /** + * {@link FlexGridConstraints} with relative coordinates.
    + * Components will be placed at the end of the bottom-most row at the time of + * {@linkplain Container#add(Component, Object) adding}. + * + *

    Relative components never overlap components added before them, but {@link Absolute Absolute} + * components added after them may overlap. + * + * @see Absolute#toRelative() Absolute.toRelative() + * @see #toAbsolute() + * @see #toAbsolute(int, int) + */ public static final class Relative extends FlexGridConstraints { public static Relative of() { return new Relative( - DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_X_EXTENT, DEFAULT_Y_EXTENT, DEFAULT_FILL_X, DEFAULT_FILL_Y, DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, DEFAULT_PRIORITY @@ -272,34 +685,48 @@ public static Relative of() { } private Relative( - int width, int height, + int xExtent, int yExtent, boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, int priority ) { - super(width, height, fillX, fillY, xAlignment, yAlignment, priority); + super(xExtent, yExtent, fillX, fillY, xAlignment, yAlignment, priority); } @Override public Relative copy() { return new Relative( - this.width, this.height, + this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, this.priority ); } + /** + * Creates {@link Absolute Absolute} constraints at ({@value Absolute#DEFAULT_X}, {@value Absolute#DEFAULT_Y}) + * with these constraints' values. + * + * @see #toAbsolute(int, int) + * @see Absolute#toRelative() Absolute.toRelative() + */ public Absolute toAbsolute() { return new Absolute( Absolute.DEFAULT_X, Absolute.DEFAULT_Y, - this.width, this.height, + this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, this.priority ); } + /** + * Creates {@link Absolute Absolute} constraints with these constraints' values and the passed + * {@code x} and {@code y} coordinates. + * + * @see #toAbsolute() + * @see Absolute#toRelative() Absolute.toRelative() + */ public Absolute toAbsolute(int x, int y) { return this.toAbsolute().pos(x, y); } @@ -310,6 +737,12 @@ Relative getSelf() { } } + /** + * {@link FlexGridConstraints} with absolute {@link #x} and {@link #y} coordinates. + * + *

    Absolute components will overlap with any components occupying the same grid cells, whether they're at the + * same coordinates or they {@linkplain #extent(int, int) extend} into each other's cells. + */ public static final class Absolute extends FlexGridConstraints { public static final int DEFAULT_X = 0; public static final int DEFAULT_Y = 0; @@ -317,24 +750,31 @@ public static final class Absolute extends FlexGridConstraints The default value is {@value #DEFAULT_X}. + * + * @see #advanceRows(int) + * @see #nextRow() + * @see #y(int) + * @see #pos(int, int) + */ public Absolute x(int x) { this.x = x; return this; } - public Absolute advanceRows(int count) { - this.x = 0; - this.y += count; + /** + * Adds the passed {@code count} to {@link #x}. + * + *

    The default value is {@value #DEFAULT_X}. + * + * @see #nextColumn() + * @see #y(int) + * @see #advanceRows(int) + */ + public Absolute advanceColumns(int count) { + this.x += count; return this.getSelf(); } - public Absolute nextRow() { - return this.advanceRows(1); + /** + * Adds {@code 1} to {@link #x}. + * + *

    The default value is {@value #DEFAULT_X}. + * + * @see #advanceColumns(int) + * @see #y(int) + * @see #nextRow() + */ + public Absolute nextColumn() { + return this.advanceColumns(1); } + /** + * Sets {@link #y} to the passed value. + * + *

    The default value is {@value #DEFAULT_Y}. + * + * @see #advanceColumns(int) + * @see #nextColumn() + * @see #x(int) + * @see #pos(int, int) + */ public Absolute y(int y) { this.y = y; return this; } - public Absolute advanceColumns(int count) { - this.x += count; + /** + * Sets {@link #x} to {@code 0} and adds the passed {@code count} to {@link #y}. + * + *

    The default values are {@value #DEFAULT_X} and {@value #DEFAULT_Y}, respectively. + * + * @see #nextRow() + * @see #x(int) + * @see #advanceColumns(int) + */ + public Absolute advanceRows(int count) { + this.x = 0; + this.y += count; return this.getSelf(); } - public Absolute nextColumn() { - return this.advanceColumns(1); + /** + * Sets {@link #x} to {@code 0} and adds {@code 1} to {@link #y}. + * + *

    The default values are {@value #DEFAULT_X} and {@value #DEFAULT_Y}, respectively. + * + * @see #advanceRows(int) + * @see #x(int) + * @see #nextColumn() + */ + public Absolute nextRow() { + return this.advanceRows(1); } + /** + * Sets {@link #x} and {@link #y} to the passed values. + * + *

    The default values are {@value #DEFAULT_X} and {@value #DEFAULT_Y}, respectively. + * + * @see #x(int) + * @see #y(int) + */ public Absolute pos(int x, int y) { - this.x(x); - this.y(y); - return this; + return this.x(x).y(y); } @Override public Absolute copy() { return new Absolute( this.x, this.y, - this.width, this.height, + this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, this.priority ); } + /** + * Creates {@link Relative Relative} constraints with these constraints' values + * ({@link #x} and {@link #y} ar ignored). + * + * @see Relative#toAbsolute() Relative.toAbsolute() + * @see Relative#toAbsolute(int, int) Relative.toAbsolute(int, int) + */ public Relative toRelative() { return new Relative( - this.width, this.height, + this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, this.priority From 5f4ed4836c87276ba12501fd99c4c4d1475be398 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 16:56:14 -0800 Subject: [PATCH 075/110] fix allocating space for components with excess extent --- .../internal/gui/visualization/Main.java | 4 +- ...a => FlexGridExtentOverlapVisualiser.java} | 4 +- .../util/layout/flex_grid/FlexGridLayout.java | 90 +++++++++++++------ 3 files changed, 65 insertions(+), 33 deletions(-) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/{FlexGridOverlapVisualiser.java => FlexGridExtentOverlapVisualiser.java} (90%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 28632d1bc..0dcc2b9ad 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -5,7 +5,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridColumnVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; -import org.quilt.internal.gui.visualization.flex_grid.FlexGridOverlapVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridExtentOverlapVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; @@ -37,7 +37,7 @@ private Main() { registerVisualizer(new FlexGridDefaultRowVisualiser()); registerVisualizer(new FlexGridColumnVisualiser()); registerVisualizer(new FlexGridQuiltVisualiser()); - registerVisualizer(new FlexGridOverlapVisualiser()); + registerVisualizer(new FlexGridExtentOverlapVisualiser()); registerVisualizer(new FlexGridFillVisualizer()); registerVisualizer(new FlexGridAlignmentVisualizer()); registerVisualizer(new FlexGridAlignAndFillVisualizer()); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java similarity index 90% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java index e200e5b50..36e1de661 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java @@ -8,10 +8,10 @@ import javax.swing.JFrame; import java.awt.Color; -public class FlexGridOverlapVisualiser implements Visualizer { +public class FlexGridExtentOverlapVisualiser implements Visualizer { @Override public String getTitle() { - return "Flex Grid Overlap"; + return "Flex Grid Extent Overlap"; } /** diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index fe3188f39..c98a5bed6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -5,7 +5,6 @@ import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment; -import org.quiltmc.enigma.util.Utils; import java.awt.Component; import java.awt.Container; @@ -21,6 +20,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import static org.quiltmc.enigma.util.Utils.ceilDiv; + /** * A layout manager that lays out components in a grid and allocates space according to priority. * @@ -254,7 +255,8 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final Dimension targetSize = targets.componentSizes.get(constrained.component); assert targetSize != null; - return Math.min(ops.getSpan(targetSize), cellSpans.get(coord)); + final int extendedCellSpan = ops.calculateExtendedCellSpan(constrained, coord, cellSpans); + return Math.min(ops.getSpan(targetSize), extendedCellSpan); }); } } else { @@ -269,7 +271,9 @@ private void layoutAxis(Container parent, CartesianOperations ops) { this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { final Dimension preferredSize = preferred.componentSizes.get(constrained.component); assert preferredSize != null; - return Math.min(ops.getSpan(preferredSize), cellSpans.get(coord)); + + final int extendedCellSpan = ops.calculateExtendedCellSpan(constrained, coord, cellSpans); + return Math.min(ops.getSpan(preferredSize), extendedCellSpan); }); } } @@ -300,32 +304,39 @@ private Map allocateCellSpace(CartesianOperations ops, int rem }); }); - while (!prioritized.isEmpty()) { + while (!prioritized.isEmpty() && remainingSpace > 0) { final Constrained.At at = prioritized.remove(); - final int currentSpan = cellSpans.get(at.coord); - final Dimension targetSize = large.componentSizes.get(at.constrained().component); assert targetSize != null; - final int targetSpan = ops.getSpan(targetSize); - final int targetDiff = targetSpan - currentSpan; - if (targetDiff > 0) { - if (targetDiff <= remainingSpace) { - cellSpans.put(at.coord, targetSpan); - if (remainingSpace == targetDiff) { - break; - } else { + final int extent = ops.getExtent(at.constrained()); + + final int targetSpan = ceilDiv(ops.getSpan(targetSize), extent); + + for (int i = 0; i < extent; i++) { + final int extendedCoord = at.coord + i; + final int currentSpan = cellSpans.get(extendedCoord); + + final int targetDiff = targetSpan - currentSpan; + if (targetDiff > 0) { + if (targetDiff <= remainingSpace) { + cellSpans.put(extendedCoord, targetSpan); + remainingSpace -= targetDiff; + if (remainingSpace == 0) { + break; + } + } else { + final int lastOfSpan = remainingSpace; + remainingSpace = 0; + cellSpans.compute(extendedCoord, (ignored, span) -> { + assert span != null; + return span + lastOfSpan; + }); + + break; } - } else { - final int lastOfSpan = remainingSpace; - cellSpans.compute(at.coord, (ignored, span) -> { - assert span != null; - return span + lastOfSpan; - }); - - break; } } } @@ -352,21 +363,20 @@ private void layoutAxisImpl( final int oppositeCoord = ops.opposite().chooseCoord(x, y); final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); - final Integer cellSpan = cellSpans.get(coord); values.forEach(constrained -> { final int span = getComponentSpan.apply(constrained, coord); - final int constrainedPos = span == cellSpan ? pos : switch (ops.getAlignment(constrained)) { + final int constrainedPos = switch (ops.getAlignment(constrained)) { case BEGIN -> pos; - case CENTER -> pos + (cellSpan - span) / 2; - case END -> pos + cellSpan - span; + case CENTER -> pos + (ops.calculateExtendedCellSpan(constrained, coord, cellSpans) - span) / 2; + case END -> pos + ops.calculateExtendedCellSpan(constrained, coord, cellSpans) - span; }; ops.setBounds(constrained.component, constrainedPos, span); }); - positions.put(oppositeCoord, pos + cellSpan); + positions.put(oppositeCoord, pos + cellSpans.get(coord)); }); } @@ -453,8 +463,8 @@ static Sizes calculate(ConstrainedGrid grid, Function getS values.forEach(constrained -> { final Dimension size = componentSizes.computeIfAbsent(constrained.component, getSize); - final int componentCellWidth = Utils.ceilDiv(size.width, constrained.xExtent); - final int componentCellHeight = Utils.ceilDiv(size.height, constrained.yExtent); + final int componentCellWidth = ceilDiv(size.width, constrained.xExtent); + final int componentCellHeight = ceilDiv(size.height, constrained.yExtent); for (int xOffset = 0; xOffset < constrained.xExtent; xOffset++) { for (int yOffset = 0; yOffset < constrained.xExtent; yOffset++) { final Dimension cellSize = cellSizes @@ -548,6 +558,11 @@ Alignment getAlignment(Constrained constrained) { return constrained.xAlignment; } + @Override + int getExtent(Constrained constrained) { + return constrained.xExtent; + } + @Override void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); @@ -609,6 +624,11 @@ Alignment getAlignment(Constrained constrained) { return constrained.yAlignment; } + @Override + int getExtent(Constrained constrained) { + return constrained.yExtent; + } + @Override void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); @@ -633,9 +653,21 @@ CartesianOperations opposite() { abstract boolean fills(Constrained constrained); abstract boolean noneFill(ConstrainedGrid grid); abstract Alignment getAlignment(Constrained constrained); + abstract int getExtent(Constrained constrained); abstract void setBounds(Component component, int pos, int span); abstract CartesianOperations opposite(); + + // TODO cache? + int calculateExtendedCellSpan(Constrained constrained, int coord, Map cellSpans) { + int extendedCellSpan = 0; + final int extent = this.getExtent(constrained); + for (int i = 0; i < extent; i++) { + extendedCellSpan += cellSpans.get(coord + i); + } + + return extendedCellSpan; + } } } From 573691057e65c9d17efd4fb093b0e3e4bfaa2b9d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 18:00:56 -0800 Subject: [PATCH 076/110] reuse extendedCellSpan calculation --- .../util/layout/flex_grid/FlexGridLayout.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index c98a5bed6..cde82cb88 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -255,8 +255,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final Dimension targetSize = targets.componentSizes.get(constrained.component); assert targetSize != null; - final int extendedCellSpan = ops.calculateExtendedCellSpan(constrained, coord, cellSpans); - return Math.min(ops.getSpan(targetSize), extendedCellSpan); + return ops.getSpan(targetSize); }); } } else { @@ -272,8 +271,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final Dimension preferredSize = preferred.componentSizes.get(constrained.component); assert preferredSize != null; - final int extendedCellSpan = ops.calculateExtendedCellSpan(constrained, coord, cellSpans); - return Math.min(ops.getSpan(preferredSize), extendedCellSpan); + return ops.getSpan(preferredSize); }); } } @@ -328,11 +326,11 @@ private Map allocateCellSpace(CartesianOperations ops, int rem break; } } else { - final int lastOfSpan = remainingSpace; + final int lastOfSpace = remainingSpace; remainingSpace = 0; cellSpans.compute(extendedCoord, (ignored, span) -> { assert span != null; - return span + lastOfSpan; + return span + lastOfSpace; }); break; @@ -365,12 +363,19 @@ private void layoutAxisImpl( final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); values.forEach(constrained -> { - final int span = getComponentSpan.apply(constrained, coord); + final int extent = ops.getExtent(constrained); + + int extendedCellSpan = 0; + for (int i = 0; i < extent; i++) { + extendedCellSpan += cellSpans.get(coord + i); + } + + final int span = Math.min(getComponentSpan.apply(constrained, coord), extendedCellSpan); final int constrainedPos = switch (ops.getAlignment(constrained)) { case BEGIN -> pos; - case CENTER -> pos + (ops.calculateExtendedCellSpan(constrained, coord, cellSpans) - span) / 2; - case END -> pos + ops.calculateExtendedCellSpan(constrained, coord, cellSpans) - span; + case CENTER -> pos + (extendedCellSpan - span) / 2; + case END -> pos + extendedCellSpan - span; }; ops.setBounds(constrained.component, constrainedPos, span); @@ -658,16 +663,5 @@ CartesianOperations opposite() { abstract void setBounds(Component component, int pos, int span); abstract CartesianOperations opposite(); - - // TODO cache? - int calculateExtendedCellSpan(Constrained constrained, int coord, Map cellSpans) { - int extendedCellSpan = 0; - final int extent = this.getExtent(constrained); - for (int i = 0; i < extent; i++) { - extendedCellSpan += cellSpans.get(coord + i); - } - - return extendedCellSpan; - } } } From d73e4ddc5d890d3943082076ad69b5d563775901 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 18:12:39 -0800 Subject: [PATCH 077/110] center components in container when filling --- .../enigma/gui/util/layout/flex_grid/FlexGridLayout.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index cde82cb88..9dc856dc7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -249,8 +249,11 @@ private void layoutAxis(Container parent, CartesianOperations ops) { } else { final Map cellSpans = this.allocateCellSpace(ops, extraSpace, true); + final int allocatedSpace = cellSpans.values().stream().mapToInt(Integer::intValue).sum(); + final int startPos = leadingInset + (availableSpace - allocatedSpace) / 2; + final Sizes max = this.getMaxSizes(); - this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { + this.layoutAxisImpl(startPos, ops, cellSpans, (constrained, coord) -> { final Sizes targets = ops.fills(constrained) ? max : preferred; final Dimension targetSize = targets.componentSizes.get(constrained.component); assert targetSize != null; From fb844fa2f62cac67339369825cd81272eee3ecc0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 18:27:50 -0800 Subject: [PATCH 078/110] tweak javadoc --- .../enigma/gui/util/layout/flex_grid/FlexGridLayout.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 9dc856dc7..fd4c8dfd1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -56,18 +56,18 @@ * child components' total required space (as before, per-axis): * * - * + * * * * - * + * * * * - * + * *
    less than minimum spacespace ≤ mineach component gets its minimum size (excess is clipped)
    between minimum and preferred spacemin < space < preferred * each component gets at least its minimum size; components get additional space * - up to their preferred size - according to priority *
    more than preferred spacespace ≥ preferred * each component gets at least is preferred size; components that * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} the axis get additional space From ef1c1f81cd833724abb0e00b0b3abd8b3599008f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 18:40:08 -0800 Subject: [PATCH 079/110] improve javadoc wording --- .../util/layout/flex_grid/FlexGridLayout.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index fd4c8dfd1..befc3a913 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -35,25 +35,25 @@ * *

    Constraints

    * - * Flex grids are configured by passing {@link FlexGridConstraints} when + * Configure flex grids by passing {@link FlexGridConstraints} when * {@linkplain Container#add(Component, Object) adding} {@link Component}s.
    - * If no constraints are specified, - * a default set of {@linkplain FlexGridConstraints#createRelative() relative} constraints are used.
    - * If non-{@linkplain FlexGridConstraints flex grid} constraints are passed, - * an {@link IllegalArgumentException} is thrown. + * When no constraints are specified, a default set of {@linkplain FlexGridConstraints#createRelative() relative} + * constraints are used.
    + * Passing non-{@linkplain FlexGridConstraints flex grid} constraints will result in an + * {@link IllegalArgumentException}. * *

    Space allocation

    * *

    Space is allocated to components per-axis, with high-{@linkplain FlexGridConstraints#priority(int) priority} * components getting space first. In ties, components with the least position on the axis get priority.
    - * Components never get less space in an axis than their {@linkplain Component#getMinimumSize() minimum sizes} - * allow, and they never get more space than their {@linkplain Component#getMaximumSize() maximum sizes} allow. - * Components only ever get more space in an axis than their - * {@linkplain Component#getPreferredSize() preferred sizes} request if they're set to + * A component never gets less space in an axis than its {@linkplain Component#getMinimumSize() minimum size} + * allows, and it never gets more space than its {@linkplain Component#getMaximumSize() maximum size} allows. + * A components only ever gets more space in an axis than its + * {@linkplain Component#getPreferredSize() preferred size} requests if it's set to * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} that axis. * *

    Space allocation behavior depends on the parent container's available space and - * child components' total required space (as before, per-axis): + * child components' total sizes (as before, per-axis): * * * @@ -81,7 +81,7 @@ *
      *
    • components with {@linkplain FlexGridConstraints#createRelative() relative constraints} (or no constraints) * are added to the end of the bottom row,
      - * or ({@value FlexGridConstraints.Absolute#DEFAULT_X}, {@value FlexGridConstraints.Absolute#DEFAULT_Y}) + * or put at ({@value FlexGridConstraints.Absolute#DEFAULT_X}, {@value FlexGridConstraints.Absolute#DEFAULT_Y}) * if no other components have been added *
    • a component can occupy multiple grid cells when its constraint * {@linkplain FlexGridConstraints#xExtent(int) xExtent} or From 97be4456a6fac5c34d047da7a539c6460ae4ba03 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 19:39:14 -0800 Subject: [PATCH 080/110] improve input verification --- .../gui/visualization/util/VisualBox.java | 33 +++++++---- .../constraints/FlexGridConstraints.java | 22 ++++---- .../org/quiltmc/enigma/util/Arguments.java | 56 +++++++++++++++++++ .../java/org/quiltmc/enigma/util/Utils.java | 5 ++ 4 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/Arguments.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index 6c66d1f93..82defd444 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -12,6 +12,9 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import static org.quiltmc.enigma.util.Arguments.requireNonNegative; +import static org.quiltmc.enigma.util.Arguments.requireNotLess; + /** * A simple box with customizable sizes, color, and name. * @@ -27,6 +30,13 @@ public class VisualBox extends JPanel { public static final Color PATCH_CYAN = new Color(39, 162, 253); public static final Color PATCH_BLUE = new Color(51, 68, 255); + private static final String MIN_WIDTH = "minWidth"; + private static final String MIN_HEIGHT = "minHeight"; + private static final String PREFERRED_WIDTH = "preferredWidth"; + private static final String PREFERRED_HEIGHT = "preferredHeight"; + private static final String MAX_WIDTH = "maxWidth"; + private static final String MAX_HEIGHT = "maxHeight"; + public static VisualBox of() { return of(null); } @@ -60,7 +70,7 @@ public static VisualBox of(@Nullable Color color, int width, int height) { } public static VisualBox of(@Nullable String name, @Nullable Color color, int width, int height) { - return new VisualBox(name, color, width, height, width / 2, height / 2, width * 2, height * 2); + return new VisualBox(name, color, width / 2, height / 2, width, height, width * 2, height * 2); } public static VisualBox ofFixed() { @@ -131,29 +141,29 @@ public static VisualBox bluePatchOf(@Nullable String name) { return of(name, PATCH_BLUE); } - private final int preferredWidth; - private final int preferredHeight; - private final int minWidth; private final int minHeight; + private final int preferredWidth; + private final int preferredHeight; + private final int maxWidth; private final int maxHeight; protected VisualBox( @Nullable String name, @Nullable Color color, - int preferredWidth, int preferredHeight, int minWidth, int minHeight, + int preferredWidth, int preferredHeight, int maxWidth, int maxHeight ) { - this.preferredWidth = preferredWidth; - this.preferredHeight = preferredHeight; + this.minWidth = requireNonNegative(minWidth, MIN_WIDTH); + this.minHeight = requireNonNegative(minHeight, MIN_HEIGHT); - this.minWidth = minWidth; - this.minHeight = minHeight; + this.preferredWidth = requireNotLess(preferredWidth, PREFERRED_WIDTH, minWidth, MIN_WIDTH); + this.preferredHeight = requireNotLess(preferredHeight, PREFERRED_HEIGHT, minHeight, MIN_HEIGHT); - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; + this.maxWidth = requireNotLess(maxWidth, MAX_WIDTH, preferredWidth, PREFERRED_WIDTH); + this.maxHeight = requireNotLess(maxHeight, MAX_HEIGHT, preferredHeight, PREFERRED_HEIGHT); this.setBackground(new Color(0, true)); @@ -164,7 +174,6 @@ protected VisualBox( final Color foreground = this.getForeground(); this.setLayout(new GridBagLayout()); - // this.setLayout(new BorderLayout()); final var center = new JPanel(new VerticalFlowLayout(5)); center.setBackground(this.getBackground()); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 76a203d2c..5e4a793d1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -1,13 +1,14 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid.constraints; -import com.google.common.base.Preconditions; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import java.awt.Component; import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.util.Objects; + +import static org.quiltmc.enigma.util.Arguments.requirePositive; +import static org.quiltmc.enigma.util.Utils.requireNonNull; /** * Constraints for components added to a {@link Container} with a {@link FlexGridLayout} using @@ -50,6 +51,9 @@ public abstract sealed class FlexGridConstraints * Always positive. @@ -155,8 +155,7 @@ public int getPriority() { * @see #extent(int, int) */ public C xExtent(int extent) { - Preconditions.checkArgument(extent > 0, "extent must be positive!"); - this.xExtent = extent; + this.xExtent = requirePositive(extent, EXTENT); return this.getSelf(); } @@ -173,8 +172,7 @@ public C xExtent(int extent) { * @see #extent(int, int) */ public C yExtent(int extent) { - Preconditions.checkArgument(extent > 0, "extent must be positive!"); - this.yExtent = extent; + this.yExtent = requirePositive(extent, EXTENT); return this.getSelf(); } @@ -343,7 +341,7 @@ public C fillNone() { * @see #align(Alignment, Alignment) */ public C alignX(Alignment alignment) { - this.xAlignment = requireNonNullAlignment(alignment); + this.xAlignment = requireNonNull(alignment, ALIGNMENT); return this.getSelf(); } @@ -408,7 +406,7 @@ public C alignRight() { * @see #align(Alignment, Alignment) */ public C alignY(Alignment alignment) { - this.yAlignment = requireNonNullAlignment(alignment); + this.yAlignment = requireNonNull(alignment, ALIGNMENT); return this.getSelf(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Arguments.java b/enigma/src/main/java/org/quiltmc/enigma/util/Arguments.java new file mode 100644 index 000000000..e05a96bf0 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Arguments.java @@ -0,0 +1,56 @@ +package org.quiltmc.enigma.util; + +@SuppressWarnings("unused") +public final class Arguments { + private Arguments() { + throw new UnsupportedOperationException(); + } + + public static int requireNonNegative(int argument, String name) { + if (argument < 0) { + throw new IllegalArgumentException("%s (%s) must not be negative!".formatted(name, argument)); + } else { + return argument; + } + } + + public static int requireNonPositive(int argument, String name) { + if (argument > 0) { + throw new IllegalArgumentException("%s (%s) must not be positive!".formatted(name, argument)); + } else { + return argument; + } + } + + public static int requirePositive(int argument, String name) { + if (argument <= 0) { + throw new IllegalArgumentException("%s (%s) must be positive!".formatted(name, argument)); + } else { + return argument; + } + } + + public static int requireNegative(int argument, String name) { + if (argument >= 0) { + throw new IllegalArgumentException("%s (%s) must be negative!".formatted(name, argument)); + } else { + return argument; + } + } + + /** + * @return the passed {@code greater} if it is not less than the passed {@code lesser} + * + * @throws IllegalArgumentException if the passed {@code greater} is less than the passed {@code lesser} + */ + public static int requireNotLess(int greater, String greaterName, int lesser, String lesserName) { + if (greater < lesser) { + throw new IllegalArgumentException( + "%s (%s) must not be less than %s (%s)!" + .formatted(greaterName, greater, lesserName, lesser) + ); + } else { + return greater; + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index 2bc8181d2..68a0abaf3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -16,6 +16,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Properties; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -204,4 +205,8 @@ public static int ceilDiv(int dividend, int divisor) { public static long ceilDiv(long dividend, long divisor) { return -Math.floorDiv(-dividend, divisor); } + + public static T requireNonNull(T object, String name) { + return Objects.requireNonNull(object, () -> name + " must not be null!"); + } } From fe903d7a56b495e7011e1cf8723c18ba692fb6d2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:05:15 -0800 Subject: [PATCH 081/110] improve VisualBox labeling --- .../flex_grid/FlexGridQuiltVisualiser.java | 6 +++--- .../gui/visualization/util/VisualBox.java | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java index 4bbb8f27d..55808f419 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java @@ -34,9 +34,9 @@ public static void visualizeQuilt( ) { visualizeQuilt( window, - "[0, 0]", constrainer1, "[1, 0]", constrainer2, "[2, 0]", constrainer3, - "[0, 1]", constrainer4, "[1, 1]", constrainer5, "[2, 1]", constrainer6, - "[0, 2]", constrainer7, "[1, 2]", constrainer8, "[2, 2]", constrainer9 + "(0, 0)", constrainer1, "(1, 0)", constrainer2, "(2, 0)", constrainer3, + "(0, 1)", constrainer4, "(1, 1)", constrainer5, "(2, 1)", constrainer6, + "(0, 2)", constrainer7, "(1, 2)", constrainer8, "(2, 2)", constrainer9 ); } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index 82defd444..d0e063f7f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -11,6 +11,8 @@ import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import static org.quiltmc.enigma.util.Arguments.requireNonNegative; import static org.quiltmc.enigma.util.Arguments.requireNotLess; @@ -150,6 +152,8 @@ public static VisualBox bluePatchOf(@Nullable String name) { private final int maxWidth; private final int maxHeight; + private final JLabel sizeLabel = new JLabel(); + protected VisualBox( @Nullable String name, @Nullable Color color, int minWidth, int minHeight, @@ -184,7 +188,17 @@ protected VisualBox( center.add(nameLabel, BorderLayout.WEST); } - final JLabel dimensions = new JLabel("%s x %s".formatted(this.preferredWidth, this.preferredHeight)); + this.sizeLabel.setForeground(foreground); + center.add(this.sizeLabel); + this.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + final Dimension size = VisualBox.this.getSize(); + VisualBox.this.sizeLabel.setText("%s x %s".formatted(size.width, size.height)); + } + }); + + final JLabel dimensions = new JLabel("[%s x %s]".formatted(this.preferredWidth, this.preferredHeight)); dimensions.setForeground(foreground); center.add(dimensions, BorderLayout.EAST); From db71080876373665dd87358b36777ca74a0a98f8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:13:06 -0800 Subject: [PATCH 082/110] add FlexGridPriorityFillVisualizer --- .../internal/gui/visualization/Main.java | 2 ++ .../FlexGridPriorityFillVisualizer.java | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 0dcc2b9ad..70155c474 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -6,6 +6,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridExtentOverlapVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; @@ -42,6 +43,7 @@ private Main() { registerVisualizer(new FlexGridAlignmentVisualizer()); registerVisualizer(new FlexGridAlignAndFillVisualizer()); registerVisualizer(new FlexGridPriorityVisualizer()); + registerVisualizer(new FlexGridPriorityFillVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java new file mode 100644 index 000000000..54db4311d --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java @@ -0,0 +1,32 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; + +import javax.swing.JFrame; + +public class FlexGridPriorityFillVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Priority Fill"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + c -> c.fillOnlyX().incrementPriority(), + c -> c.fillOnlyY().incrementPriority(), + c -> c.fillOnlyX().incrementPriority(), + + c -> c.fillOnlyY().incrementPriority(), + c -> c.fillOnlyX().incrementPriority(), + c -> c.fillOnlyY().incrementPriority(), + + c -> c.fillOnlyX().incrementPriority(), + c -> c.fillOnlyY().incrementPriority(), + c -> c.fillOnlyX().incrementPriority() + ); + + window.pack(); + } +} From 95beb9445ebaeb44d6aa7718dc063966542b0eb6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:23:07 -0800 Subject: [PATCH 083/110] add FlexGridSparseVisualizer --- .../internal/gui/visualization/Main.java | 2 ++ .../flex_grid/FlexGridSparseVisualizer.java | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 70155c474..0c9d9604e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -9,6 +9,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; import javax.swing.JButton; import javax.swing.JFrame; @@ -44,6 +45,7 @@ private Main() { registerVisualizer(new FlexGridAlignAndFillVisualizer()); registerVisualizer(new FlexGridPriorityVisualizer()); registerVisualizer(new FlexGridPriorityFillVisualizer()); + registerVisualizer(new FlexGridSparseVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java new file mode 100644 index 000000000..a8aa13a76 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java @@ -0,0 +1,31 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; + +public class FlexGridSparseVisualizer implements Visualizer { + private static final int STEP = 1000; + + private static FlexGridConstraints.Absolute stepColumns(FlexGridConstraints.Absolute constraints) { + return constraints.advanceColumns(STEP); + } + + @Override + public String getTitle() { + return "Flex Grid Sparse"; + } + + @Override + public void visualize(JFrame window) { + FlexGridQuiltVisualiser.visualizeQuilt( + window, + c -> c.pos(-STEP, -STEP), FlexGridSparseVisualizer::stepColumns, FlexGridSparseVisualizer::stepColumns, + c -> c.pos(-STEP, 0), FlexGridSparseVisualizer::stepColumns, FlexGridSparseVisualizer::stepColumns, + c -> c.pos(-STEP, STEP), FlexGridSparseVisualizer::stepColumns, FlexGridSparseVisualizer::stepColumns + ); + + window.pack(); + } +} From 9685f8efced63c838291627c17d7133aff2e896b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:30:42 -0800 Subject: [PATCH 084/110] add FlexGridVVisualizer --- .../internal/gui/visualization/Main.java | 2 ++ .../flex_grid/FlexGridVVisualizer.java | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 0c9d9604e..5bac73796 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -10,6 +10,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridVVisualizer; import javax.swing.JButton; import javax.swing.JFrame; @@ -46,6 +47,7 @@ private Main() { registerVisualizer(new FlexGridPriorityVisualizer()); registerVisualizer(new FlexGridPriorityFillVisualizer()); registerVisualizer(new FlexGridSparseVisualizer()); + registerVisualizer(new FlexGridVVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java new file mode 100644 index 000000000..29b21f638 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java @@ -0,0 +1,30 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; + +public class FlexGridVVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid V"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + + window.add(VisualBox.of(), constraints); + window.add(VisualBox.of(), constraints.pos(1, 1)); + window.add(VisualBox.of(), constraints.pos(2, 2)); + window.add(VisualBox.of(), constraints.pos(3, 1)); + window.add(VisualBox.of(), constraints.pos(4, 0)); + + window.pack(); + } +} From 080e5ea035e8385b83d958694f69ffc3aea9ff79 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:54:54 -0800 Subject: [PATCH 085/110] fix empty cells in non-empty rows/columns collapsing --- .../util/layout/flex_grid/FlexGridLayout.java | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index befc3a913..2743c2774 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; @@ -17,6 +18,8 @@ import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.BiFunction; import java.util.function.Function; @@ -247,7 +250,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { if (extraSpace == 0 || ops.noneFill(this.grid)) { this.layoutFixedAxis(preferred, leadingInset + extraSpace / 2, ops); } else { - final Map cellSpans = this.allocateCellSpace(ops, extraSpace, true); + final SortedMap cellSpans = this.allocateCellSpace(ops, extraSpace, true); final int allocatedSpace = cellSpans.values().stream().mapToInt(Integer::intValue).sum(); final int startPos = leadingInset + (availableSpace - allocatedSpace) / 2; @@ -268,7 +271,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { if (extraMinSpace <= 0) { this.layoutFixedAxis(min, leadingInset, ops); } else { - final Map cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); + final SortedMap cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { final Dimension preferredSize = preferred.componentSizes.get(constrained.component); @@ -280,10 +283,10 @@ private void layoutAxis(Container parent, CartesianOperations ops) { } } - private Map allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { + private SortedMap allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { final Sizes large; final Sizes small; - final Map cellSpans; + final SortedMap cellSpans; if (fill) { large = this.getMaxSizes(); small = this.getPreferredSizes(); @@ -292,7 +295,7 @@ private Map allocateCellSpace(CartesianOperations ops, int rem small = this.getMinSizes(); } - cellSpans = new HashMap<>(ops.getCellSpans(small)); + cellSpans = new TreeMap<>(ops.getCellSpans(small)); final PriorityQueue prioritized = new PriorityQueue<>(this.grid.getSize()); this.grid.forEach((x, y, values) -> { @@ -354,16 +357,24 @@ private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) private void layoutAxisImpl( int startPos, CartesianOperations ops, - Map cellSpans, + SortedMap cellSpans, BiFunction getComponentSpan ) { - final Map positions = new HashMap<>(); + final Map beginPositions = new HashMap<>(); + int currentPos = startPos; + for (final Map.Entry entry : cellSpans.entrySet()) { + final int coord = entry.getKey(); + final int span = entry.getValue(); + + beginPositions.put(coord, currentPos); + + currentPos += span; + } this.grid.forEach((x, y, values) -> { final int coord = ops.chooseCoord(x, y); - final int oppositeCoord = ops.opposite().chooseCoord(x, y); - final int pos = positions.computeIfAbsent(oppositeCoord, ignored -> startPos); + final int beginPos = beginPositions.get(coord); values.forEach(constrained -> { final int extent = ops.getExtent(constrained); @@ -376,15 +387,13 @@ private void layoutAxisImpl( final int span = Math.min(getComponentSpan.apply(constrained, coord), extendedCellSpan); final int constrainedPos = switch (ops.getAlignment(constrained)) { - case BEGIN -> pos; - case CENTER -> pos + (extendedCellSpan - span) / 2; - case END -> pos + extendedCellSpan - span; + case BEGIN -> beginPos; + case CENTER -> beginPos + (extendedCellSpan - span) / 2; + case END -> beginPos + extendedCellSpan - span; }; ops.setBounds(constrained.component, constrainedPos, span); }); - - positions.put(oppositeCoord, pos + cellSpans.get(coord)); }); } @@ -459,7 +468,7 @@ public int compareTo(@NonNull At other) { */ private record Sizes( int totalWidth, int totalHeight, - ImmutableMap rowHeights, ImmutableMap columnWidths, + ImmutableSortedMap rowHeights, ImmutableSortedMap columnWidths, ImmutableMap componentSizes ) { static Sizes calculate(ConstrainedGrid grid, Function getSize) { @@ -498,7 +507,7 @@ static Sizes calculate(ConstrainedGrid grid, Function getS return new Sizes( columnWidths.values().stream().mapToInt(Integer::intValue).sum(), rowHeights.values().stream().mapToInt(Integer::intValue).sum(), - ImmutableMap.copyOf(rowHeights), ImmutableMap.copyOf(columnWidths), + ImmutableSortedMap.copyOf(rowHeights), ImmutableSortedMap.copyOf(columnWidths), ImmutableMap.copyOf(componentSizes) ); } @@ -542,7 +551,7 @@ int getTotalSpace(Sizes sizes) { } @Override - ImmutableMap getCellSpans(Sizes sizes) { + ImmutableSortedMap getCellSpans(Sizes sizes) { return sizes.columnWidths; } @@ -608,7 +617,7 @@ int getTotalSpace(Sizes sizes) { } @Override - ImmutableMap getCellSpans(Sizes sizes) { + ImmutableSortedMap getCellSpans(Sizes sizes) { return sizes.rowHeights; } @@ -655,7 +664,7 @@ CartesianOperations opposite() { abstract int getParentSpace(Container parent); abstract int getTotalSpace(Sizes sizes); - abstract ImmutableMap getCellSpans(Sizes sizes); + abstract ImmutableSortedMap getCellSpans(Sizes sizes); abstract int getSpan(Dimension size); abstract boolean fills(Constrained constrained); From ce1428c95be4cfcacca81db84e7e273309510b1b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 5 Dec 2025 20:57:39 -0800 Subject: [PATCH 086/110] rename visualizer --- .../org/quilt/internal/gui/visualization/Main.java | 4 ++-- ...ualizer.java => FlexGridGreaterVisualizer.java} | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/{FlexGridVVisualizer.java => FlexGridGreaterVisualizer.java} (68%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 5bac73796..f063988c3 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -10,7 +10,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; -import org.quilt.internal.gui.visualization.flex_grid.FlexGridVVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridGreaterVisualizer; import javax.swing.JButton; import javax.swing.JFrame; @@ -47,7 +47,7 @@ private Main() { registerVisualizer(new FlexGridPriorityVisualizer()); registerVisualizer(new FlexGridPriorityFillVisualizer()); registerVisualizer(new FlexGridSparseVisualizer()); - registerVisualizer(new FlexGridVVisualizer()); + registerVisualizer(new FlexGridGreaterVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridGreaterVisualizer.java similarity index 68% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridGreaterVisualizer.java index 29b21f638..73ef279f7 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridVVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridGreaterVisualizer.java @@ -7,10 +7,10 @@ import javax.swing.JFrame; -public class FlexGridVVisualizer implements Visualizer { +public class FlexGridGreaterVisualizer implements Visualizer { @Override public String getTitle() { - return "Flex Grid V"; + return "Flex Grid >"; } @Override @@ -19,11 +19,13 @@ public void visualize(JFrame window) { final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - window.add(VisualBox.of(), constraints); + window.add(VisualBox.of(), constraints.pos(0, 2)); + window.add(VisualBox.of(), constraints.pos(0, -2)); + window.add(VisualBox.of(), constraints.pos(1, 1)); - window.add(VisualBox.of(), constraints.pos(2, 2)); - window.add(VisualBox.of(), constraints.pos(3, 1)); - window.add(VisualBox.of(), constraints.pos(4, 0)); + window.add(VisualBox.of(), constraints.pos(1, -1)); + + window.add(VisualBox.of(), constraints.pos(2, 0)); window.pack(); } From c72137955762026f967d0e4f03f3ba47b9164a3e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 6 Dec 2025 07:46:22 -0800 Subject: [PATCH 087/110] add FlexGridCheckersVisualizer --- .../internal/gui/visualization/Main.java | 2 + .../flex_grid/FlexGridCheckersVisualizer.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridCheckersVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index f063988c3..eacd1e158 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -2,6 +2,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridAlignAndFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridAlignmentVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridCheckersVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridColumnVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; @@ -48,6 +49,7 @@ private Main() { registerVisualizer(new FlexGridPriorityFillVisualizer()); registerVisualizer(new FlexGridSparseVisualizer()); registerVisualizer(new FlexGridGreaterVisualizer()); + registerVisualizer(new FlexGridCheckersVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridCheckersVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridCheckersVisualizer.java new file mode 100644 index 000000000..3df2cc6b2 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridCheckersVisualizer.java @@ -0,0 +1,38 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridCheckersVisualizer implements Visualizer { + private static final int BOARD_SIZE = 8; + + @Override + public String getTitle() { + return "Flex Grid Checkers"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + + boolean placeOnEvenY = false; + for (int x = 0; x < BOARD_SIZE; x++) { + for (int y = 0; y < BOARD_SIZE; y++) { + if (placeOnEvenY == (y % 2 == 0)) { + window.add(VisualBox.of(Color.RED), constraints.pos(x, y)); + } + } + + placeOnEvenY = !placeOnEvenY; + } + + window.pack(); + } +} From eab46d7063cfc2d046b49fa3859d2068ea66224b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 6 Dec 2025 11:48:47 -0800 Subject: [PATCH 088/110] fix using xExtent instead of yExtent in Sizes::calculate add FlexGridDiagonalBricksVisualizer --- .../internal/gui/visualization/Main.java | 2 + .../FlexGridDiagonalBricksVisualizer.java | 46 +++++++++++++++++++ .../util/layout/flex_grid/FlexGridLayout.java | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDiagonalBricksVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index eacd1e158..5f90bfcbd 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -5,6 +5,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridCheckersVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridColumnVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridDefaultRowVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridDiagonalBricksVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridExtentOverlapVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityFillVisualizer; @@ -50,6 +51,7 @@ private Main() { registerVisualizer(new FlexGridSparseVisualizer()); registerVisualizer(new FlexGridGreaterVisualizer()); registerVisualizer(new FlexGridCheckersVisualizer()); + registerVisualizer(new FlexGridDiagonalBricksVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDiagonalBricksVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDiagonalBricksVisualizer.java new file mode 100644 index 000000000..f8fd4e08c --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridDiagonalBricksVisualizer.java @@ -0,0 +1,46 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridDiagonalBricksVisualizer implements Visualizer { + private static final Color BRICK_COLOR = new Color(170, 74, 68); + private static final int BRICK_COUNT = 5; + + private static final int BRICK_X_EXTENT = 4; + private static final int BRICK_Y_EXTENT = 2; + + private static final int HALF_BRICK_X_EXTENT = BRICK_X_EXTENT / 2; + + private static final int SIZE_UNIT = 30; + private static final int BRICK_WIDTH = BRICK_X_EXTENT * SIZE_UNIT; + private static final int BRICK_HEIGHT = BRICK_Y_EXTENT * SIZE_UNIT; + + @Override + public String getTitle() { + return "Flex Grid Diagonal Bricks"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute() + .extent(BRICK_X_EXTENT, BRICK_Y_EXTENT); + + final int xEnd = HALF_BRICK_X_EXTENT * BRICK_COUNT; + for (int x = 0, y = 0; x < xEnd; x += HALF_BRICK_X_EXTENT, y += BRICK_Y_EXTENT) { + window.add( + VisualBox.of(BRICK_COLOR, BRICK_WIDTH, BRICK_HEIGHT), + constraints.pos(x, y) + ); + } + + window.pack(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 2743c2774..633d1930f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -483,7 +483,7 @@ static Sizes calculate(ConstrainedGrid grid, Function getS final int componentCellWidth = ceilDiv(size.width, constrained.xExtent); final int componentCellHeight = ceilDiv(size.height, constrained.yExtent); for (int xOffset = 0; xOffset < constrained.xExtent; xOffset++) { - for (int yOffset = 0; yOffset < constrained.xExtent; yOffset++) { + for (int yOffset = 0; yOffset < constrained.yExtent; yOffset++) { final Dimension cellSize = cellSizes .computeIfAbsent(x + xOffset, ignored -> new HashMap<>()) .computeIfAbsent(y + yOffset, ignored -> new Dimension()); From 4118ed23acf8fe822c0672353cc86eb96598deef Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 6 Dec 2025 12:39:18 -0800 Subject: [PATCH 089/110] add FlexGridTetrisVisualizer --- .../internal/gui/visualization/Main.java | 2 + .../flex_grid/FlexGridTetrisVisualizer.java | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridTetrisVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 5f90bfcbd..e08d0a887 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -13,6 +13,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridGreaterVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridTetrisVisualizer; import javax.swing.JButton; import javax.swing.JFrame; @@ -52,6 +53,7 @@ private Main() { registerVisualizer(new FlexGridGreaterVisualizer()); registerVisualizer(new FlexGridCheckersVisualizer()); registerVisualizer(new FlexGridDiagonalBricksVisualizer()); + registerVisualizer(new FlexGridTetrisVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridTetrisVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridTetrisVisualizer.java new file mode 100644 index 000000000..735b89bd8 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridTetrisVisualizer.java @@ -0,0 +1,69 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridTetrisVisualizer implements Visualizer { + private static final int SQUARE_SIZE = 50; + + private static final Color I_COLOR = Color.CYAN; + private static final Color O_COLOR = Color.YELLOW; + private static final Color T_COLOR = Color.MAGENTA; + private static final Color J_COLOR = Color.BLUE; + private static final Color L_COLOR = new Color(255, 128, 0); + private static final Color S_COLOR = Color.GREEN; + private static final Color Z_COLOR = Color.RED; + + private static void addPart(JFrame window, Color color, int xExtent, int yExtent, int x, int y) { + window.add( + VisualBox.of(color, SQUARE_SIZE * xExtent, SQUARE_SIZE * yExtent), + FlexGridConstraints.createAbsolute().extent(xExtent, yExtent).pos(x, y) + ); + } + + @Override + public String getTitle() { + return "Flex Grid Tetris"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + // default I + addPart(window, I_COLOR, 1, 4, 0, 0); + + // inverted T + addPart(window, T_COLOR, 3, 1, 1, 3); + addPart(window, T_COLOR, 1, 2, 2, 2); + + // vertical Z + addPart(window, Z_COLOR, 1, 2, 1, 1); + addPart(window, Z_COLOR, 1, 2, 2, 0); + + // default J + addPart(window, J_COLOR, 2, 1, 4, 3); + addPart(window, J_COLOR, 1, 2, 5, 1); + + // O + addPart(window, O_COLOR, 2, 2, 3, 1); + + // horizontal I + addPart(window, I_COLOR, 4, 1, 3, 0); + + // default L + addPart(window, L_COLOR, 2, 1, 6, 3); + addPart(window, L_COLOR, 1, 3, 6, 1); + + // vertical S + addPart(window, S_COLOR, 1, 2, 7, 1); + addPart(window, S_COLOR, 1, 2, 8, 2); + + window.pack(); + } +} From 6cf9ee9d2ad56bae837b000a224f09f22a03d3f5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 6 Dec 2025 13:01:19 -0800 Subject: [PATCH 090/110] add FlexGridPriorityScrollPanes --- .../internal/gui/visualization/Main.java | 2 + .../FlexGridPriorityScrollPanes.java | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index e08d0a887..93affdb4e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -9,6 +9,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridExtentOverlapVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityFillVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityScrollPanes; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; @@ -54,6 +55,7 @@ private Main() { registerVisualizer(new FlexGridCheckersVisualizer()); registerVisualizer(new FlexGridDiagonalBricksVisualizer()); registerVisualizer(new FlexGridTetrisVisualizer()); + registerVisualizer(new FlexGridPriorityScrollPanes()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java new file mode 100644 index 000000000..7e98d305c --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java @@ -0,0 +1,55 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; + +public class FlexGridPriorityScrollPanes implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Priority Scroll Panes"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + + final var firstText = new JTextArea( + """ + 5 + 4 + 3 + 2 + 1\ + """ + ); + + window.add(new JScrollPane(firstText, VERTICAL_SCROLLBAR_NEVER, HORIZONTAL_SCROLLBAR_NEVER), constraints); + + final var secondText = new JTextArea( + """ + e + d + c + b + a\ + """ + ); + + window.add( + new JScrollPane(secondText, VERTICAL_SCROLLBAR_NEVER, HORIZONTAL_SCROLLBAR_NEVER), + constraints.nextRow().incrementPriority() + ); + + window.pack(); + } +} From e33228170a6ee4c6a6730ada06aa88636647b3c6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 7 Dec 2025 09:57:36 -0800 Subject: [PATCH 091/110] rename FlexGridPriorityScrollPanes -> FlexGridPriorityScrollPanesVisualizer fix type alightCenterLeft -> alignCenterLeft --- .../java/org/quilt/internal/gui/visualization/Main.java | 4 ++-- .../flex_grid/FlexGridAlignAndFillVisualizer.java | 2 +- .../visualization/flex_grid/FlexGridAlignmentVisualizer.java | 2 +- ...lPanes.java => FlexGridPriorityScrollPanesVisualizer.java} | 2 +- .../layout/flex_grid/constraints/FlexGridConstraints.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/{FlexGridPriorityScrollPanes.java => FlexGridPriorityScrollPanesVisualizer.java} (94%) diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index 93affdb4e..de51d2a62 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -9,7 +9,7 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridFillVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridExtentOverlapVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityFillVisualizer; -import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityScrollPanes; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityScrollPanesVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; @@ -55,7 +55,7 @@ private Main() { registerVisualizer(new FlexGridCheckersVisualizer()); registerVisualizer(new FlexGridDiagonalBricksVisualizer()); registerVisualizer(new FlexGridTetrisVisualizer()); - registerVisualizer(new FlexGridPriorityScrollPanes()); + registerVisualizer(new FlexGridPriorityScrollPanesVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java index 089d90fb2..83309047d 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java @@ -18,7 +18,7 @@ public void visualize(JFrame window) { c -> c.fillOnlyY().alignTopCenter(), c -> c.fillNone().alignTopRight(), - c -> c.fillOnlyX().alightCenterLeft(), + c -> c.fillOnlyX().alignCenterLeft(), c -> c.fillBoth().alignCenter(), c -> c.fillOnlyX().alignCenterRight(), diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java index 8c5b0729e..8ad07f2ba 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java @@ -24,7 +24,7 @@ public void visualize(JFrame window) { FlexGridQuiltVisualiser.visualizeQuilt( window, Absolute::alignTopLeft, Absolute::alignTopCenter, Absolute::alignTopRight, - Absolute::alightCenterLeft, Absolute::alignCenter, Absolute::alignCenterRight, + Absolute::alignCenterLeft, Absolute::alignCenter, Absolute::alignCenterRight, Absolute::alignBottomLeft, Absolute::alignBottomCenter, Absolute::alignBottomRight ); diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java similarity index 94% rename from enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java rename to enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java index 7e98d305c..0a85fc991 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanes.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java @@ -11,7 +11,7 @@ import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; -public class FlexGridPriorityScrollPanes implements Visualizer { +public class FlexGridPriorityScrollPanesVisualizer implements Visualizer { @Override public String getTitle() { return "Flex Grid Priority Scroll Panes"; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 5e4a793d1..c32f6edd5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -520,7 +520,7 @@ public C alignTopRight() { * @see #alignLeft() * @see #align(Alignment, Alignment) */ - public C alightCenterLeft() { + public C alignCenterLeft() { return this.alignCenterY().alignLeft(); } From 13e4ce01c05d5cbbdb69dfb710b4139e688cb08d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 7 Dec 2025 09:59:05 -0800 Subject: [PATCH 092/110] fix not tracking component positions in ConstrainedGrid, preventing removal --- .../enigma/gui/util/layout/flex_grid/ConstrainedGrid.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 16ea56c87..3231f4cd7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -36,6 +36,8 @@ void put(int x, int y, Constrained value) { this.remove(value.component()); + this.componentPositions.put(component, new Position(x, y)); + this.grid .computeIfAbsent(x, ignored -> new TreeMap<>()) .computeIfAbsent(y, ignored -> new HashMap<>(1)) From ed2ac170a6406b8f4a1bd1934922e0285308f42d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 7 Dec 2025 09:59:44 -0800 Subject: [PATCH 093/110] cache immutable Size instead of mutable Dimension --- .../util/layout/flex_grid/FlexGridLayout.java | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 633d1930f..bb8b2ad14 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -258,7 +258,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final Sizes max = this.getMaxSizes(); this.layoutAxisImpl(startPos, ops, cellSpans, (constrained, coord) -> { final Sizes targets = ops.fills(constrained) ? max : preferred; - final Dimension targetSize = targets.componentSizes.get(constrained.component); + final Size targetSize = targets.componentSizes.get(constrained.component); assert targetSize != null; return ops.getSpan(targetSize); @@ -274,7 +274,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final SortedMap cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { - final Dimension preferredSize = preferred.componentSizes.get(constrained.component); + final Size preferredSize = preferred.componentSizes.get(constrained.component); assert preferredSize != null; return ops.getSpan(preferredSize); @@ -311,7 +311,7 @@ private SortedMap allocateCellSpace(CartesianOperations ops, i while (!prioritized.isEmpty() && remainingSpace > 0) { final Constrained.At at = prioritized.remove(); - final Dimension targetSize = large.componentSizes.get(at.constrained().component); + final Size targetSize = large.componentSizes.get(at.constrained().component); assert targetSize != null; final int extent = ops.getExtent(at.constrained()); @@ -462,6 +462,12 @@ public int compareTo(@NonNull At other) { } } + private record Size(int width, int height) { + static Size of(Dimension dimension) { + return new Size(dimension.width, dimension.height); + } + } + /** * A collection of sizes and size metrics use for calculating min/max/preferred container size and * for laying out the container. @@ -469,16 +475,17 @@ public int compareTo(@NonNull At other) { private record Sizes( int totalWidth, int totalHeight, ImmutableSortedMap rowHeights, ImmutableSortedMap columnWidths, - ImmutableMap componentSizes + ImmutableMap componentSizes ) { static Sizes calculate(ConstrainedGrid grid, Function getSize) { - final Map componentSizes = new HashMap<>(); + final Map componentSizes = new HashMap<>(); final Map> cellSizes = new HashMap<>(); grid.forEach((x, y, values) -> { values.forEach(constrained -> { - final Dimension size = componentSizes.computeIfAbsent(constrained.component, getSize); + final Size size = componentSizes + .computeIfAbsent(constrained.component, component -> Size.of(getSize.apply(component))); final int componentCellWidth = ceilDiv(size.width, constrained.xExtent); final int componentCellHeight = ceilDiv(size.height, constrained.yExtent); @@ -499,8 +506,14 @@ static Sizes calculate(ConstrainedGrid grid, Function getS final Map columnWidths = new HashMap<>(); cellSizes.forEach((x, column) -> { column.forEach((y, size) -> { - rowHeights.compute(y, (ignored, height) -> height == null ? size.height : Math.max(height, size.height)); - columnWidths.compute(x, (ignored, width) -> width == null ? size.width : Math.max(width, size.width)); + rowHeights.compute(y, (ignored, height) -> height == null + ? size.height + : Math.max(height, size.height) + ); + columnWidths.compute(x, (ignored, width) -> width == null + ? size.width + : Math.max(width, size.width) + ); }); }); @@ -560,6 +573,11 @@ int getSpan(Dimension size) { return size.width; } + @Override + int getSpan(Size size) { + return size.width; + } + @Override boolean fills(Constrained constrained) { return constrained.fillX; @@ -626,6 +644,11 @@ int getSpan(Dimension size) { return size.height; } + @Override + int getSpan(Size size) { + return size.height; + } + @Override boolean fills(Constrained constrained) { return constrained.fillY; @@ -666,6 +689,7 @@ CartesianOperations opposite() { abstract int getTotalSpace(Sizes sizes); abstract ImmutableSortedMap getCellSpans(Sizes sizes); abstract int getSpan(Dimension size); + abstract int getSpan(Size size); abstract boolean fills(Constrained constrained); abstract boolean noneFill(ConstrainedGrid grid); From 2e780778df89a8410d289048af1601bce9643f02 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 8 Dec 2025 12:05:34 -0800 Subject: [PATCH 094/110] fix several bugs in FlexGridLayout temporarily? hide horizontal scroll bars of DeclarationSnippetPanels use FlexGridLayout in EntryTooltip --- .../gui/panel/DeclarationSnippetPanel.java | 3 + .../enigma/gui/panel/EntryTooltip.java | 140 +++++---------- .../layout/flex_grid/ConstrainedGrid.java | 31 ++-- .../util/layout/flex_grid/FlexGridLayout.java | 160 +++++++++--------- 4 files changed, 144 insertions(+), 190 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index 1fcb8bd84..7cf494c60 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -72,6 +72,9 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.editor.setCaretColor(GuiUtil.TRANSPARENT); this.editor.getCaret().setSelectionVisible(true); + // TODO try custom scroll pane that accounts for scroll bars in pref size instead? + this.editorScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + this.addSourceSetListener(source -> { if (!this.isBounded()) { // the source isn't very useful if it couldn't be trimmed diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 208346b4f..3af232d87 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -20,28 +20,27 @@ import org.quiltmc.enigma.gui.docker.DeobfuscatedClassesDocker; import org.quiltmc.enigma.gui.docker.Docker; import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; -import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.Utils; import javax.swing.Box; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.JWindow; +import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Toolkit; @@ -128,7 +127,7 @@ public EntryTooltip(Gui gui) { super(gui.getFrame()); this.gui = gui; - this.content = new JPanel(new GridBagLayout()); + this.content = new JPanel(new FlexGridLayout()); this.setAlwaysOnTop(true); this.setType(Window.Type.POPUP); @@ -211,7 +210,6 @@ private void populateWith(Entry target, boolean inherited, boolean opening) { this.repopulated = !opening; this.content.removeAll(); - @Nullable final MouseAdapter stopInteraction = Config.editor().entryTooltips.interactable.value() ? null : new MouseAdapter() { @Override @@ -226,7 +224,7 @@ public void mousePressed(MouseEvent e) { final Font editorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value()); final Font italEditorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); - int gridY = 0; + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); { final Box parentLabelRow = Box.createHorizontalBox(); @@ -241,75 +239,49 @@ public void mousePressed(MouseEvent e) { parentLabelRow.add(colonLabelOf("", editorFont)); parentLabelRow.add(this.parentLabelOf(target, editorFont, stopInteraction)); - parentLabelRow.add(Box.createHorizontalGlue()); - this.add(parentLabelRow, GridBagConstraintsBuilder.create() - .pos(0, gridY++) - .insets(ROW_OUTER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET) - .anchor(GridBagConstraints.LINE_START) - .build() - ); + parentLabelRow.setBorder(createEmptyBorder(ROW_OUTER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); + this.add(parentLabelRow, constraints.copy().alignCenterLeft()); } - final var mainContent = new JPanel(new GridBagLayout()); - // Put all main content in one big scroll pane. - // Ideally there'd be separate javadoc and snippet scroll panes, but multiple scroll pane children - // of a grid bag parent don't play nice when space is limited. - // The snippet has its own scroll pane, but wrapping it in this one effectively disables its resizing. - final var mainScroll = new JScrollPane(mainContent); - mainScroll.setBorder(createEmptyBorder()); - int mainGridY = 0; - final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, mainGridY++) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .build() - ); + this.add(new JSeparator(), constraints.nextRow().copy().fillX()); + + final var javadocs = new JPanel(new FlexGridLayout()); + final FlexGridConstraints.Absolute javadocsConstraints = FlexGridConstraints.createAbsolute(); if (javadoc != null) { - mainContent.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() - .pos(0, mainGridY++) - .insets(ROW_INNER_INSET, ROW_OUTER_INSET) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .anchor(GridBagConstraints.LINE_START) - .build() - ); + final JTextArea javadocText = javadocOf(javadoc, italEditorFont, stopInteraction); + javadocText.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); + javadocs.add(javadocText, javadocsConstraints.copy().fillX()); } if (!paramJavadocs.isEmpty()) { - final JPanel params = new JPanel(new GridBagLayout()); - int paramsGridY = 0; + final JPanel params = new JPanel(new FlexGridLayout()); + + final FlexGridConstraints.Absolute paramsConstraints = FlexGridConstraints.createAbsolute(); for (final ParamJavadoc paramJavadoc : paramJavadocs) { - params.add(paramJavadoc.name, GridBagConstraintsBuilder.create() - .pos(0, paramsGridY) - .anchor(GridBagConstraints.FIRST_LINE_END) - .build() - ); + params.add(paramJavadoc.name, paramsConstraints.copy().alignTopRight()); - params.add(paramJavadoc.javadoc, GridBagConstraintsBuilder.create() - .pos(1, paramsGridY++) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .anchor(GridBagConstraints.LINE_START) - .build() + params.add(paramJavadoc.javadoc, paramsConstraints.nextColumn().copy() + .fillX() + .alignTopLeft() ); + + paramsConstraints.nextRow(); } - mainContent.add(params, GridBagConstraintsBuilder.create() - .insets(ROW_INNER_INSET, ROW_OUTER_INSET) - .pos(0, mainGridY++) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .build() - ); + params.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); + javadocs.add(params, javadocsConstraints.nextRow().copy().fillX()); } + + final JScrollPane javadocsScroll = new JScrollPane(javadocs); + javadocsScroll.setBorder(createEmptyBorder()); + this.add(javadocsScroll, constraints.nextRow().copy().fillX()); } if (this.declarationSnippet != null) { @@ -345,10 +317,14 @@ public void mouseClicked(MouseEvent e) { final Dimension oldSize = opening ? null : this.getSize(); final Point oldMousePos = MouseInfo.getPointerInfo().getLocation(); this.declarationSnippet.addSourceSetListener(source -> { + // JTextAreas (javadocs) adjust their preferred sizes after the first pack, so pack twice this.pack(); - // swing entry, Font font, @Nullable MouseAdapter s nameBuilder.insert(0, packageName.replace('/', '.')); } - @Nullable final MouseListener parentClicked; if (stopInteraction == null) { if (immediateParent != null) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 3231f4cd7..ea85315e8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid; -import com.google.common.collect.ImmutableSortedMap; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.Constrained; import java.awt.Component; @@ -55,13 +54,6 @@ void put(int x, int y, Constrained value) { } } - Stream get(int x, int y) { - return this.grid.getOrDefault(x, ImmutableSortedMap.of()) - .getOrDefault(y, Map.of()) - .values() - .stream(); - } - void remove(Component component) { final Position pos = this.componentPositions.remove(component); if (pos != null) { @@ -111,10 +103,6 @@ boolean noneFillY() { return this.yFillers.isEmpty(); } - int getSize() { - return this.valuesByMaxX.size(); - } - boolean isEmpty() { return this.valuesByMaxX.isEmpty(); } @@ -127,10 +115,29 @@ void forEach(EntriesConsumer action) { }); } + Stream map(EntryFunction mapper) { + return this.grid.entrySet().stream().flatMap(columnEntry -> columnEntry + .getValue() + .entrySet() + .stream() + .flatMap(rowEntry -> rowEntry + .getValue() + .values() + .stream() + .map(constrained -> mapper.apply(columnEntry.getKey(), rowEntry.getKey(), constrained)) + ) + ); + } + private record Position(int x, int y) { } @FunctionalInterface interface EntriesConsumer { void accept(int x, int y, Stream values); } + + @FunctionalInterface + interface EntryFunction { + T apply(int x, int y, Constrained value); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index bb8b2ad14..5f7625db9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -14,13 +14,14 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.LayoutManager2; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.PriorityQueue; +import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; -import java.util.function.BiFunction; import java.util.function.Function; import static org.quiltmc.enigma.util.Utils.ceilDiv; @@ -51,7 +52,7 @@ * components getting space first. In ties, components with the least position on the axis get priority.
      * A component never gets less space in an axis than its {@linkplain Component#getMinimumSize() minimum size} * allows, and it never gets more space than its {@linkplain Component#getMaximumSize() maximum size} allows. - * A components only ever gets more space in an axis than its + * A component only ever gets more space in an axis than its * {@linkplain Component#getPreferredSize() preferred size} requests if it's set to * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} that axis. * @@ -66,7 +67,7 @@ *
    * * * @@ -74,7 +75,7 @@ * * *
    space ≤ minmin < space < preferred * each component gets at least its minimum size; components get additional space - * - up to their preferred size - according to priority + * - up to their preferred sizes - according to priority *
    * each component gets at least is preferred size; components that * {@linkplain FlexGridConstraints#fill(boolean, boolean) fill} the axis get additional space - * - up to their max size - according to priority + * - up to their max sizes - according to priority *
    @@ -98,6 +99,24 @@ * */ public class FlexGridLayout implements LayoutManager2 { + // simplified integer overflow detection + // only correctly handles overflow if all values are positive + private static int sumOrMax(Collection values) { + int sum = 0; + for (final int value : values) { + sum += value; + if (sum < 0) { + return Integer.MAX_VALUE; + } + } + + return sum; + } + + private static int positiveOrMax(int value) { + return value < 0 ? Integer.MAX_VALUE : value; + } + private final ConstrainedGrid grid = new ConstrainedGrid(); /** @@ -149,7 +168,7 @@ public void addLayoutComponent(Component component, @Nullable FlexGridConstraint y = this.getRelativeY(); } - this.grid.put(x, y, new Constrained(component, constraints)); + this.grid.put(x, y, Constrained.of(component, constraints)); } } @@ -163,6 +182,7 @@ private void addDefaultConstrainedLayoutComponent(Component component) { } private int getRelativeX() { + // TODO this gives max x, but should give max x *of bottom row* return this.grid.isEmpty() ? FlexGridConstraints.Absolute.DEFAULT_X : this.grid.getMaxXOrThrow() + 1; } @@ -233,8 +253,10 @@ private Sizes getMaxSizes() { @Override public void layoutContainer(Container parent) { - this.layoutAxis(parent, CartesianOperations.X); - this.layoutAxis(parent, CartesianOperations.Y); + if (!this.grid.isEmpty()) { + this.layoutAxis(parent, CartesianOperations.X); + this.layoutAxis(parent, CartesianOperations.Y); + } } private void layoutAxis(Container parent, CartesianOperations ops) { @@ -248,37 +270,25 @@ private void layoutAxis(Container parent, CartesianOperations ops) { final int extraSpace = availableSpace - ops.getTotalSpace(preferred); if (extraSpace >= 0) { if (extraSpace == 0 || ops.noneFill(this.grid)) { - this.layoutFixedAxis(preferred, leadingInset + extraSpace / 2, ops); + this.layoutAxisImpl(leadingInset + extraSpace / 2, ops, ops.getCellSpans(preferred)); } else { final SortedMap cellSpans = this.allocateCellSpace(ops, extraSpace, true); - final int allocatedSpace = cellSpans.values().stream().mapToInt(Integer::intValue).sum(); + final int allocatedSpace = sumOrMax(cellSpans.values()); final int startPos = leadingInset + (availableSpace - allocatedSpace) / 2; - final Sizes max = this.getMaxSizes(); - this.layoutAxisImpl(startPos, ops, cellSpans, (constrained, coord) -> { - final Sizes targets = ops.fills(constrained) ? max : preferred; - final Size targetSize = targets.componentSizes.get(constrained.component); - assert targetSize != null; - - return ops.getSpan(targetSize); - }); + this.layoutAxisImpl(startPos, ops, cellSpans); } } else { final Sizes min = this.getMinSizes(); final int extraMinSpace = availableSpace - ops.getTotalSpace(min); if (extraMinSpace <= 0) { - this.layoutFixedAxis(min, leadingInset, ops); + this.layoutAxisImpl(leadingInset, ops, ops.getCellSpans(min)); } else { final SortedMap cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); - this.layoutAxisImpl(leadingInset, ops, cellSpans, (constrained, coord) -> { - final Size preferredSize = preferred.componentSizes.get(constrained.component); - assert preferredSize != null; - - return ops.getSpan(preferredSize); - }); + this.layoutAxisImpl(leadingInset, ops, cellSpans); } } } @@ -286,7 +296,6 @@ private void layoutAxis(Container parent, CartesianOperations ops) { private SortedMap allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { final Sizes large; final Sizes small; - final SortedMap cellSpans; if (fill) { large = this.getMaxSizes(); small = this.getPreferredSizes(); @@ -295,27 +304,22 @@ private SortedMap allocateCellSpace(CartesianOperations ops, i small = this.getMinSizes(); } - cellSpans = new TreeMap<>(ops.getCellSpans(small)); - - final PriorityQueue prioritized = new PriorityQueue<>(this.grid.getSize()); - this.grid.forEach((x, y, values) -> { - if (fill) { - values = values.filter(ops::fills); - } + final SortedMap cellSpans = new TreeMap<>(ops.getCellSpans(small)); - values.forEach(constrained -> { - prioritized.add(constrained.new At(ops.chooseCoord(x, y))); - }); - }); - - while (!prioritized.isEmpty() && remainingSpace > 0) { - final Constrained.At at = prioritized.remove(); + final List prioritized = this.grid + .map((x, y, constrained) -> fill && !ops.fills(constrained) + ? Optional.empty() + : Optional.of(constrained.new At(ops.chooseCoord(x, y))) + ) + .flatMap(Optional::stream) + .sorted() + .toList(); + for (final Constrained.At at : prioritized) { + final int extent = ops.getExtent(at.constrained()); final Size targetSize = large.componentSizes.get(at.constrained().component); assert targetSize != null; - final int extent = ops.getExtent(at.constrained()); - final int targetSpan = ceilDiv(ops.getSpan(targetSize), extent); for (int i = 0; i < extent; i++) { @@ -343,23 +347,16 @@ private SortedMap allocateCellSpace(CartesianOperations ops, i } } } + + if (remainingSpace <= 0) { + break; + } } return cellSpans; } - private void layoutFixedAxis(Sizes sizes, int startPos, CartesianOperations ops) { - this.layoutAxisImpl( - startPos, ops, ops.getCellSpans(sizes), - (constrained, coord) -> ops.getSpan(sizes.componentSizes.get(constrained.component)) - ); - } - - private void layoutAxisImpl( - int startPos, CartesianOperations ops, - SortedMap cellSpans, - BiFunction getComponentSpan - ) { + private void layoutAxisImpl(int startPos, CartesianOperations ops, SortedMap cellSpans) { final Map beginPositions = new HashMap<>(); int currentPos = startPos; for (final Map.Entry entry : cellSpans.entrySet()) { @@ -371,6 +368,9 @@ private void layoutAxisImpl( currentPos += span; } + final Sizes preferred = this.getPreferredSizes(); + final Sizes max = this.getMaxSizes(); + this.grid.forEach((x, y, values) -> { final int coord = ops.chooseCoord(x, y); @@ -384,7 +384,11 @@ private void layoutAxisImpl( extendedCellSpan += cellSpans.get(coord + i); } - final int span = Math.min(getComponentSpan.apply(constrained, coord), extendedCellSpan); + final Sizes targets = ops.fills(constrained) ? max : preferred; + final Size targetSize = targets.componentSizes.get(constrained.component); + assert targetSize != null; + + final int span = Math.min(ops.getSpan(targetSize), extendedCellSpan); final int constrainedPos = switch (ops.getAlignment(constrained)) { case BEGIN -> beginPos; @@ -414,8 +418,8 @@ static Constrained defaultOf(Component component) { ); } - Constrained(Component component, FlexGridConstraints constraints) { - this( + static Constrained of(Component component, FlexGridConstraints constraints) { + return new Constrained( component, constraints.getXExtent(), constraints.getYExtent(), constraints.fillsX(), constraints.fillsY(), @@ -477,7 +481,17 @@ private record Sizes( ImmutableSortedMap rowHeights, ImmutableSortedMap columnWidths, ImmutableMap componentSizes ) { + static Sizes EMPTY = new Sizes( + 0, 0, + ImmutableSortedMap.of(), ImmutableSortedMap.of(), + ImmutableMap.of() + ); + static Sizes calculate(ConstrainedGrid grid, Function getSize) { + if (grid.isEmpty()) { + return EMPTY; + } + final Map componentSizes = new HashMap<>(); final Map> cellSizes = new HashMap<>(); @@ -518,8 +532,8 @@ static Sizes calculate(ConstrainedGrid grid, Function getS }); return new Sizes( - columnWidths.values().stream().mapToInt(Integer::intValue).sum(), - rowHeights.values().stream().mapToInt(Integer::intValue).sum(), + sumOrMax(columnWidths.values()), + sumOrMax(rowHeights.values()), ImmutableSortedMap.copyOf(rowHeights), ImmutableSortedMap.copyOf(columnWidths), ImmutableMap.copyOf(componentSizes) ); @@ -527,8 +541,8 @@ static Sizes calculate(ConstrainedGrid grid, Function getS Dimension createTotalDimension(Insets insets) { return new Dimension( - this.totalWidth + insets.left + insets.right, - this.totalHeight + insets.top + insets.bottom + positiveOrMax(this.totalWidth + insets.left + insets.right), + positiveOrMax(this.totalHeight + insets.top + insets.bottom) ); } } @@ -568,11 +582,6 @@ ImmutableSortedMap getCellSpans(Sizes sizes) { return sizes.columnWidths; } - @Override - int getSpan(Dimension size) { - return size.width; - } - @Override int getSpan(Size size) { return size.width; @@ -602,11 +611,6 @@ int getExtent(Constrained constrained) { void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); } - - @Override - CartesianOperations opposite() { - return Y; - } }, Y() { @Override @@ -639,11 +643,6 @@ ImmutableSortedMap getCellSpans(Sizes sizes) { return sizes.rowHeights; } - @Override - int getSpan(Dimension size) { - return size.height; - } - @Override int getSpan(Size size) { return size.height; @@ -673,11 +672,6 @@ int getExtent(Constrained constrained) { void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); } - - @Override - CartesianOperations opposite() { - return X; - } }; abstract int chooseCoord(int x, int y); @@ -688,7 +682,7 @@ CartesianOperations opposite() { abstract int getTotalSpace(Sizes sizes); abstract ImmutableSortedMap getCellSpans(Sizes sizes); - abstract int getSpan(Dimension size); + abstract int getSpan(Size size); abstract boolean fills(Constrained constrained); @@ -697,7 +691,5 @@ CartesianOperations opposite() { abstract int getExtent(Constrained constrained); abstract void setBounds(Component component, int pos, int span); - - abstract CartesianOperations opposite(); } } From dc68fb6f86e2ce42774e2d2d2c3c9dbe2a2e8dbb Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 8 Dec 2025 14:49:39 -0800 Subject: [PATCH 095/110] fix relative FlexGridLayout components --- .../internal/gui/visualization/Main.java | 4 + .../FlexGridExtentOverlapVisualiser.java | 8 +- ...exGridRelativeExtentOverlapVisualizer.java | 52 +++++++++ .../FlexGridRelativeRowsVisualizer.java | 32 ++++++ .../layout/flex_grid/ConstrainedGrid.java | 101 +++++++++++------- .../util/layout/flex_grid/FlexGridLayout.java | 23 +--- 6 files changed, 159 insertions(+), 61 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java index de51d2a62..5bf33124e 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/Main.java @@ -12,6 +12,8 @@ import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityScrollPanesVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridPriorityVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridQuiltVisualiser; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridRelativeExtentOverlapVisualizer; +import org.quilt.internal.gui.visualization.flex_grid.FlexGridRelativeRowsVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridSparseVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridGreaterVisualizer; import org.quilt.internal.gui.visualization.flex_grid.FlexGridTetrisVisualizer; @@ -56,6 +58,8 @@ private Main() { registerVisualizer(new FlexGridDiagonalBricksVisualizer()); registerVisualizer(new FlexGridTetrisVisualizer()); registerVisualizer(new FlexGridPriorityScrollPanesVisualizer()); + registerVisualizer(new FlexGridRelativeRowsVisualizer()); + registerVisualizer(new FlexGridRelativeExtentOverlapVisualizer()); } private static void position(Window window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java index 36e1de661..5ad21e341 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridExtentOverlapVisualiser.java @@ -15,15 +15,15 @@ public String getTitle() { } /** - *

    +	 * 
    
     	 * -------------------
     	 * | RGB | RB  |  R  |
    -	 * -------------------
    +	 * ------+-----+------
     	 * | GB  |  B  |
    -	 * -------------
    +	 * ------+------
     	 * |  G  |
     	 * -------
    -	 * 
    + *
    */ @Override public void visualize(JFrame window) { diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java new file mode 100644 index 000000000..f158a2985 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java @@ -0,0 +1,52 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridRelativeExtentOverlapVisualizer implements Visualizer { + public static final int MAX_EXTENT = 3; + private static final int SQUARE_SIZE = 50; + + @Override + public String getTitle() { + return "Flex Grid Relative Extent Overlap"; + } + + /** + *
    
    +	 * -----------------
    +	 * |           | R |
    +	 * |   --------+----
    +	 * |   |       |
    +	 * |   |   -----
    +	 * |   |   |   |
    +	 * ----+---+----
    +	 * |   | B |
    +	 * ---------
    +	 * 
    + */ + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + for (int coord = 0; coord < MAX_EXTENT; coord++) { + final int extent = MAX_EXTENT - coord; + window.add(VisualBox.of(SQUARE_SIZE * extent), FlexGridConstraints.createAbsolute() + .pos(coord, coord) + .extent(extent, extent) + ); + } + + window.add(VisualBox.of(Color.RED, SQUARE_SIZE)); + + window.add(VisualBox.of(SQUARE_SIZE), FlexGridConstraints.createAbsolute().y(MAX_EXTENT)); + window.add(VisualBox.of(Color.BLUE, SQUARE_SIZE)); + + window.pack(); + } +} diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java new file mode 100644 index 000000000..bc927ba9d --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java @@ -0,0 +1,32 @@ +package org.quilt.internal.gui.visualization.flex_grid; + +import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; + +public class FlexGridRelativeRowsVisualizer implements Visualizer { + @Override + public String getTitle() { + return "Flex Grid Relative Rows"; + } + + @Override + public void visualize(JFrame window) { + window.setLayout(new FlexGridLayout()); + + window.add(VisualBox.of(Color.RED)); + window.add(VisualBox.of(new Color(255, 128, 0))); + window.add(VisualBox.of(Color.YELLOW)); + + // force next row with absolute + window.add(VisualBox.of(Color.GREEN), FlexGridConstraints.createAbsolute().y(1)); + window.add(VisualBox.of(Color.BLUE)); + window.add(VisualBox.of(new Color(128, 0, 255))); + + window.pack(); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index ea85315e8..5f2609dae 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.Constrained; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import java.awt.Component; import java.util.HashMap; @@ -19,14 +20,15 @@ * but a value may only be associated with one coordinate pair at a time. */ class ConstrainedGrid { - private static Set createValueSet(Integer ignored) { - return new HashSet<>(); - } - + // outer sorted map maps y coordinates to rows + // inner sorted map maps x coordinates to values + // component map holds values by component private final SortedMap>> grid = new TreeMap<>(); private final Map componentPositions = new HashMap<>(); - private final SortedMap> valuesByMaxX = new TreeMap<>(); - private final SortedMap> valuesByMaxY = new TreeMap<>(); + // outer sorted map maps constrained max y to rows + // mid sorted map maps constrained max x to values by min y + // used to find relative position + private final SortedMap>>> maxGrid = new TreeMap<>(); private final Set xFillers = new HashSet<>(); private final Set yFillers = new HashSet<>(); @@ -38,12 +40,15 @@ void put(int x, int y, Constrained value) { this.componentPositions.put(component, new Position(x, y)); this.grid - .computeIfAbsent(x, ignored -> new TreeMap<>()) - .computeIfAbsent(y, ignored -> new HashMap<>(1)) + .computeIfAbsent(y, ignored -> new TreeMap<>()) + .computeIfAbsent(x, ignored -> new HashMap<>(1)) .put(component, value); - this.valuesByMaxX.computeIfAbsent(x + value.getXExcess(), ConstrainedGrid::createValueSet).add(component); - this.valuesByMaxY.computeIfAbsent(y + value.getYExcess(), ConstrainedGrid::createValueSet).add(component); + this.maxGrid + .computeIfAbsent(y + value.getYExcess(), ignored -> new TreeMap<>()) + .computeIfAbsent(x + value.getXExcess(), ignored -> new TreeMap<>()) + .computeIfAbsent(y, ignored -> new HashSet<>(1)) + .add(component); if (value.fillX()) { this.xFillers.add(component); @@ -54,32 +59,60 @@ void put(int x, int y, Constrained value) { } } + void putRelative(Constrained value) { + final int x; + final int y; + if (this.isEmpty()) { + x = FlexGridConstraints.Absolute.DEFAULT_X; + y = FlexGridConstraints.Absolute.DEFAULT_Y; + } else { + final int maxY = this.maxGrid.lastKey(); + final SortedMap>> maxRow = this.maxGrid.get(maxY); + final SortedMap> maxXComponentsByY = maxRow.get(maxRow.lastKey()); + + final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); + final Component component = minYMaxXComponents.iterator().next(); + final Position pos = this.componentPositions.get(component); + + x = pos.x + this.grid.get(pos.y).get(pos.x).get(component).getXExcess() + 1; + y = pos.y; + } + + this.put(x, y, value); + } + void remove(Component component) { final Position pos = this.componentPositions.remove(component); if (pos != null) { - final SortedMap> column = this.grid.get(pos.x); - final Map values = column.get(pos.y); + final SortedMap> row = this.grid.get(pos.y); + final Map values = row.get(pos.x); final Constrained removed = values.remove(component); + if (values.isEmpty()) { - column.remove(pos.y); + row.remove(pos.x); - if (column.isEmpty()) { - this.grid.remove(pos.x); + if (row.isEmpty()) { + this.grid.remove(pos.y); } } + final int maxY = pos.y + removed.getYExcess(); + final SortedMap>> maxRow = this.maxGrid.get(maxY); final int maxX = pos.x + removed.getXExcess(); - final Set maxXValues = this.valuesByMaxX.get(maxX); - maxXValues.remove(component); - if (maxXValues.isEmpty()) { - this.valuesByMaxX.remove(maxX); - } + final SortedMap> maximumsByY = maxRow.get(maxX); + final Set maxComponents = maximumsByY.get(pos.y); + maxComponents.remove(component); - final int maxY = pos.y + removed.getYExcess(); - final Set maxYValues = this.valuesByMaxY.get(maxY); - maxYValues.remove(component); - if (maxYValues.isEmpty()) { - this.valuesByMaxY.remove(maxY); + if (maxComponents.isEmpty()) { + maximumsByY.remove(pos.y); + + if (maximumsByY.isEmpty()) { + maxRow.remove(maxX); + + if (maxRow.isEmpty()) { + this.maxGrid.remove(maxY); + } + } } this.xFillers.remove(component); @@ -87,14 +120,6 @@ void remove(Component component) { } } - int getMaxXOrThrow() { - return this.valuesByMaxX.lastKey(); - } - - int getMaxYOrThrow() { - return this.valuesByMaxY.lastKey(); - } - boolean noneFillX() { return this.xFillers.isEmpty(); } @@ -104,23 +129,23 @@ boolean noneFillY() { } boolean isEmpty() { - return this.valuesByMaxX.isEmpty(); + return this.componentPositions.isEmpty(); } void forEach(EntriesConsumer action) { - this.grid.forEach((x, column) -> { - column.forEach((y, constrainedByComponent) -> { + this.grid.forEach((y, row) -> { + row.forEach((x, constrainedByComponent) -> { action.accept(x, y, constrainedByComponent.values().stream()); }); }); } Stream map(EntryFunction mapper) { - return this.grid.entrySet().stream().flatMap(columnEntry -> columnEntry + return this.grid.entrySet().stream().flatMap(rowEntry -> rowEntry .getValue() .entrySet() .stream() - .flatMap(rowEntry -> rowEntry + .flatMap(columnEntry -> columnEntry .getValue() .values() .stream() diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 5f7625db9..851ded5c8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -158,17 +158,12 @@ public void addLayoutComponent(Component component, @Nullable FlexGridConstraint if (constraints == null) { this.addDefaultConstrainedLayoutComponent(component); } else { - final int x; - final int y; + final Constrained constrained = Constrained.of(component, constraints); if (constraints instanceof FlexGridConstraints.Absolute absolute) { - x = absolute.getX(); - y = absolute.getY(); + this.grid.put(absolute.getX(), absolute.getY(), constrained); } else { - x = this.getRelativeX(); - y = this.getRelativeY(); + this.grid.putRelative(constrained); } - - this.grid.put(x, y, Constrained.of(component, constraints)); } } @@ -178,16 +173,7 @@ public void addLayoutComponent(String ignored, Component component) { } private void addDefaultConstrainedLayoutComponent(Component component) { - this.grid.put(this.getRelativeX(), this.getRelativeY(), Constrained.defaultOf(component)); - } - - private int getRelativeX() { - // TODO this gives max x, but should give max x *of bottom row* - return this.grid.isEmpty() ? FlexGridConstraints.Absolute.DEFAULT_X : this.grid.getMaxXOrThrow() + 1; - } - - private int getRelativeY() { - return this.grid.isEmpty() ? FlexGridConstraints.Absolute.DEFAULT_Y : this.grid.getMaxYOrThrow(); + this.grid.putRelative(Constrained.defaultOf(component)); } @Override @@ -682,7 +668,6 @@ void setBounds(Component component, int y, int height) { abstract int getTotalSpace(Sizes sizes); abstract ImmutableSortedMap getCellSpans(Sizes sizes); - abstract int getSpan(Size size); abstract boolean fills(Constrained constrained); From 00dbc54f5aee8ae9e24a8a1e66f7e7a847c01acf Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 8 Dec 2025 15:12:47 -0800 Subject: [PATCH 096/110] add note that FlexGridLayout does not support insets --- .../enigma/gui/util/layout/flex_grid/FlexGridLayout.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 851ded5c8..d8843e513 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -7,6 +7,8 @@ import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment; +import javax.swing.JComponent; +import javax.swing.border.Border; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; @@ -35,6 +37,7 @@ * allocated according to {@linkplain FlexGridConstraints#priority(int) priority} and position *
  • flex grids respect each component's {@linkplain Component#getMaximumSize() maximum size} *
  • flex grids support negative coordinates + *
  • flex grids do not support insets; use {@linkplain JComponent#setBorder(Border) borders} instead * * *

    Constraints

    From ec91fda4cce346d1b05471648caba14947d3c522 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 8 Dec 2025 15:44:17 -0800 Subject: [PATCH 097/110] repaint before packing --- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 3af232d87..f787fbe89 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -317,6 +317,7 @@ public void mouseClicked(MouseEvent e) { final Dimension oldSize = opening ? null : this.getSize(); final Point oldMousePos = MouseInfo.getPointerInfo().getLocation(); this.declarationSnippet.addSourceSetListener(source -> { + this.repaint(); // JTextAreas (javadocs) adjust their preferred sizes after the first pack, so pack twice this.pack(); // There seems to be a race condition when packing twice in a row where @@ -324,7 +325,6 @@ public void mouseClicked(MouseEvent e) { // based on the second pack. // Using invokeLater for *only* the second pack *seems* to solve it. SwingUtilities.invokeLater(this::pack); - this.repaint(); if (this.declarationSnippet != null) { // without this, the editor gets focus and has a blue border @@ -364,8 +364,8 @@ public void mouseClicked(MouseEvent e) { } } - this.pack(); this.repaint(); + this.pack(); if (opening) { this.moveNearCursor(); From 6dcd10b7539f2be4da636f84fa4b672162941b38 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 8 Dec 2025 15:53:37 -0800 Subject: [PATCH 098/110] make 'no package', 'synthetic', and 'anonymous' translatable --- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 13 ++++++++++--- enigma/src/main/resources/lang/en_us.json | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index f787fbe89..0af6c259a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -573,6 +573,10 @@ private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopI return text; } + private static String translatePlaceholder(String key) { + return "<%s>".formatted(I18n.translate(key)); + } + private ImmutableList paramJavadocsOf( Entry target, Font nameFont, Font javadocFont, MouseAdapter stopInteraction ) { @@ -681,7 +685,10 @@ public void mouseClicked(MouseEvent e) { parentClicked = null; } - final JLabel parentLabel = new JLabel(nameBuilder.isEmpty() ? "" : nameBuilder.toString()); + final JLabel parentLabel = new JLabel(nameBuilder.isEmpty() + ? translatePlaceholder("editor.tooltip.label.no_package") + : nameBuilder.toString() + ); final Font parentFont; if (parentClicked == null) { @@ -779,12 +786,12 @@ private String getSimpleName(Entry entry) { if (access == null || !(access.isSynthetic())) { return project.getRemapper().deobfuscate(entry).getSimpleName(); } else { - return ""; + return translatePlaceholder("editor.tooltip.label.synthetic"); } } } - return ""; + return translatePlaceholder("editor.tooltip.label.anonymous"); } public void setZoom(int amount) { diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 637e50e9d..a4e8fc731 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -171,6 +171,9 @@ "editor.quick_find.persistent": "Persistent", "editor.tooltip.label.from": "from", "editor.tooltip.label.inherited_from": "inherited from", + "editor.tooltip.label.no_package": "no package", + "editor.tooltip.label.synthetic": "synthetic", + "editor.tooltip.label.anonymous": "anonymous", "editor.tooltip.message.no_source": "No source available", "editor.snippet.message.no_declaration_found": "Unable to locate declaration", From 8c7aab4d81ff9709caa4a6201e6a94fb9297a69f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 9 Dec 2025 11:47:49 -0800 Subject: [PATCH 099/110] account for screen insets when positioning EntryTooltip (tooltip doesn't intersect dock) put post-click repopulating moving in SwingUtilities::invokeLater after re-pack so it positions based on updated size --- .../enigma/gui/panel/EntryTooltip.java | 93 ++++++++++++------- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 22 +++++ 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 0af6c259a..e79050826 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -41,6 +41,7 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.Font; +import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Toolkit; @@ -318,6 +319,13 @@ public void mouseClicked(MouseEvent e) { final Point oldMousePos = MouseInfo.getPointerInfo().getLocation(); this.declarationSnippet.addSourceSetListener(source -> { this.repaint(); + + if (this.declarationSnippet != null) { + // without this, the editor gets focus and has a blue border + // but only when it's in a scroll pane, for some reason + this.declarationSnippet.ui.requestFocus(); + } + // JTextAreas (javadocs) adjust their preferred sizes after the first pack, so pack twice this.pack(); // There seems to be a race condition when packing twice in a row where @@ -326,23 +334,19 @@ public void mouseClicked(MouseEvent e) { // Using invokeLater for *only* the second pack *seems* to solve it. SwingUtilities.invokeLater(this::pack); - if (this.declarationSnippet != null) { - // without this, the editor gets focus and has a blue border - // but only when it's in a scroll pane, for some reason - this.declarationSnippet.ui.requestFocus(); - } - - if (oldSize == null) { - // opening - if (oldMousePos.distance(MouseInfo.getPointerInfo().getLocation()) < SMALL_MOVE_THRESHOLD) { - this.moveNearCursor(); + SwingUtilities.invokeLater(() -> { + if (oldSize == null) { + // opening + if (oldMousePos.distance(MouseInfo.getPointerInfo().getLocation()) < SMALL_MOVE_THRESHOLD) { + this.moveNearCursor(); + } else { + this.moveOnScreen(); + } } else { - this.moveOnScreen(); + // not opening + this.moveMaintainingAnchor(oldMousePos, oldSize); } - } else { - // not opening - this.moveMaintainingAnchor(oldMousePos, oldSize); - } + }); }); } @@ -419,16 +423,22 @@ private void moveNearCursor() { } final Dimension size = this.getSize(); - final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final Toolkit toolkit = Toolkit.getDefaultToolkit(); + final Dimension screenSize = toolkit.getScreenSize(); + final Point mousePos = MouseInfo.getPointerInfo().getLocation(); + final Insets screenInsets = GuiUtil.findGraphicsConfig(mousePos.x, mousePos.y) + .map(toolkit::getScreenInsets) + .orElse(new Insets(0, 0, 0, 0)); + final int x = findCoordinateSpace( - size.width, screenSize.width, + size.width, screenInsets.left, screenSize.width - screenInsets.right, mousePos.x - MOUSE_PAD, mousePos.x + MOUSE_PAD ); final int y = findCoordinateSpace( - size.height, screenSize.height, + size.height, screenInsets.top, screenSize.height - screenInsets.bottom, mousePos.y - MOUSE_PAD, mousePos.y + MOUSE_PAD ); @@ -481,16 +491,26 @@ private void moveMaintainingAnchor(Point oldMousePos, Dimension oldSize) { anchoredY = pos.y + yDiff; } - final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - final int targetX = Utils.clamp(anchoredX, 0, screenSize.width - newSize.width); - final int targetY = Utils.clamp(anchoredY, 0, screenSize.height - newSize.height); + final Toolkit toolkit = Toolkit.getDefaultToolkit(); + final Dimension screenSize = toolkit.getScreenSize(); + final Insets screenInsets = GuiUtil.findGraphicsConfig(pos.x, pos.y) + .map(toolkit::getScreenInsets) + .orElse(new Insets(0, 0, 0, 0)); + + final int targetX = Utils.clamp( + anchoredX, screenInsets.left, + screenSize.width - screenInsets.right - newSize.width + ); + final int targetY = Utils.clamp( + anchoredY, screenInsets.top, + screenSize.height - screenInsets.bottom - newSize.height + ); if (targetX != pos.x || targetY != pos.y) { this.setLocation(targetX, targetY); } } - // TODO account for screen insets /** * Ensures this is entirely on-screen. */ @@ -499,20 +519,21 @@ private void moveOnScreen() { return; } - final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - final Dimension size = this.getSize(); final Point pos = this.getLocationOnScreen(); + final Toolkit toolkit = Toolkit.getDefaultToolkit(); + final Dimension screenSize = toolkit.getScreenSize(); + final Insets screenInsets = GuiUtil.findGraphicsConfig(pos.x, pos.y) + .map(toolkit::getScreenInsets) + .orElse(new Insets(0, 0, 0, 0)); + final Dimension size = this.getSize(); - final int xOffScreen = pos.x + size.width - screenSize.width; - final int yOffScreen = pos.y + size.height - screenSize.height; - - final boolean moveX = xOffScreen > 0; - final boolean moveY = yOffScreen > 0; + final int offRight = pos.x + size.width - screenSize.width - screenInsets.right; + final int x = Math.max(screenInsets.left, offRight > 0 ? pos.x - offRight : pos.x); - if (moveX || moveY) { - final int x = pos.x - (moveX ? xOffScreen : 0); - final int y = pos.y - (moveY ? yOffScreen : 0); + final int offBottom = pos.y + size.height - screenSize.height - screenInsets.bottom; + final int y = Math.max(screenInsets.top, offBottom > 0 ? pos.y - offBottom : pos.y); + if (x != pos.x || y != pos.y) { this.setLocation(x, y); } } @@ -526,17 +547,17 @@ private void onEntryClick(Entry entry, int modifiers) { } } - private static int findCoordinateSpace(int size, int screenSize, int mouseMin, int mouseMax) { - final double spaceAfter = screenSize - mouseMax; + private static int findCoordinateSpace(int size, int screenMin, int screenMax, int mouseMin, int mouseMax) { + final double spaceAfter = screenMax - mouseMax; if (spaceAfter >= size) { return mouseMax; } else { final int spaceBefore = mouseMin - size; - if (spaceBefore >= 0) { + if (spaceBefore >= screenMin) { return spaceBefore; } else { // doesn't fit before or after; align with screen edge that gives more space - return spaceAfter < spaceBefore ? 0 : screenSize - size; + return spaceAfter < spaceBefore ? 0 : screenMax - size; } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java index 427e4717c..9b07b8ee2 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java @@ -44,6 +44,9 @@ import java.awt.Cursor; import java.awt.Desktop; import java.awt.Font; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Toolkit; @@ -548,6 +551,25 @@ public static JMenu createIntConfigRadioMenu( return menu; } + // based on JPopupMenu::getCurrentGraphicsConfiguration + /** + * @return an {@link Optional} holding the {@link GraphicsConfiguration} of the + * {@linkplain GraphicsEnvironment#getScreenDevices() screen device} that contains the passed + * {@code x} and {@code y} coordinates if one could be found, or {@link Optional#empty()} otherwise + */ + public static Optional findGraphicsConfig(int x, int y) { + for (final GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { + if (device.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) { + final GraphicsConfiguration config = device.getDefaultConfiguration(); + if (config.getBounds().contains(x, y)) { + return Optional.of(config); + } + } + } + + return Optional.empty(); + } + public enum FocusCondition { /** * @see JComponent#WHEN_IN_FOCUSED_WINDOW From 04d9b64fcd081c70e1a707504341ff41f3e9e67b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 9 Dec 2025 15:06:42 -0800 Subject: [PATCH 100/110] use ImmutableMaps with correct order instead of SortedMaps for cellSpans --- .../util/layout/flex_grid/FlexGridLayout.java | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index d8843e513..bb3e8443c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -261,7 +261,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { if (extraSpace == 0 || ops.noneFill(this.grid)) { this.layoutAxisImpl(leadingInset + extraSpace / 2, ops, ops.getCellSpans(preferred)); } else { - final SortedMap cellSpans = this.allocateCellSpace(ops, extraSpace, true); + final ImmutableMap cellSpans = this.allocateCellSpace(ops, extraSpace, true); final int allocatedSpace = sumOrMax(cellSpans.values()); final int startPos = leadingInset + (availableSpace - allocatedSpace) / 2; @@ -275,14 +275,16 @@ private void layoutAxis(Container parent, CartesianOperations ops) { if (extraMinSpace <= 0) { this.layoutAxisImpl(leadingInset, ops, ops.getCellSpans(min)); } else { - final SortedMap cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); + final ImmutableMap cellSpans = this.allocateCellSpace(ops, extraMinSpace, false); this.layoutAxisImpl(leadingInset, ops, cellSpans); } } } - private SortedMap allocateCellSpace(CartesianOperations ops, int remainingSpace, boolean fill) { + private ImmutableMap allocateCellSpace( + CartesianOperations ops, int remainingSpace, boolean fill + ) { final Sizes large; final Sizes small; if (fill) { @@ -342,10 +344,14 @@ private SortedMap allocateCellSpace(CartesianOperations ops, i } } - return cellSpans; + // ImmutableMaps maintain order and provide O(1) lookups + return ImmutableMap.copyOf(cellSpans); } - private void layoutAxisImpl(int startPos, CartesianOperations ops, SortedMap cellSpans) { + /** + * @implNote the passed {@code cellSpans} must be ordered with increasing keys + */ + private void layoutAxisImpl(int startPos, CartesianOperations ops, ImmutableMap cellSpans) { final Map beginPositions = new HashMap<>(); int currentPos = startPos; for (final Map.Entry entry : cellSpans.entrySet()) { @@ -370,7 +376,9 @@ private void layoutAxisImpl(int startPos, CartesianOperations ops, SortedMap rowHeights, ImmutableSortedMap columnWidths, + ImmutableMap rowHeights, ImmutableMap columnWidths, ImmutableMap componentSizes ) { static Sizes EMPTY = new Sizes( @@ -505,8 +515,8 @@ static Sizes calculate(ConstrainedGrid grid, Function getS }); }); - final Map rowHeights = new HashMap<>(); - final Map columnWidths = new HashMap<>(); + final SortedMap rowHeights = new TreeMap<>(); + final SortedMap columnWidths = new TreeMap<>(); cellSizes.forEach((x, column) -> { column.forEach((y, size) -> { rowHeights.compute(y, (ignored, height) -> height == null @@ -523,7 +533,8 @@ static Sizes calculate(ConstrainedGrid grid, Function getS return new Sizes( sumOrMax(columnWidths.values()), sumOrMax(rowHeights.values()), - ImmutableSortedMap.copyOf(rowHeights), ImmutableSortedMap.copyOf(columnWidths), + // ImmutableMaps maintain order and provide O(1) lookups + ImmutableMap.copyOf(rowHeights), ImmutableMap.copyOf(columnWidths), ImmutableMap.copyOf(componentSizes) ); } @@ -567,7 +578,7 @@ int getTotalSpace(Sizes sizes) { } @Override - ImmutableSortedMap getCellSpans(Sizes sizes) { + ImmutableMap getCellSpans(Sizes sizes) { return sizes.columnWidths; } @@ -628,7 +639,7 @@ int getTotalSpace(Sizes sizes) { } @Override - ImmutableSortedMap getCellSpans(Sizes sizes) { + ImmutableMap getCellSpans(Sizes sizes) { return sizes.rowHeights; } @@ -670,7 +681,7 @@ void setBounds(Component component, int y, int height) { abstract int getParentSpace(Container parent); abstract int getTotalSpace(Sizes sizes); - abstract ImmutableSortedMap getCellSpans(Sizes sizes); + abstract ImmutableMap getCellSpans(Sizes sizes); abstract int getSpan(Size size); abstract boolean fills(Constrained constrained); From 9a341882b78a45f52747d58414b2a412a3f9d8c8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 9 Dec 2025 16:04:28 -0800 Subject: [PATCH 101/110] add SmartScrollPane and use it in DeclarationSnippetPanel --- ...FlexGridPriorityScrollPanesVisualizer.java | 16 +++------- .../gui/panel/DeclarationSnippetPanel.java | 5 +-- .../enigma/gui/panel/EntryTooltip.java | 2 +- .../enigma/gui/panel/SmartScrollPane.java | 32 +++++++++++++++++++ 4 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java index 0a85fc991..1afe93abe 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityScrollPanesVisualizer.java @@ -1,16 +1,13 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quiltmc.enigma.gui.panel.SmartScrollPane; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import javax.swing.JFrame; -import javax.swing.JScrollPane; import javax.swing.JTextArea; -import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; -import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; - public class FlexGridPriorityScrollPanesVisualizer implements Visualizer { @Override public String getTitle() { @@ -25,7 +22,7 @@ public void visualize(JFrame window) { final var firstText = new JTextArea( """ - 5 + 5 4 3 2 1 4 3 2 @@ -33,11 +30,11 @@ public void visualize(JFrame window) { """ ); - window.add(new JScrollPane(firstText, VERTICAL_SCROLLBAR_NEVER, HORIZONTAL_SCROLLBAR_NEVER), constraints); + window.add(new SmartScrollPane(firstText), constraints); final var secondText = new JTextArea( """ - e + e d c b a d c b @@ -45,10 +42,7 @@ public void visualize(JFrame window) { """ ); - window.add( - new JScrollPane(secondText, VERTICAL_SCROLLBAR_NEVER, HORIZONTAL_SCROLLBAR_NEVER), - constraints.nextRow().incrementPriority() - ); + window.add(new SmartScrollPane(secondText), constraints.nextRow().incrementPriority()); window.pack(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index 7cf494c60..7d92ae2a4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -72,9 +72,6 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.editor.setCaretColor(GuiUtil.TRANSPARENT); this.editor.getCaret().setSelectionVisible(true); - // TODO try custom scroll pane that accounts for scroll bars in pref size instead? - this.editorScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - this.addSourceSetListener(source -> { if (!this.isBounded()) { // the source isn't very useful if it couldn't be trimmed @@ -99,7 +96,7 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl @Override protected JScrollPane createEditorScrollPane(JEditorPane editor) { - return new JScrollPane(editor); + return new SmartScrollPane(editor); } private Snippet createSnippet(DecompiledClassSource source, Entry targetEntry) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index e79050826..365d27c38 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -280,7 +280,7 @@ public void mousePressed(MouseEvent e) { javadocs.add(params, javadocsConstraints.nextRow().copy().fillX()); } - final JScrollPane javadocsScroll = new JScrollPane(javadocs); + final JScrollPane javadocsScroll = new SmartScrollPane(javadocs); javadocsScroll.setBorder(createEmptyBorder()); this.add(javadocsScroll, constraints.nextRow().copy().fillX()); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java new file mode 100644 index 000000000..08792afd7 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java @@ -0,0 +1,32 @@ +package org.quiltmc.enigma.gui.panel; + +import javax.swing.JScrollPane; +import java.awt.Component; +import java.awt.Dimension; + +/** + * A {@link JScrollPane} with QoL improvements. + * + *

    Currently it just requests space for its scroll bars in {@link #getPreferredSize()}. + */ +public class SmartScrollPane extends JScrollPane { + // TODO create constructors using ScrollBarPolicy once #320's MarkableScrollPane is merged + public SmartScrollPane(Component view) { + super(view); + } + + @Override + public Dimension getPreferredSize() { + final Dimension size = super.getPreferredSize(); + + if (this.verticalScrollBar.isShowing()) { + size.width += this.verticalScrollBar.getPreferredSize().width; + } + + if (this.horizontalScrollBar.isShowing()) { + size.height += this.horizontalScrollBar.getPreferredSize().height; + } + + return size; + } +} From 69fb36ff3cd32c48e8bbac63c4e26e88c21c61a7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 9 Dec 2025 18:11:06 -0800 Subject: [PATCH 102/110] make MarkableScrollPane extend SmartScrollPane utilize new utils --- .../gui/panel/DeclarationSnippetPanel.java | 3 +- .../enigma/gui/panel/MarkableScrollPane.java | 66 +++++++------------ .../enigma/gui/panel/SmartScrollPane.java | 50 ++++++++++++-- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index 7d92ae2a4..4f552be59 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -48,7 +48,6 @@ import javax.swing.JEditorPane; import javax.swing.JScrollPane; -import java.awt.Color; import java.util.Comparator; import java.util.Optional; import java.util.function.Function; @@ -85,7 +84,7 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl .map(Target::token) .map(this::navigateToTokenImpl) .ifPresent(boundedToken -> this.addHighlight(boundedToken, BoxHighlightPainter.create( - new Color(0, 0, 0, 0), + GuiUtil.TRANSPARENT, Config.getCurrentSyntaxPaneColors().selectionHighlight.value() ))); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java index ca2359967..8a4a37ef7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/MarkableScrollPane.java @@ -11,8 +11,6 @@ import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; -import javax.swing.JScrollPane; -import javax.swing.ScrollPaneConstants; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; @@ -34,6 +32,8 @@ import java.util.Set; import java.util.TreeMap; +import static org.quiltmc.enigma.util.Arguments.requireNonNegative; + /** * A scroll pane that renders markers in its view along the right edge, to the left of the vertical scroll bar.
    * Markers support custom {@linkplain Color colors} and {@linkplain MarkerListener listeners}. @@ -51,11 +51,7 @@ * @see #removeMarker(Object) * @see MarkerListener */ -public class MarkableScrollPane extends JScrollPane { - private static void requireNonNegative(int value, String name) { - Preconditions.checkArgument(value >= 0, "%s (%s) must not be negative!".formatted(name, value)); - } - +public class MarkableScrollPane extends SmartScrollPane { private static final int DEFAULT_MARKER_WIDTH = 10; private static final int DEFAULT_MARKER_HEIGHT = 5; @@ -71,8 +67,8 @@ private static void requireNonNegative(int value, String name) { private MouseAdapter viewMouseAdapter; /** - * Constructs a scroll pane displaying the passed {@code view} and {@code maxConcurrentMarkers}, - * and {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bar policies. + * Constructs a scroll pane displaying the passed {@code view} and {@code maxConcurrentMarkers} + * with {@link ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bars. * * @see #MarkableScrollPane(Component, int, ScrollBarPolicy, ScrollBarPolicy) */ @@ -83,8 +79,8 @@ public MarkableScrollPane(@Nullable Component view, int maxConcurrentMarkers) { /** * @param view the component to display in this scroll pane's view port * @param maxConcurrentMarkers see {@link #setMaxConcurrentMarkers(int)} - * @param verticalPolicy the vertical scroll bar policy - * @param horizontalPolicy the horizontal scroll bar policy + * @param vertical the vertical scroll bar policy + * @param horizontal the horizontal scroll bar policy * * @throws IllegalArgumentException if {@code maxConcurrentMarkers} is negative * @@ -92,9 +88,9 @@ public MarkableScrollPane(@Nullable Component view, int maxConcurrentMarkers) { */ public MarkableScrollPane( @Nullable Component view, int maxConcurrentMarkers, - ScrollBarPolicy verticalPolicy, ScrollBarPolicy horizontalPolicy + ScrollBarPolicy vertical, ScrollBarPolicy horizontal ) { - super(view, verticalPolicy.vertical, horizontalPolicy.horizontal); + super(view, vertical, horizontal); this.setMaxConcurrentMarkers(maxConcurrentMarkers); @@ -352,32 +348,6 @@ private MarkersPainter markersPainterOf(List markers, int scaledPos, int } } - public enum ScrollBarPolicy { - /** - * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_AS_NEEDED - * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_AS_NEEDED - */ - AS_NEEDED(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED), - /** - * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_ALWAYS - * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_ALWAYS - */ - ALWAYS(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS), - /** - * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_NEVER - * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_NEVER - */ - NEVER(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); - - private final int horizontal; - private final int vertical; - - ScrollBarPolicy(int horizontal, int vertical) { - this.horizontal = horizontal; - this.vertical = vertical; - } - } - private class PaintState { final NavigableMap paintersByPos = new TreeMap<>(); @@ -420,7 +390,10 @@ void update() { final MarkersPainterBuilder builder = builderByScaledPos.computeIfAbsent(scaledPos, builderPos -> { final int top = Math.max(builderPos - MarkableScrollPane.this.markerHeight / 2, this.areaY); - final int bottom = Math.min(top + MarkableScrollPane.this.markerHeight, this.areaY + this.areaHeight); + final int bottom = Math.min( + top + MarkableScrollPane.this.markerHeight, + this.areaY + this.areaHeight + ); return new MarkersPainterBuilder(pos, scaledPos, top, bottom); }); @@ -518,7 +491,10 @@ Optional findListenerPos(int x, int y) { final Point absolutePos = GuiUtil .getAbsolutePos(MarkableScrollPane.this, this.areaX, painter.scaledPos); - return new ListenerPos(span.getMarker().listener.orElseThrow(), absolutePos.x, absolutePos.y); + return new ListenerPos( + span.getMarker().listener.orElseThrow(), + absolutePos.x, absolutePos.y + ); }) .stream() ) @@ -637,7 +613,11 @@ MarkersPainter build(int x) { .limit(MarkableScrollPane.this.maxConcurrentMarkers) .toList(); - return MarkableScrollPane.this.markersPainterOf(markers, this.scaledPos, x, this.top, this.bottom - this.top); + return MarkableScrollPane.this.markersPainterOf( + markers, this.scaledPos, + x, this.top, + this.bottom - this.top + ); } } @@ -673,7 +653,7 @@ public interface MarkerListener { void mouseTransferred(int x, int y); /** - * Called when the mouse within the marker. + * Called when the mouse moves within the marker. */ void mouseMoved(int x, int y); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java index 08792afd7..2bd49b5e5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/SmartScrollPane.java @@ -1,18 +1,34 @@ package org.quiltmc.enigma.gui.panel; +import org.jspecify.annotations.Nullable; + import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; import java.awt.Component; import java.awt.Dimension; /** * A {@link JScrollPane} with QoL improvements. * - *

    Currently it just requests space for its scroll bars in {@link #getPreferredSize()}. + *

    Currently it requests space for its scroll bars in {@link #getPreferredSize()} and uses + * {@link ScrollBarPolicy ScrollBarPolicy} instead of magic constants. */ public class SmartScrollPane extends JScrollPane { - // TODO create constructors using ScrollBarPolicy once #320's MarkableScrollPane is merged - public SmartScrollPane(Component view) { - super(view); + /** + * Constructs a scroll pane displaying the passed {@code view} + * with {@link SmartScrollPane.ScrollBarPolicy#AS_NEEDED AS_NEEDED} scroll bars. + * + * @see #SmartScrollPane(Component, ScrollBarPolicy, ScrollBarPolicy) + */ + public SmartScrollPane(@Nullable Component view) { + this(view, ScrollBarPolicy.AS_NEEDED, ScrollBarPolicy.AS_NEEDED); + } + + /** + * @see #SmartScrollPane(Component) + */ + public SmartScrollPane(@Nullable Component view, ScrollBarPolicy vertical, ScrollBarPolicy horizontal) { + super(view, vertical.vertical, horizontal.horizontal); } @Override @@ -29,4 +45,30 @@ public Dimension getPreferredSize() { return size; } + + public enum ScrollBarPolicy { + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_AS_NEEDED + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_AS_NEEDED + */ + AS_NEEDED(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED), + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_ALWAYS + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_ALWAYS + */ + ALWAYS(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS), + /** + * @see ScrollPaneConstants#HORIZONTAL_SCROLLBAR_NEVER + * @see ScrollPaneConstants#VERTICAL_SCROLLBAR_NEVER + */ + NEVER(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + + public final int horizontal; + public final int vertical; + + ScrollBarPolicy(int horizontal, int vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + } } From 4d0a28a3751f1da628f67de654db5e307d13d0b1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 11 Dec 2025 15:31:43 -0800 Subject: [PATCH 103/110] implement FlexGridConstraints.Relative.Placement update visualizers --- .../FlexGridAlignAndFillVisualizer.java | 3 +- .../FlexGridAlignmentVisualizer.java | 3 +- .../flex_grid/FlexGridFillVisualizer.java | 3 +- .../FlexGridPriorityFillVisualizer.java | 3 +- .../flex_grid/FlexGridPriorityVisualizer.java | 3 +- .../flex_grid/FlexGridQuiltVisualiser.java | 79 +------- ...exGridRelativeExtentOverlapVisualizer.java | 22 ++- .../FlexGridRelativeRowsVisualizer.java | 8 +- .../flex_grid/FlexGridSparseVisualizer.java | 3 +- .../gui/visualization/util/VisualBox.java | 13 +- .../gui/visualization/util/VisualUtils.java | 96 +++++++++ .../layout/flex_grid/ConstrainedGrid.java | 185 +++++++++++++++--- .../util/layout/flex_grid/FlexGridLayout.java | 61 +++++- .../constraints/FlexGridConstraints.java | 125 +++++++++++- 14 files changed, 456 insertions(+), 151 deletions(-) create mode 100644 enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualUtils.java diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java index 83309047d..faed18c42 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignAndFillVisualizer.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualUtils; import javax.swing.JFrame; @@ -12,7 +13,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, c -> c.fillNone().alignTopLeft(), c -> c.fillOnlyY().alignTopCenter(), diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java index 8ad07f2ba..828d7779c 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridAlignmentVisualizer.java @@ -2,6 +2,7 @@ import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; @@ -21,7 +22,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, Absolute::alignTopLeft, Absolute::alignTopCenter, Absolute::alignTopRight, Absolute::alignCenterLeft, Absolute::alignCenter, Absolute::alignCenterRight, diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java index 754fb4df4..b41a9469f 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridFillVisualizer.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import javax.swing.JFrame; @@ -13,7 +14,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, FlexGridConstraints::fillNone, FlexGridConstraints::fillOnlyY, FlexGridConstraints::fillNone, FlexGridConstraints::fillOnlyX, FlexGridConstraints::fillBoth, FlexGridConstraints::fillOnlyX, diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java index 54db4311d..582818563 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityFillVisualizer.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualUtils; import javax.swing.JFrame; @@ -12,7 +13,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, c -> c.fillOnlyX().incrementPriority(), c -> c.fillOnlyY().incrementPriority(), diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java index 1b4cb68f7..c1a889ed4 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridPriorityVisualizer.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; import javax.swing.JFrame; @@ -14,7 +15,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, UnaryOperator.identity(), Absolute::incrementPriority, Absolute::incrementPriority, Absolute::incrementPriority, Absolute::incrementPriority, Absolute::incrementPriority, diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java index 55808f419..b6d3fcbe1 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridQuiltVisualiser.java @@ -1,87 +1,12 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; -import org.quilt.internal.gui.visualization.util.VisualBox; -import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; -import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; -import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Absolute; +import org.quilt.internal.gui.visualization.util.VisualUtils; import javax.swing.JFrame; import java.util.function.UnaryOperator; public class FlexGridQuiltVisualiser implements Visualizer { - /** - * Visualizes Quilt's logo, giving each patch a name indicating its coordinates. - * - * @see #visualizeQuilt(JFrame, - * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, - * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, - * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator) - */ - public static void visualizeQuilt( - JFrame window, - UnaryOperator constrainer1, - UnaryOperator constrainer2, - UnaryOperator constrainer3, - - UnaryOperator constrainer4, - UnaryOperator constrainer5, - UnaryOperator constrainer6, - - UnaryOperator constrainer7, - UnaryOperator constrainer8, - UnaryOperator constrainer9 - ) { - visualizeQuilt( - window, - "(0, 0)", constrainer1, "(1, 0)", constrainer2, "(2, 0)", constrainer3, - "(0, 1)", constrainer4, "(1, 1)", constrainer5, "(2, 1)", constrainer6, - "(0, 2)", constrainer7, "(1, 2)", constrainer8, "(2, 2)", constrainer9 - ); - } - - /** - * Gives the passed {@code window} a {@link FlexGridLayout} and forms Quilt's logo out of a 3 x 3 grid of - * {@link VisualBox} patches. - * - *

    The patches are given the passed names and their constraints are adjusted using the passed constrainers.
    - * The same {@link FlexGridConstraints} instance is passed to each constrainer, and its x and y coordinates are - * updated for each patch before passing it. - * - * @see #visualizeQuilt(JFrame, - * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, - * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator) - */ - public static void visualizeQuilt( - JFrame window, - String name1, UnaryOperator constrainer1, - String name2, UnaryOperator constrainer2, - String name3, UnaryOperator constrainer3, - - String name4, UnaryOperator constrainer4, - String name5, UnaryOperator constrainer5, - String name6, UnaryOperator constrainer6, - - String name7, UnaryOperator constrainer7, - String name8, UnaryOperator constrainer8, - String name9, UnaryOperator constrainer9 - ) { - window.setLayout(new FlexGridLayout()); - - final Absolute constraints = FlexGridConstraints.createAbsolute(); - window.add(VisualBox.purplePatchOf(name1), constrainer1.apply(constraints)); - window.add(VisualBox.magentaPatchOf(name2), constrainer2.apply(constraints.nextColumn())); - window.add(VisualBox.cyanPatchOf(name3), constrainer3.apply(constraints.nextColumn())); - - window.add(VisualBox.magentaPatchOf(name4), constrainer4.apply(constraints.nextRow())); - window.add(VisualBox.cyanPatchOf(name5), constrainer5.apply(constraints.nextColumn())); - window.add(VisualBox.bluePatchOf(name6), constrainer6.apply(constraints.nextColumn())); - - window.add(VisualBox.purplePatchOf(name7), constrainer7.apply(constraints.nextRow())); - window.add(VisualBox.bluePatchOf(name8), constrainer8.apply(constraints.nextColumn())); - window.add(VisualBox.purplePatchOf(name9), constrainer9.apply(constraints.nextColumn())); - } - @Override public String getTitle() { return "Flex Grid Quilt"; @@ -89,7 +14,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), UnaryOperator.identity(), diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java index f158a2985..caf4e2293 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeExtentOverlapVisualizer.java @@ -2,6 +2,7 @@ import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; @@ -19,14 +20,14 @@ public String getTitle() { /** *

    
    -	 * -----------------
    -	 * |           | R |
    -	 * |   --------+----
    -	 * |   |       |
    -	 * |   |   -----
    +	 * ---------------------
    +	 * |           | R | P |
    +	 * |   --------+--------
    +	 * |   |       |   | O |
    +	 * |   |   -----   -----
     	 * |   |   |   |
     	 * ----+---+----
    -	 * |   | B |
    +	 * | G | B |
     	 * ---------
     	 * 
    */ @@ -42,10 +43,13 @@ public void visualize(JFrame window) { ); } - window.add(VisualBox.of(Color.RED, SQUARE_SIZE)); + window.add(VisualBox.of(Color.RED, SQUARE_SIZE), FlexGridConstraints.createRelative().rowEnd()); - window.add(VisualBox.of(SQUARE_SIZE), FlexGridConstraints.createAbsolute().y(MAX_EXTENT)); - window.add(VisualBox.of(Color.BLUE, SQUARE_SIZE)); + window.add(VisualBox.of(Color.GREEN, SQUARE_SIZE), FlexGridConstraints.createRelative().newRow()); + window.add(VisualBox.of(Color.BLUE, SQUARE_SIZE), FlexGridConstraints.createRelative().rowEnd()); + + window.add(VisualBox.of(VisualUtils.PURPLE, SQUARE_SIZE), FlexGridConstraints.createRelative().newColumn()); + window.add(VisualBox.of(VisualUtils.ORANGE, SQUARE_SIZE), FlexGridConstraints.createRelative().columnEnd()); window.pack(); } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java index bc927ba9d..56137ef0c 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridRelativeRowsVisualizer.java @@ -2,6 +2,7 @@ import org.quilt.internal.gui.visualization.Visualizer; import org.quilt.internal.gui.visualization.util.VisualBox; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; @@ -19,13 +20,12 @@ public void visualize(JFrame window) { window.setLayout(new FlexGridLayout()); window.add(VisualBox.of(Color.RED)); - window.add(VisualBox.of(new Color(255, 128, 0))); + window.add(VisualBox.of(VisualUtils.ORANGE)); window.add(VisualBox.of(Color.YELLOW)); - // force next row with absolute - window.add(VisualBox.of(Color.GREEN), FlexGridConstraints.createAbsolute().y(1)); + window.add(VisualBox.of(Color.GREEN), FlexGridConstraints.createRelative().newRow()); window.add(VisualBox.of(Color.BLUE)); - window.add(VisualBox.of(new Color(128, 0, 255))); + window.add(VisualBox.of(VisualUtils.PURPLE)); window.pack(); } diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java index a8aa13a76..dc2e0d61d 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/flex_grid/FlexGridSparseVisualizer.java @@ -1,6 +1,7 @@ package org.quilt.internal.gui.visualization.flex_grid; import org.quilt.internal.gui.visualization.Visualizer; +import org.quilt.internal.gui.visualization.util.VisualUtils; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import javax.swing.JFrame; @@ -19,7 +20,7 @@ public String getTitle() { @Override public void visualize(JFrame window) { - FlexGridQuiltVisualiser.visualizeQuilt( + VisualUtils.visualizeFlexGridQuilt( window, c -> c.pos(-STEP, -STEP), FlexGridSparseVisualizer::stepColumns, FlexGridSparseVisualizer::stepColumns, c -> c.pos(-STEP, 0), FlexGridSparseVisualizer::stepColumns, FlexGridSparseVisualizer::stepColumns, diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java index d0e063f7f..bb8a69ee5 100644 --- a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualBox.java @@ -27,11 +27,6 @@ public class VisualBox extends JPanel { public static final int DEFAULT_SIZE = 100; - public static final Color PATCH_PURPLE = new Color(151, 34, 255); - public static final Color PATCH_MAGENTA = new Color(220, 41, 221); - public static final Color PATCH_CYAN = new Color(39, 162, 253); - public static final Color PATCH_BLUE = new Color(51, 68, 255); - private static final String MIN_WIDTH = "minWidth"; private static final String MIN_HEIGHT = "minHeight"; private static final String PREFERRED_WIDTH = "preferredWidth"; @@ -116,7 +111,7 @@ public static VisualBox purplePatchOf() { } public static VisualBox purplePatchOf(@Nullable String name) { - return of(name, PATCH_PURPLE); + return of(name, VisualUtils.PATCH_PURPLE); } public static VisualBox magentaPatchOf() { @@ -124,7 +119,7 @@ public static VisualBox magentaPatchOf() { } public static VisualBox magentaPatchOf(@Nullable String name) { - return of(name, PATCH_MAGENTA); + return of(name, VisualUtils.PATCH_MAGENTA); } public static VisualBox cyanPatchOf() { @@ -132,7 +127,7 @@ public static VisualBox cyanPatchOf() { } public static VisualBox cyanPatchOf(@Nullable String name) { - return of(name, PATCH_CYAN); + return of(name, VisualUtils.PATCH_CYAN); } public static VisualBox bluePatchOf() { @@ -140,7 +135,7 @@ public static VisualBox bluePatchOf() { } public static VisualBox bluePatchOf(@Nullable String name) { - return of(name, PATCH_BLUE); + return of(name, VisualUtils.PATCH_BLUE); } private final int minWidth; diff --git a/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualUtils.java b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualUtils.java new file mode 100644 index 000000000..a3e7e6d33 --- /dev/null +++ b/enigma-swing/src/guiVisualization/java/org/quilt/internal/gui/visualization/util/VisualUtils.java @@ -0,0 +1,96 @@ +package org.quilt.internal.gui.visualization.util; + +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout; +import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; + +import javax.swing.JFrame; +import java.awt.Color; +import java.util.function.UnaryOperator; + +public final class VisualUtils { + private VisualUtils() { + throw new UnsupportedOperationException(); + } + + // Color.ORANGE is not very orange + public static final Color ORANGE = new Color(255, 128, 0); + public static final Color PURPLE = new Color(128, 0, 255); + + // Quilt patch colors + public static final Color PATCH_PURPLE = new Color(151, 34, 255); + public static final Color PATCH_MAGENTA = new Color(220, 41, 221); + public static final Color PATCH_CYAN = new Color(39, 162, 253); + public static final Color PATCH_BLUE = new Color(51, 68, 255); + + /** + * Visualizes Quilt's logo, giving each patch a name indicating its coordinates. + * + * @see #visualizeFlexGridQuilt(JFrame, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator, + * String, UnaryOperator, String, UnaryOperator, String, UnaryOperator) + */ + public static void visualizeFlexGridQuilt( + JFrame window, + UnaryOperator constrainer1, + UnaryOperator constrainer2, + UnaryOperator constrainer3, + + UnaryOperator constrainer4, + UnaryOperator constrainer5, + UnaryOperator constrainer6, + + UnaryOperator constrainer7, + UnaryOperator constrainer8, + UnaryOperator constrainer9 + ) { + visualizeFlexGridQuilt( + window, + "(0, 0)", constrainer1, "(1, 0)", constrainer2, "(2, 0)", constrainer3, + "(0, 1)", constrainer4, "(1, 1)", constrainer5, "(2, 1)", constrainer6, + "(0, 2)", constrainer7, "(1, 2)", constrainer8, "(2, 2)", constrainer9 + ); + } + + /** + * Gives the passed {@code window} a {@link FlexGridLayout} and forms Quilt's logo out of a 3 x 3 grid of + * {@link VisualBox} patches. + * + *

    The patches are given the passed names and their constraints are adjusted using the passed constrainers.
    + * The same {@link FlexGridConstraints} instance is passed to each constrainer, and its x and y coordinates are + * updated for each patch before passing it. + * + * @see #visualizeFlexGridQuilt(JFrame, + * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator, + * UnaryOperator, UnaryOperator, UnaryOperator, UnaryOperator) + */ + public static void visualizeFlexGridQuilt( + JFrame window, + String name1, UnaryOperator constrainer1, + String name2, UnaryOperator constrainer2, + String name3, UnaryOperator constrainer3, + + String name4, UnaryOperator constrainer4, + String name5, UnaryOperator constrainer5, + String name6, UnaryOperator constrainer6, + + String name7, UnaryOperator constrainer7, + String name8, UnaryOperator constrainer8, + String name9, UnaryOperator constrainer9 + ) { + window.setLayout(new FlexGridLayout()); + + final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); + window.add(VisualBox.purplePatchOf(name1), constrainer1.apply(constraints)); + window.add(VisualBox.magentaPatchOf(name2), constrainer2.apply(constraints.nextColumn())); + window.add(VisualBox.cyanPatchOf(name3), constrainer3.apply(constraints.nextColumn())); + + window.add(VisualBox.magentaPatchOf(name4), constrainer4.apply(constraints.nextRow())); + window.add(VisualBox.cyanPatchOf(name5), constrainer5.apply(constraints.nextColumn())); + window.add(VisualBox.bluePatchOf(name6), constrainer6.apply(constraints.nextColumn())); + + window.add(VisualBox.purplePatchOf(name7), constrainer7.apply(constraints.nextRow())); + window.add(VisualBox.bluePatchOf(name8), constrainer8.apply(constraints.nextColumn())); + window.add(VisualBox.purplePatchOf(name9), constrainer9.apply(constraints.nextColumn())); + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 5f2609dae..33334a14f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid; +import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.CartesianOperations; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.Constrained; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; @@ -27,8 +28,16 @@ class ConstrainedGrid { private final Map componentPositions = new HashMap<>(); // outer sorted map maps constrained max y to rows // mid sorted map maps constrained max x to values by min y - // used to find relative position - private final SortedMap>>> maxGrid = new TreeMap<>(); + // used to find relative placements + private final SortedMap>>> maxYXGrid = new TreeMap<>(); + // outer sorted map maps constrained max x to columns + // mid sorted map maps constrained max y to values by min x + // used to find relative placements + private final SortedMap>>> maxXYGrid = new TreeMap<>(); + + // used to find relative placements + private final SortedMap> componentsByX = new TreeMap<>(); + private final Set xFillers = new HashSet<>(); private final Set yFillers = new HashSet<>(); @@ -44,12 +53,23 @@ void put(int x, int y, Constrained value) { .computeIfAbsent(x, ignored -> new HashMap<>(1)) .put(component, value); - this.maxGrid - .computeIfAbsent(y + value.getYExcess(), ignored -> new TreeMap<>()) - .computeIfAbsent(x + value.getXExcess(), ignored -> new TreeMap<>()) + final int maxY = y + value.getYExcess(); + final int maxX = x + value.getXExcess(); + + this.maxYXGrid + .computeIfAbsent(maxY, ignored -> new TreeMap<>()) + .computeIfAbsent(maxX, ignored -> new TreeMap<>()) .computeIfAbsent(y, ignored -> new HashSet<>(1)) .add(component); + this.maxXYGrid + .computeIfAbsent(maxX, ignore -> new TreeMap<>()) + .computeIfAbsent(maxY, ignored -> new TreeMap<>()) + .computeIfAbsent(x, ignored -> new HashSet<>(1)) + .add(component); + + this.componentsByX.computeIfAbsent(x, ignored -> new HashSet<>()).add(component); + if (value.fillX()) { this.xFillers.add(component); } @@ -59,28 +79,81 @@ void put(int x, int y, Constrained value) { } } - void putRelative(Constrained value) { + void putRelative(Constrained value, FlexGridConstraints.Relative.Placement placement) { final int x; final int y; if (this.isEmpty()) { x = FlexGridConstraints.Absolute.DEFAULT_X; y = FlexGridConstraints.Absolute.DEFAULT_Y; } else { - final int maxY = this.maxGrid.lastKey(); - final SortedMap>> maxRow = this.maxGrid.get(maxY); - final SortedMap> maxXComponentsByY = maxRow.get(maxRow.lastKey()); - - final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); - final Component component = minYMaxXComponents.iterator().next(); - final Position pos = this.componentPositions.get(component); - - x = pos.x + this.grid.get(pos.y).get(pos.x).get(component).getXExcess() + 1; - y = pos.y; + switch (placement) { + case ROW_END -> { + // final int maxY = this.maxYXGrid.lastKey(); + // final SortedMap>> maxXRow = this.maxYXGrid.get(maxY); + // final SortedMap> maxXComponentsByY = maxXRow.get(maxXRow.lastKey()); + // + // final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); + // final Component component = minYMaxXComponents.iterator().next(); + // final Position pos = this.componentPositions.get(component); + // + // x = pos.x + this.grid.get(pos.y).get(pos.x).get(component).getXExcess() + 1; + // y = pos.y; + final Position pos = this.findEndPos(CartesianOperations.X, this.maxYXGrid); + x = pos.x; + y = pos.y; + } + case NEW_ROW -> { + // min x + x = this.componentsByX.firstKey(); + // max y + 1 + y = this.maxYXGrid.lastKey() + 1; + } + case COLUMN_END -> { + // final int maxX = this.maxXYGrid.lastKey(); + // final SortedMap>> maxYRow = this.maxXYGrid.get(maxX); + // final SortedMap> maxYComponentsByX = maxYRow.get(maxYRow.lastKey()); + // + // final Set minXMaxYComponents = maxYComponentsByX.get(maxYComponentsByX.firstKey()); + // final Component component = minXMaxYComponents.iterator().next(); + // final Position pos = this.componentPositions.get(component); + // + // x = pos.x; + // y = pos.y + this.grid.get(pos.y).get(pos.x).get(component).getYExcess() + 1; + final Position pos = this.findEndPos(CartesianOperations.Y, this.maxXYGrid); + x = pos.x; + y = pos.y; + } + case NEW_COLUMN -> { + // max x + 1 + x = this.maxXYGrid.lastKey() + 1; + // min y + y = this.grid.firstKey(); + } + default -> throw new AssertionError(); + } } this.put(x, y, value); } + private Position findEndPos( + CartesianOperations ops, + SortedMap>>> maxGrid + ) { + final int max = maxGrid.lastKey(); + final SortedMap>> maxXRow = maxGrid.get(max); + final SortedMap> maxXComponentsByY = maxXRow.get(maxXRow.lastKey()); + + final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); + final Component component = minYMaxXComponents.iterator().next(); + final Position pos = this.componentPositions.get(component); + + final int coord = ops.chooseCoord(pos) + ops.getExcess(this.grid.get(pos.y).get(pos.x).get(component)) + 1; + final int oppositeCoord = ops.opposite().chooseCoord(pos); + + return ops.createPos(coord, oppositeCoord); + } + void remove(Component component) { final Position pos = this.componentPositions.remove(component); if (pos != null) { @@ -97,22 +170,48 @@ void remove(Component component) { } final int maxY = pos.y + removed.getYExcess(); - final SortedMap>> maxRow = this.maxGrid.get(maxY); final int maxX = pos.x + removed.getXExcess(); - final SortedMap> maximumsByY = maxRow.get(maxX); - final Set maxComponents = maximumsByY.get(pos.y); - maxComponents.remove(component); - if (maxComponents.isEmpty()) { - maximumsByY.remove(pos.y); + this.removeFromMaxGrid(component, pos, maxX, maxY, CartesianOperations.Y, this.maxYXGrid); + // final SortedMap>> maxXRow = this.maxYXGrid.get(maxY); + // final SortedMap> maximumsByY = maxXRow.get(maxX); + // final Set maxXComponents = maximumsByY.get(pos.y); + // maxXComponents.remove(component); + // + // if (maxXComponents.isEmpty()) { + // maximumsByY.remove(pos.y); + // + // if (maximumsByY.isEmpty()) { + // maxXRow.remove(maxX); + // + // if (maxXRow.isEmpty()) { + // this.maxYXGrid.remove(maxY); + // } + // } + // } - if (maximumsByY.isEmpty()) { - maxRow.remove(maxX); + this.removeFromMaxGrid(component, pos, maxX, maxY, CartesianOperations.X, this.maxXYGrid); + // final SortedMap>> maxYRow = this.maxXYGrid.get(maxX); + // final SortedMap> maximumsByX = maxYRow.get(maxY); + // final Set maxYComponents = maximumsByX.get(pos.x); + // maxYComponents.remove(component); + // + // if (maxYComponents.isEmpty()) { + // maximumsByX.remove(pos.y); + // + // if (maximumsByX.isEmpty()) { + // maxYRow.remove(maxY); + // + // if (maxYRow.isEmpty()) { + // this.maxXYGrid.remove(maxX); + // } + // } + // } - if (maxRow.isEmpty()) { - this.maxGrid.remove(maxY); - } - } + final Set xComponents = this.componentsByX.get(pos.x); + xComponents.remove(component); + if (xComponents.isEmpty()) { + this.componentsByX.remove(pos.x); } this.xFillers.remove(component); @@ -120,6 +219,32 @@ void remove(Component component) { } } + private void removeFromMaxGrid( + Component component, Position pos, int maxX, int maxY, CartesianOperations ops, + SortedMap>>> maxGrid + ) { + final int coord = ops.chooseCoord(pos); + final int max = ops.chooseCoord(maxX, maxY); + final int oppositeMax = ops.opposite().chooseCoord(maxX, maxY); + + final SortedMap>> maxRow = maxGrid.get(max); + final SortedMap> maximumsByCoord = maxRow.get(oppositeMax); + final Set maxComponents = maximumsByCoord.get(coord); + maxComponents.remove(component); + + if (maxComponents.isEmpty()) { + maximumsByCoord.remove(coord); + + if (maximumsByCoord.isEmpty()) { + maxRow.remove(oppositeMax); + + if (maxRow.isEmpty()) { + maxGrid.remove(max); + } + } + } + } + boolean noneFillX() { return this.xFillers.isEmpty(); } @@ -154,8 +279,6 @@ Stream map(EntryFunction mapper) { ); } - private record Position(int x, int y) { } - @FunctionalInterface interface EntriesConsumer { void accept(int x, int y, Stream values); @@ -165,4 +288,6 @@ interface EntriesConsumer { interface EntryFunction { T apply(int x, int y, Constrained value); } + + record Position(int x, int y) { } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index bb3e8443c..9f5c70e16 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableSortedMap; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import org.quiltmc.enigma.gui.util.layout.flex_grid.ConstrainedGrid.Position; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment; @@ -86,10 +87,15 @@ *

    Grid specifics

    * *
      - *
    • components with {@linkplain FlexGridConstraints#createRelative() relative constraints} (or no constraints) - * are added to the end of the bottom row,
      - * or put at ({@value FlexGridConstraints.Absolute#DEFAULT_X}, {@value FlexGridConstraints.Absolute#DEFAULT_Y}) - * if no other components have been added + *
    • if a {@link FlexGridConstraints.Relative Relative} component is the first component to be added, + * it's placed at + * ({@value FlexGridConstraints.Absolute#DEFAULT_X}, {@value FlexGridConstraints.Absolute#DEFAULT_Y}); + * otherwise its + * {@linkplain FlexGridConstraints.Relative#placement(FlexGridConstraints.Relative.Placement) placement} + * determines its position + *
    • components with no constraints are treated are treated as though they have + * {@link FlexGridConstraints.Relative Relative} constraints with + * {@link FlexGridConstraints.Relative#DEFAULT_PLACEMENT DEFAULT_PLACEMENT} *
    • a component can occupy multiple grid cells when its constraint * {@linkplain FlexGridConstraints#xExtent(int) xExtent} or * {@linkplain FlexGridConstraints#yExtent(int) yExtent} exceeds {@code 1}; @@ -164,8 +170,10 @@ public void addLayoutComponent(Component component, @Nullable FlexGridConstraint final Constrained constrained = Constrained.of(component, constraints); if (constraints instanceof FlexGridConstraints.Absolute absolute) { this.grid.put(absolute.getX(), absolute.getY(), constrained); + } else if (constraints instanceof FlexGridConstraints.Relative relative) { + this.grid.putRelative(constrained, relative.getPlacement()); } else { - this.grid.putRelative(constrained); + throw new AssertionError(); } } } @@ -176,7 +184,7 @@ public void addLayoutComponent(String ignored, Component component) { } private void addDefaultConstrainedLayoutComponent(Component component) { - this.grid.putRelative(Constrained.defaultOf(component)); + this.grid.putRelative(Constrained.defaultOf(component), FlexGridConstraints.Relative.DEFAULT_PLACEMENT); } @Override @@ -550,7 +558,7 @@ Dimension createTotalDimension(Insets insets) { /** * Sets of operations for the {@link #X} and {@link #Y} axes of a cartesian plane. */ - private enum CartesianOperations { + enum CartesianOperations { X() { @Override int chooseCoord(int x, int y) { @@ -607,10 +615,25 @@ int getExtent(Constrained constrained) { return constrained.xExtent; } + @Override + int getExcess(Constrained constrained) { + return constrained.getXExcess(); + } + + @Override + Position createPos(int coord, int oppositeCoord) { + return new Position(coord, oppositeCoord); + } + @Override void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); } + + @Override + CartesianOperations opposite() { + return Y; + } }, Y() { @Override @@ -668,14 +691,33 @@ int getExtent(Constrained constrained) { return constrained.yExtent; } + @Override + int getExcess(Constrained constrained) { + return constrained.getYExcess(); + } + + @Override + Position createPos(int coord, int oppositeCoord) { + return new Position(oppositeCoord, coord); + } + @Override void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); } + + @Override + CartesianOperations opposite() { + return X; + } }; abstract int chooseCoord(int x, int y); + int chooseCoord(Position pos) { + return this.chooseCoord(pos.x(), pos.y()); + } + abstract int getLeadingInset(Insets insets); abstract int getTrailingInset(Insets insets); abstract int getParentSpace(Container parent); @@ -688,7 +730,12 @@ void setBounds(Component component, int y, int height) { abstract boolean noneFill(ConstrainedGrid grid); abstract Alignment getAlignment(Constrained constrained); abstract int getExtent(Constrained constrained); + abstract int getExcess(Constrained constrained); + + abstract Position createPos(int coord, int oppositeCoord); abstract void setBounds(Component component, int pos, int span); + + abstract CartesianOperations opposite(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index c32f6edd5..0e51ad0ec 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -673,22 +673,88 @@ public enum Alignment { * @see #toAbsolute(int, int) */ public static final class Relative extends FlexGridConstraints { + public static final Placement DEFAULT_PLACEMENT = Placement.ROW_END; + public static Relative of() { return new Relative( DEFAULT_X_EXTENT, DEFAULT_Y_EXTENT, DEFAULT_FILL_X, DEFAULT_FILL_Y, DEFAULT_X_ALIGNMENT, DEFAULT_Y_ALIGNMENT, - DEFAULT_PRIORITY + DEFAULT_PRIORITY, + DEFAULT_PLACEMENT ); } + private Placement placement; + private Relative( int xExtent, int yExtent, boolean fillX, boolean fillY, Alignment xAlignment, Alignment yAlignment, - int priority + int priority, + Placement placement ) { super(xExtent, yExtent, fillX, fillY, xAlignment, yAlignment, priority); + + this.placement = placement; + } + + public Placement getPlacement() { + return this.placement; + } + + /** + * Sets {@link #placement} to the passed value. + * + *

      The default value is {@link #DEFAULT_PLACEMENT}. + */ + public Relative placement(Placement placement) { + this.placement = placement; + return this; + } + + /** + * Sets {@link #placement} to {@link Placement#ROW_END ROW_END}. + * + *

      The default value is {@link #DEFAULT_PLACEMENT}. + * + * @see #placement(Placement) + */ + public Relative rowEnd() { + return this.placement(Placement.ROW_END); + } + + /** + * Sets {@link #placement} to {@link Placement#NEW_ROW NEW_ROW}. + * + *

      The default value is {@link #DEFAULT_PLACEMENT}. + * + * @see #placement(Placement) + */ + public Relative newRow() { + return this.placement(Placement.NEW_ROW); + } + + /** + * Sets {@link #placement} to {@link Placement#COLUMN_END COLUMN_END}. + * + *

      The default value is {@link #DEFAULT_PLACEMENT}. + * + * @see #placement(Placement) + */ + public Relative columnEnd() { + return this.placement(Placement.COLUMN_END); + } + + /** + * Sets {@link #placement} to {@link Placement#NEW_COLUMN NEW_COLUMN}. + * + *

      The default value is {@link #DEFAULT_PLACEMENT}. + * + * @see #placement(Placement) + */ + public Relative newColumn() { + return this.placement(Placement.NEW_COLUMN); } @Override @@ -697,13 +763,14 @@ public Relative copy() { this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, - this.priority + this.priority, + this.placement ); } /** * Creates {@link Absolute Absolute} constraints at ({@value Absolute#DEFAULT_X}, {@value Absolute#DEFAULT_Y}) - * with these constraints' values. + * with these constraints' values ({@link #placement} is ignored). * * @see #toAbsolute(int, int) * @see Absolute#toRelative() Absolute.toRelative() @@ -719,8 +786,8 @@ public Absolute toAbsolute() { } /** - * Creates {@link Absolute Absolute} constraints with these constraints' values and the passed - * {@code x} and {@code y} coordinates. + * Creates {@link Absolute Absolute} constraints with these constraints' values at the passed + * {@code x} and {@code y} coordinates ({@link #placement} is ignored). * * @see #toAbsolute() * @see Absolute#toRelative() Absolute.toRelative() @@ -733,6 +800,32 @@ public Absolute toAbsolute(int x, int y) { Relative getSelf() { return this; } + + /** + * Represents the placement of {@link Relative} constraints. + * + *

      All placements will put a {@link Component} at ({@value Absolute#DEFAULT_X}, {@value Absolute#DEFAULT_Y}) + * if the component is the first to be {@linkplain Container#add(Component, Object) added} + * to its {@link Container}. + */ + public enum Placement { + /** + * At the end of the last row. + */ + ROW_END, + /** + * In a new row after the current last row, with x equal to the current minimum x. + */ + NEW_ROW, + /** + * At the bottom of the last column. + */ + COLUMN_END, + /** + * In a new column after the current last column, with y equal to the current minimum y. + */ + NEW_COLUMN + } } /** @@ -895,9 +988,10 @@ public Absolute copy() { } /** - * Creates {@link Relative Relative} constraints with these constraints' values - * ({@link #x} and {@link #y} ar ignored). + * Creates {@link Relative Relative} constraints with these constraints' values and + * {@link Relative#DEFAULT_PLACEMENT DEFAULT_PLACEMENT} ({@link #x} and {@link #y} ar ignored). * + * @see #toRelative(Relative.Placement) * @see Relative#toAbsolute() Relative.toAbsolute() * @see Relative#toAbsolute(int, int) Relative.toAbsolute(int, int) */ @@ -906,10 +1000,23 @@ public Relative toRelative() { this.xExtent, this.yExtent, this.fillX, this.fillY, this.xAlignment, this.yAlignment, - this.priority + this.priority, + Relative.DEFAULT_PLACEMENT ); } + /** + * Creates {@link Relative Relative} constraints with these constraints' values and + * the passed {@code placement} ({@link #x} and {@link #y} ar ignored). + * + * @see #toRelative() + * @see Relative#toAbsolute() Relative.toAbsolute() + * @see Relative#toAbsolute(int, int) Relative.toAbsolute(int, int) + */ + public Relative toRelative(Relative.Placement placement) { + return this.toRelative().placement(placement); + } + @Override Absolute getSelf() { return this; From 972ada8a86c95bea15c1d4c341729490fb0d0154 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 11 Dec 2025 15:41:30 -0800 Subject: [PATCH 104/110] improve javadoc --- .../layout/flex_grid/constraints/FlexGridConstraints.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index 0e51ad0ec..a8c97ffff 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -21,21 +21,23 @@ *

    • flex constraints have separate {@link Relative Relative} and {@link Absolute Absolute} * types; {@link Absolute#toRelative() toRelative()} and {@link Relative#toAbsolute() toAbsolute()} * convert between them - *
    • flex constraints support negative {@linkplain Absolute#pos(int, int) coordinates} + *
    • {@link Relative Relative} constraints support different + * {@linkplain Relative#placement(Relative.Placement) placements} + *
    • {@link Absolute Absolute} constraints support negative {@linkplain Absolute#pos(int, int) coordinates} *
    • flex constraints don't use magic constants *
    * *

    Convenience

    *
      *
    • constraints use the builder pattern; they're designed for method chaining - *
    • constraints are mutable, but their values are copied when + *
    • constraints are mutable but {@linkplain #copy() copyable}, and their values are copied when * {@linkplain Container#add(Component, Object) adding} to a container *
    • they have numerous method variations for common use cases, including: *
        *
      • {@link Absolute#nextRow() nextRow()} and {@link Absolute#nextColumn() nextColumn()} *
      • {@link #incrementPriority()} and {@link #decrementPriority()} *
      • a method for each combination of vertical and horizontal alignments - *
      • {@link #copy()} + *
      • a method for each {@link Relative.Placement Placement} *
      *
    * From 68b2b0fecdfd631af938390afd88435711c53e17 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 11 Dec 2025 16:25:24 -0800 Subject: [PATCH 105/110] extract common part of CartesianOperations and give ConstrainedGrid and FlexGridLayout their own implementations cleanup --- .../enigma/gui/util/CartesianOperations.java | 61 +++++++ .../layout/flex_grid/ConstrainedGrid.java | 170 +++++++++--------- .../util/layout/flex_grid/FlexGridLayout.java | 156 +++++----------- 3 files changed, 188 insertions(+), 199 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/CartesianOperations.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/CartesianOperations.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/CartesianOperations.java new file mode 100644 index 000000000..bbc530173 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/CartesianOperations.java @@ -0,0 +1,61 @@ +package org.quiltmc.enigma.gui.util; + +import java.awt.Component; +import java.awt.Insets; + +/** + * Sets of operations for the {@link X} and {@link Y} axes of a cartesian plane. + */ +public interface CartesianOperations> { + int chooseCoord(int x, int y); + + int getLeadingInset(Insets insets); + int getTrailingInset(Insets insets); + int getSpan(Component component); + + O opposite(); + + interface X> extends CartesianOperations { + @Override + default int chooseCoord(int x, int y) { + return x; + } + + @Override + default int getLeadingInset(Insets insets) { + return insets.left; + } + + @Override + default int getTrailingInset(Insets insets) { + return insets.right; + } + + @Override + default int getSpan(Component component) { + return component.getWidth(); + } + } + + interface Y> extends CartesianOperations { + @Override + default int chooseCoord(int x, int y) { + return y; + } + + @Override + default int getLeadingInset(Insets insets) { + return insets.top; + } + + @Override + default int getTrailingInset(Insets insets) { + return insets.bottom; + } + + @Override + default int getSpan(Component component) { + return component.getHeight(); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java index 33334a14f..d4b88c4b4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/ConstrainedGrid.java @@ -1,6 +1,6 @@ package org.quiltmc.enigma.gui.util.layout.flex_grid; -import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.CartesianOperations; +import org.quiltmc.enigma.gui.util.CartesianOperations; import org.quiltmc.enigma.gui.util.layout.flex_grid.FlexGridLayout.Constrained; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; @@ -25,7 +25,9 @@ class ConstrainedGrid { // inner sorted map maps x coordinates to values // component map holds values by component private final SortedMap>> grid = new TreeMap<>(); - private final Map componentPositions = new HashMap<>(); + + private final Map componentCoordinates = new HashMap<>(); + // outer sorted map maps constrained max y to rows // mid sorted map maps constrained max x to values by min y // used to find relative placements @@ -35,7 +37,7 @@ class ConstrainedGrid { // used to find relative placements private final SortedMap>>> maxXYGrid = new TreeMap<>(); - // used to find relative placements + // used to find min x for relative placements private final SortedMap> componentsByX = new TreeMap<>(); private final Set xFillers = new HashSet<>(); @@ -46,7 +48,7 @@ void put(int x, int y, Constrained value) { this.remove(value.component()); - this.componentPositions.put(component, new Position(x, y)); + this.componentCoordinates.put(component, new Coordinates(x, y)); this.grid .computeIfAbsent(y, ignored -> new TreeMap<>()) @@ -88,19 +90,9 @@ void putRelative(Constrained value, FlexGridConstraints.Relative.Placement place } else { switch (placement) { case ROW_END -> { - // final int maxY = this.maxYXGrid.lastKey(); - // final SortedMap>> maxXRow = this.maxYXGrid.get(maxY); - // final SortedMap> maxXComponentsByY = maxXRow.get(maxXRow.lastKey()); - // - // final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); - // final Component component = minYMaxXComponents.iterator().next(); - // final Position pos = this.componentPositions.get(component); - // - // x = pos.x + this.grid.get(pos.y).get(pos.x).get(component).getXExcess() + 1; - // y = pos.y; - final Position pos = this.findEndPos(CartesianOperations.X, this.maxYXGrid); - x = pos.x; - y = pos.y; + final Coordinates coords = this.findEndPos(Operations.X.INSTANCE, this.maxYXGrid); + x = coords.x; + y = coords.y; } case NEW_ROW -> { // min x @@ -109,19 +101,9 @@ void putRelative(Constrained value, FlexGridConstraints.Relative.Placement place y = this.maxYXGrid.lastKey() + 1; } case COLUMN_END -> { - // final int maxX = this.maxXYGrid.lastKey(); - // final SortedMap>> maxYRow = this.maxXYGrid.get(maxX); - // final SortedMap> maxYComponentsByX = maxYRow.get(maxYRow.lastKey()); - // - // final Set minXMaxYComponents = maxYComponentsByX.get(maxYComponentsByX.firstKey()); - // final Component component = minXMaxYComponents.iterator().next(); - // final Position pos = this.componentPositions.get(component); - // - // x = pos.x; - // y = pos.y + this.grid.get(pos.y).get(pos.x).get(component).getYExcess() + 1; - final Position pos = this.findEndPos(CartesianOperations.Y, this.maxXYGrid); - x = pos.x; - y = pos.y; + final Coordinates coords = this.findEndPos(Operations.Y.INSTANCE, this.maxXYGrid); + x = coords.x; + y = coords.y; } case NEW_COLUMN -> { // max x + 1 @@ -136,8 +118,8 @@ void putRelative(Constrained value, FlexGridConstraints.Relative.Placement place this.put(x, y, value); } - private Position findEndPos( - CartesianOperations ops, + private Coordinates findEndPos( + Operations ops, SortedMap>>> maxGrid ) { final int max = maxGrid.lastKey(); @@ -146,72 +128,40 @@ private Position findEndPos( final Set minYMaxXComponents = maxXComponentsByY.get(maxXComponentsByY.firstKey()); final Component component = minYMaxXComponents.iterator().next(); - final Position pos = this.componentPositions.get(component); + final Coordinates coords = this.componentCoordinates.get(component); - final int coord = ops.chooseCoord(pos) + ops.getExcess(this.grid.get(pos.y).get(pos.x).get(component)) + 1; - final int oppositeCoord = ops.opposite().chooseCoord(pos); + final int coord = ops.chooseCoord(coords) + + ops.getExcess(this.grid.get(coords.y).get(coords.x).get(component)) + 1; + final int oppositeCoord = ops.opposite().chooseCoord(coords); return ops.createPos(coord, oppositeCoord); } void remove(Component component) { - final Position pos = this.componentPositions.remove(component); - if (pos != null) { - final SortedMap> row = this.grid.get(pos.y); - final Map values = row.get(pos.x); + final Coordinates coords = this.componentCoordinates.remove(component); + if (coords != null) { + final SortedMap> row = this.grid.get(coords.y); + final Map values = row.get(coords.x); final Constrained removed = values.remove(component); if (values.isEmpty()) { - row.remove(pos.x); + row.remove(coords.x); if (row.isEmpty()) { - this.grid.remove(pos.y); + this.grid.remove(coords.y); } } - final int maxY = pos.y + removed.getYExcess(); - final int maxX = pos.x + removed.getXExcess(); - - this.removeFromMaxGrid(component, pos, maxX, maxY, CartesianOperations.Y, this.maxYXGrid); - // final SortedMap>> maxXRow = this.maxYXGrid.get(maxY); - // final SortedMap> maximumsByY = maxXRow.get(maxX); - // final Set maxXComponents = maximumsByY.get(pos.y); - // maxXComponents.remove(component); - // - // if (maxXComponents.isEmpty()) { - // maximumsByY.remove(pos.y); - // - // if (maximumsByY.isEmpty()) { - // maxXRow.remove(maxX); - // - // if (maxXRow.isEmpty()) { - // this.maxYXGrid.remove(maxY); - // } - // } - // } - - this.removeFromMaxGrid(component, pos, maxX, maxY, CartesianOperations.X, this.maxXYGrid); - // final SortedMap>> maxYRow = this.maxXYGrid.get(maxX); - // final SortedMap> maximumsByX = maxYRow.get(maxY); - // final Set maxYComponents = maximumsByX.get(pos.x); - // maxYComponents.remove(component); - // - // if (maxYComponents.isEmpty()) { - // maximumsByX.remove(pos.y); - // - // if (maximumsByX.isEmpty()) { - // maxYRow.remove(maxY); - // - // if (maxYRow.isEmpty()) { - // this.maxXYGrid.remove(maxX); - // } - // } - // } - - final Set xComponents = this.componentsByX.get(pos.x); + final int maxY = coords.y + removed.getYExcess(); + final int maxX = coords.x + removed.getXExcess(); + + this.removeFromMaxGrid(component, coords, maxX, maxY, Operations.Y.INSTANCE, this.maxYXGrid); + this.removeFromMaxGrid(component, coords, maxX, maxY, Operations.X.INSTANCE, this.maxXYGrid); + + final Set xComponents = this.componentsByX.get(coords.x); xComponents.remove(component); if (xComponents.isEmpty()) { - this.componentsByX.remove(pos.x); + this.componentsByX.remove(coords.x); } this.xFillers.remove(component); @@ -220,10 +170,10 @@ void remove(Component component) { } private void removeFromMaxGrid( - Component component, Position pos, int maxX, int maxY, CartesianOperations ops, + Component component, Coordinates coords, int maxX, int maxY, Operations ops, SortedMap>>> maxGrid ) { - final int coord = ops.chooseCoord(pos); + final int coord = ops.chooseCoord(coords); final int max = ops.chooseCoord(maxX, maxY); final int oppositeMax = ops.opposite().chooseCoord(maxX, maxY); @@ -254,7 +204,7 @@ boolean noneFillY() { } boolean isEmpty() { - return this.componentPositions.isEmpty(); + return this.componentCoordinates.isEmpty(); } void forEach(EntriesConsumer action) { @@ -289,5 +239,53 @@ interface EntryFunction { T apply(int x, int y, Constrained value); } - record Position(int x, int y) { } + private record Coordinates(int x, int y) { } + + private interface Operations extends CartesianOperations { + class X implements Operations, CartesianOperations.X { + static final Operations.X INSTANCE = new Operations.X(); + + @Override + public int getExcess(Constrained constrained) { + return constrained.getXExcess(); + } + + @Override + public Coordinates createPos(int coord, int oppositeCoord) { + return new Coordinates(coord, oppositeCoord); + } + + @Override + public Operations opposite() { + return Operations.Y.INSTANCE; + } + } + + class Y implements Operations, CartesianOperations.Y { + static final Operations.Y INSTANCE = new Operations.Y(); + + @Override + public int getExcess(Constrained constrained) { + return constrained.getYExcess(); + } + + @Override + public Coordinates createPos(int coord, int oppositeCoord) { + return new Coordinates(oppositeCoord, coord); + } + + @Override + public Operations opposite() { + return Operations.X.INSTANCE; + } + } + + default int chooseCoord(Coordinates coords) { + return this.chooseCoord(coords.x, coords.y); + } + + int getExcess(Constrained constrained); + + Coordinates createPos(int coord, int oppositeCoord); + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java index 9f5c70e16..07ff45f92 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/FlexGridLayout.java @@ -4,7 +4,7 @@ import com.google.common.collect.ImmutableSortedMap; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import org.quiltmc.enigma.gui.util.layout.flex_grid.ConstrainedGrid.Position; +import org.quiltmc.enigma.gui.util.CartesianOperations; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints; import org.quiltmc.enigma.gui.util.layout.flex_grid.constraints.FlexGridConstraints.Alignment; @@ -251,16 +251,16 @@ private Sizes getMaxSizes() { @Override public void layoutContainer(Container parent) { if (!this.grid.isEmpty()) { - this.layoutAxis(parent, CartesianOperations.X); - this.layoutAxis(parent, CartesianOperations.Y); + this.layoutAxis(parent, Operations.X.INSTANCE); + this.layoutAxis(parent, Operations.Y.INSTANCE); } } - private void layoutAxis(Container parent, CartesianOperations ops) { + private void layoutAxis(Container parent, Operations ops) { final Insets insets = parent.getInsets(); final int leadingInset = ops.getLeadingInset(insets); - final int availableSpace = ops.getParentSpace(parent) - leadingInset - ops.getTrailingInset(insets); + final int availableSpace = ops.getSpan(parent) - leadingInset - ops.getTrailingInset(insets); final Sizes preferred = this.getPreferredSizes(); @@ -291,7 +291,7 @@ private void layoutAxis(Container parent, CartesianOperations ops) { } private ImmutableMap allocateCellSpace( - CartesianOperations ops, int remainingSpace, boolean fill + Operations ops, int remainingSpace, boolean fill ) { final Sizes large; final Sizes small; @@ -359,7 +359,7 @@ private ImmutableMap allocateCellSpace( /** * @implNote the passed {@code cellSpans} must be ordered with increasing keys */ - private void layoutAxisImpl(int startPos, CartesianOperations ops, ImmutableMap cellSpans) { + private void layoutAxisImpl(int startPos, Operations ops, ImmutableMap cellSpans) { final Map beginPositions = new HashMap<>(); int currentPos = startPos; for (final Map.Entry entry : cellSpans.entrySet()) { @@ -555,187 +555,117 @@ Dimension createTotalDimension(Insets insets) { } } - /** - * Sets of operations for the {@link #X} and {@link #Y} axes of a cartesian plane. - */ - enum CartesianOperations { - X() { - @Override - int chooseCoord(int x, int y) { - return x; - } - - @Override - int getLeadingInset(Insets insets) { - return insets.left; - } - - @Override - int getTrailingInset(Insets insets) { - return insets.right; - } + private interface Operations extends CartesianOperations { + class X implements Operations, CartesianOperations.X { + static final Operations.X INSTANCE = new Operations.X(); @Override - int getParentSpace(Container parent) { - return parent.getWidth(); - } - - @Override - int getTotalSpace(Sizes sizes) { + public int getTotalSpace(Sizes sizes) { return sizes.totalWidth; } @Override - ImmutableMap getCellSpans(Sizes sizes) { + public ImmutableMap getCellSpans(Sizes sizes) { return sizes.columnWidths; } @Override - int getSpan(Size size) { + public int getSpan(Size size) { return size.width; } @Override - boolean fills(Constrained constrained) { + public boolean fills(Constrained constrained) { return constrained.fillX; } @Override - boolean noneFill(ConstrainedGrid grid) { + public boolean noneFill(ConstrainedGrid grid) { return grid.noneFillX(); } @Override - Alignment getAlignment(Constrained constrained) { + public Alignment getAlignment(Constrained constrained) { return constrained.xAlignment; } @Override - int getExtent(Constrained constrained) { + public int getExtent(Constrained constrained) { return constrained.xExtent; } @Override - int getExcess(Constrained constrained) { - return constrained.getXExcess(); - } - - @Override - Position createPos(int coord, int oppositeCoord) { - return new Position(coord, oppositeCoord); - } - - @Override - void setBounds(Component component, int x, int width) { + public void setBounds(Component component, int x, int width) { component.setBounds(x, component.getY(), width, component.getHeight()); } @Override - CartesianOperations opposite() { - return Y; - } - }, - Y() { - @Override - int chooseCoord(int x, int y) { - return y; - } - - @Override - int getLeadingInset(Insets insets) { - return insets.top; - } - - @Override - int getTrailingInset(Insets insets) { - return insets.bottom; + public Operations opposite() { + return Operations.Y.INSTANCE; } + } - @Override - int getParentSpace(Container parent) { - return parent.getHeight(); - } + class Y implements Operations, CartesianOperations.Y { + static final Operations.Y INSTANCE = new Operations.Y(); @Override - int getTotalSpace(Sizes sizes) { + public int getTotalSpace(Sizes sizes) { return sizes.totalHeight; } @Override - ImmutableMap getCellSpans(Sizes sizes) { + public ImmutableMap getCellSpans(Sizes sizes) { return sizes.rowHeights; } @Override - int getSpan(Size size) { + public int getSpan(Size size) { return size.height; } @Override - boolean fills(Constrained constrained) { + public boolean fills(Constrained constrained) { return constrained.fillY; } @Override - boolean noneFill(ConstrainedGrid grid) { + public boolean noneFill(ConstrainedGrid grid) { return grid.noneFillY(); } @Override - Alignment getAlignment(Constrained constrained) { + public Alignment getAlignment(Constrained constrained) { return constrained.yAlignment; } @Override - int getExtent(Constrained constrained) { + public int getExtent(Constrained constrained) { return constrained.yExtent; } @Override - int getExcess(Constrained constrained) { - return constrained.getYExcess(); - } - - @Override - Position createPos(int coord, int oppositeCoord) { - return new Position(oppositeCoord, coord); - } - - @Override - void setBounds(Component component, int y, int height) { + public void setBounds(Component component, int y, int height) { component.setBounds(component.getX(), y, component.getWidth(), height); } @Override - CartesianOperations opposite() { - return X; + public Operations opposite() { + return Operations.X.INSTANCE; } - }; - - abstract int chooseCoord(int x, int y); - - int chooseCoord(Position pos) { - return this.chooseCoord(pos.x(), pos.y()); } - abstract int getLeadingInset(Insets insets); - abstract int getTrailingInset(Insets insets); - abstract int getParentSpace(Container parent); - - abstract int getTotalSpace(Sizes sizes); - abstract ImmutableMap getCellSpans(Sizes sizes); - abstract int getSpan(Size size); - - abstract boolean fills(Constrained constrained); - abstract boolean noneFill(ConstrainedGrid grid); - abstract Alignment getAlignment(Constrained constrained); - abstract int getExtent(Constrained constrained); - abstract int getExcess(Constrained constrained); + int getTotalSpace(Sizes sizes); + ImmutableMap getCellSpans(Sizes sizes); + int getSpan(Size size); - abstract Position createPos(int coord, int oppositeCoord); + boolean fills(Constrained constrained); + boolean noneFill(ConstrainedGrid grid); + Alignment getAlignment(Constrained constrained); + int getExtent(Constrained constrained); - abstract void setBounds(Component component, int pos, int span); + void setBounds(Component component, int pos, int span); - abstract CartesianOperations opposite(); + @Override + Operations opposite(); } } From 25596ce008e7e1d9896710387323283958c25095 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 11 Dec 2025 16:40:23 -0800 Subject: [PATCH 106/110] use relative constraints in EntryTooltip --- .../enigma/gui/panel/EntryTooltip.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 365d27c38..c2dcdf1a9 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -225,8 +225,6 @@ public void mousePressed(MouseEvent e) { final Font editorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value()); final Font italEditorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); - final FlexGridConstraints.Absolute constraints = FlexGridConstraints.createAbsolute(); - { final Box parentLabelRow = Box.createHorizontalBox(); @@ -242,47 +240,42 @@ public void mousePressed(MouseEvent e) { parentLabelRow.add(this.parentLabelOf(target, editorFont, stopInteraction)); parentLabelRow.setBorder(createEmptyBorder(ROW_OUTER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); - this.add(parentLabelRow, constraints.copy().alignCenterLeft()); + this.add(parentLabelRow, FlexGridConstraints.createRelative().alignCenterLeft()); } final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - this.add(new JSeparator(), constraints.nextRow().copy().fillX()); + this.add(new JSeparator(), FlexGridConstraints.createRelative().newRow().copy().fillX()); final var javadocs = new JPanel(new FlexGridLayout()); - final FlexGridConstraints.Absolute javadocsConstraints = FlexGridConstraints.createAbsolute(); if (javadoc != null) { final JTextArea javadocText = javadocOf(javadoc, italEditorFont, stopInteraction); javadocText.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); - javadocs.add(javadocText, javadocsConstraints.copy().fillX()); + javadocs.add(javadocText, FlexGridConstraints.createRelative().fillX()); } if (!paramJavadocs.isEmpty()) { final JPanel params = new JPanel(new FlexGridLayout()); - final FlexGridConstraints.Absolute paramsConstraints = FlexGridConstraints.createAbsolute(); - for (final ParamJavadoc paramJavadoc : paramJavadocs) { - params.add(paramJavadoc.name, paramsConstraints.copy().alignTopRight()); + params.add(paramJavadoc.name, FlexGridConstraints.createRelative().newRow().alignTopRight()); - params.add(paramJavadoc.javadoc, paramsConstraints.nextColumn().copy() + params.add(paramJavadoc.javadoc, FlexGridConstraints.createRelative() .fillX() .alignTopLeft() ); - - paramsConstraints.nextRow(); } params.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); - javadocs.add(params, javadocsConstraints.nextRow().copy().fillX()); + javadocs.add(params, FlexGridConstraints.createRelative().newRow().fillX()); } final JScrollPane javadocsScroll = new SmartScrollPane(javadocs); javadocsScroll.setBorder(createEmptyBorder()); - this.add(javadocsScroll, constraints.nextRow().copy().fillX()); + this.add(javadocsScroll, FlexGridConstraints.createRelative().newRow().fillX()); } if (this.declarationSnippet != null) { @@ -354,17 +347,17 @@ public void mouseClicked(MouseEvent e) { this.declarationSnippet.editor.addMouseListener(stopInteraction); } - this.add(this.declarationSnippet.ui, constraints.nextRow().copy() + this.add(this.declarationSnippet.ui, FlexGridConstraints.createRelative().newRow() .fillX() .alignCenterLeft() .incrementPriority() ); } else { - this.add(new JSeparator(), constraints.nextRow().copy().fillX()); + this.add(new JSeparator(), FlexGridConstraints.createRelative().newRow().fillX()); final JLabel noSource = labelOf(I18n.translate("editor.tooltip.message.no_source"), italEditorFont); noSource.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); - this.add(noSource, constraints.nextRow().copy().fillX()); + this.add(noSource, FlexGridConstraints.createRelative().newRow().fillX()); } } From 7b7b654e4e6c2a53753ce1f909645635d3024324 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 13 Dec 2025 11:36:13 -0800 Subject: [PATCH 107/110] improve javadocs --- .../constraints/FlexGridConstraints.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index a8c97ffff..deacb2046 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -35,9 +35,9 @@ *
  • they have numerous method variations for common use cases, including: *
      *
    • {@link Absolute#nextRow() nextRow()} and {@link Absolute#nextColumn() nextColumn()} - *
    • {@link #incrementPriority()} and {@link #decrementPriority()} - *
    • a method for each combination of vertical and horizontal alignments *
    • a method for each {@link Relative.Placement Placement} + *
    • {@link #incrementPriority()} and {@link #decrementPriority()} + *
    • a method for each combination of vertical and horizontal {@link Alignment Alignment}s *
    * * @@ -659,13 +659,24 @@ public C decrementPriority() { abstract C getSelf(); public enum Alignment { - BEGIN, CENTER, END + /** + * Left for horizontal alignment; top for vertical alignment. + */ + BEGIN, + /** + * Middle for both horizontal and vertical alignments. + */ + CENTER, + /** + * Right for horizontal alignment; bottom for vertical alignment. + */ + END } /** * {@link FlexGridConstraints} with relative coordinates.
    - * Components will be placed at the end of the bottom-most row at the time of - * {@linkplain Container#add(Component, Object) adding}. + * Components' {@linkplain Placement placements} determine their positions relative to components + * {@linkplain Container#add(Component, Object) added} before them. * *

    Relative components never overlap components added before them, but {@link Absolute Absolute} * components added after them may overlap. @@ -687,6 +698,9 @@ public static Relative of() { ); } + /** + * Defaults to {@link #DEFAULT_PLACEMENT}. + */ private Placement placement; private Relative( @@ -709,6 +723,11 @@ public Placement getPlacement() { * Sets {@link #placement} to the passed value. * *

    The default value is {@link #DEFAULT_PLACEMENT}. + * + * @see #rowEnd() + * @see #newRow() + * @see #columnEnd() + * @see #newColumn() */ public Relative placement(Placement placement) { this.placement = placement; From 831002392e9904d2a4443b7fb72341ea83c0d3a0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 13 Dec 2025 18:22:24 -0800 Subject: [PATCH 108/110] note another difference between flex grid and grid bag relative constraints --- .../util/layout/flex_grid/constraints/FlexGridConstraints.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java index deacb2046..3964f1ed0 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/layout/flex_grid/constraints/FlexGridConstraints.java @@ -21,6 +21,8 @@ *

  • flex constraints have separate {@link Relative Relative} and {@link Absolute Absolute} * types; {@link Absolute#toRelative() toRelative()} and {@link Relative#toAbsolute() toAbsolute()} * convert between them + *
  • {@link Relative Relative} constraints positioning considers all components added before them, + * not just the component added immediately before them *
  • {@link Relative Relative} constraints support different * {@linkplain Relative#placement(Relative.Placement) placements} *
  • {@link Absolute Absolute} constraints support negative {@linkplain Absolute#pos(int, int) coordinates} From 75ce4c4782d6368f353d371bb61fa97223ab2edc Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 13 Dec 2025 19:31:58 -0800 Subject: [PATCH 109/110] fix broken text rendering when selecting javadoc text --- .../quiltmc/enigma/gui/panel/EntryTooltip.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index c2dcdf1a9..062083cac 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -38,6 +38,7 @@ import javax.swing.tree.TreePath; import java.awt.AWTEvent; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; @@ -243,16 +244,18 @@ public void mousePressed(MouseEvent e) { this.add(parentLabelRow, FlexGridConstraints.createRelative().alignCenterLeft()); } + final Color background = this.content.getBackground(); + final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = - this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); + this.paramJavadocsOf(target, editorFont, italEditorFont, background, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { this.add(new JSeparator(), FlexGridConstraints.createRelative().newRow().copy().fillX()); final var javadocs = new JPanel(new FlexGridLayout()); if (javadoc != null) { - final JTextArea javadocText = javadocOf(javadoc, italEditorFont, stopInteraction); + final JTextArea javadocText = javadocOf(javadoc, italEditorFont, background, stopInteraction); javadocText.setBorder(createEmptyBorder(ROW_INNER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET)); javadocs.add(javadocText, FlexGridConstraints.createRelative().fillX()); } @@ -569,13 +572,13 @@ private static JLabel labelOf(String text, Font font) { return label; } - private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopInteraction) { + private static JTextArea javadocOf(String javadoc, Font font, Color background, MouseAdapter stopInteraction) { final JTextArea text = new JTextArea(javadoc); text.setLineWrap(true); text.setWrapStyleWord(true); text.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); text.setFont(font); - text.setBackground(GuiUtil.TRANSPARENT); + text.setBackground(background); text.setCaretColor(GuiUtil.TRANSPARENT); text.getCaret().setSelectionVisible(true); text.setBorder(createEmptyBorder()); @@ -592,7 +595,7 @@ private static String translatePlaceholder(String key) { } private ImmutableList paramJavadocsOf( - Entry target, Font nameFont, Font javadocFont, MouseAdapter stopInteraction + Entry target, Font nameFont, Font javadocFont, Color background, MouseAdapter stopInteraction ) { final EnigmaProject project = this.gui.getController().getProject(); final EntryIndex entryIndex = project.getJarIndex().getIndex(EntryIndex.class); @@ -618,7 +621,7 @@ private ImmutableList paramJavadocsOf( final EntryMapping mapping = remapper.getMapping(param); if (mapping.javadoc() != null) { final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName(), nameFont); - final JTextArea javadoc = javadocOf(mapping.javadoc(), javadocFont, stopInteraction); + final JTextArea javadoc = javadocOf(mapping.javadoc(), javadocFont, background, stopInteraction); add.accept(new ParamJavadoc(name, javadoc)); } From 3017ac257192d0670a6d19514ac0c100ff309952 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 13 Dec 2025 19:41:30 -0800 Subject: [PATCH 110/110] remove superfluous copy --- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 062083cac..b5aadb1d6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -250,7 +250,7 @@ public void mousePressed(MouseEvent e) { final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, background, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - this.add(new JSeparator(), FlexGridConstraints.createRelative().newRow().copy().fillX()); + this.add(new JSeparator(), FlexGridConstraints.createRelative().newRow().fillX()); final var javadocs = new JPanel(new FlexGridLayout());