From 4b9b6821f9c21adf7666239423af0e27b3666ab3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 13:52:00 -0700 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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(); } });