From 4b9b6821f9c21adf7666239423af0e27b3666ab3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 13:52:00 -0700 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 11/42] 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 12/42] 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 13/42] 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 14/42] 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 15/42] 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 16/42] 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 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 22/42] 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 23/42] 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 24/42] 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 25/42] 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 26/42] 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 27/42] 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 28/42] 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 29/42] 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 30/42] 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 31/42] 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 32/42] 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 33/42] 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 34/42] 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 35/42] 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 36/42] 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 37/42] 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 38/42] 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 39/42] 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 40/42] 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 41/42] 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 42/42] 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(); }