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..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 @@ -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 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-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..ed3aee3d6 --- /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.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 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 = MIN_BLINKS, max = MAX_BLINKS) + public final TrackedValue blinks = this.value(3); + + @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/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/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..2544e4568 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/SelectionHighlightMenu.java @@ -0,0 +1,54 @@ +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.gui.util.GuiUtil; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JMenu; + +public class SelectionHighlightMenu extends AbstractEnigmaMenu { + private final JMenu blinksMenu; + private final IntRangeConfigMenuItem blinkDelay; + + protected SelectionHighlightMenu(Gui gui) { + super(gui); + + final SelectionHighlightSection config = Config.editor().selectionHighlight; + + this.blinksMenu = GuiUtil.createIntConfigRadioMenu( + config.blinks, + SelectionHighlightSection.MIN_BLINKS, SelectionHighlightSection.MAX_BLINKS, + this::retranslateBlinksMenu + ); + + this.blinkDelay = new IntRangeConfigMenuItem( + gui, config.blinkDelay, + SelectionHighlightSection.MIN_BLINK_DELAY, SelectionHighlightSection.MAX_BLINK_DELAY, + "menu.view.selection_highlight.blink_delay" + ); + + this.add(this.blinksMenu); + this.add(this.blinkDelay); + + this.retranslate(); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.view.selection_highlight")); + + 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/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-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..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 @@ -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; @@ -54,14 +55,13 @@ 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; 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; @@ -102,6 +102,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(); @@ -154,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 ) { @@ -170,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 ) { @@ -196,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() { @@ -212,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) { @@ -560,27 +571,24 @@ 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; + this.startHighlightingSelection(boundedToken); + } - @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); - } + private void startHighlightingSelection(Token token) { + if (this.selectionHighlightHandler != null) { + this.selectionHighlightHandler.finish(); + } - if (this.counter++ > 6) { - timer.stop(); - } - } - }); + final SelectionHighlightSection config = Config.editor().selectionHighlight; + final int blinks = config.blinks.value(); + if (blinks > 0) { + final SelectionHighlightHandler handler = + new SelectionHighlightHandler(token, config.blinkDelay.value(), blinks); + + handler.start(); - timer.start(); + this.selectionHighlightHandler = handler; + } } /** @@ -668,6 +676,51 @@ private ClassEntry getDeobfOrObfHandleRef() { return deobfRef == null ? this.classHandler.handle.getRef() : deobfRef; } + private class SelectionHighlightHandler extends Timer { + static final int BLINK_INTERVAL = 2; + + final int counterMax; + + int counter = 0; + Object highlight = null; + + SelectionHighlightHandler(Token token, int delay, int blinks) { + super(delay, null); + + this.counterMax = blinks * BLINK_INTERVAL; + + this.setInitialDelay(0); + + this.addActionListener(e -> { + if (this.counter < this.counterMax) { + if (this.counter % BLINK_INTERVAL == 0) { + this.highlight = BaseEditorPanel.this.addHighlight(token, SelectionHighlightPainter.INSTANCE); + } else { + this.removeHighlight(); + } + + this.counter++; + } else { + this.finish(); + } + }); + } + + void removeHighlight() { + if (this.highlight != null) { + BaseEditorPanel.this.editor.getHighlighter().removeHighlight(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) { 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) { 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..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 @@ -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,67 @@ 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); + } + + choiceItem.addActionListener(e -> { + if (!config.value().equals(choice)) { + config.setValue(choice); + 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 7fce5ddaa..2fc9a5b33 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -73,6 +73,12 @@ "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.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.", "menu.view.languages": "Languages", "menu.view.scale": "Scale", "menu.view.scale.custom": "Custom...", @@ -281,6 +287,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)!",