From 7660632cce1afbd806240acf445f4e42523b5081 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 27 Sep 2025 14:50:55 -0700 Subject: [PATCH 001/109] add POC tooltip to EditorPanel editor that shows full name of entry under cursor --- .../enigma/gui/panel/ContainerToolTip.java | 30 +++++++++++++++ .../gui/panel/CustomTooltipEditorPane.java | 33 +++++++++++++++++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 37 ++++++++++++++++++- 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java new file mode 100644 index 000000000..a43bcda4e --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java @@ -0,0 +1,30 @@ +package org.quiltmc.enigma.gui.panel; + +import javax.swing.BorderFactory; +import javax.swing.JToolTip; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; + +/** + * A {@link JToolTip} that does its best to act like a proper container for a root component. + */ +public class ContainerToolTip extends JToolTip { + private final Component root; + + public ContainerToolTip(Component root) { + this.root = root; + this.setLayout(new BorderLayout()); + this.add(this.root); + this.setBorder(BorderFactory.createEmptyBorder()); + } + + @Override + public Dimension getPreferredSize() { + if (this.isPreferredSizeSet()) { + return super.getPreferredSize(); + } else { + return this.root.getPreferredSize(); + } + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java new file mode 100644 index 000000000..3139f473a --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java @@ -0,0 +1,33 @@ +package org.quiltmc.enigma.gui.panel; + +import javax.swing.JEditorPane; +import javax.swing.JToolTip; +import java.util.function.Supplier; + +public class CustomTooltipEditorPane extends JEditorPane { + private final Supplier toolTipFactory; + + public CustomTooltipEditorPane(Supplier toolTipFactory) { + this.toolTipFactory = toolTipFactory; + this.enableTooltip(); + } + + @Override + public JToolTip createToolTip() { + final JToolTip toolTip = this.toolTipFactory.get(); + toolTip.setComponent(this); + return toolTip; + } + + public void enableTooltip() { + // ToolTipManager will only create a tooltip if its test is non-null + // this also ensures this component is registered with the ToolTipManager + this.setToolTipText(""); + } + + public void disableToolTip() { + // if tooltip text is null, ToolTipManager will never create a tooltip + // this also unregisters this component with the ToolTipManager + this.setToolTipText(null); + } +} 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 d4cded46c..593e53a01 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 @@ -33,6 +33,7 @@ import org.quiltmc.syntaxpain.SyntaxDocument; import org.tinylog.Logger; +import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.FlowLayout; @@ -41,6 +42,8 @@ import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; +import java.awt.MouseInfo; +import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -56,6 +59,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; import javax.swing.JButton; import javax.swing.JComponent; @@ -66,6 +70,7 @@ import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTextArea; +import javax.swing.JToolTip; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -79,7 +84,7 @@ public class EditorPanel { private final JPanel ui = new JPanel(); - private final JEditorPane editor = new JEditorPane(); + private final CustomTooltipEditorPane editor = new CustomTooltipEditorPane(this::createEditorTooltip); private final JScrollPane editorScrollPane = new JScrollPane(this.editor); private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; @@ -225,6 +230,36 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } + private JToolTip createEditorTooltip() { + return getMousePositionIn(this.editor) + .map(this.editor::viewToModel2D) + .filter(textPos -> textPos >= 0) + .map(this::getToken) + .map(this::getReference) + .map(EntryReference::getNameableEntry) + .map(this.gui.getController().getProject().getRemapper()::deobfuscate) + .map(Entry::getFullName) + .map(entryName -> { + final JPanel root = new JPanel(new BorderLayout()); + root.add(new JLabel(entryName)); + return new ContainerToolTip(root); + }) + // empty dummy tooltip + .orElseGet(JToolTip::new); + } + + // getMousePosition(true) always returns null for editor, editorScrollPane, and ui + private static Optional getMousePositionIn(Component component) { + return Optional.of(MouseInfo.getPointerInfo().getLocation()) + .map(mouse -> { + final Point editorLocation = component.getLocationOnScreen(); + final Point point = new Point(mouse); + point.translate(-editorLocation.x, -editorLocation.y); + return point; + }) + .filter(component::contains); + } + public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { From c22af36784d5cf24d7d3888c1a3c826c9696bd43 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 07:38:25 -0700 Subject: [PATCH 002/109] update tooltip content when mouse moves, but can't get JToolTip to resize+move --- .../enigma/gui/panel/ContainerToolTip.java | 10 ++- .../quiltmc/enigma/gui/panel/EditorPanel.java | 73 +++++++++++++++---- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java index a43bcda4e..8d06499c8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java @@ -9,16 +9,20 @@ /** * A {@link JToolTip} that does its best to act like a proper container for a root component. */ -public class ContainerToolTip extends JToolTip { - private final Component root; +public class ContainerToolTip extends JToolTip { + private final C root; - public ContainerToolTip(Component root) { + public ContainerToolTip(C root) { this.root = root; this.setLayout(new BorderLayout()); this.add(this.root); this.setBorder(BorderFactory.createEmptyBorder()); } + public C getRoot() { + return this.root; + } + @Override public Dimension getPreferredSize() { if (this.isPreferredSizeSet()) { 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 593e53a01..d8e9fcb63 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.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; @@ -61,6 +62,7 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; +import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JEditorPane; @@ -74,6 +76,7 @@ import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.Timer; +import javax.swing.ToolTipManager; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Highlighter.HighlightPainter; @@ -84,7 +87,7 @@ public class EditorPanel { private final JPanel ui = new JPanel(); - private final CustomTooltipEditorPane editor = new CustomTooltipEditorPane(this::createEditorTooltip); + private final CustomTooltipEditorPane editor = new CustomTooltipEditorPane(this::createToolTip); private final JScrollPane editorScrollPane = new JScrollPane(this.editor); private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; @@ -108,6 +111,12 @@ public class EditorPanel { private EntryReference, Entry> cursorReference; private EntryReference, Entry> nextReference; + @Nullable + private ContainerToolTip toolTip; + private final Timer mouseStoppedMovingTimer = new Timer(100, e -> { + this.getMouseEntry().ifPresent(this::updateToolTip); + }); + private int fontSize = 12; private final BoxHighlightPainter obfuscatedPainter; private final BoxHighlightPainter proposedPainter; @@ -198,6 +207,14 @@ public void mouseReleased(MouseEvent e) { } }); + this.mouseStoppedMovingTimer.setRepeats(false); + this.editor.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + EditorPanel.this.mouseStoppedMovingTimer.restart(); + } + }); + this.editor.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent event) { @@ -230,22 +247,48 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } - private JToolTip createEditorTooltip() { - return getMousePositionIn(this.editor) - .map(this.editor::viewToModel2D) - .filter(textPos -> textPos >= 0) - .map(this::getToken) - .map(this::getReference) - .map(EntryReference::getNameableEntry) - .map(this.gui.getController().getProject().getRemapper()::deobfuscate) - .map(Entry::getFullName) - .map(entryName -> { - final JPanel root = new JPanel(new BorderLayout()); - root.add(new JLabel(entryName)); - return new ContainerToolTip(root); + private JToolTip createToolTip() { + return this.getMouseEntry() + .map(targetEntry -> { + this.toolTip = new ContainerToolTip<>(new JPanel(new BorderLayout()));; + this.toolTip.getRoot().setBorder(BorderFactory.createEmptyBorder()); + this.updateToolTip(targetEntry); + return this.toolTip; }) // empty dummy tooltip - .orElseGet(JToolTip::new); + .orElseGet(() -> { + this.toolTip = null; + return new JToolTip(); + }); + } + + private void updateToolTip(Entry target) { + if (this.toolTip != null) { + final JPanel toolTipContent = this.toolTip.getRoot(); + toolTipContent.removeAll(); + toolTipContent.setLayout(new BorderLayout()); + final JLabel label = new JLabel(target.getFullName()); + label.setBorder(BorderFactory.createEmptyBorder()); + toolTipContent.add(label); + + toolTipContent.setSize(this.toolTip.getPreferredSize()); + + this.toolTip.setSize(this.toolTip.getPreferredSize()); + // this.toolTip.setLocation(MouseInfo.getPointerInfo().getLocation()); + + this.toolTip.validate(); + this.toolTip.repaint(); + } + } + + private Optional> getMouseEntry() { + return getMousePositionIn(this.editor) + .map(this.editor::viewToModel2D) + .filter(textPos -> textPos >= 0) + .map(this::getToken) + .map(this::getReference) + .map(EntryReference::getNameableEntry) + .map(this.gui.getController().getProject().getRemapper()::deobfuscate); } // getMousePosition(true) always returns null for editor, editorScrollPane, and ui From 8b33d5c04a3da72ed7c27e23be28de8a451a97c9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 10:26:26 -0700 Subject: [PATCH 003/109] replace EditorPanel's use of JToolTip and TooltipManager with JWindow and timers, respectively --- .../enigma/gui/panel/ContainerToolTip.java | 34 ---- .../gui/panel/CustomTooltipEditorPane.java | 33 ---- .../quiltmc/enigma/gui/panel/EditorPanel.java | 157 +++++++++++++----- 3 files changed, 111 insertions(+), 113 deletions(-) delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java deleted file mode 100644 index 8d06499c8..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/ContainerToolTip.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.quiltmc.enigma.gui.panel; - -import javax.swing.BorderFactory; -import javax.swing.JToolTip; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; - -/** - * A {@link JToolTip} that does its best to act like a proper container for a root component. - */ -public class ContainerToolTip extends JToolTip { - private final C root; - - public ContainerToolTip(C root) { - this.root = root; - this.setLayout(new BorderLayout()); - this.add(this.root); - this.setBorder(BorderFactory.createEmptyBorder()); - } - - public C getRoot() { - return this.root; - } - - @Override - public Dimension getPreferredSize() { - if (this.isPreferredSizeSet()) { - return super.getPreferredSize(); - } else { - return this.root.getPreferredSize(); - } - } -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java deleted file mode 100644 index 3139f473a..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/CustomTooltipEditorPane.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.quiltmc.enigma.gui.panel; - -import javax.swing.JEditorPane; -import javax.swing.JToolTip; -import java.util.function.Supplier; - -public class CustomTooltipEditorPane extends JEditorPane { - private final Supplier toolTipFactory; - - public CustomTooltipEditorPane(Supplier toolTipFactory) { - this.toolTipFactory = toolTipFactory; - this.enableTooltip(); - } - - @Override - public JToolTip createToolTip() { - final JToolTip toolTip = this.toolTipFactory.get(); - toolTip.setComponent(this); - return toolTip; - } - - public void enableTooltip() { - // ToolTipManager will only create a tooltip if its test is non-null - // this also ensures this component is registered with the ToolTipManager - this.setToolTipText(""); - } - - public void disableToolTip() { - // if tooltip text is null, ToolTipManager will never create a tooltip - // this also unregisters this component with the ToolTipManager - this.setToolTipText(null); - } -} 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 d8e9fcb63..95414aef7 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,7 +36,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; -import java.awt.Dimension; +import java.awt.Container; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; @@ -46,6 +46,7 @@ import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; +import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; @@ -55,6 +56,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; @@ -62,7 +64,6 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JEditorPane; @@ -72,7 +73,7 @@ import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTextArea; -import javax.swing.JToolTip; +import javax.swing.JWindow; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -86,15 +87,16 @@ import static java.awt.event.InputEvent.CTRL_DOWN_MASK; public class EditorPanel { + public static final int MOUSE_STOPPED_MOVING_DELAY = 100; private final JPanel ui = new JPanel(); - private final CustomTooltipEditorPane editor = new CustomTooltipEditorPane(this::createToolTip); + private final JEditorPane editor = new JEditorPane(); private final JScrollPane editorScrollPane = new JScrollPane(this.editor); private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; // progress UI private final JLabel decompilingLabel = new JLabel(I18n.translate("editor.decompiling"), SwingConstants.CENTER); - private final JProgressBar decompilingProgressBar = new JProgressBar(0, 100); + private final JProgressBar decompilingProgressBar = new JProgressBar(0, MOUSE_STOPPED_MOVING_DELAY); // error display UI private final JLabel errorLabel = new JLabel(); @@ -103,6 +105,10 @@ public class EditorPanel { private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); private final NavigatorPanel navigatorPanel; + // DIY tooltip because JToolTip can't be moved or resized + // private final JFrame tooltip = new JFrame("Editor tooltip"); + private final JWindow tooltip = new JWindow(); + private DisplayMode mode = DisplayMode.INACTIVE; private final GuiController controller; @@ -112,10 +118,46 @@ public class EditorPanel { private EntryReference, Entry> nextReference; @Nullable - private ContainerToolTip toolTip; - private final Timer mouseStoppedMovingTimer = new Timer(100, e -> { - this.getMouseEntry().ifPresent(this::updateToolTip); + private Entry lastMouseTarget; + + // avoid finding the mouse entry every mouse movement update + private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { + this.getMouseTarget().ifPresentOrElse( + target -> { + this.hideTokenTooltipTimer.restart(); + if (this.tooltip.isVisible()) { + this.showTokenTooltipTimer.stop(); + + if (!target.equals(this.lastMouseTarget)) { + this.lastMouseTarget = target; + this.updateToolTip(target); + } + } else { + this.lastMouseTarget = target; + this.showTokenTooltipTimer.start(); + } + }, + () -> { + this.lastMouseTarget = null; + this.showTokenTooltipTimer.stop(); + } + ); }); + private final Timer showTokenTooltipTimer = new Timer( + ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { + this.getMouseTarget().ifPresent(target -> { + this.hideTokenTooltipTimer.restart(); + if (target.equals(this.lastMouseTarget)) { + this.tooltip.setVisible(true); + this.updateToolTip(target); + } + }); + } + ); + private final Timer hideTokenTooltipTimer = new Timer( + ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, + e -> this.tooltip.setVisible(false) + ); private int fontSize = 12; private final BoxHighlightPainter obfuscatedPainter; @@ -185,7 +227,7 @@ public void focusLost(FocusEvent e) { this.fallbackPainter = ThemeUtil.createFallbackPainter(); this.deobfuscatedPainter = ThemeUtil.createDeobfuscatedPainter(); - this.editor.addMouseListener(new MouseAdapter() { + final MouseAdapter editorMouseAdapter = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if ((e.getModifiersEx() & CTRL_DOWN_MASK) != 0 && e.getButton() == MouseEvent.BUTTON1) { @@ -194,25 +236,67 @@ public void mouseClicked(MouseEvent e) { } } + @Override + public void mousePressed(MouseEvent mouseEvent) { + EditorPanel.this.tooltip.setVisible(false); + EditorPanel.this.mouseStoppedMovingTimer.stop(); + EditorPanel.this.showTokenTooltipTimer.stop(); + EditorPanel.this.hideTokenTooltipTimer.stop(); + } + @Override public void mouseReleased(MouseEvent e) { switch (e.getButton()) { case MouseEvent.BUTTON3 -> // Right click - EditorPanel.this.editor.setCaretPosition(EditorPanel.this.editor.viewToModel2D(e.getPoint())); + EditorPanel.this.editor.setCaretPosition(EditorPanel.this.editor.viewToModel2D(e.getPoint())); case 4 -> // Back navigation - gui.getController().openPreviousReference(); + gui.getController().openPreviousReference(); case 5 -> // Forward navigation - gui.getController().openNextReference(); + gui.getController().openNextReference(); } } - }); - this.mouseStoppedMovingTimer.setRepeats(false); - this.editor.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { EditorPanel.this.mouseStoppedMovingTimer.restart(); } + }; + + this.editor.addMouseListener(editorMouseAdapter); + this.editor.addMouseMotionListener(editorMouseAdapter); + + this.mouseStoppedMovingTimer.setRepeats(false); + this.showTokenTooltipTimer.setRepeats(false); + this.hideTokenTooltipTimer.setRepeats(false); + + this.tooltip.setVisible(false); + this.tooltip.setAlwaysOnTop(true); + this.tooltip.setType(Window.Type.POPUP); + this.tooltip.setLayout(new BorderLayout()); + this.tooltip.setContentPane(new JPanel()); + + this.tooltip.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + final Point absolutMousePosition = MouseInfo.getPointerInfo().getLocation(); + mapMousePositionTo(absolutMousePosition, EditorPanel.this.editor).ifPresent(editorMousePosition -> { + final MouseEvent editorMouseEvent = new MouseEvent( + EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), + editorMousePosition.x, editorMousePosition.y, + absolutMousePosition.x, absolutMousePosition.y, + e.getClickCount(), e.isPopupTrigger(), e.getButton() + ); + + for (final MouseListener listener : EditorPanel.this.editor.getMouseListeners()) { + listener.mousePressed(editorMouseEvent); + if (editorMouseEvent.isConsumed()) { + break; + } + } + }); + + e.consume(); + } }); this.editor.addKeyListener(new KeyAdapter() { @@ -247,41 +331,18 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } - private JToolTip createToolTip() { - return this.getMouseEntry() - .map(targetEntry -> { - this.toolTip = new ContainerToolTip<>(new JPanel(new BorderLayout()));; - this.toolTip.getRoot().setBorder(BorderFactory.createEmptyBorder()); - this.updateToolTip(targetEntry); - return this.toolTip; - }) - // empty dummy tooltip - .orElseGet(() -> { - this.toolTip = null; - return new JToolTip(); - }); - } - private void updateToolTip(Entry target) { - if (this.toolTip != null) { - final JPanel toolTipContent = this.toolTip.getRoot(); - toolTipContent.removeAll(); - toolTipContent.setLayout(new BorderLayout()); - final JLabel label = new JLabel(target.getFullName()); - label.setBorder(BorderFactory.createEmptyBorder()); - toolTipContent.add(label); + final Container tooltipContent = this.tooltip.getContentPane(); + tooltipContent.removeAll(); + final JLabel label = new JLabel(target.getFullName()); + tooltipContent.add(label); - toolTipContent.setSize(this.toolTip.getPreferredSize()); + this.tooltip.setLocation(MouseInfo.getPointerInfo().getLocation()); - this.toolTip.setSize(this.toolTip.getPreferredSize()); - // this.toolTip.setLocation(MouseInfo.getPointerInfo().getLocation()); - - this.toolTip.validate(); - this.toolTip.repaint(); - } + this.tooltip.pack(); } - private Optional> getMouseEntry() { + private Optional> getMouseTarget() { return getMousePositionIn(this.editor) .map(this.editor::viewToModel2D) .filter(textPos -> textPos >= 0) @@ -293,7 +354,11 @@ private Optional> getMouseEntry() { // getMousePosition(true) always returns null for editor, editorScrollPane, and ui private static Optional getMousePositionIn(Component component) { - return Optional.of(MouseInfo.getPointerInfo().getLocation()) + return mapMousePositionTo(MouseInfo.getPointerInfo().getLocation(), component); + } + + private static Optional mapMousePositionTo(Point mousePosition, Component component) { + return Optional.of(mousePosition) .map(mouse -> { final Point editorLocation = component.getLocationOnScreen(); final Point point = new Point(mouse); From ea22d931e302e48cd9792a718ceee938679d2ed9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 10:47:24 -0700 Subject: [PATCH 004/109] check if same token instead of same entry when updating tooltip --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 66 +++++++++++-------- .../org/quiltmc/enigma/api/source/Token.java | 2 +- 2 files changed, 38 insertions(+), 30 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 95414aef7..98676f14f 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 @@ -106,7 +106,6 @@ public class EditorPanel { private final NavigatorPanel navigatorPanel; // DIY tooltip because JToolTip can't be moved or resized - // private final JFrame tooltip = new JFrame("Editor tooltip"); private final JWindow tooltip = new JWindow(); private DisplayMode mode = DisplayMode.INACTIVE; @@ -118,40 +117,45 @@ public class EditorPanel { private EntryReference, Entry> nextReference; @Nullable - private Entry lastMouseTarget; + private Token lastMouseTarget; // avoid finding the mouse entry every mouse movement update private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - this.getMouseTarget().ifPresentOrElse( - target -> { - this.hideTokenTooltipTimer.restart(); - if (this.tooltip.isVisible()) { - this.showTokenTooltipTimer.stop(); + final Runnable onNoTarget = () -> { + this.lastMouseTarget = null; + this.showTokenTooltipTimer.stop(); + }; - if (!target.equals(this.lastMouseTarget)) { - this.lastMouseTarget = target; - this.updateToolTip(target); - } - } else { - this.lastMouseTarget = target; - this.showTokenTooltipTimer.start(); - } - }, - () -> { - this.lastMouseTarget = null; - this.showTokenTooltipTimer.stop(); - } + this.getMouseTarget().ifPresentOrElse( + target -> this.getTokenEntry(target).ifPresentOrElse( + targetEntry -> { + this.hideTokenTooltipTimer.restart(); + if (this.tooltip.isVisible()) { + this.showTokenTooltipTimer.stop(); + + if (!target.equals(this.lastMouseTarget)) { + this.lastMouseTarget = target; + this.updateToolTip(targetEntry); + } + } else { + this.lastMouseTarget = target; + this.showTokenTooltipTimer.start(); + } + }, + onNoTarget + ), + onNoTarget ); }); private final Timer showTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { - this.getMouseTarget().ifPresent(target -> { + this.getMouseTarget().ifPresent(target -> this.getTokenEntry(target).ifPresent(targetEntry -> { this.hideTokenTooltipTimer.restart(); if (target.equals(this.lastMouseTarget)) { this.tooltip.setVisible(true); - this.updateToolTip(target); + this.updateToolTip(targetEntry); } - }); + })); } ); private final Timer hideTokenTooltipTimer = new Timer( @@ -342,17 +346,21 @@ private void updateToolTip(Entry target) { this.tooltip.pack(); } - private Optional> getMouseTarget() { + private Optional getMouseTarget() { return getMousePositionIn(this.editor) .map(this.editor::viewToModel2D) .filter(textPos -> textPos >= 0) - .map(this::getToken) - .map(this::getReference) - .map(EntryReference::getNameableEntry) - .map(this.gui.getController().getProject().getRemapper()::deobfuscate); + .map(this::getToken); + } + + private Optional> getTokenEntry(Token token) { + return Optional.of(token) + .map(this::getReference) + .map(reference -> reference.entry) + .map(this.gui.getController().getProject().getRemapper()::deobfuscate); } - // getMousePosition(true) always returns null for editor, editorScrollPane, and ui + // component.getMousePosition(true) always returns null for editor, editorScrollPane, and ui private static Optional getMousePositionIn(Component component) { return mapMousePositionTo(MouseInfo.getPointerInfo().getLocation(), component); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/Token.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/Token.java index 233ec7817..2d9e545ca 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/Token.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/Token.java @@ -53,7 +53,7 @@ public int hashCode() { } public boolean equals(Token other) { - return this.start == other.start && this.end == other.end && this.text.equals(other.text); + return other != null && this.start == other.start && this.end == other.end && this.text.equals(other.text); } @Override From e5336a233d700cad7364df2c53c9f9eef40fe287 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 11:36:28 -0700 Subject: [PATCH 005/109] replace complex Optional composition with consumer methods --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 147 +++++++++++------- 1 file changed, 92 insertions(+), 55 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 98676f14f..80a23924e 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,5 +1,6 @@ package org.quiltmc.enigma.gui.panel; +import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; @@ -63,6 +64,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import javax.annotation.Nullable; import javax.swing.JButton; import javax.swing.JComponent; @@ -117,45 +120,40 @@ public class EditorPanel { private EntryReference, Entry> nextReference; @Nullable - private Token lastMouseTarget; + private Token lastMouseTargetToken; // avoid finding the mouse entry every mouse movement update private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - final Runnable onNoTarget = () -> { - this.lastMouseTarget = null; - this.showTokenTooltipTimer.stop(); - }; + this.consumeEditorMouseTarget( + (targetToken, targetEntry) -> { + this.hideTokenTooltipTimer.restart(); + if (this.tooltip.isVisible()) { + this.showTokenTooltipTimer.stop(); - this.getMouseTarget().ifPresentOrElse( - target -> this.getTokenEntry(target).ifPresentOrElse( - targetEntry -> { - this.hideTokenTooltipTimer.restart(); - if (this.tooltip.isVisible()) { - this.showTokenTooltipTimer.stop(); - - if (!target.equals(this.lastMouseTarget)) { - this.lastMouseTarget = target; - this.updateToolTip(targetEntry); - } - } else { - this.lastMouseTarget = target; - this.showTokenTooltipTimer.start(); - } - }, - onNoTarget - ), - onNoTarget + if (!targetToken.equals(this.lastMouseTargetToken)) { + this.lastMouseTargetToken = targetToken; + this.updateToolTip(targetEntry); + } + } else { + this.lastMouseTargetToken = targetToken; + this.showTokenTooltipTimer.start(); + } + }, + () -> { + this.lastMouseTargetToken = null; + this.showTokenTooltipTimer.stop(); + } ); }); private final Timer showTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { - this.getMouseTarget().ifPresent(target -> this.getTokenEntry(target).ifPresent(targetEntry -> { + this.consumeEditorMouseTarget((targetToken, targetEntry) -> { this.hideTokenTooltipTimer.restart(); - if (target.equals(this.lastMouseTarget)) { + if (targetToken.equals(this.lastMouseTargetToken)) { this.tooltip.setVisible(true); this.updateToolTip(targetEntry); } - })); + }); } ); private final Timer hideTokenTooltipTimer = new Timer( @@ -282,13 +280,12 @@ public void mouseMoved(MouseEvent e) { this.tooltip.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - final Point absolutMousePosition = MouseInfo.getPointerInfo().getLocation(); - mapMousePositionTo(absolutMousePosition, EditorPanel.this.editor).ifPresent(editorMousePosition -> { + consumeMousePositionIn(EditorPanel.this.editor, (absolutMousePosition, editorMousePosition) -> { final MouseEvent editorMouseEvent = new MouseEvent( - EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), - editorMousePosition.x, editorMousePosition.y, - absolutMousePosition.x, absolutMousePosition.y, - e.getClickCount(), e.isPopupTrigger(), e.getButton() + EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), + editorMousePosition.x, editorMousePosition.y, + absolutMousePosition.x, absolutMousePosition.y, + e.getClickCount(), e.isPopupTrigger(), e.getButton() ); for (final MouseListener listener : EditorPanel.this.editor.getMouseListeners()) { @@ -346,34 +343,74 @@ private void updateToolTip(Entry target) { this.tooltip.pack(); } - private Optional getMouseTarget() { - return getMousePositionIn(this.editor) - .map(this.editor::viewToModel2D) - .filter(textPos -> textPos >= 0) - .map(this::getToken); + /** + * @see #consumeEditorMouseTarget(BiConsumer, Runnable) + */ + private void consumeEditorMouseTarget(BiConsumer> action) { + this.consumeEditorMouseTarget(action, Runnables.doNothing()); } - private Optional> getTokenEntry(Token token) { - return Optional.of(token) - .map(this::getReference) - .map(reference -> reference.entry) - .map(this.gui.getController().getProject().getRemapper()::deobfuscate); + /** + * If the mouse is currently over a {@link Token} in the {@link #editor} that resolves to an {@link Entry}, passes + * the token and entry to the passed {@code action}.
+ * Otherwise, calls the passed {@code onNoTarget}. + * + * @param action the action to run when the mouse is over a token that resolves to an entry + * @param onNoTarget the action to run when the mouse is not over a token that resolves to an entry + */ + private void consumeEditorMouseTarget(BiConsumer> action, Runnable onNoTarget) { + consumeMousePositionIn(this.editor, + (absoluteMouse, relativeMouse) -> Optional.of(relativeMouse) + .map(this.editor::viewToModel2D) + .filter(textPos -> textPos >= 0) + .map(this::getToken) + .ifPresentOrElse( + token -> Optional.of(token) + .map(this::getReference) + .map(reference -> reference.entry) + .map(this.gui.getController().getProject().getRemapper()::deobfuscate) + .ifPresentOrElse( + entry -> action.accept(token, entry), + onNoTarget + ), + onNoTarget + ), + ignored -> onNoTarget.run() + ); } - // component.getMousePosition(true) always returns null for editor, editorScrollPane, and ui - private static Optional getMousePositionIn(Component component) { - return mapMousePositionTo(MouseInfo.getPointerInfo().getLocation(), component); + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + private static void consumeMousePositionIn(Component component, BiConsumer inAction) { + consumeMousePositionIn(component, inAction, pos -> { }); } - private static Optional mapMousePositionTo(Point mousePosition, Component component) { - return Optional.of(mousePosition) - .map(mouse -> { - final Point editorLocation = component.getLocationOnScreen(); - final Point point = new Point(mouse); - point.translate(-editorLocation.x, -editorLocation.y); - return point; - }) - .filter(component::contains); + /** + * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse + * position and its position relative to the passed {@code component} to the passed {@code inAction}.
+ * Otherwise, passes the absolute mouse position to the passed {@code outAction}. + * + * @param component the component which may contain the mouse pointer + * @param inAction the action to run if the mouse is inside the passed {@code component}; + * receives the mouse's absolute position and its position relative to the component + * @param outAction the action to run if the mouse is outside the passed {@code component}; + * receives the mouse's absolute position + */ + private static void consumeMousePositionIn( + Component component, BiConsumer inAction, Consumer outAction + ) { + final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); + + final Point componentPos = component.getLocationOnScreen(); + final Point relativePos = new Point(absolutePos); + relativePos.translate(-componentPos.x, -componentPos.y); + + if (component.contains(relativePos)) { + inAction.accept(absolutePos, relativePos); + } else { + outAction.accept(absolutePos); + } } public void onRename(boolean isNewMapping) { From 79fe0e750eb08c92957e3ca22b8a5dfc3d523b77 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 15:48:22 -0700 Subject: [PATCH 006/109] extract BaseEditorPanel without tooltip, navigator, or listeners --- .../main/java/org/quiltmc/enigma/gui/Gui.java | 3 +- .../enigma/gui/panel/BaseEditorPanel.java | 508 +++++++++++++++++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 516 ++---------------- 3 files changed, 554 insertions(+), 473 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java 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 f0270a1e1..588772bc5 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 @@ -28,6 +28,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.EditorPanel; import org.quiltmc.enigma.gui.panel.IdentifierPanel; import org.quiltmc.enigma.gui.renderer.MessageListCellRenderer; @@ -402,7 +403,7 @@ public void setMappingsFile(Path path) { this.updateUiState(); } - public void showTokens(EditorPanel editor, List tokens) { + public void showTokens(BaseEditorPanel 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/BaseEditorPanel.java new file mode 100644 index 000000000..1e080928e --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java @@ -0,0 +1,508 @@ +package org.quiltmc.enigma.gui.panel; + +import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.class_handle.ClassHandleError; +import org.quiltmc.enigma.api.event.ClassHandleListener; +import org.quiltmc.enigma.api.source.DecompiledClassSource; +import org.quiltmc.enigma.api.source.Token; +import org.quiltmc.enigma.api.source.TokenStore; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.gui.BrowserCaret; +import org.quiltmc.enigma.gui.EditableType; +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.theme.ThemeUtil; +import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; +import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; +import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; +import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.Result; +import org.tinylog.Logger; + +import javax.annotation.Nullable; +import javax.swing.JButton; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTextArea; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Highlighter.HighlightPainter; +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +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.Collection; +import java.util.List; +import java.util.Map; + +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 GuiController controller; + protected final Gui gui; + + // progress UI + private final JLabel decompilingLabel = new JLabel(I18n.translate("editor.decompiling"), SwingConstants.CENTER); + private final JProgressBar decompilingProgressBar = new JProgressBar(0, 100); + + // error display UI + private final JLabel errorLabel = new JLabel(); + private final JTextArea errorTextArea = new JTextArea(); + private final JScrollPane errorScrollPane = new JScrollPane(this.errorTextArea); + private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); + + private DisplayMode mode = DisplayMode.INACTIVE; + + protected EntryReference, Entry> cursorReference; + private EntryReference, Entry> nextReference; + + private int fontSize = 12; + private final BoxHighlightPainter obfuscatedPainter; + private final BoxHighlightPainter proposedPainter; + private final BoxHighlightPainter deobfuscatedPainter; + private final BoxHighlightPainter debugPainter; + private final BoxHighlightPainter fallbackPainter; + + protected ClassHandle classHandle; + private DecompiledClassSource source; + protected boolean settingSource; + + public BaseEditorPanel(Gui gui) { + this.gui = gui; + this.controller = gui.getController(); + + this.editor.setEditable(false); + this.editor.setLayout(new FlowLayout(FlowLayout.RIGHT)); + this.editor.setSelectionColor(new Color(31, 46, 90)); + this.editor.setCaret(new BrowserCaret()); + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); + this.editor.setCaretColor(Config.getCurrentSyntaxPaneColors().caret.value()); + this.editor.setContentType("text/enigma-sources"); + this.editor.setBackground(Config.getCurrentSyntaxPaneColors().editorBackground.value()); + // set unit increment to height of one line, the amount scrolled per + // mouse wheel rotation is then controlled by OS settings + this.editorScrollPane.getVerticalScrollBar().setUnitIncrement(this.editor.getFontMetrics(this.editor.getFont()).getHeight()); + + this.decompilingLabel.setFont(ScaleUtil.getFont(this.decompilingLabel.getFont().getFontName(), Font.BOLD, 26)); + this.decompilingProgressBar.setIndeterminate(true); + this.errorTextArea.setEditable(false); + this.errorTextArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 10)); + + this.obfuscatedPainter = ThemeUtil.createObfuscatedPainter(); + this.proposedPainter = ThemeUtil.createProposedPainter(); + this.debugPainter = ThemeUtil.createDebugPainter(); + this.fallbackPainter = ThemeUtil.createFallbackPainter(); + this.deobfuscatedPainter = ThemeUtil.createDeobfuscatedPainter(); + + this.retryButton.addActionListener(e -> this.redecompileClass()); + } + + public void setClassHandle(ClassHandle handle) { + ClassEntry old = null; + if (this.classHandle != null) { + old = this.classHandle.getRef(); + this.classHandle.close(); + } + + this.setClassHandleImpl(old, handle); + } + + protected void setClassHandleImpl(ClassEntry old, ClassHandle handle) { + this.setDisplayMode(DisplayMode.IN_PROGRESS); + this.setCursorReference(null); + + handle.addListener(new ClassHandleListener() { + @Override + public void onMappedSourceChanged(ClassHandle h, Result res) { + BaseEditorPanel.this.handleDecompilerResult(res); + } + + @Override + public void onInvalidate(ClassHandle h, InvalidationType t) { + SwingUtilities.invokeLater(() -> { + if (t == InvalidationType.FULL) { + BaseEditorPanel.this.setDisplayMode(DisplayMode.IN_PROGRESS); + } + }); + } + }); + + handle.getSource().thenAcceptAsync(this::handleDecompilerResult, SwingUtilities::invokeLater); + + this.classHandle = handle; + } + + public void destroy() { + this.classHandle.close(); + } + + private void redecompileClass() { + if (this.classHandle != null) { + this.classHandle.invalidate(); + } + } + + private void handleDecompilerResult(Result res) { + SwingUtilities.invokeLater(() -> { + if (res.isOk()) { + this.setSource(res.unwrap()); + } else { + this.displayError(res.unwrapErr()); + } + + this.nextReference = null; + }); + } + + public void displayError(ClassHandleError t) { + this.setDisplayMode(DisplayMode.ERRORED); + String str = switch (t.type) { + case DECOMPILE -> "editor.decompile_error"; + case REMAP -> "editor.remap_error"; + }; + this.errorLabel.setText(I18n.translate(str)); + this.errorTextArea.setText(t.getStackTrace()); + this.errorTextArea.setCaretPosition(0); + } + + private void setDisplayMode(DisplayMode mode) { + if (this.mode == mode) return; + this.ui.removeAll(); + switch (mode) { + case INACTIVE: + break; + case IN_PROGRESS: { + // make progress bar start from the left every time + this.decompilingProgressBar.setIndeterminate(false); + this.decompilingProgressBar.setIndeterminate(true); + + this.ui.setLayout(new GridBagLayout()); + GridBagConstraintsBuilder cb = GridBagConstraintsBuilder.create().insets(2); + this.ui.add(this.decompilingLabel, cb.pos(0, 0).anchor(GridBagConstraints.SOUTH).build()); + this.ui.add(this.decompilingProgressBar, cb.pos(0, 1).anchor(GridBagConstraints.NORTH).build()); + break; + } + case SUCCESS: { + this.ui.setLayout(new GridLayout(1, 1, 0, 0)); + + final JPanel editorPane = new JPanel() { + @Override + public boolean isOptimizedDrawingEnabled() { + return false; + } + }; + editorPane.setLayout(new GridBagLayout()); + + this.initEditorPane(editorPane); + + this.ui.add(editorPane); + break; + } + case ERRORED: { + this.ui.setLayout(new GridBagLayout()); + GridBagConstraintsBuilder cb = GridBagConstraintsBuilder.create().insets(2).weight(1.0, 0.0).anchor(GridBagConstraints.WEST); + this.ui.add(this.errorLabel, cb.pos(0, 0).build()); + this.ui.add(new JSeparator(SwingConstants.HORIZONTAL), cb.pos(0, 1).fill(GridBagConstraints.HORIZONTAL).build()); + this.ui.add(this.errorScrollPane, cb.pos(0, 2).weight(1.0, 1.0).fill(GridBagConstraints.BOTH).build()); + this.ui.add(this.retryButton, cb.pos(0, 3).weight(0.0, 0.0).anchor(GridBagConstraints.EAST).build()); + break; + } + } + + this.ui.validate(); + this.ui.repaint(); + this.mode = mode; + } + + protected void initEditorPane(JPanel editorPane) { + final GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridx = 0; + constraints.gridy = 0; + constraints.weightx = 1.0; + constraints.weighty = 1.0; + constraints.fill = GridBagConstraints.BOTH; + editorPane.add(this.editorScrollPane, constraints); + } + + public void offsetEditorZoom(int zoomAmount) { + int newResult = this.fontSize + zoomAmount; + if (newResult > 8 && newResult < 72) { + this.fontSize = newResult; + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); + } + } + + public void resetEditorZoom() { + this.fontSize = 12; + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); + } + + protected void setCursorReference(EntryReference, Entry> ref) { + this.cursorReference = ref; + } + + public Token getToken(int pos) { + if (this.source == null) { + return null; + } + + return this.source.getIndex().getReferenceToken(pos); + } + + @Nullable + public EntryReference, Entry> getReference(Token token) { + if (this.source == null) { + return null; + } + + return this.source.getIndex().getReference(token); + } + + public void setSource(DecompiledClassSource source) { + this.setDisplayMode(DisplayMode.SUCCESS); + if (source == null) return; + try { + this.settingSource = true; + + int newCaretPos = 0; + if (this.source != null && this.source.getEntry().equals(source.getEntry())) { + int caretPos = this.editor.getCaretPosition(); + + if (this.source.getTokenStore().isCompatible(source.getTokenStore())) { + newCaretPos = this.source.getTokenStore().mapPosition(source.getTokenStore(), caretPos); + } else { + // if the class is the same but the token stores aren't + // compatible, then the user probably switched decompilers + + // check if there's a selected reference we can navigate to, + // but only if there's none already queued up for being selected + if (this.getCursorReference() != null && this.nextReference == null) { + this.nextReference = this.getCursorReference(); + } + + // otherwise fall back to just using the same average + // position in the file + float scale = (float) source.toString().length() / this.source.toString().length(); + newCaretPos = (int) (caretPos * scale); + } + } + + this.source = source; + this.editor.getHighlighter().removeAllHighlights(); + this.editor.setText(source.toString()); + + this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); + if (this.source != null) { + this.editor.setCaretPosition(newCaretPos); + + this.onSourceSet(source); + } + + this.setCursorReference(this.getReference(this.getToken(this.editor.getCaretPosition()))); + } finally { + this.settingSource = false; + } + + if (this.nextReference != null) { + this.showReferenceImpl(this.nextReference); + this.nextReference = null; + } + } + + protected void onSourceSet(DecompiledClassSource source) { } + + public void setHighlightedTokens(TokenStore tokenStore, Map> tokens) { + // remove any old highlighters + this.editor.getHighlighter().removeAllHighlights(); + + for (TokenType type : tokens.keySet()) { + BoxHighlightPainter typePainter = switch (type) { + case OBFUSCATED -> this.obfuscatedPainter; + case DEOBFUSCATED -> this.deobfuscatedPainter; + case DEBUG -> this.debugPainter; + case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposedPainter; + }; + + for (Token token : tokens.get(type)) { + BoxHighlightPainter tokenPainter = typePainter; + EntryReference, Entry> reference = this.getReference(token); + + if (reference != null) { + EditableType t = EditableType.fromEntry(reference.entry); + boolean editable = t == null || this.gui.isEditable(t); + boolean fallback = tokenStore.isFallback(token); + tokenPainter = editable ? (fallback ? this.fallbackPainter : typePainter) : this.proposedPainter; + } + + this.addHighlightedToken(token, tokenPainter); + } + } + + this.editor.validate(); + this.editor.repaint(); + } + + private void addHighlightedToken(Token token, HighlightPainter tokenPainter) { + try { + this.editor.getHighlighter().addHighlight(token.start, token.end, tokenPainter); + } catch (BadLocationException ex) { + throw new IllegalArgumentException(ex); + } + } + + public EntryReference, Entry> getCursorReference() { + return this.cursorReference; + } + + public void showReference(EntryReference, Entry> reference) { + if (this.mode == DisplayMode.SUCCESS) { + this.showReferenceImpl(reference); + } else if (this.mode != DisplayMode.ERRORED) { + this.nextReference = reference; + } + } + + /** + * Navigates to the reference without modifying history. Assumes the class is loaded. + */ + private void showReferenceImpl(EntryReference, Entry> reference) { + if (this.source == null || reference == null) { + return; + } + + List tokens = this.controller.getTokensForReference(this.source, reference); + if (tokens.isEmpty()) { + // DEBUG + Logger.debug("No tokens found for {} in {}", reference, this.classHandle.getRef()); + } else { + this.gui.showTokens(this, tokens); + } + } + + public void navigateToToken(Token token) { + if (token == null) { + throw new IllegalArgumentException("Token cannot be null!"); + } + + this.navigateToToken(token, SelectionHighlightPainter.INSTANCE); + } + + private void navigateToToken(Token token, HighlightPainter highlightPainter) { + // set the caret position to the token + Document document = this.editor.getDocument(); + int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); + + this.editor.setCaretPosition(clampedPosition); + this.editor.grabFocus(); + + try { + // make sure the token is visible in the scroll window + Rectangle2D start = this.editor.modelToView2D(token.start); + Rectangle2D end = this.editor.modelToView2D(token.end); + if (start == null || end == null) { + return; + } + + Rectangle show = new Rectangle(); + Rectangle2D.union(start, end, show); + show.grow((int) (start.getWidth() * 10), (int) (start.getHeight() * 6)); + SwingUtilities.invokeLater(() -> this.editor.scrollRectToVisible(show)); + } catch (BadLocationException ex) { + if (!this.settingSource) { + throw new RuntimeException(ex); + } else { + return; + } + } + + // highlight the token momentarily + Timer timer = new Timer(200, new ActionListener() { + private int counter = 0; + private Object highlight = null; + + @Override + public void actionPerformed(ActionEvent event) { + if (this.counter % 2 == 0) { + try { + this.highlight = BaseEditorPanel.this.editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); + } catch (BadLocationException ex) { + // don't care + } + } else if (this.highlight != null) { + BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); + } + + if (this.counter++ > 6) { + Timer timer = (Timer) event.getSource(); + timer.stop(); + } + } + }); + + timer.start(); + } + + // public void addListener(EditorActionListener listener) { + // this.listeners.add(listener); + // } + + // public void removeListener(EditorActionListener listener) { + // this.listeners.remove(listener); + // } + + public JPanel getUi() { + return this.ui; + } + + public JEditorPane getEditor() { + return this.editor; + } + + public DecompiledClassSource getSource() { + return this.source; + } + + public ClassHandle getClassHandle() { + return this.classHandle; + } + + public String getSimpleClassName() { + return this.getDeobfOrObfHandleRef().getSimpleName(); + } + + public String getFullClassName() { + return this.getDeobfOrObfHandleRef().getFullName(); + } + + private ClassEntry getDeobfOrObfHandleRef() { + final ClassEntry deobfRef = this.classHandle.getDeobfRef(); + return deobfRef == null ? this.classHandle.getRef() : deobfRef; + } + + private enum DisplayMode { + INACTIVE, + IN_PROGRESS, + SUCCESS, + ERRORED, + } +} 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 80a23924e..05c8f2295 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 @@ -4,7 +4,6 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; -import org.quiltmc.enigma.api.class_handle.ClassHandleError; import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.TokenStore; import org.quiltmc.enigma.gui.BrowserCaret; @@ -25,7 +24,15 @@ import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.source.Token; +import org.quiltmc.enigma.api.source.DecompiledClassSource; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.keybind.KeyBinds; +import org.quiltmc.enigma.gui.element.EditorPopupMenu; +import org.quiltmc.enigma.gui.element.NavigatorPanel; +import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.util.I18n; @@ -33,20 +40,15 @@ import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; import org.tinylog.Logger; +import org.quiltmc.enigma.gui.event.EditorActionListener; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Component; import java.awt.Container; -import java.awt.FlowLayout; -import java.awt.Font; import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; -import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -58,26 +60,16 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; -import java.awt.geom.Rectangle2D; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; import javax.annotation.Nullable; -import javax.swing.JButton; import javax.swing.JComponent; -import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.JTextArea; import javax.swing.JWindow; -import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; @@ -89,36 +81,16 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; import static java.awt.event.InputEvent.CTRL_DOWN_MASK; -public class EditorPanel { - public static final int MOUSE_STOPPED_MOVING_DELAY = 100; - private final JPanel ui = new JPanel(); - private final JEditorPane editor = new JEditorPane(); - private final JScrollPane editorScrollPane = new JScrollPane(this.editor); - private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); - private final EditorPopupMenu popupMenu; - - // progress UI - private final JLabel decompilingLabel = new JLabel(I18n.translate("editor.decompiling"), SwingConstants.CENTER); - private final JProgressBar decompilingProgressBar = new JProgressBar(0, MOUSE_STOPPED_MOVING_DELAY); +public class EditorPanel extends BaseEditorPanel { + private static final int MOUSE_STOPPED_MOVING_DELAY = 100; - // error display UI - private final JLabel errorLabel = new JLabel(); - private final JTextArea errorTextArea = new JTextArea(); - private final JScrollPane errorScrollPane = new JScrollPane(this.errorTextArea); - private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); private final NavigatorPanel navigatorPanel; + private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); + private final EditorPopupMenu popupMenu; // DIY tooltip because JToolTip can't be moved or resized private final JWindow tooltip = new JWindow(); - private DisplayMode mode = DisplayMode.INACTIVE; - - private final GuiController controller; - private final Gui gui; - - private EntryReference, Entry> cursorReference; - private EntryReference, Entry> nextReference; - @Nullable private Token lastMouseTargetToken; @@ -161,33 +133,14 @@ public class EditorPanel { e -> this.tooltip.setVisible(false) ); - private int fontSize = 12; - private final BoxHighlightPainter obfuscatedPainter; - private final BoxHighlightPainter proposedPainter; - private final BoxHighlightPainter deobfuscatedPainter; - private final BoxHighlightPainter debugPainter; - public final BoxHighlightPainter fallbackPainter; - private final List listeners = new ArrayList<>(); - private ClassHandle classHandle; - private DecompiledClassSource source; - private boolean settingSource; - public EditorPanel(Gui gui, NavigatorPanel navigator) { - this.gui = gui; - this.controller = gui.getController(); + super(gui); + this.navigatorPanel = navigator; - this.editor.setEditable(false); - this.editor.setLayout(new FlowLayout(FlowLayout.RIGHT)); - this.editor.setSelectionColor(new Color(31, 46, 90)); - this.editor.setCaret(new BrowserCaret()); - this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); - this.editor.setCaretColor(Config.getCurrentSyntaxPaneColors().caret.value()); - this.editor.setContentType("text/enigma-sources"); - this.editor.setBackground(Config.getCurrentSyntaxPaneColors().editorBackground.value()); // HACK to prevent DefaultCaret from calling setSelectionVisible(false) when quickFind gains focus if (this.editor.getCaret() instanceof FocusListener caretFocusListener) { @@ -209,25 +162,8 @@ public void focusLost(FocusEvent e) { } this.quickFindToolBar.setVisible(false); - - // set unit increment to height of one line, the amount scrolled per - // mouse wheel rotation is then controlled by OS settings - this.editorScrollPane.getVerticalScrollBar().setUnitIncrement(this.editor.getFontMetrics(this.editor.getFont()).getHeight()); - // init editor popup menu this.popupMenu = new EditorPopupMenu(this, gui); - this.editor.setComponentPopupMenu(this.popupMenu.getUi()); - - this.decompilingLabel.setFont(ScaleUtil.getFont(this.decompilingLabel.getFont().getFontName(), Font.BOLD, 26)); - this.decompilingProgressBar.setIndeterminate(true); - this.errorTextArea.setEditable(false); - this.errorTextArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 10)); - - this.obfuscatedPainter = ThemeUtil.createObfuscatedPainter(); - this.proposedPainter = ThemeUtil.createProposedPainter(); - this.debugPainter = ThemeUtil.createDebugPainter(); - this.fallbackPainter = ThemeUtil.createFallbackPainter(); - this.deobfuscatedPainter = ThemeUtil.createDeobfuscatedPainter(); final MouseAdapter editorMouseAdapter = new MouseAdapter() { @Override @@ -266,6 +202,7 @@ public void mouseMoved(MouseEvent e) { this.editor.addMouseListener(editorMouseAdapter); this.editor.addMouseMotionListener(editorMouseAdapter); + this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); this.mouseStoppedMovingTimer.setRepeats(false); this.showTokenTooltipTimer.setRepeats(false); @@ -327,8 +264,6 @@ public void keyTyped(KeyEvent event) { this.reloadKeyBinds(); - this.retryButton.addActionListener(e -> this.redecompileClass()); - this.ui.putClientProperty(EditorPanel.class, this); } @@ -420,6 +355,22 @@ public void onRename(boolean isNewMapping) { } } + @Override + protected void initEditorPane(JPanel editorPane) { + final GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridx = 0; + constraints.gridy = 0; + constraints.weightx = 1.0; + constraints.weighty = 1.0; + constraints.anchor = GridBagConstraints.FIRST_LINE_END; + constraints.insets = new Insets(32, 32, 32, 32); + constraints.ipadx = 16; + constraints.ipady = 16; + editorPane.add(this.navigatorPanel, constraints); + + super.initEditorPane(editorPane); + } + @Nullable public static EditorPanel byUi(Component ui) { if (ui instanceof JComponent component) { @@ -436,19 +387,9 @@ public NavigatorPanel getNavigatorPanel() { return this.navigatorPanel; } - public void setClassHandle(ClassHandle handle) { - ClassEntry old = null; - if (this.classHandle != null) { - old = this.classHandle.getRef(); - this.classHandle.close(); - } - - this.setClassHandle0(old, handle); - } - - private void setClassHandle0(ClassEntry old, ClassHandle handle) { - this.setDisplayMode(DisplayMode.IN_PROGRESS); - this.setCursorReference(null); + @Override + protected void setClassHandleImpl(ClassEntry old, ClassHandle handle) { + super.setClassHandleImpl(old, handle); handle.addListener(new ClassHandleListener() { @Override @@ -456,158 +397,15 @@ public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { SwingUtilities.invokeLater(() -> EditorPanel.this.listeners.forEach(l -> l.onTitleChanged(EditorPanel.this, EditorPanel.this.getSimpleClassName()))); } - @Override - public void onMappedSourceChanged(ClassHandle h, Result res) { - EditorPanel.this.handleDecompilerResult(res); - } - - @Override - public void onInvalidate(ClassHandle h, InvalidationType t) { - SwingUtilities.invokeLater(() -> { - if (t == InvalidationType.FULL) { - EditorPanel.this.setDisplayMode(DisplayMode.IN_PROGRESS); - } - }); - } - @Override public void onDeleted(ClassHandle h) { SwingUtilities.invokeLater(() -> EditorPanel.this.gui.closeEditor(EditorPanel.this)); } }); - handle.getSource().thenAcceptAsync(this::handleDecompilerResult, SwingUtilities::invokeLater); - - this.classHandle = handle; this.listeners.forEach(l -> l.onClassHandleChanged(this, old, handle)); } - public void destroy() { - this.classHandle.close(); - } - - private void redecompileClass() { - if (this.classHandle != null) { - this.classHandle.invalidate(); - } - } - - private void handleDecompilerResult(Result res) { - SwingUtilities.invokeLater(() -> { - if (res.isOk()) { - this.setSource(res.unwrap()); - } else { - this.displayError(res.unwrapErr()); - } - - this.nextReference = null; - }); - } - - public void displayError(ClassHandleError t) { - this.setDisplayMode(DisplayMode.ERRORED); - String str = switch (t.type) { - case DECOMPILE -> "editor.decompile_error"; - case REMAP -> "editor.remap_error"; - }; - this.errorLabel.setText(I18n.translate(str)); - this.errorTextArea.setText(t.getStackTrace()); - this.errorTextArea.setCaretPosition(0); - } - - public void setDisplayMode(DisplayMode mode) { - if (this.mode == mode) return; - this.ui.removeAll(); - switch (mode) { - case INACTIVE: - break; - case IN_PROGRESS: { - // make progress bar start from the left every time - this.decompilingProgressBar.setIndeterminate(false); - this.decompilingProgressBar.setIndeterminate(true); - - this.ui.setLayout(new GridBagLayout()); - GridBagConstraintsBuilder cb = GridBagConstraintsBuilder.create().insets(2); - this.ui.add(this.decompilingLabel, cb.pos(0, 0).anchor(GridBagConstraints.SOUTH).build()); - this.ui.add(this.decompilingProgressBar, cb.pos(0, 1).anchor(GridBagConstraints.NORTH).build()); - break; - } - case SUCCESS: { - this.ui.setLayout(new GridLayout(1, 1, 0, 0)); - - JPanel editorPane = new JPanel() { - @Override - public boolean isOptimizedDrawingEnabled() { - return false; - } - }; - editorPane.setLayout(new GridBagLayout()); - - { - final var navigatorConstraints = new GridBagConstraints(); - navigatorConstraints.gridx = 0; - navigatorConstraints.gridy = 0; - navigatorConstraints.weightx = 1.0; - navigatorConstraints.weighty = 1.0; - navigatorConstraints.anchor = GridBagConstraints.FIRST_LINE_END; - navigatorConstraints.insets = new Insets(32, 32, 32, 32); - navigatorConstraints.ipadx = 16; - navigatorConstraints.ipady = 16; - editorPane.add(this.navigatorPanel, navigatorConstraints); - } - - { - final var scrollConstraints = new GridBagConstraints(); - scrollConstraints.gridx = 0; - scrollConstraints.gridy = 0; - scrollConstraints.weightx = 1.0; - scrollConstraints.weighty = 1.0; - scrollConstraints.fill = GridBagConstraints.BOTH; - editorPane.add(this.editorScrollPane, scrollConstraints); - } - - { - final var quickFindConstraints = new GridBagConstraints(); - quickFindConstraints.gridx = 0; - quickFindConstraints.weightx = 1.0; - quickFindConstraints.weighty = 0; - quickFindConstraints.anchor = GridBagConstraints.PAGE_END; - quickFindConstraints.fill = GridBagConstraints.HORIZONTAL; - editorPane.add(this.quickFindToolBar, quickFindConstraints); - } - - this.ui.add(editorPane); - break; - } - case ERRORED: { - this.ui.setLayout(new GridBagLayout()); - GridBagConstraintsBuilder cb = GridBagConstraintsBuilder.create().insets(2).weight(1.0, 0.0).anchor(GridBagConstraints.WEST); - this.ui.add(this.errorLabel, cb.pos(0, 0).build()); - this.ui.add(new JSeparator(SwingConstants.HORIZONTAL), cb.pos(0, 1).fill(GridBagConstraints.HORIZONTAL).build()); - this.ui.add(this.errorScrollPane, cb.pos(0, 2).weight(1.0, 1.0).fill(GridBagConstraints.BOTH).build()); - this.ui.add(this.retryButton, cb.pos(0, 3).weight(0.0, 0.0).anchor(GridBagConstraints.EAST).build()); - break; - } - } - - this.ui.validate(); - this.ui.repaint(); - this.mode = mode; - } - - public void offsetEditorZoom(int zoomAmount) { - int newResult = this.fontSize + zoomAmount; - if (newResult > 8 && newResult < 72) { - this.fontSize = newResult; - this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); - } - } - - public void resetEditorZoom() { - this.fontSize = 12; - this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); - } - private void onCaretMove(int pos) { if (this.settingSource || this.controller.getProject() == null) { return; @@ -628,215 +426,25 @@ private void navigateToCursorReference() { } } - private void setCursorReference(EntryReference, Entry> ref) { - this.cursorReference = ref; + @Override + protected void setCursorReference(EntryReference, Entry> ref) { + super.setCursorReference(ref); this.popupMenu.updateUiState(); this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref)); } - public Token getToken(int pos) { - if (this.source == null) { - return null; - } - - return this.source.getIndex().getReferenceToken(pos); - } - - @Nullable - public EntryReference, Entry> getReference(Token token) { - if (this.source == null) { - return null; - } - - return this.source.getIndex().getReference(token); - } - - public void setSource(DecompiledClassSource source) { - this.setDisplayMode(DisplayMode.SUCCESS); - if (source == null) return; - try { - this.settingSource = true; - - int newCaretPos = 0; - if (this.source != null && this.source.getEntry().equals(source.getEntry())) { - int caretPos = this.editor.getCaretPosition(); - - if (this.source.getTokenStore().isCompatible(source.getTokenStore())) { - newCaretPos = this.source.getTokenStore().mapPosition(source.getTokenStore(), caretPos); - } else { - // if the class is the same but the token stores aren't - // compatible, then the user probably switched decompilers - - // check if there's a selected reference we can navigate to, - // but only if there's none already queued up for being selected - if (this.getCursorReference() != null && this.nextReference == null) { - this.nextReference = this.getCursorReference(); - } - - // otherwise fall back to just using the same average - // position in the file - float scale = (float) source.toString().length() / this.source.toString().length(); - newCaretPos = (int) (caretPos * scale); - } - } - - this.source = source; - this.editor.getHighlighter().removeAllHighlights(); - this.editor.setText(source.toString()); - - this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); - if (this.source != null) { - this.editor.setCaretPosition(newCaretPos); - - for (Entry entry : this.source.getIndex().declarations()) { - this.navigatorPanel.addEntry(entry); - } + @Override + protected void onSourceSet(DecompiledClassSource source) { + super.onSourceSet(source); + if (this.navigatorPanel != null) { + for (Entry entry : source.getIndex().declarations()) { + this.navigatorPanel.addEntry(entry); } - - this.setCursorReference(this.getReference(this.getToken(this.editor.getCaretPosition()))); - } finally { - this.settingSource = false; - } - - if (this.nextReference != null) { - this.showReference0(this.nextReference); - this.nextReference = null; - } - } - - public void setHighlightedTokens(TokenStore tokenStore, Map> tokens) { - // remove any old highlighters - this.editor.getHighlighter().removeAllHighlights(); - - for (TokenType type : tokens.keySet()) { - BoxHighlightPainter typePainter = switch (type) { - case OBFUSCATED -> this.obfuscatedPainter; - case DEOBFUSCATED -> this.deobfuscatedPainter; - case DEBUG -> this.debugPainter; - case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposedPainter; - }; - - for (Token token : tokens.get(type)) { - BoxHighlightPainter tokenPainter = typePainter; - EntryReference, Entry> reference = this.getReference(token); - - if (reference != null) { - EditableType t = EditableType.fromEntry(reference.entry); - boolean editable = t == null || this.gui.isEditable(t); - boolean fallback = tokenStore.isFallback(token); - tokenPainter = editable ? (fallback ? this.fallbackPainter : typePainter) : this.proposedPainter; - } - - this.addHighlightedToken(token, tokenPainter); - } - } - - this.editor.validate(); - this.editor.repaint(); - } - - private void addHighlightedToken(Token token, HighlightPainter tokenPainter) { - try { - this.editor.getHighlighter().addHighlight(token.start, token.end, tokenPainter); - } catch (BadLocationException ex) { - throw new IllegalArgumentException(ex); - } - } - - public EntryReference, Entry> getCursorReference() { - return this.cursorReference; - } - - public void showReference(EntryReference, Entry> reference) { - if (this.mode == DisplayMode.SUCCESS) { - this.showReference0(reference); - } else if (this.mode != DisplayMode.ERRORED) { - this.nextReference = reference; - } - } - - /** - * Navigates to the reference without modifying history. Assumes the class is loaded. - */ - private void showReference0(EntryReference, Entry> reference) { - if (this.source == null || reference == null) { - return; - } - - List tokens = this.controller.getTokensForReference(this.source, reference); - if (tokens.isEmpty()) { - // DEBUG - Logger.debug("No tokens found for {} in {}", reference, this.classHandle.getRef()); - } else { - this.gui.showTokens(this, tokens); } } - public void navigateToToken(Token token) { - if (token == null) { - throw new IllegalArgumentException("Token cannot be null!"); - } - - this.navigateToToken(token, SelectionHighlightPainter.INSTANCE); - } - - private void navigateToToken(Token token, HighlightPainter highlightPainter) { - // set the caret position to the token - Document document = this.editor.getDocument(); - int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); - - this.editor.setCaretPosition(clampedPosition); - this.editor.grabFocus(); - - try { - // make sure the token is visible in the scroll window - Rectangle2D start = this.editor.modelToView2D(token.start); - Rectangle2D end = this.editor.modelToView2D(token.end); - if (start == null || end == null) { - return; - } - - Rectangle show = new Rectangle(); - Rectangle2D.union(start, end, show); - show.grow((int) (start.getWidth() * 10), (int) (start.getHeight() * 6)); - SwingUtilities.invokeLater(() -> this.editor.scrollRectToVisible(show)); - } catch (BadLocationException ex) { - if (!this.settingSource) { - throw new RuntimeException(ex); - } else { - return; - } - } - - // highlight the token momentarily - Timer timer = new Timer(200, new ActionListener() { - private int counter = 0; - private Object highlight = null; - - @Override - public void actionPerformed(ActionEvent event) { - if (this.counter % 2 == 0) { - try { - this.highlight = EditorPanel.this.editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); - } catch (BadLocationException ex) { - // don't care - } - } else if (this.highlight != null) { - EditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); - } - - if (this.counter++ > 6) { - Timer timer = (Timer) event.getSource(); - timer.stop(); - } - } - }); - - timer.start(); - } - public void addListener(EditorActionListener listener) { this.listeners.add(listener); } @@ -845,35 +453,6 @@ public void removeListener(EditorActionListener listener) { this.listeners.remove(listener); } - public JPanel getUi() { - return this.ui; - } - - public JEditorPane getEditor() { - return this.editor; - } - - public DecompiledClassSource getSource() { - return this.source; - } - - public ClassHandle getClassHandle() { - return this.classHandle; - } - - public String getSimpleClassName() { - return this.getDeobfOrObfHandleRef().getSimpleName(); - } - - public String getFullClassName() { - return this.getDeobfOrObfHandleRef().getFullName(); - } - - private ClassEntry getDeobfOrObfHandleRef() { - final ClassEntry deobfRef = this.classHandle.getDeobfRef(); - return deobfRef == null ? this.classHandle.getRef() : deobfRef; - } - public void retranslateUi() { this.popupMenu.retranslateUi(); this.quickFindToolBar.translate(); @@ -897,11 +476,4 @@ public void actionPerformed(JTextComponent target, SyntaxDocument sDoc, int dot, this.popupMenu.getButtonKeyBinds().forEach((key, button) -> putKeyBindAction(key, this.editor, e -> button.doClick())); } - - private enum DisplayMode { - INACTIVE, - IN_PROGRESS, - SUCCESS, - ERRORED, - } } From 1fb4c3ed44348f1d43f50e16212700c35315694d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 28 Sep 2025 19:01:12 -0700 Subject: [PATCH 007/109] display entries' entire outer source class in their tooltips --- .../enigma/gui/panel/BaseEditorPanel.java | 20 ++- .../quiltmc/enigma/gui/panel/EditorPanel.java | 118 +++++++++++------- .../representation/entry/Entry.java | 6 +- 3 files changed, 94 insertions(+), 50 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 1e080928e..a1a70a14e 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 @@ -49,9 +49,11 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Rectangle2D; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Consumer; public class BaseEditorPanel { protected final JPanel ui = new JPanel(); @@ -71,6 +73,8 @@ public class BaseEditorPanel { private final JScrollPane errorScrollPane = new JScrollPane(this.errorTextArea); private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); + private final List> sourceSetListeners = new ArrayList<>(); + private DisplayMode mode = DisplayMode.INACTIVE; protected EntryReference, Entry> cursorReference; @@ -174,7 +178,7 @@ private void handleDecompilerResult(Result "editor.decompile_error"; @@ -278,7 +282,7 @@ public EntryReference, Entry> getReference(Token token) { return this.source.getIndex().getReference(token); } - public void setSource(DecompiledClassSource source) { + protected void setSource(DecompiledClassSource source) { this.setDisplayMode(DisplayMode.SUCCESS); if (source == null) return; try { @@ -315,7 +319,9 @@ public void setSource(DecompiledClassSource source) { if (this.source != null) { this.editor.setCaretPosition(newCaretPos); - this.onSourceSet(source); + for (final Consumer listener : this.sourceSetListeners) { + listener.accept(this.source); + } } this.setCursorReference(this.getReference(this.getToken(this.editor.getCaretPosition()))); @@ -329,7 +335,13 @@ public void setSource(DecompiledClassSource source) { } } - protected void onSourceSet(DecompiledClassSource source) { } + protected void addSourceSetListener(Consumer listener) { + this.sourceSetListeners.add(listener); + } + + protected void removeSourceSetListener(Consumer listener) { + this.sourceSetListeners.remove(listener); + } public void setHighlightedTokens(TokenStore tokenStore, Map> tokens) { // remove any old highlighters 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 05c8f2295..7147cc56b 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 @@ -5,42 +5,19 @@ import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.event.ClassHandleListener; -import org.quiltmc.enigma.api.source.TokenStore; -import org.quiltmc.enigma.gui.BrowserCaret; -import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; -import org.quiltmc.enigma.gui.GuiController; -import org.quiltmc.enigma.gui.config.theme.ThemeUtil; -import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; import org.quiltmc.enigma.gui.element.EditorPopupMenu; import org.quiltmc.enigma.gui.element.NavigatorPanel; import org.quiltmc.enigma.gui.event.EditorActionListener; -import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; -import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; -import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; -import org.quiltmc.enigma.gui.util.ScaleUtil; -import org.quiltmc.enigma.api.source.DecompiledClassSource; -import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.source.Token; -import org.quiltmc.enigma.api.source.DecompiledClassSource; -import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; -import org.quiltmc.enigma.api.translation.mapping.EntryResolver; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.gui.Gui; -import org.quiltmc.enigma.gui.config.keybind.KeyBinds; -import org.quiltmc.enigma.gui.element.EditorPopupMenu; -import org.quiltmc.enigma.gui.element.NavigatorPanel; -import org.quiltmc.enigma.api.source.Token; +import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import org.quiltmc.enigma.util.I18n; -import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; -import org.tinylog.Logger; -import org.quiltmc.enigma.gui.event.EditorActionListener; import java.awt.BorderLayout; import java.awt.Component; @@ -49,9 +26,9 @@ import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; +import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; @@ -66,6 +43,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import javax.annotation.Nullable; +import javax.swing.Box; +import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; @@ -73,9 +52,6 @@ import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.Highlighter.HighlightPainter; import javax.swing.text.JTextComponent; import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; @@ -130,7 +106,7 @@ public class EditorPanel extends BaseEditorPanel { ); private final Timer hideTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, - e -> this.tooltip.setVisible(false) + e -> this.closeTooltip() ); private final List listeners = new ArrayList<>(); @@ -165,6 +141,16 @@ public void focusLost(FocusEvent e) { // init editor popup menu this.popupMenu = new EditorPopupMenu(this, gui); + // global listener so tooltip hides even if clicking outside editor + Toolkit.getDefaultToolkit().addAWTEventListener( + e -> { + if (e.getID() == MouseEvent.MOUSE_PRESSED) { + this.closeTooltip(); + } + }, + MouseEvent.MOUSE_PRESSED + ); + final MouseAdapter editorMouseAdapter = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -200,6 +186,13 @@ public void mouseMoved(MouseEvent e) { } }; + this.editor.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + EditorPanel.this.closeTooltip(); + } + }); + this.editor.addMouseListener(editorMouseAdapter); this.editor.addMouseMotionListener(editorMouseAdapter); this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); @@ -212,7 +205,7 @@ public void mouseMoved(MouseEvent e) { this.tooltip.setAlwaysOnTop(true); this.tooltip.setType(Window.Type.POPUP); this.tooltip.setLayout(new BorderLayout()); - this.tooltip.setContentPane(new JPanel()); + this.tooltip.setContentPane(new Box(BoxLayout.PAGE_AXIS)); this.tooltip.addMouseListener(new MouseAdapter() { @Override @@ -263,15 +256,57 @@ public void keyTyped(KeyEvent event) { }); this.reloadKeyBinds(); + this.addSourceSetListener(source -> { + if (this.navigatorPanel != null) { + for (Entry entry : source.getIndex().declarations()) { + this.navigatorPanel.addEntry(entry); + } + } + }); this.ui.putClientProperty(EditorPanel.class, this); } + private void closeTooltip() { + EditorPanel.this.tooltip.setVisible(false); + EditorPanel.this.lastMouseTargetToken = null; + EditorPanel.this.mouseStoppedMovingTimer.stop(); + EditorPanel.this.showTokenTooltipTimer.stop(); + EditorPanel.this.hideTokenTooltipTimer.stop(); + } + private void updateToolTip(Entry target) { final Container tooltipContent = this.tooltip.getContentPane(); tooltipContent.removeAll(); - final JLabel label = new JLabel(target.getFullName()); - tooltipContent.add(label); + + final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); + + tooltipContent.add(new JLabel(deobfTarget.getFullName())); + if (target instanceof ParentedEntry parentedTarget) { + final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); + + @Nullable + final Consumer tooltipEditorSourceSetter; + if (targetTopClass.equals(this.getSource().getEntry())) { + tooltipEditorSourceSetter = tooltipEditor -> tooltipEditor.setSource(this.getSource()); + } else { + final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() + .openClass(targetTopClass); + if (targetTopClassHandle == null) { + tooltipEditorSourceSetter = null; + } else { + tooltipEditorSourceSetter = tooltipEditor -> tooltipEditor.setClassHandle(targetTopClassHandle); + } + } + + if (tooltipEditorSourceSetter != null) { + final BaseEditorPanel tooltipEditor = new BaseEditorPanel(this.gui); + tooltipEditorSourceSetter.accept(tooltipEditor); + tooltipEditor.getEditor().setEditable(false); + tooltipEditor.addSourceSetListener(source -> this.tooltip.pack()); + tooltipContent.add(tooltipEditor.ui); + } + } this.tooltip.setLocation(MouseInfo.getPointerInfo().getLocation()); @@ -303,7 +338,6 @@ private void consumeEditorMouseTarget(BiConsumer> action, Runnab token -> Optional.of(token) .map(this::getReference) .map(reference -> reference.entry) - .map(this.gui.getController().getProject().getRemapper()::deobfuscate) .ifPresentOrElse( entry -> action.accept(token, entry), onNoTarget @@ -435,15 +469,15 @@ protected void setCursorReference(EntryReference, Entry> ref) { this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref)); } - @Override - protected void onSourceSet(DecompiledClassSource source) { - super.onSourceSet(source); - if (this.navigatorPanel != null) { - for (Entry entry : source.getIndex().declarations()) { - this.navigatorPanel.addEntry(entry); - } - } - } + // @Override + // protected void onSourceSet(DecompiledClassSource source) { + // super.onSourceSet(source); + // if (this.navigatorPanel != null) { + // for (Entry entry : source.getIndex().declarations()) { + // this.navigatorPanel.addEntry(entry); + // } + // } + // } public void addListener(EditorActionListener listener) { this.listeners.add(listener); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java index 171707e0d..5db4c764c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/Entry.java @@ -130,18 +130,16 @@ default String getSourceRemapName() { boolean canShadow(Entry entry); default ClassEntry getContainingClass() { - ClassEntry last = null; Entry current = this; while (current != null) { if (current instanceof ClassEntry classEntry) { - last = classEntry; - break; + return classEntry; } current = current.getParent(); } - return Objects.requireNonNull(last, () -> String.format("%s has no containing class?", this)); + throw new IllegalStateException(String.format("%s has no containing class?", this)); } default ClassEntry getTopLevelClass() { From 3565ce48755778b78a549cb9dc42d7f0e107524c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 29 Sep 2025 07:35:20 -0700 Subject: [PATCH 008/109] always set tooltipEditor classhandle instead of setting source when it's the same class (editor bounds were 0x0 when setting source) navigate to tooltip target entry declaration when showing tooltip --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 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 7147cc56b..41479acf9 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 @@ -286,24 +286,21 @@ private void updateToolTip(Entry target) { final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); @Nullable - final Consumer tooltipEditorSourceSetter; - if (targetTopClass.equals(this.getSource().getEntry())) { - tooltipEditorSourceSetter = tooltipEditor -> tooltipEditor.setSource(this.getSource()); - } else { - final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() - .openClass(targetTopClass); - if (targetTopClassHandle == null) { - tooltipEditorSourceSetter = null; - } else { - tooltipEditorSourceSetter = tooltipEditor -> tooltipEditor.setClassHandle(targetTopClassHandle); - } - } + final ClassHandle targetTopClassHandle = targetTopClass.equals(this.getSource().getEntry()) + ? this.classHandle + : this.gui.getController().getClassHandleProvider().openClass(targetTopClass); - if (tooltipEditorSourceSetter != null) { + if (targetTopClassHandle != null) { final BaseEditorPanel tooltipEditor = new BaseEditorPanel(this.gui); - tooltipEditorSourceSetter.accept(tooltipEditor); tooltipEditor.getEditor().setEditable(false); - tooltipEditor.addSourceSetListener(source -> this.tooltip.pack()); + tooltipEditor.addSourceSetListener(source -> { + this.tooltip.pack(); + final Token declarationToken = source.getIndex().getDeclarationToken(target); + if (declarationToken != null) { + tooltipEditor.navigateToToken(declarationToken); + } + }); + tooltipEditor.setClassHandle(targetTopClassHandle); tooltipContent.add(tooltipEditor.ui); } } From 504f3046c3526e94d6c6e48d47207421a4a16cd6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 29 Sep 2025 19:16:54 -0700 Subject: [PATCH 009/109] add javaparser dep matching #308 --- enigma-swing/build.gradle | 1 + gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle index 53a61b84e..3e52fdd85 100644 --- a/enigma-swing/build.gradle +++ b/enigma-swing/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation libs.bundles.quilt.config implementation libs.swing.dpi implementation libs.fontchooser + implementation libs.javaparser testImplementation(testFixtures(project(':enigma'))) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a40417cdf..a45e61ef5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ swing_dpi = "0.10" fontchooser = "2.5.2" tinylog = "2.6.2" quilt_config = "1.3.2" +javaparser = "3.27.0" vineflower = "1.11.0" cfr = "0.2.2" @@ -41,6 +42,7 @@ flatlaf_extras = { module = "com.formdev:flatlaf-extras", version.ref = "flatlaf syntaxpain = { module = "org.quiltmc:syntaxpain", version.ref = "syntaxpain" } swing_dpi = { module = "com.github.lukeu:swing-dpi", version.ref = "swing_dpi" } fontchooser = { module = "org.drjekyll:fontchooser", version.ref = "fontchooser" } +javaparser = { module = "com.github.javaparser:javaparser-core", version.ref = "javaparser" } vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" } cfr = { module = "net.fabricmc:cfr", version.ref = "cfr" } From bd772be7f1702917ec248cd6a0a2377f2f3ff654 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 30 Sep 2025 19:12:02 -0700 Subject: [PATCH 010/109] implement source trimming for classes using javaparser --- .../enigma/gui/panel/BaseEditorPanel.java | 154 ++++++++++++++--- .../quiltmc/enigma/gui/panel/EditorPanel.java | 162 +++++++++++++++++- .../quiltmc/enigma/gui/util/ScaleUtil.java | 3 +- .../org/quiltmc/enigma/util/LineIndexer.java | 27 +++ .../java/org/quiltmc/enigma/util/Utils.java | 16 ++ 5 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.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 a1a70a14e..a864a3241 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 @@ -22,6 +22,7 @@ import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.Result; +import org.quiltmc.enigma.util.Utils; import org.tinylog.Logger; import javax.annotation.Nullable; @@ -37,7 +38,6 @@ import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.text.BadLocationException; -import javax.swing.text.Document; import javax.swing.text.Highlighter.HighlightPainter; import java.awt.Color; import java.awt.FlowLayout; @@ -53,7 +53,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; public class BaseEditorPanel { protected final JPanel ui = new JPanel(); @@ -74,6 +76,8 @@ public class BaseEditorPanel { private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); private final List> sourceSetListeners = new ArrayList<>(); + @Nullable + private Function trimFactory; private DisplayMode mode = DisplayMode.INACTIVE; @@ -89,6 +93,7 @@ public class BaseEditorPanel { protected ClassHandle classHandle; private DecompiledClassSource source; + private SourceBounds sourceBounds = new DefaultBounds(); protected boolean settingSource; public BaseEditorPanel(Gui gui) { @@ -312,8 +317,16 @@ protected void setSource(DecompiledClassSource source) { } this.source = source; - this.editor.getHighlighter().removeAllHighlights(); this.editor.setText(source.toString()); + final TrimmedBounds trimmedBounds = this.trimFactory == null ? null : this.trimFactory.apply(this.source); + if (trimmedBounds == null) { + this.sourceBounds = new DefaultBounds(); + } else { + this.sourceBounds = trimmedBounds; + this.trimSource(trimmedBounds); + } + + this.editor.getHighlighter().removeAllHighlights(); this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); if (this.source != null) { @@ -335,6 +348,46 @@ protected void setSource(DecompiledClassSource source) { } } + // TODO strip indent + private void trimSource(TrimmedBounds bounds) { + final long oldCaretPos = this.editor.getCaretPosition(); + + final String sourceString = this.source.toString(); + this.sourceBounds = new TrimmedBounds(bounds.start(), Math.min(bounds.end(), sourceString.length())); + this.editor.setText(sourceString.substring(this.sourceBounds.start(), this.sourceBounds.end())); + this.editor.setCaretPosition( + Utils.clamp(oldCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()) + ); + } + + protected void setTrimFactory(Function factory) { + this.trimFactory = factory; + } + + // protected void trimSource(int start, int end) { + // if (this.source == null) { + // throw new UnsupportedOperationException("Cannot trim null source!"); + // } + // + // final long oldCaretPos = this.editor.getCaretPosition(); + // + // final String sourceString = this.source.toString(); + // this.sourceBounds = new TrimmedBounds(start, Math.min(end, sourceString.length())); + // this.editor.setText(sourceString.substring(this.sourceBounds.start(), this.sourceBounds.end())); + // this.editor.setCaretPosition( + // Utils.clamp(oldCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()) + // ); + // } + + // protected void unTrimSource() { + // if (this.source == null) { + // throw new UnsupportedOperationException("Cannot un-trim null source!"); + // } + // + // this.sourceBounds = new DefaultBounds(); + // this.editor.setText(this.source.toString()); + // } + protected void addSourceSetListener(Consumer listener) { this.sourceSetListeners.add(listener); } @@ -375,11 +428,13 @@ public void setHighlightedTokens(TokenStore tokenStore, Map { + try { + this.editor.getHighlighter().addHighlight(offsetToken.start, offsetToken.end, tokenPainter); + } catch (BadLocationException ex) { + throw new IllegalArgumentException(ex); + } + }); } public EntryReference, Entry> getCursorReference() { @@ -419,18 +474,31 @@ public void navigateToToken(Token token) { this.navigateToToken(token, SelectionHighlightPainter.INSTANCE); } - private void navigateToToken(Token token, HighlightPainter highlightPainter) { + protected void navigateToToken(Token token, HighlightPainter highlightPainter) { // set the caret position to the token - Document document = this.editor.getDocument(); - int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); + // Document document = this.editor.getDocument(); + // if (!this.sourceBounds.contains(token)) { + // return; + // } + + final Token offsetToken = this.sourceBounds.offsetOf(token).orElse(null); + if (offsetToken == null) { + // token out of bounds + return; + } - this.editor.setCaretPosition(clampedPosition); + // int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); + // int clampedPosition = Utils.clamp(token.start, this.sourceBounds.start(), this.sourceBounds.end()); + + // final int offsetStart = token.start - this.sourceBounds.start(); + + this.editor.setCaretPosition(offsetToken.start); this.editor.grabFocus(); try { // make sure the token is visible in the scroll window - Rectangle2D start = this.editor.modelToView2D(token.start); - Rectangle2D end = this.editor.modelToView2D(token.end); + Rectangle2D start = this.editor.modelToView2D(offsetToken.start); + Rectangle2D end = this.editor.modelToView2D(offsetToken.start); if (start == null || end == null) { return; } @@ -456,7 +524,8 @@ private void navigateToToken(Token token, HighlightPainter highlightPainter) { public void actionPerformed(ActionEvent event) { if (this.counter % 2 == 0) { try { - this.highlight = BaseEditorPanel.this.editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); + // final int offsetEnd = token.end - BaseEditorPanel.this.sourceBounds.start(); + this.highlight = BaseEditorPanel.this.editor.getHighlighter().addHighlight(offsetToken.start, offsetToken.end, highlightPainter); } catch (BadLocationException ex) { // don't care } @@ -474,14 +543,6 @@ public void actionPerformed(ActionEvent event) { timer.start(); } - // public void addListener(EditorActionListener listener) { - // this.listeners.add(listener); - // } - - // public void removeListener(EditorActionListener listener) { - // this.listeners.remove(listener); - // } - public JPanel getUi() { return this.ui; } @@ -511,6 +572,55 @@ private ClassEntry getDeobfOrObfHandleRef() { return deobfRef == null ? this.classHandle.getRef() : deobfRef; } + protected sealed interface SourceBounds { + int start(); + + int end(); + + default boolean contains(int pos) { + return pos >= this.start() && pos <= this.end(); + } + + default boolean contains(Token token) { + return this.contains(token.start) && this.contains(token.end); + } + + default Optional offsetOf(Token token) { + return this.contains(token) + ? Optional.of(new Token(token.start - this.start(), token.end - this.start(), token.text)) + : Optional.empty(); + } + } + + protected record TrimmedBounds(int start, int end) implements SourceBounds { + public TrimmedBounds { + if (start < 0) { + throw new IllegalArgumentException("start must not be negative!"); + } + + if (start > end) { + throw new IllegalArgumentException("start must not be greater than end!"); + } + } + } + + private final class DefaultBounds implements SourceBounds { + @Override + public int start() { + return 0; + } + + @Override + public int end() { + return BaseEditorPanel.this.source.toString().length(); + } + + @Override + public Optional offsetOf(Token token) { + return this.end() < token.end ? Optional.empty() : Optional.of(token); + } + } + private enum DisplayMode { INACTIVE, IN_PROGRESS, 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 41479acf9..afe56883d 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,10 +1,30 @@ package org.quiltmc.enigma.gui.panel; +import com.github.javaparser.JavaParser; +import com.github.javaparser.JavaToken; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.Position; +import com.github.javaparser.TokenRange; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.RecordDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.event.ClassHandleListener; +import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.api.translation.representation.AccessFlags; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; @@ -12,12 +32,15 @@ import org.quiltmc.enigma.gui.element.NavigatorPanel; import org.quiltmc.enigma.gui.event.EditorActionListener; import org.quiltmc.enigma.api.source.Token; -import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; +import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; +import org.quiltmc.enigma.util.LineIndexer; +import org.quiltmc.enigma.util.Result; +import org.quiltmc.syntaxpain.LineNumbersRuler; +import org.tinylog.Logger; import java.awt.BorderLayout; import java.awt.Component; @@ -42,12 +65,14 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JViewport; import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -59,6 +84,7 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; + private static final Pattern CLASS_PUNCTUATION = Pattern.compile("/|\\$"); private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -104,6 +130,8 @@ public class EditorPanel extends BaseEditorPanel { }); } ); + // TODO stop hide timer when mouse is over tooltip + // TODO tooltip re-shows after short delay after hiding private final Timer hideTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> this.closeTooltip() @@ -292,14 +320,56 @@ private void updateToolTip(Entry target) { if (targetTopClassHandle != null) { final BaseEditorPanel tooltipEditor = new BaseEditorPanel(this.gui); - tooltipEditor.getEditor().setEditable(false); + + Optional.ofNullable(tooltipEditor.editorScrollPane.getRowHeader()) + .map(JViewport::getView) + // LineNumbersRuler is installed by syntaxpain + .map(view -> view instanceof LineNumbersRuler lineNumbers ? lineNumbers : null) + // TODO offset line numbers instead of removing them once + // offsets are implemented in syntaxpain + .ifPresent(lineNumbers -> lineNumbers.deinstall(tooltipEditor.editor)); + + tooltipEditor.setTrimFactory(source -> { + if (target instanceof ClassEntry targetClass) { + final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); + + return this.getClassBounds(source.toString(), targetDotName, targetClass).unwrapOrElse(error -> { + Logger.error(error); + return null; + }); + } else if (target instanceof MethodEntry targetMethod) { + // TODO + return null; + } else if (target instanceof FieldEntry targetField) { + // TODO + return null; + } else if (target instanceof LocalVariableEntry targetLocal) { + if (targetLocal.isArgument()) { + // TODO + return null; + } else { + // TODO + return null; + + // nothing? or show parent method? + } + } else { + // TODO + return null; + } + }); + tooltipEditor.addSourceSetListener(source -> { - this.tooltip.pack(); final Token declarationToken = source.getIndex().getDeclarationToken(target); if (declarationToken != null) { - tooltipEditor.navigateToToken(declarationToken); + this.tooltip.pack(); + + // TODO create custom highlighter + tooltipEditor.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE); } }); + + tooltipEditor.getEditor().setEditable(false); tooltipEditor.setClassHandle(targetTopClassHandle); tooltipContent.add(tooltipEditor.ui); } @@ -310,6 +380,88 @@ private void updateToolTip(Entry target) { this.tooltip.pack(); } + private Result getClassBounds(String source, String targetName, ClassEntry target) { + return this.getNodeType(target) + .map(nodeType -> { + final ParserConfiguration config = new ParserConfiguration() + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + + final ParseResult parseResult = new JavaParser(config).parse(source); + return parseResult + .getResult() + .map(unit -> unit + .findFirst(nodeType, declaration -> declaration + .getFullyQualifiedName() + .filter(name -> name.equals(targetName)) + .isPresent() + ) + .map(targetDeclaration -> targetDeclaration + .getRange() + .map(range -> targetDeclaration + .getTokenRange() + .map(TokenRange::iterator) + .map(tokenItr -> { + while (tokenItr.hasNext()) { + final JavaToken javaToken = tokenItr.next(); + if (javaToken.asString().equals("{")) { + return javaToken.getRange() + .map(openRange -> { + final Position startPos = range.begin; + final Position endPos = openRange.begin; + + final LineIndexer lineIndexer = new LineIndexer(source); + // subtract 1 because Position line/column start at index 1, not 0 + final int start = lineIndexer.getIndex(startPos.line - 1) + + startPos.column - 1; + int end = lineIndexer.getIndex(endPos.line - 1) + + endPos.column - 1; + while (Character.isWhitespace(source.charAt(end - 1))) { + end--; + } + + return Result.ok(new TrimmedBounds(start, end)); + }) + .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); + } + } + + return Result.err("No open curly brace for %s!".formatted(targetName)); + }) + .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName))) + ) + .orElseGet(() -> Result.err("No parsed range for %s!".formatted(targetName))) + ) + .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))) + ) + .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + }) + .orElseGet(() -> Result.err("No definition for %s!".formatted(targetName))); + } + + private Optional>> getNodeType(ClassEntry targetClass) { + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex() + .getIndex(EntryIndex.class); + + return Optional + .ofNullable(entryIndex.getDefinition(targetClass)) + .map(targetDef -> { + final InheritanceIndex inheritanceIndex = this.gui.getController().getProject().getJarIndex() + .getIndex(InheritanceIndex.class); + + final AccessFlags access = targetDef.getAccess(); + if (access.isEnum()) { + return EnumDeclaration.class; + } else if (access.isAnnotation()) { + return AnnotationDeclaration.class; + } else if (inheritanceIndex.getParents(targetDef).contains(new ClassEntry("java/lang/Record"))) { + return RecordDeclaration.class; + } else { + return ClassOrInterfaceDeclaration.class; + } + }); + } + /** * @see #consumeEditorMouseTarget(BiConsumer, Runnable) */ diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ScaleUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ScaleUtil.java index 5740ca2d1..e71645649 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ScaleUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ScaleUtil.java @@ -6,6 +6,7 @@ import com.github.swingdpi.plaf.NimbusTweaker; import com.github.swingdpi.plaf.WindowsTweaker; import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.util.Utils; import org.quiltmc.syntaxpain.SyntaxpainConfiguration; import java.awt.Dimension; @@ -26,7 +27,7 @@ public class ScaleUtil { public static void setScaleFactor(float scaleFactor) { float oldScale = Config.main().scaleFactor.value(); - float clamped = Math.min(Math.max(0.25f, scaleFactor), 10.0f); + float clamped = Utils.clamp(scaleFactor, 0.25f, 10.0f); Config.main().scaleFactor.setValue(clamped, true); listeners.forEach(l -> l.onScaleChanged(clamped, oldScale)); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java new file mode 100644 index 000000000..99b4a9ab3 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -0,0 +1,27 @@ +package org.quiltmc.enigma.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LineIndexer { + private static final Pattern LINE_END = Pattern.compile("\\r\\n?|\\n"); + + private final List indexesByLine = new ArrayList<>(); + private final Matcher lineEndMatcher; + + public LineIndexer(String string) { + // the first line always starts at 0 + this.indexesByLine.add(0); + this.lineEndMatcher = LINE_END.matcher(string); + } + + public int getIndex(int line) { + while (line >= this.indexesByLine.size() && this.lineEndMatcher.find()) { + this.indexesByLine.add(this.lineEndMatcher.end()); + } + + return line < this.indexesByLine.size() ? this.indexesByLine.get(line) : -1; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java index 0cfc931d8..5020116f6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/Utils.java @@ -180,4 +180,20 @@ public static String naturalJoin(String finalSeparator, boolean oxfordComma, Lis } }; } + + public static int clamp(long value, int min, int max) { + return (int) clamp(value, (long) min, (long) max); + } + + public static long clamp(long value, long min, long max) { + return Math.min(max, Math.max(value, min)); + } + + public static float clamp(double value, float min, float max) { + return (float) clamp(value, (double) min, (double) max); + } + + public static double clamp(double value, double min, double max) { + return Math.min(max, Math.max(value, min)); + } } From d0d0cff3bba11b60a17f73b8cc5bce3f86619426 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 06:39:51 -0700 Subject: [PATCH 011/109] minor improvements --- .../enigma/gui/panel/BaseEditorPanel.java | 36 +------------------ .../quiltmc/enigma/gui/panel/EditorPanel.java | 7 ++-- enigma/build.gradle | 1 + .../org/quiltmc/enigma/util/LineIndexer.java | 9 ++++- 4 files changed, 12 insertions(+), 41 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 a864a3241..8f0439952 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 @@ -364,30 +364,6 @@ protected void setTrimFactory(Function fac this.trimFactory = factory; } - // protected void trimSource(int start, int end) { - // if (this.source == null) { - // throw new UnsupportedOperationException("Cannot trim null source!"); - // } - // - // final long oldCaretPos = this.editor.getCaretPosition(); - // - // final String sourceString = this.source.toString(); - // this.sourceBounds = new TrimmedBounds(start, Math.min(end, sourceString.length())); - // this.editor.setText(sourceString.substring(this.sourceBounds.start(), this.sourceBounds.end())); - // this.editor.setCaretPosition( - // Utils.clamp(oldCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()) - // ); - // } - - // protected void unTrimSource() { - // if (this.source == null) { - // throw new UnsupportedOperationException("Cannot un-trim null source!"); - // } - // - // this.sourceBounds = new DefaultBounds(); - // this.editor.setText(this.source.toString()); - // } - protected void addSourceSetListener(Consumer listener) { this.sourceSetListeners.add(listener); } @@ -475,23 +451,13 @@ public void navigateToToken(Token token) { } protected void navigateToToken(Token token, HighlightPainter highlightPainter) { - // set the caret position to the token - // Document document = this.editor.getDocument(); - // if (!this.sourceBounds.contains(token)) { - // return; - // } - final Token offsetToken = this.sourceBounds.offsetOf(token).orElse(null); if (offsetToken == null) { // token out of bounds return; } - // int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); - // int clampedPosition = Utils.clamp(token.start, this.sourceBounds.start(), this.sourceBounds.end()); - - // final int offsetStart = token.start - this.sourceBounds.start(); - + // set the caret position to the token this.editor.setCaretPosition(offsetToken.start); this.editor.grabFocus(); 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 afe56883d..0fa2f2e82 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 @@ -411,11 +411,8 @@ private Result getClassBounds(String source, String targe final Position endPos = openRange.begin; final LineIndexer lineIndexer = new LineIndexer(source); - // subtract 1 because Position line/column start at index 1, not 0 - final int start = lineIndexer.getIndex(startPos.line - 1) - + startPos.column - 1; - int end = lineIndexer.getIndex(endPos.line - 1) - + endPos.column - 1; + final int start = lineIndexer.getIndex(startPos); + int end = lineIndexer.getIndex(endPos); while (Character.isWhitespace(source.charAt(end - 1))) { end--; } diff --git a/enigma/build.gradle b/enigma/build.gradle index ed399a9a1..a51282cfc 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation libs.vineflower implementation libs.cfr implementation libs.procyon + implementation libs.javaparser implementation libs.quilt.config diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java index 99b4a9ab3..e121895b2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -1,5 +1,7 @@ package org.quiltmc.enigma.util; +import com.github.javaparser.Position; + import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -17,11 +19,16 @@ public LineIndexer(String string) { this.lineEndMatcher = LINE_END.matcher(string); } - public int getIndex(int line) { + public int getStartIndex(int line) { while (line >= this.indexesByLine.size() && this.lineEndMatcher.find()) { this.indexesByLine.add(this.lineEndMatcher.end()); } return line < this.indexesByLine.size() ? this.indexesByLine.get(line) : -1; } + + public int getIndex(Position position) { + final int lineIndex = this.getStartIndex(position.line - Position.FIRST_LINE); + return lineIndex < 0 ? lineIndex : lineIndex + position.column - Position.FIRST_COLUMN; + } } From ce87d4d81beab20d40b9916369447cc2a7180206 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 07:23:13 -0700 Subject: [PATCH 012/109] minor cleanup --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 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 0fa2f2e82..d467b49a7 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 @@ -16,11 +16,9 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; -import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.AccessFlags; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -381,82 +379,80 @@ private void updateToolTip(Entry target) { } private Result getClassBounds(String source, String targetName, ClassEntry target) { - return this.getNodeType(target) - .map(nodeType -> { - final ParserConfiguration config = new ParserConfiguration() - .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - - final ParseResult parseResult = new JavaParser(config).parse(source); - return parseResult - .getResult() - .map(unit -> unit - .findFirst(nodeType, declaration -> declaration - .getFullyQualifiedName() - .filter(name -> name.equals(targetName)) - .isPresent() - ) - .map(targetDeclaration -> targetDeclaration - .getRange() - .map(range -> targetDeclaration - .getTokenRange() - .map(TokenRange::iterator) - .map(tokenItr -> { - while (tokenItr.hasNext()) { - final JavaToken javaToken = tokenItr.next(); - if (javaToken.asString().equals("{")) { - return javaToken.getRange() - .map(openRange -> { - final Position startPos = range.begin; - final Position endPos = openRange.begin; - - final LineIndexer lineIndexer = new LineIndexer(source); - final int start = lineIndexer.getIndex(startPos); - int end = lineIndexer.getIndex(endPos); - while (Character.isWhitespace(source.charAt(end - 1))) { - end--; - } - - return Result.ok(new TrimmedBounds(start, end)); - }) - .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); - } + return this.getNodeType(target, targetName).andThen(nodeType -> { + final ParserConfiguration config = new ParserConfiguration() + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + + final ParseResult parseResult = new JavaParser(config).parse(source); + return parseResult + .getResult() + .map(unit -> unit + .findFirst(nodeType, declaration -> declaration + .getFullyQualifiedName() + .filter(name -> name.equals(targetName)) + .isPresent() + ) + .map(targetDeclaration -> targetDeclaration + .getRange() + .map(range -> targetDeclaration + .getTokenRange() + .map(TokenRange::iterator) + .map(tokenItr -> { + while (tokenItr.hasNext()) { + final JavaToken javaToken = tokenItr.next(); + if (javaToken.asString().equals("{")) { + return javaToken.getRange() + .map(openRange -> { + final Position startPos = range.begin; + final Position endPos = openRange.begin; + + final LineIndexer lineIndexer = new LineIndexer(source); + final int start = lineIndexer.getIndex(startPos); + int end = lineIndexer.getIndex(endPos); + while (Character.isWhitespace(source.charAt(end - 1))) { + end--; + } + + return Result.ok(new TrimmedBounds(start, end)); + }) + .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); } + } - return Result.err("No open curly brace for %s!".formatted(targetName)); - }) - .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName))) - ) - .orElseGet(() -> Result.err("No parsed range for %s!".formatted(targetName))) + return Result.err("No open curly brace for %s!".formatted(targetName)); + }) + .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName))) ) - .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))) + .orElseGet(() -> Result.err("No declaration range for %s!".formatted(targetName))) ) - .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); - }) - .orElseGet(() -> Result.err("No definition for %s!".formatted(targetName))); + .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))) + ) + .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + }); } - private Optional>> getNodeType(ClassEntry targetClass) { + private Result>, String> getNodeType( + ClassEntry targetClass, String targetName + ) { final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex() .getIndex(EntryIndex.class); return Optional .ofNullable(entryIndex.getDefinition(targetClass)) .map(targetDef -> { - final InheritanceIndex inheritanceIndex = this.gui.getController().getProject().getJarIndex() - .getIndex(InheritanceIndex.class); - - final AccessFlags access = targetDef.getAccess(); - if (access.isEnum()) { - return EnumDeclaration.class; - } else if (access.isAnnotation()) { + if (targetDef.getAccess().isAnnotation()) { return AnnotationDeclaration.class; - } else if (inheritanceIndex.getParents(targetDef).contains(new ClassEntry("java/lang/Record"))) { + } else if (targetDef.isEnum()) { + return EnumDeclaration.class; + } else if (targetDef.isRecord()) { return RecordDeclaration.class; } else { return ClassOrInterfaceDeclaration.class; } - }); + }) + .>, String>>map(Result::ok) + .orElseGet(() -> Result.err("No definition for %s!".formatted(targetName))); } /** From dcbdc48841c63392bbf3893651a7b178c82dc057 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 11:33:50 -0700 Subject: [PATCH 013/109] make trimFactor a param instead of a field --- .../enigma/gui/panel/BaseEditorPanel.java | 40 ++++++--- .../quiltmc/enigma/gui/panel/EditorPanel.java | 87 +++++++++---------- 2 files changed, 69 insertions(+), 58 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 8f0439952..05e2ce02c 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 @@ -76,8 +76,6 @@ public class BaseEditorPanel { private final JButton retryButton = new JButton(I18n.translate("prompt.retry")); private final List> sourceSetListeners = new ArrayList<>(); - @Nullable - private Function trimFactory; private DisplayMode mode = DisplayMode.INACTIVE; @@ -127,23 +125,32 @@ public BaseEditorPanel(Gui gui) { } public void setClassHandle(ClassHandle handle) { + this.setClassHandle(handle, null); + } + + protected void setClassHandle( + ClassHandle handle, @Nullable Function trimFactory + ) { ClassEntry old = null; if (this.classHandle != null) { old = this.classHandle.getRef(); this.classHandle.close(); } - this.setClassHandleImpl(old, handle); + this.setClassHandleImpl(old, handle, trimFactory); } - protected void setClassHandleImpl(ClassEntry old, ClassHandle handle) { + protected void setClassHandleImpl( + ClassEntry old, ClassHandle handle, + @Nullable Function trimFactory + ) { this.setDisplayMode(DisplayMode.IN_PROGRESS); this.setCursorReference(null); handle.addListener(new ClassHandleListener() { @Override public void onMappedSourceChanged(ClassHandle h, Result res) { - BaseEditorPanel.this.handleDecompilerResult(res); + BaseEditorPanel.this.handleDecompilerResult(res, trimFactory); } @Override @@ -156,7 +163,10 @@ public void onInvalidate(ClassHandle h, InvalidationType t) { } }); - handle.getSource().thenAcceptAsync(this::handleDecompilerResult, SwingUtilities::invokeLater); + handle.getSource().thenAcceptAsync( + res -> BaseEditorPanel.this.handleDecompilerResult(res, trimFactory), + SwingUtilities::invokeLater + ); this.classHandle = handle; } @@ -171,10 +181,13 @@ private void redecompileClass() { } } - private void handleDecompilerResult(Result res) { + private void handleDecompilerResult( + Result res, + @Nullable Function trimFactory + ) { SwingUtilities.invokeLater(() -> { if (res.isOk()) { - this.setSource(res.unwrap()); + this.setSource(res.unwrap(), trimFactory); } else { this.displayError(res.unwrapErr()); } @@ -287,7 +300,10 @@ public EntryReference, Entry> getReference(Token token) { return this.source.getIndex().getReference(token); } - protected void setSource(DecompiledClassSource source) { + protected void setSource( + DecompiledClassSource source, + @Nullable Function trimFactory + ) { this.setDisplayMode(DisplayMode.SUCCESS); if (source == null) return; try { @@ -318,7 +334,7 @@ protected void setSource(DecompiledClassSource source) { this.source = source; this.editor.setText(source.toString()); - final TrimmedBounds trimmedBounds = this.trimFactory == null ? null : this.trimFactory.apply(this.source); + final TrimmedBounds trimmedBounds = trimFactory == null ? null : trimFactory.apply(this.source); if (trimmedBounds == null) { this.sourceBounds = new DefaultBounds(); } else { @@ -360,10 +376,6 @@ private void trimSource(TrimmedBounds bounds) { ); } - protected void setTrimFactory(Function factory) { - this.trimFactory = factory; - } - protected void addSourceSetListener(Consumer listener) { this.sourceSetListeners.add(listener); } 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 d467b49a7..292ed10aa 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 @@ -18,6 +18,7 @@ import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; 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.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; @@ -63,6 +64,7 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.Box; @@ -327,36 +329,6 @@ private void updateToolTip(Entry target) { // offsets are implemented in syntaxpain .ifPresent(lineNumbers -> lineNumbers.deinstall(tooltipEditor.editor)); - tooltipEditor.setTrimFactory(source -> { - if (target instanceof ClassEntry targetClass) { - final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); - - return this.getClassBounds(source.toString(), targetDotName, targetClass).unwrapOrElse(error -> { - Logger.error(error); - return null; - }); - } else if (target instanceof MethodEntry targetMethod) { - // TODO - return null; - } else if (target instanceof FieldEntry targetField) { - // TODO - return null; - } else if (target instanceof LocalVariableEntry targetLocal) { - if (targetLocal.isArgument()) { - // TODO - return null; - } else { - // TODO - return null; - - // nothing? or show parent method? - } - } else { - // TODO - return null; - } - }); - tooltipEditor.addSourceSetListener(source -> { final Token declarationToken = source.getIndex().getDeclarationToken(target); if (declarationToken != null) { @@ -368,7 +340,9 @@ private void updateToolTip(Entry target) { }); tooltipEditor.getEditor().setEditable(false); - tooltipEditor.setClassHandle(targetTopClassHandle); + tooltipEditor.setClassHandle(targetTopClassHandle, source -> this + .createTrimmedBounds(source, target, deobfTarget) + ); tooltipContent.add(tooltipEditor.ui); } } @@ -524,6 +498,36 @@ private static void consumeMousePositionIn( } } + private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { + if (target instanceof ClassEntry targetClass) { + final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); + + return this.getClassBounds(source.toString(), targetDotName, targetClass).unwrapOrElse(error -> { + Logger.error(error); + return null; + }); + } else if (target instanceof MethodEntry targetMethod) { + // TODO + return null; + } else if (target instanceof FieldEntry targetField) { + // TODO + return null; + } else if (target instanceof LocalVariableEntry targetLocal) { + if (targetLocal.isArgument()) { + // TODO + return null; + } else { + // TODO + return null; + + // nothing? or show parent method? + } + } else { + // TODO + return null; + } + } + public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { @@ -564,13 +568,18 @@ public NavigatorPanel getNavigatorPanel() { } @Override - protected void setClassHandleImpl(ClassEntry old, ClassHandle handle) { - super.setClassHandleImpl(old, handle); + protected void setClassHandleImpl( + ClassEntry old, ClassHandle handle, + @Nullable Function trimFactory + ) { + super.setClassHandleImpl(old, handle, trimFactory); handle.addListener(new ClassHandleListener() { @Override public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { - SwingUtilities.invokeLater(() -> EditorPanel.this.listeners.forEach(l -> l.onTitleChanged(EditorPanel.this, EditorPanel.this.getSimpleClassName()))); + SwingUtilities.invokeLater(() -> EditorPanel.this.listeners.forEach(l -> l + .onTitleChanged(EditorPanel.this, EditorPanel.this.getSimpleClassName())) + ); } @Override @@ -611,16 +620,6 @@ protected void setCursorReference(EntryReference, Entry> ref) { this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref)); } - // @Override - // protected void onSourceSet(DecompiledClassSource source) { - // super.onSourceSet(source); - // if (this.navigatorPanel != null) { - // for (Entry entry : source.getIndex().declarations()) { - // this.navigatorPanel.addEntry(entry); - // } - // } - // } - public void addListener(EditorActionListener listener) { this.listeners.add(listener); } From 86a707cd69927362990c529c7f4eb90eae5c1d86 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 14:32:03 -0700 Subject: [PATCH 014/109] implement source trimming for methods --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 129 ++++++++++++++---- .../org/quiltmc/enigma/util/LineIndexer.java | 8 +- 2 files changed, 107 insertions(+), 30 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 292ed10aa..c4e064844 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 @@ -5,11 +5,13 @@ import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration; import com.github.javaparser.Position; +import com.github.javaparser.Range; import com.github.javaparser.TokenRange; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; import com.google.common.util.concurrent.Runnables; @@ -20,6 +22,7 @@ import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.api.translation.representation.MethodDescriptor; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -60,6 +63,7 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; @@ -130,7 +134,7 @@ public class EditorPanel extends BaseEditorPanel { }); } ); - // TODO stop hide timer when mouse is over tooltip + // TODO stop hide timer when mouse is over tooltip or target token // TODO tooltip re-shows after short delay after hiding private final Timer hideTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, @@ -313,7 +317,6 @@ private void updateToolTip(Entry target) { if (target instanceof ParentedEntry parentedTarget) { final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); - @Nullable final ClassHandle targetTopClassHandle = targetTopClass.equals(this.getSource().getEntry()) ? this.classHandle : this.gui.getController().getClassHandleProvider().openClass(targetTopClass); @@ -352,13 +355,10 @@ private void updateToolTip(Entry target) { this.tooltip.pack(); } - private Result getClassBounds(String source, String targetName, ClassEntry target) { + private Result findClassBounds(DecompiledClassSource source, ClassEntry target, String targetName) { return this.getNodeType(target, targetName).andThen(nodeType -> { - final ParserConfiguration config = new ParserConfiguration() - .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - - final ParseResult parseResult = new JavaParser(config).parse(source); + final String sourceString = source.toString(); + final ParseResult parseResult = parse(sourceString); return parseResult .getResult() .map(unit -> unit @@ -377,19 +377,9 @@ private Result getClassBounds(String source, String targe final JavaToken javaToken = tokenItr.next(); if (javaToken.asString().equals("{")) { return javaToken.getRange() - .map(openRange -> { - final Position startPos = range.begin; - final Position endPos = openRange.begin; - - final LineIndexer lineIndexer = new LineIndexer(source); - final int start = lineIndexer.getIndex(startPos); - int end = lineIndexer.getIndex(endPos); - while (Character.isWhitespace(source.charAt(end - 1))) { - end--; - } - - return Result.ok(new TrimmedBounds(start, end)); - }) + .map(openRange -> Result.ok( + toTrimmedBounds(new LineIndexer(sourceString), range.begin, openRange.begin) + )) .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); } } @@ -406,6 +396,77 @@ private Result getClassBounds(String source, String targe }); } + private Result findMethodBounds( + DecompiledClassSource source, MethodEntry target, + String targetName, String targetSimpleName + ) { + final String sourceString = source.toString(); + + final ParseResult parseResult = parse(sourceString); + return parseResult + .getResult() + .map(unit -> { + final LineIndexer lineIndexer = new LineIndexer(sourceString); + final Token targetToken = source.getIndex().getDeclarationToken(target); + + return unit + .findAll(MethodDeclaration.class, declaration -> { + if (declaration.getNameAsString().equals(targetSimpleName) && declaration.hasRange()) { + final Range range = declaration.getRange().orElseThrow(); + + return lineIndexer.getIndex(range.begin) <= targetToken.start + && lineIndexer.getIndex(range.end) >= targetToken.end; + } else { + return false; + } + }) + .stream() + // deepest first + .min(Comparator.comparingInt( + // hasRange() already checked in filter + declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end) + )) + .map(targetDeclaration -> { + // hasRange() already checked in filter + final Range range = targetDeclaration.getRange().orElseThrow(); + + return targetDeclaration + .getBody() + .map(body -> body.getRange() + .map(bodyRange -> Result.ok( + toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin) + )) + .orElseGet(() -> Result.err("No body range for %s!".formatted(targetName))) + ) + // no body: abstract + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range.begin, range.end))); + } + ) + .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))); + }) + .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + } + + private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { + final int start = lineIndexer.getIndex(startPos); + int end = lineIndexer.getIndex(endPos); + while (Character.isWhitespace(lineIndexer.getString().charAt(end - 1))) { + end--; + } + + return new TrimmedBounds(start, end); + } + + private static ParseResult parse(String source) { + final ParserConfiguration config = new ParserConfiguration() + .setStoreTokens(true) + // .setSymbolResolver(new JavaSymbolSolver()) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + + final ParseResult parseResult = new JavaParser(config).parse(source); + return parseResult; + } + private Result>, String> getNodeType( ClassEntry targetClass, String targetName ) { @@ -426,7 +487,11 @@ private Result>, String> getNodeTyp } }) .>, String>>map(Result::ok) - .orElseGet(() -> Result.err("No definition for %s!".formatted(targetName))); + .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); + } + + private static String noDefinitionErrorOf(String targetName) { + return "No definition for %s!".formatted(targetName); } /** @@ -499,16 +564,15 @@ private static void consumeMousePositionIn( } private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { - if (target instanceof ClassEntry targetClass) { - final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); + final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); - return this.getClassBounds(source.toString(), targetDotName, targetClass).unwrapOrElse(error -> { - Logger.error(error); - return null; - }); + if (target instanceof ClassEntry targetClass) { + return unwrapOrNull(this.findClassBounds(source, targetClass, targetDotName)); } else if (target instanceof MethodEntry targetMethod) { // TODO - return null; + return unwrapOrNull( + this.findMethodBounds(source, targetMethod, targetDotName, deobfTarget.getSimpleName()) + ); } else if (target instanceof FieldEntry targetField) { // TODO return null; @@ -528,6 +592,13 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry } } + private static TrimmedBounds unwrapOrNull(Result boundsResult) { + return boundsResult.unwrapOrElse(error -> { + Logger.error(error); + return null; + }); + } + public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java index e121895b2..700270f74 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -12,11 +12,17 @@ public class LineIndexer { private final List indexesByLine = new ArrayList<>(); private final Matcher lineEndMatcher; + private final String string; public LineIndexer(String string) { // the first line always starts at 0 this.indexesByLine.add(0); - this.lineEndMatcher = LINE_END.matcher(string); + this.string = string; + this.lineEndMatcher = LINE_END.matcher(this.string); + } + + public String getString() { + return this.string; } public int getStartIndex(int line) { From e3034b14857ba6deb806439e1ae266604218acb8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 15:18:58 -0700 Subject: [PATCH 015/109] match declarations only by range and extract findDeclaration --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 166 ++++++++---------- 1 file changed, 76 insertions(+), 90 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 c4e064844..dd390fe0c 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 @@ -9,6 +9,7 @@ import com.github.javaparser.TokenRange; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; @@ -22,7 +23,6 @@ import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.MethodDescriptor; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -355,96 +355,85 @@ private void updateToolTip(Entry target) { this.tooltip.pack(); } - private Result findClassBounds(DecompiledClassSource source, ClassEntry target, String targetName) { + private Result findClassBounds( + DecompiledClassSource source, ClassEntry target, String targetName + ) { return this.getNodeType(target, targetName).andThen(nodeType -> { - final String sourceString = source.toString(); - final ParseResult parseResult = parse(sourceString); - return parseResult - .getResult() - .map(unit -> unit - .findFirst(nodeType, declaration -> declaration - .getFullyQualifiedName() - .filter(name -> name.equals(targetName)) - .isPresent() - ) - .map(targetDeclaration -> targetDeclaration - .getRange() - .map(range -> targetDeclaration - .getTokenRange() - .map(TokenRange::iterator) - .map(tokenItr -> { - while (tokenItr.hasNext()) { - final JavaToken javaToken = tokenItr.next(); - if (javaToken.asString().equals("{")) { - return javaToken.getRange() - .map(openRange -> Result.ok( - toTrimmedBounds(new LineIndexer(sourceString), range.begin, openRange.begin) - )) - .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); - } - } - - return Result.err("No open curly brace for %s!".formatted(targetName)); - }) - .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName))) - ) - .orElseGet(() -> Result.err("No declaration range for %s!".formatted(targetName))) - ) - .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))) - ) - .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + return findDeclaration(source, lineIndexer, nodeType, target, targetName).andThen(declaration -> declaration + .getTokenRange() + .map(TokenRange::iterator) + .map(tokenItr -> { + while (tokenItr.hasNext()) { + final JavaToken javaToken = tokenItr.next(); + if (javaToken.asString().equals("{")) { + return javaToken.getRange() + .map(openRange -> toTrimmedBounds( + lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); + } + } + + return Result.err("No open curly brace for %s!".formatted(targetName)); + }) + .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName)))); }); } private Result findMethodBounds( - DecompiledClassSource source, MethodEntry target, - String targetName, String targetSimpleName + DecompiledClassSource source, MethodEntry target, String targetName + ) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + return findDeclaration(source, lineIndexer, MethodDeclaration.class, target, targetName) + .andThen(declaration -> { + final Range range = declaration.getRange().orElseThrow(); + + return declaration + .getBody() + .map(body -> body.getRange() + .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) + .>map(Result::ok) + .orElseGet(() -> Result.err("No body range for %s!".formatted(targetName))) + ) + // no body: abstract + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range.begin, range.end))); + }); + } + + /** + * @return an {@linkplain Result#ok(Object) ok result} containing the declaration representing the passed + * {@code declarationToken}, or an {@linkplain Result#err(Object) error result} if it could not be found; + * found declarations always {@linkplain TypeDeclaration#hasRange() have a range} + */ + private static > Result findDeclaration( + DecompiledClassSource source, LineIndexer lineIndexer, Class nodeType, Entry target, String targetName ) { - final String sourceString = source.toString(); - - final ParseResult parseResult = parse(sourceString); - return parseResult - .getResult() - .map(unit -> { - final LineIndexer lineIndexer = new LineIndexer(sourceString); - final Token targetToken = source.getIndex().getDeclarationToken(target); - - return unit - .findAll(MethodDeclaration.class, declaration -> { - if (declaration.getNameAsString().equals(targetSimpleName) && declaration.hasRange()) { - final Range range = declaration.getRange().orElseThrow(); - - return lineIndexer.getIndex(range.begin) <= targetToken.start - && lineIndexer.getIndex(range.end) >= targetToken.end; - } else { - return false; - } - }) - .stream() - // deepest first - .min(Comparator.comparingInt( - // hasRange() already checked in filter - declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end) - )) - .map(targetDeclaration -> { - // hasRange() already checked in filter - final Range range = targetDeclaration.getRange().orElseThrow(); - - return targetDeclaration - .getBody() - .map(body -> body.getRange() - .map(bodyRange -> Result.ok( - toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin) - )) - .orElseGet(() -> Result.err("No body range for %s!".formatted(targetName))) - ) - // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range.begin, range.end))); - } - ) - .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName))); + final ParseResult parseResult = parse(source.toString()); + return parseResult + .getResult() + .map(unit -> unit + .findAll(nodeType, declaration -> { + if (declaration.hasRange()) { + final Range range = declaration.getRange().orElseThrow(); + final Token targetToken = source.getIndex().getDeclarationToken(target); + + return lineIndexer.getIndex(range.begin) <= targetToken.start + && lineIndexer.getIndex(range.end) >= targetToken.end; + } else { + return false; + } }) - .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + .stream() + // deepest + .min(Comparator.comparingInt( + // hasRange() already checked in filter + declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end) + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName)))) + .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); } private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { @@ -459,9 +448,8 @@ private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position s private static ParseResult parse(String source) { final ParserConfiguration config = new ParserConfiguration() - .setStoreTokens(true) - // .setSymbolResolver(new JavaSymbolSolver()) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); final ParseResult parseResult = new JavaParser(config).parse(source); return parseResult; @@ -570,9 +558,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return unwrapOrNull(this.findClassBounds(source, targetClass, targetDotName)); } else if (target instanceof MethodEntry targetMethod) { // TODO - return unwrapOrNull( - this.findMethodBounds(source, targetMethod, targetDotName, deobfTarget.getSimpleName()) - ); + return unwrapOrNull(this.findMethodBounds(source, targetMethod, targetDotName)); } else if (target instanceof FieldEntry targetField) { // TODO return null; From d9678ebfc52ee6115b30a46007ba8243e6dca64c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 15:21:58 -0700 Subject: [PATCH 016/109] inline variable --- .../main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java | 3 +-- 1 file changed, 1 insertion(+), 2 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 dd390fe0c..4303c7736 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 @@ -451,8 +451,7 @@ private static ParseResult parse(String source) { .setStoreTokens(true) .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - final ParseResult parseResult = new JavaParser(config).parse(source); - return parseResult; + return new JavaParser(config).parse(source); } private Result>, String> getNodeType( From 26d56e9390acec0a9667e1b131281bbed6ec99d8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 1 Oct 2025 19:46:31 -0700 Subject: [PATCH 017/109] implement field source trimming stop excluding curly braces from method and class sources --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 206 ++++++++++++++---- 1 file changed, 164 insertions(+), 42 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 4303c7736..83f34e1c0 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,18 +11,25 @@ import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.BodyDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.nodeTypes.NodeWithRange; import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; 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.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.api.translation.representation.AccessFlags; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -69,6 +76,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.Box; @@ -88,7 +96,7 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; - private static final Pattern CLASS_PUNCTUATION = Pattern.compile("/|\\$"); + private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -360,25 +368,21 @@ private Result findClassBounds( ) { return this.getNodeType(target, targetName).andThen(nodeType -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, lineIndexer, nodeType, target, targetName).andThen(declaration -> declaration + final Token targetToken = source.getIndex().getDeclarationToken(target); + return findDeclaration(source, targetToken, targetName, nodeType, lineIndexer).andThen(declaration -> declaration .getTokenRange() - .map(TokenRange::iterator) - .map(tokenItr -> { - while (tokenItr.hasNext()) { - final JavaToken javaToken = tokenItr.next(); - if (javaToken.asString().equals("{")) { - return javaToken.getRange() - .map(openRange -> toTrimmedBounds( - lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin - )) - .>map(Result::ok) - .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName))); - } - } - - return Result.err("No open curly brace for %s!".formatted(targetName)); - }) - .orElseGet(() -> Result.err("No token range for %s!".formatted(targetName)))); + .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) + .map(openCurlyBrace -> openCurlyBrace + .getRange() + .map(openRange -> toTrimmedBounds( + lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName)))) + .orElseGet(() -> Result.err("No open curly brace for %s!".formatted(targetName))) + ) + .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) + ); }); } @@ -386,7 +390,7 @@ private Result findMethodBounds( DecompiledClassSource source, MethodEntry target, String targetName ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, lineIndexer, MethodDeclaration.class, target, targetName) + return findDeclaration(source, source.getIndex().getDeclarationToken(target), targetName, MethodDeclaration.class, lineIndexer) .andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); @@ -398,33 +402,112 @@ private Result findMethodBounds( .orElseGet(() -> Result.err("No body range for %s!".formatted(targetName))) ) // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range.begin, range.end))); + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); }); } + private Result findFieldBounds( + DecompiledClassSource source, FieldEntry target, String targetName + ) { + final Token targetToken = source.getIndex().getDeclarationToken(target); + + final JarIndex jarIndex = this.gui.getController().getProject().getJarIndex(); + final EntryIndex entryIndex = jarIndex.getIndex(EntryIndex.class); + + return Optional.ofNullable(entryIndex.getDefinition(target)) + .map(targetDef -> { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + if (targetDef.getAccess().isEnum()) { + return findEnumConstantBounds(source, targetToken, targetName, lineIndexer); + } else { + return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) + .map(parent -> { + if (parent.isRecord()) { + return this.findRecordComponent(source, targetToken, targetName, parent, lineIndexer); + } else { + return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); + } + }) + .orElseGet(() -> Result.err("No parent definition for %s!".formatted(targetName))); + } + }) + .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); + } + + private Result findRecordComponent( + DecompiledClassSource source, Token target, String targetName, ClassDefEntry parent, LineIndexer lineIndexer + ) { + final Token parentToken = source.getIndex().getDeclarationToken(parent); + final String parentName = this.gui.getController().getProject().getRemapper().deobfuscate(parent).getFullName(); + + return findDeclaration(source, parentToken, parentName, RecordDeclaration.class, lineIndexer) + .andThen(parentDeclaration -> parentDeclaration + .getParameters().stream() + .filter(component -> rangeContains(lineIndexer, component, target)) + .findFirst() + .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) + .>map(Result::ok) + .orElseGet(() -> Result.err("Could not find record component %s!".formatted(targetName))) + ); + } + + private static Result findEnumConstantBounds( + DecompiledClassSource source, Token target, String targetName, LineIndexer lineIndexer + ) { + return findDeclaration(source, target, targetName, EnumConstantDeclaration.class, lineIndexer) + .andThen(declaration -> Result.ok(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); + } + + private static Result findRegularFieldBounds( + DecompiledClassSource source, Token target, String targetName, LineIndexer lineIndexer + ) { + return findDeclaration(source, target, targetName, FieldDeclaration.class, lineIndexer) + .andThen(declaration -> declaration + .getTokenRange() + .map(tokenRange -> { + final Range range = declaration.getRange().orElseThrow(); + return declaration.getVariables().stream() + .filter(variable -> rangeContains(lineIndexer, variable, target)) + .findFirst() + .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) + .map(terminator -> toTrimmedBounds( + lineIndexer, range.begin, terminator.getRange().orElseThrow().begin + )) + .>map(Result::ok) + // no assignment in field declaration + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) + ) + .orElseGet(() -> Result.err( + "No matching variable declarator for: %s!".formatted(targetName) + )); + }) + .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) + ); + } + + private static Optional findFirstToken(TokenRange range, Predicate predicate) { + for (final JavaToken token : range) { + if (predicate.test(token)) { + return Optional.of(token); + } + } + + return Optional.empty(); + } + /** * @return an {@linkplain Result#ok(Object) ok result} containing the declaration representing the passed - * {@code declarationToken}, or an {@linkplain Result#err(Object) error result} if it could not be found; + * {@code token}, or an {@linkplain Result#err(Object) error result} if it could not be found; * found declarations always {@linkplain TypeDeclaration#hasRange() have a range} */ private static > Result findDeclaration( - DecompiledClassSource source, LineIndexer lineIndexer, Class nodeType, Entry target, String targetName + DecompiledClassSource source, Token target, String targetName, Class nodeType, LineIndexer lineIndexer ) { final ParseResult parseResult = parse(source.toString()); return parseResult .getResult() .map(unit -> unit - .findAll(nodeType, declaration -> { - if (declaration.hasRange()) { - final Range range = declaration.getRange().orElseThrow(); - final Token targetToken = source.getIndex().getDeclarationToken(target); - - return lineIndexer.getIndex(range.begin) <= targetToken.start - && lineIndexer.getIndex(range.end) >= targetToken.end; - } else { - return false; - } - }) + .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) .stream() // deepest .min(Comparator.comparingInt( @@ -436,14 +519,30 @@ private static > Result findDeclaration( .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); } + private static > boolean rangeContains(LineIndexer lineIndexer, N node, Token token) { + if (node.hasRange()) { + final Range range = node.getRange().orElseThrow(); + + return lineIndexer.getIndex(range.begin) <= token.start + // subtract one because Token.end is exclusive + && lineIndexer.getIndex(range.end) >= token.end - 1; + } else { + return false; + } + } + + private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Range range) { + return toTrimmedBounds(lineIndexer, range.begin, range.end); + } + private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { final int start = lineIndexer.getIndex(startPos); int end = lineIndexer.getIndex(endPos); - while (Character.isWhitespace(lineIndexer.getString().charAt(end - 1))) { + while (Character.isWhitespace(lineIndexer.getString().charAt(end))) { end--; } - return new TrimmedBounds(start, end); + return new TrimmedBounds(start, end + 1); } private static ParseResult parse(String source) { @@ -454,11 +553,10 @@ private static ParseResult parse(String source) { return new JavaParser(config).parse(source); } - private Result>, String> getNodeType( + private Result>, String> getNodeType( ClassEntry targetClass, String targetName ) { - final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex() - .getIndex(EntryIndex.class); + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); return Optional .ofNullable(entryIndex.getDefinition(targetClass)) @@ -473,7 +571,27 @@ private Result>, String> getNodeTyp return ClassOrInterfaceDeclaration.class; } }) - .>, String>>map(Result::ok) + .>, String>>map(Result::ok) + .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); + } + + private Result>, String> getNodeType(FieldEntry target, String targetName) { + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); + + return Optional.ofNullable(entryIndex.getDefinition(target)) + .map(FieldDefEntry::getAccess) + .map(access -> access.isEnum() ? EnumConstantDeclaration.class : FieldDeclaration.class) + .>, String>>map(Result::ok) + .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); + } + + private Result isEnum(FieldEntry target, String targetName) { + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); + + return Optional.ofNullable(entryIndex.getDefinition(target)) + .map(FieldDefEntry::getAccess) + .map(AccessFlags::isEnum) + .>map(Result::ok) .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); } @@ -481,6 +599,10 @@ private static String noDefinitionErrorOf(String targetName) { return "No definition for %s!".formatted(targetName); } + private static String noTokenRangeErrorOf(String targetName) { + return "No token range for %s!".formatted(targetName); + } + /** * @see #consumeEditorMouseTarget(BiConsumer, Runnable) */ @@ -550,6 +672,7 @@ private static void consumeMousePositionIn( } } + // TODO extract to util class or TooltipEditorPanel private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); @@ -559,8 +682,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry // TODO return unwrapOrNull(this.findMethodBounds(source, targetMethod, targetDotName)); } else if (target instanceof FieldEntry targetField) { - // TODO - return null; + return unwrapOrNull(this.findFieldBounds(source, targetField, targetDotName)); } else if (target instanceof LocalVariableEntry targetLocal) { if (targetLocal.isArgument()) { // TODO From 74ef674196d4dc27e94c2af56661a304578f25ba Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 07:53:15 -0700 Subject: [PATCH 018/109] fix tooltip source trimming for static record fields add "No source available" label when there's no classhandle --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 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 83f34e1c0..fb76085ab 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 @@ -355,6 +355,8 @@ private void updateToolTip(Entry target) { .createTrimmedBounds(source, target, deobfTarget) ); tooltipContent.add(tooltipEditor.ui); + } else { + tooltipContent.add(new JLabel("No source available")); } } @@ -420,15 +422,20 @@ private Result findFieldBounds( if (targetDef.getAccess().isEnum()) { return findEnumConstantBounds(source, targetToken, targetName, lineIndexer); } else { - return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) - .map(parent -> { - if (parent.isRecord()) { - return this.findRecordComponent(source, targetToken, targetName, parent, lineIndexer); - } else { - return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); - } - }) - .orElseGet(() -> Result.err("No parent definition for %s!".formatted(targetName))); + if (targetDef.getAccess().isStatic()) { + // don't check whether it's a record component if it's static + return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); + } else { + return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) + .map(parent -> { + if (parent.isRecord()) { + return this.findRecordComponent(source, targetToken, targetName, parent, lineIndexer); + } else { + return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); + } + }) + .orElseGet(() -> Result.err("No parent definition for %s!".formatted(targetName))); + } } }) .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); @@ -685,16 +692,15 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return unwrapOrNull(this.findFieldBounds(source, targetField, targetDotName)); } else if (target instanceof LocalVariableEntry targetLocal) { if (targetLocal.isArgument()) { - // TODO + // TODO show method declaration return null; } else { - // TODO + // TODO show local declaration return null; - - // nothing? or show parent method? } } else { - // TODO + // this should never be reached + Logger.error("Unrecognized target entry type: " + target); return null; } } From fe8e65304bd1845b0eef5a014b657975fba7f033 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 12:43:38 -0700 Subject: [PATCH 019/109] update TODOs centralize target name reporting --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 88 ++++++------------- 1 file changed, 29 insertions(+), 59 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 fb76085ab..d2b14cc45 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 @@ -27,9 +27,7 @@ import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.AccessFlags; import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -97,6 +95,7 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); + public static final String NO_ENTRY_DEFINITION = "No entry definition!"; private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -368,10 +367,10 @@ private void updateToolTip(Entry target) { private Result findClassBounds( DecompiledClassSource source, ClassEntry target, String targetName ) { - return this.getNodeType(target, targetName).andThen(nodeType -> { + return this.getNodeType(target).andThen(nodeType -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); final Token targetToken = source.getIndex().getDeclarationToken(target); - return findDeclaration(source, targetToken, targetName, nodeType, lineIndexer).andThen(declaration -> declaration + return findDeclaration(source, targetToken, nodeType, lineIndexer).andThen(declaration -> declaration .getTokenRange() .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) .map(openCurlyBrace -> openCurlyBrace @@ -380,8 +379,8 @@ private Result findClassBounds( lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin )) .>map(Result::ok) - .orElseGet(() -> Result.err("No open curly brace range for %s!".formatted(targetName)))) - .orElseGet(() -> Result.err("No open curly brace for %s!".formatted(targetName))) + .orElseGet(() -> Result.err("No class open curly brace range!"))) + .orElseGet(() -> Result.err("No class open curly brace!")) ) .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) ); @@ -389,10 +388,10 @@ private Result findClassBounds( } private Result findMethodBounds( - DecompiledClassSource source, MethodEntry target, String targetName + DecompiledClassSource source, MethodEntry target ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, source.getIndex().getDeclarationToken(target), targetName, MethodDeclaration.class, lineIndexer) + return findDeclaration(source, source.getIndex().getDeclarationToken(target), MethodDeclaration.class, lineIndexer) .andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); @@ -401,7 +400,7 @@ private Result findMethodBounds( .map(body -> body.getRange() .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) .>map(Result::ok) - .orElseGet(() -> Result.err("No body range for %s!".formatted(targetName))) + .orElseGet(() -> Result.err("No method body range!")) ) // no body: abstract .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); @@ -420,7 +419,7 @@ private Result findFieldBounds( .map(targetDef -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); if (targetDef.getAccess().isEnum()) { - return findEnumConstantBounds(source, targetToken, targetName, lineIndexer); + return findEnumConstantBounds(source, targetToken, lineIndexer); } else { if (targetDef.getAccess().isStatic()) { // don't check whether it's a record component if it's static @@ -434,41 +433,40 @@ private Result findFieldBounds( return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); } }) - .orElseGet(() -> Result.err("No parent definition for %s!".formatted(targetName))); + .orElseGet(() -> Result.err("No field parent definition!")); } } }) - .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); + .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } private Result findRecordComponent( DecompiledClassSource source, Token target, String targetName, ClassDefEntry parent, LineIndexer lineIndexer ) { final Token parentToken = source.getIndex().getDeclarationToken(parent); - final String parentName = this.gui.getController().getProject().getRemapper().deobfuscate(parent).getFullName(); - return findDeclaration(source, parentToken, parentName, RecordDeclaration.class, lineIndexer) + return findDeclaration(source, parentToken, RecordDeclaration.class, lineIndexer) .andThen(parentDeclaration -> parentDeclaration .getParameters().stream() .filter(component -> rangeContains(lineIndexer, component, target)) .findFirst() .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) .>map(Result::ok) - .orElseGet(() -> Result.err("Could not find record component %s!".formatted(targetName))) + .orElseGet(() -> Result.err("Could not find record component!")) ); } private static Result findEnumConstantBounds( - DecompiledClassSource source, Token target, String targetName, LineIndexer lineIndexer + DecompiledClassSource source, Token target, LineIndexer lineIndexer ) { - return findDeclaration(source, target, targetName, EnumConstantDeclaration.class, lineIndexer) + return findDeclaration(source, target, EnumConstantDeclaration.class, lineIndexer) .andThen(declaration -> Result.ok(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); } private static Result findRegularFieldBounds( DecompiledClassSource source, Token target, String targetName, LineIndexer lineIndexer ) { - return findDeclaration(source, target, targetName, FieldDeclaration.class, lineIndexer) + return findDeclaration(source, target, FieldDeclaration.class, lineIndexer) .andThen(declaration -> declaration .getTokenRange() .map(tokenRange -> { @@ -484,9 +482,7 @@ private static Result findRegularFieldBounds( // no assignment in field declaration .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) ) - .orElseGet(() -> Result.err( - "No matching variable declarator for: %s!".formatted(targetName) - )); + .orElseGet(() -> Result.err("No matching variable declarator!")); }) .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) ); @@ -508,7 +504,7 @@ private static Optional findFirstToken(TokenRange range, Predicate> Result findDeclaration( - DecompiledClassSource source, Token target, String targetName, Class nodeType, LineIndexer lineIndexer + DecompiledClassSource source, Token target, Class nodeType, LineIndexer lineIndexer ) { final ParseResult parseResult = parse(source.toString()); return parseResult @@ -522,7 +518,7 @@ private static > Result findDeclaration( declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end) )) .>map(Result::ok) - .orElseGet(() -> Result.err("Failed to find %s in parsed source!".formatted(targetName)))) + .orElseGet(() -> Result.err("Not found in parsed source!"))) .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); } @@ -560,9 +556,7 @@ private static ParseResult parse(String source) { return new JavaParser(config).parse(source); } - private Result>, String> getNodeType( - ClassEntry targetClass, String targetName - ) { + private Result>, String> getNodeType(ClassEntry targetClass) { final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); return Optional @@ -579,31 +573,7 @@ private Result>, String> getNodeType( } }) .>, String>>map(Result::ok) - .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); - } - - private Result>, String> getNodeType(FieldEntry target, String targetName) { - final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); - - return Optional.ofNullable(entryIndex.getDefinition(target)) - .map(FieldDefEntry::getAccess) - .map(access -> access.isEnum() ? EnumConstantDeclaration.class : FieldDeclaration.class) - .>, String>>map(Result::ok) - .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); - } - - private Result isEnum(FieldEntry target, String targetName) { - final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); - - return Optional.ofNullable(entryIndex.getDefinition(target)) - .map(FieldDefEntry::getAccess) - .map(AccessFlags::isEnum) - .>map(Result::ok) - .orElseGet(() -> Result.err(noDefinitionErrorOf(targetName))); - } - - private static String noDefinitionErrorOf(String targetName) { - return "No definition for %s!".formatted(targetName); + .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } private static String noTokenRangeErrorOf(String targetName) { @@ -684,12 +654,11 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); if (target instanceof ClassEntry targetClass) { - return unwrapOrNull(this.findClassBounds(source, targetClass, targetDotName)); + return unwrapTooltipBoundsOrNull(this.findClassBounds(source, targetClass, targetDotName), targetDotName); } else if (target instanceof MethodEntry targetMethod) { - // TODO - return unwrapOrNull(this.findMethodBounds(source, targetMethod, targetDotName)); + return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetMethod), targetDotName); } else if (target instanceof FieldEntry targetField) { - return unwrapOrNull(this.findFieldBounds(source, targetField, targetDotName)); + return unwrapTooltipBoundsOrNull(this.findFieldBounds(source, targetField, targetDotName), targetDotName); } else if (target instanceof LocalVariableEntry targetLocal) { if (targetLocal.isArgument()) { // TODO show method declaration @@ -699,15 +668,16 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return null; } } else { + // TODO use same message formatting as unwrapOrNull // this should never be reached - Logger.error("Unrecognized target entry type: " + target); + Logger.error("Unrecognized target entry type: {}!", target); return null; } } - private static TrimmedBounds unwrapOrNull(Result boundsResult) { - return boundsResult.unwrapOrElse(error -> { - Logger.error(error); + private static TrimmedBounds unwrapTooltipBoundsOrNull(Result bounds, String targetName) { + return bounds.unwrapOrElse(error -> { + Logger.error("Error finding declaration of '{}' for tooltip: {}", targetName, error); return null; }); } From 349ba7041985e57c561598e878e2ba3de48c75c3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 13:32:00 -0700 Subject: [PATCH 020/109] extract TooltipEditorPanel --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 319 +----------------- .../enigma/gui/panel/TooltipEditorPanel.java | 308 +++++++++++++++++ 2 files changed, 317 insertions(+), 310 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java 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 d2b14cc45..2a6a212f5 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,36 +1,12 @@ package org.quiltmc.enigma.gui.panel; -import com.github.javaparser.JavaParser; -import com.github.javaparser.JavaToken; -import com.github.javaparser.ParseResult; -import com.github.javaparser.ParserConfiguration; -import com.github.javaparser.Position; -import com.github.javaparser.Range; -import com.github.javaparser.TokenRange; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.AnnotationDeclaration; -import com.github.javaparser.ast.body.BodyDeclaration; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.RecordDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.nodeTypes.NodeWithRange; import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; -import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; 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.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; @@ -43,11 +19,6 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; -import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; -import org.quiltmc.enigma.util.LineIndexer; -import org.quiltmc.enigma.util.Result; -import org.quiltmc.syntaxpain.LineNumbersRuler; -import org.tinylog.Logger; import java.awt.BorderLayout; import java.awt.Component; @@ -68,21 +39,17 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JViewport; import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -94,8 +61,6 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; - private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); - public static final String NO_ENTRY_DEFINITION = "No entry definition!"; private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -117,7 +82,7 @@ public class EditorPanel extends BaseEditorPanel { if (!targetToken.equals(this.lastMouseTargetToken)) { this.lastMouseTargetToken = targetToken; - this.updateToolTip(targetEntry); + this.updateTooltip(targetEntry); } } else { this.lastMouseTargetToken = targetToken; @@ -136,7 +101,7 @@ public class EditorPanel extends BaseEditorPanel { this.hideTokenTooltipTimer.restart(); if (targetToken.equals(this.lastMouseTargetToken)) { this.tooltip.setVisible(true); - this.updateToolTip(targetEntry); + this.updateTooltip(targetEntry); } }); } @@ -183,6 +148,7 @@ public void focusLost(FocusEvent e) { // global listener so tooltip hides even if clicking outside editor Toolkit.getDefaultToolkit().addAWTEventListener( e -> { + // TODO configurably allow clicking tooltip if (e.getID() == MouseEvent.MOUSE_PRESSED) { this.closeTooltip(); } @@ -314,7 +280,7 @@ private void closeTooltip() { EditorPanel.this.hideTokenTooltipTimer.stop(); } - private void updateToolTip(Entry target) { + private void updateTooltip(Entry target) { final Container tooltipContent = this.tooltip.getContentPane(); tooltipContent.removeAll(); @@ -329,257 +295,23 @@ private void updateToolTip(Entry target) { : this.gui.getController().getClassHandleProvider().openClass(targetTopClass); if (targetTopClassHandle != null) { - final BaseEditorPanel tooltipEditor = new BaseEditorPanel(this.gui); - - Optional.ofNullable(tooltipEditor.editorScrollPane.getRowHeader()) - .map(JViewport::getView) - // LineNumbersRuler is installed by syntaxpain - .map(view -> view instanceof LineNumbersRuler lineNumbers ? lineNumbers : null) - // TODO offset line numbers instead of removing them once - // offsets are implemented in syntaxpain - .ifPresent(lineNumbers -> lineNumbers.deinstall(tooltipEditor.editor)); - - tooltipEditor.addSourceSetListener(source -> { - final Token declarationToken = source.getIndex().getDeclarationToken(target); - if (declarationToken != null) { - this.tooltip.pack(); - - // TODO create custom highlighter - tooltipEditor.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE); - } - }); + final TooltipEditorPanel tooltipEditor = new TooltipEditorPanel(this.gui, target, targetTopClassHandle); + + tooltipEditor.addSourceSetListener(source -> this.tooltip.pack()); - tooltipEditor.getEditor().setEditable(false); - tooltipEditor.setClassHandle(targetTopClassHandle, source -> this - .createTrimmedBounds(source, target, deobfTarget) - ); tooltipContent.add(tooltipEditor.ui); } else { tooltipContent.add(new JLabel("No source available")); } } + // TODO offset from cursor slightly + ensure on-screen this.tooltip.setLocation(MouseInfo.getPointerInfo().getLocation()); + // TODO clamp size this.tooltip.pack(); } - private Result findClassBounds( - DecompiledClassSource source, ClassEntry target, String targetName - ) { - return this.getNodeType(target).andThen(nodeType -> { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - final Token targetToken = source.getIndex().getDeclarationToken(target); - return findDeclaration(source, targetToken, nodeType, lineIndexer).andThen(declaration -> declaration - .getTokenRange() - .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) - .map(openCurlyBrace -> openCurlyBrace - .getRange() - .map(openRange -> toTrimmedBounds( - lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin - )) - .>map(Result::ok) - .orElseGet(() -> Result.err("No class open curly brace range!"))) - .orElseGet(() -> Result.err("No class open curly brace!")) - ) - .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) - ); - }); - } - - private Result findMethodBounds( - DecompiledClassSource source, MethodEntry target - ) { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, source.getIndex().getDeclarationToken(target), MethodDeclaration.class, lineIndexer) - .andThen(declaration -> { - final Range range = declaration.getRange().orElseThrow(); - - return declaration - .getBody() - .map(body -> body.getRange() - .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) - .>map(Result::ok) - .orElseGet(() -> Result.err("No method body range!")) - ) - // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); - }); - } - - private Result findFieldBounds( - DecompiledClassSource source, FieldEntry target, String targetName - ) { - final Token targetToken = source.getIndex().getDeclarationToken(target); - - final JarIndex jarIndex = this.gui.getController().getProject().getJarIndex(); - final EntryIndex entryIndex = jarIndex.getIndex(EntryIndex.class); - - return Optional.ofNullable(entryIndex.getDefinition(target)) - .map(targetDef -> { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); - if (targetDef.getAccess().isEnum()) { - return findEnumConstantBounds(source, targetToken, lineIndexer); - } else { - if (targetDef.getAccess().isStatic()) { - // don't check whether it's a record component if it's static - return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); - } else { - return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) - .map(parent -> { - if (parent.isRecord()) { - return this.findRecordComponent(source, targetToken, targetName, parent, lineIndexer); - } else { - return findRegularFieldBounds(source, targetToken, targetName, lineIndexer); - } - }) - .orElseGet(() -> Result.err("No field parent definition!")); - } - } - }) - .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); - } - - private Result findRecordComponent( - DecompiledClassSource source, Token target, String targetName, ClassDefEntry parent, LineIndexer lineIndexer - ) { - final Token parentToken = source.getIndex().getDeclarationToken(parent); - - return findDeclaration(source, parentToken, RecordDeclaration.class, lineIndexer) - .andThen(parentDeclaration -> parentDeclaration - .getParameters().stream() - .filter(component -> rangeContains(lineIndexer, component, target)) - .findFirst() - .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) - .>map(Result::ok) - .orElseGet(() -> Result.err("Could not find record component!")) - ); - } - - private static Result findEnumConstantBounds( - DecompiledClassSource source, Token target, LineIndexer lineIndexer - ) { - return findDeclaration(source, target, EnumConstantDeclaration.class, lineIndexer) - .andThen(declaration -> Result.ok(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); - } - - private static Result findRegularFieldBounds( - DecompiledClassSource source, Token target, String targetName, LineIndexer lineIndexer - ) { - return findDeclaration(source, target, FieldDeclaration.class, lineIndexer) - .andThen(declaration -> declaration - .getTokenRange() - .map(tokenRange -> { - final Range range = declaration.getRange().orElseThrow(); - return declaration.getVariables().stream() - .filter(variable -> rangeContains(lineIndexer, variable, target)) - .findFirst() - .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) - .map(terminator -> toTrimmedBounds( - lineIndexer, range.begin, terminator.getRange().orElseThrow().begin - )) - .>map(Result::ok) - // no assignment in field declaration - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) - ) - .orElseGet(() -> Result.err("No matching variable declarator!")); - }) - .orElseGet(() -> Result.err(noTokenRangeErrorOf(targetName))) - ); - } - - private static Optional findFirstToken(TokenRange range, Predicate predicate) { - for (final JavaToken token : range) { - if (predicate.test(token)) { - return Optional.of(token); - } - } - - return Optional.empty(); - } - - /** - * @return an {@linkplain Result#ok(Object) ok result} containing the declaration representing the passed - * {@code token}, or an {@linkplain Result#err(Object) error result} if it could not be found; - * found declarations always {@linkplain TypeDeclaration#hasRange() have a range} - */ - private static > Result findDeclaration( - DecompiledClassSource source, Token target, Class nodeType, LineIndexer lineIndexer - ) { - final ParseResult parseResult = parse(source.toString()); - return parseResult - .getResult() - .map(unit -> unit - .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) - .stream() - // deepest - .min(Comparator.comparingInt( - // hasRange() already checked in filter - declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end) - )) - .>map(Result::ok) - .orElseGet(() -> Result.err("Not found in parsed source!"))) - .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); - } - - private static > boolean rangeContains(LineIndexer lineIndexer, N node, Token token) { - if (node.hasRange()) { - final Range range = node.getRange().orElseThrow(); - - return lineIndexer.getIndex(range.begin) <= token.start - // subtract one because Token.end is exclusive - && lineIndexer.getIndex(range.end) >= token.end - 1; - } else { - return false; - } - } - - private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Range range) { - return toTrimmedBounds(lineIndexer, range.begin, range.end); - } - - private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { - final int start = lineIndexer.getIndex(startPos); - int end = lineIndexer.getIndex(endPos); - while (Character.isWhitespace(lineIndexer.getString().charAt(end))) { - end--; - } - - return new TrimmedBounds(start, end + 1); - } - - private static ParseResult parse(String source) { - final ParserConfiguration config = new ParserConfiguration() - .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - - return new JavaParser(config).parse(source); - } - - private Result>, String> getNodeType(ClassEntry targetClass) { - final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); - - return Optional - .ofNullable(entryIndex.getDefinition(targetClass)) - .map(targetDef -> { - if (targetDef.getAccess().isAnnotation()) { - return AnnotationDeclaration.class; - } else if (targetDef.isEnum()) { - return EnumDeclaration.class; - } else if (targetDef.isRecord()) { - return RecordDeclaration.class; - } else { - return ClassOrInterfaceDeclaration.class; - } - }) - .>, String>>map(Result::ok) - .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); - } - - private static String noTokenRangeErrorOf(String targetName) { - return "No token range for %s!".formatted(targetName); - } - /** * @see #consumeEditorMouseTarget(BiConsumer, Runnable) */ @@ -649,39 +381,6 @@ private static void consumeMousePositionIn( } } - // TODO extract to util class or TooltipEditorPanel - private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { - final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); - - if (target instanceof ClassEntry targetClass) { - return unwrapTooltipBoundsOrNull(this.findClassBounds(source, targetClass, targetDotName), targetDotName); - } else if (target instanceof MethodEntry targetMethod) { - return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetMethod), targetDotName); - } else if (target instanceof FieldEntry targetField) { - return unwrapTooltipBoundsOrNull(this.findFieldBounds(source, targetField, targetDotName), targetDotName); - } else if (target instanceof LocalVariableEntry targetLocal) { - if (targetLocal.isArgument()) { - // TODO show method declaration - return null; - } else { - // TODO show local declaration - return null; - } - } else { - // TODO use same message formatting as unwrapOrNull - // this should never be reached - Logger.error("Unrecognized target entry type: {}!", target); - return null; - } - } - - private static TrimmedBounds unwrapTooltipBoundsOrNull(Result bounds, String targetName) { - return bounds.unwrapOrElse(error -> { - Logger.error("Error finding declaration of '{}' for tooltip: {}", targetName, error); - return null; - }); - } - public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java new file mode 100644 index 000000000..bee1b40ff --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -0,0 +1,308 @@ +package org.quiltmc.enigma.gui.panel; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.JavaToken; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.TokenRange; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.RecordDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.nodeTypes.NodeWithRange; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.source.DecompiledClassSource; +import org.quiltmc.enigma.api.source.Token; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; +import org.quiltmc.enigma.util.LineIndexer; +import org.quiltmc.enigma.util.Result; +import org.quiltmc.syntaxpain.LineNumbersRuler; +import org.tinylog.Logger; + +import javax.swing.JViewport; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import static java.util.Comparator.comparingInt; + +public class TooltipEditorPanel extends BaseEditorPanel { + private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); + private static final String NO_ENTRY_DEFINITION = "No entry definition!"; + private static final String NO_TOKEN_RANGE = "No token range!"; + + public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { + super(gui); + + Optional.ofNullable(this.editorScrollPane.getRowHeader()) + .map(JViewport::getView) + // LineNumbersRuler is installed by syntaxpain + .map(view -> view instanceof LineNumbersRuler lineNumbers ? lineNumbers : null) + // TODO offset line numbers instead of removing them once + // offsets are implemented in syntaxpain + .ifPresent(lineNumbers -> lineNumbers.deinstall(this.editor)); + + this.addSourceSetListener(source -> { + final Token declarationToken = source.getIndex().getDeclarationToken(target); + if (declarationToken != null) { + // TODO create custom highlighter + this.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE); + } + }); + + this.getEditor().setEditable(false); + final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); + this.setClassHandle(targetTopClassHandle, source -> this.createTrimmedBounds(source, target, deobfTarget)); + } + + private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { + final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); + + if (target instanceof ClassEntry targetClass) { + return unwrapTooltipBoundsOrNull(this.findClassBounds(source, targetClass), targetDotName); + } else if (target instanceof MethodEntry targetMethod) { + return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetMethod), targetDotName); + } else if (target instanceof FieldEntry targetField) { + return unwrapTooltipBoundsOrNull(this.findFieldBounds(source, targetField), targetDotName); + } else if (target instanceof LocalVariableEntry targetLocal) { + if (targetLocal.isArgument()) { + // TODO show method declaration + return null; + } else { + // TODO show local declaration + return null; + } + } else { + // TODO use same message formatting as unwrapOrNull + // this should never be reached + Logger.error("Unrecognized target entry type: {}!", target); + return null; + } + } + + private Result findClassBounds(DecompiledClassSource source, ClassEntry target) { + return this.getNodeType(target).andThen(nodeType -> { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + final Token targetToken = source.getIndex().getDeclarationToken(target); + return findDeclaration(source, targetToken, nodeType, lineIndexer).andThen(declaration -> declaration + .getTokenRange() + .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) + .map(openCurlyBrace -> openCurlyBrace + .getRange() + .map(openRange -> toTrimmedBounds( + lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("No class open curly brace range!"))) + .orElseGet(() -> Result.err("No class open curly brace!")) + ) + .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) + ); + }); + } + + private Result>, String> getNodeType(ClassEntry targetClass) { + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); + + return Optional + .ofNullable(entryIndex.getDefinition(targetClass)) + .map(targetDef -> { + if (targetDef.getAccess().isAnnotation()) { + return AnnotationDeclaration.class; + } else if (targetDef.isEnum()) { + return EnumDeclaration.class; + } else if (targetDef.isRecord()) { + return RecordDeclaration.class; + } else { + return ClassOrInterfaceDeclaration.class; + } + }) + .>, String>>map(Result::ok) + .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); + } + + private Result findMethodBounds(DecompiledClassSource source, MethodEntry target) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + final Token targetToken = source.getIndex().getDeclarationToken(target); + return findDeclaration(source, targetToken, MethodDeclaration.class, lineIndexer) + .andThen(declaration -> { + final Range range = declaration.getRange().orElseThrow(); + + return declaration + .getBody() + .map(body -> body.getRange() + .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) + .>map(Result::ok) + .orElseGet(() -> Result.err("No method body range!")) + ) + // no body: abstract + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); + }); + } + + private Result findFieldBounds(DecompiledClassSource source, FieldEntry target) { + final Token targetToken = source.getIndex().getDeclarationToken(target); + + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); + + return Optional.ofNullable(entryIndex.getDefinition(target)) + .map(targetDef -> { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + if (targetDef.getAccess().isEnum()) { + return findEnumConstantBounds(source, targetToken, lineIndexer); + } else { + if (targetDef.getAccess().isStatic()) { + // don't check whether it's a record component if it's static + return findRegularFieldBounds(source, targetToken, lineIndexer); + } else { + return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) + .map(parent -> { + if (parent.isRecord()) { + return this.findRecordComponent(source, targetToken, parent, lineIndexer); + } else { + return findRegularFieldBounds(source, targetToken, lineIndexer); + } + }) + .orElseGet(() -> Result.err("No field parent definition!")); + } + } + }) + .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); + } + + private Result findRecordComponent( + DecompiledClassSource source, Token target, ClassDefEntry parent, LineIndexer lineIndexer + ) { + final Token parentToken = source.getIndex().getDeclarationToken(parent); + + return findDeclaration(source, parentToken, RecordDeclaration.class, lineIndexer) + .andThen(parentDeclaration -> parentDeclaration + .getParameters().stream() + .filter(component -> rangeContains(lineIndexer, component, target)) + .findFirst() + .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) + .>map(Result::ok) + .orElseGet(() -> Result.err("Could not find record component!")) + ); + } + + private static Result findEnumConstantBounds( + DecompiledClassSource source, Token target, LineIndexer lineIndexer + ) { + return findDeclaration(source, target, EnumConstantDeclaration.class, lineIndexer) + .andThen(declaration -> Result.ok(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); + } + + private static Result findRegularFieldBounds( + DecompiledClassSource source, Token target, LineIndexer lineIndexer + ) { + return findDeclaration(source, target, FieldDeclaration.class, lineIndexer) + .andThen(declaration -> declaration + .getTokenRange() + .map(tokenRange -> { + final Range range = declaration.getRange().orElseThrow(); + return declaration.getVariables().stream() + .filter(variable -> rangeContains(lineIndexer, variable, target)) + .findFirst() + .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) + .map(terminator -> toTrimmedBounds( + lineIndexer, range.begin, terminator.getRange().orElseThrow().begin + )) + .>map(Result::ok) + // no assignment in field declaration + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) + ) + .orElseGet(() -> Result.err("No matching variable declarator!")); + }) + .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) + ); + } + + /** + * @return an {@linkplain Result#ok(Object) ok result} containing the declaration representing the passed + * {@code token}, or an {@linkplain Result#err(Object) error result} if it could not be found; + * found declarations always {@linkplain TypeDeclaration#hasRange() have a range} + */ + private static > Result findDeclaration( + DecompiledClassSource source, Token target, Class nodeType, LineIndexer lineIndexer + ) { + final ParseResult parseResult = parse(source.toString()); + return parseResult + .getResult() + .map(unit -> unit + .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) + .stream() + // deepest + .min(comparingInt(declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end))) + .>map(Result::ok) + .orElseGet(() -> Result.err("Not found in parsed source!"))) + .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + } + + private static Optional findFirstToken(TokenRange range, Predicate predicate) { + for (final JavaToken token : range) { + if (predicate.test(token)) { + return Optional.of(token); + } + } + + return Optional.empty(); + } + + private static > boolean rangeContains(LineIndexer lineIndexer, N node, Token token) { + if (node.hasRange()) { + final Range range = node.getRange().orElseThrow(); + + return lineIndexer.getIndex(range.begin) <= token.start + // subtract one because Token.end is exclusive + && lineIndexer.getIndex(range.end) >= token.end - 1; + } else { + return false; + } + } + + private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Range range) { + return toTrimmedBounds(lineIndexer, range.begin, range.end); + } + + private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { + final int start = lineIndexer.getIndex(startPos); + int end = lineIndexer.getIndex(endPos); + while (Character.isWhitespace(lineIndexer.getString().charAt(end))) { + end--; + } + + return new TrimmedBounds(start, end + 1); + } + + private static TrimmedBounds unwrapTooltipBoundsOrNull(Result bounds, String targetName) { + return bounds.unwrapOrElse(error -> { + Logger.error("Error finding declaration of '{}' for tooltip: {}", targetName, error); + return null; + }); + } + + private static ParseResult parse(String source) { + final ParserConfiguration config = new ParserConfiguration() + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + + return new JavaParser(config).parse(source); + } +} From 953f68d00f9573e6ab08a8fdd4c90f645256fdc9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 13:45:38 -0700 Subject: [PATCH 021/109] trim param tooltip target source to parent method refine error messages --- .../enigma/gui/panel/TooltipEditorPanel.java | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index bee1b40ff..cc68a819a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -44,8 +44,8 @@ public class TooltipEditorPanel extends BaseEditorPanel { private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); - private static final String NO_ENTRY_DEFINITION = "No entry definition!"; - private static final String NO_TOKEN_RANGE = "No token range!"; + private static final String NO_ENTRY_DEFINITION = "no entry definition!"; + private static final String NO_TOKEN_RANGE = "no token range!"; public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { super(gui); @@ -82,16 +82,14 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return unwrapTooltipBoundsOrNull(this.findFieldBounds(source, targetField), targetDotName); } else if (target instanceof LocalVariableEntry targetLocal) { if (targetLocal.isArgument()) { - // TODO show method declaration - return null; + return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetLocal.getParent()), targetDotName); } else { // TODO show local declaration return null; } } else { - // TODO use same message formatting as unwrapOrNull // this should never be reached - Logger.error("Unrecognized target entry type: {}!", target); + Logger.error("Error trimming tooltip for '{}': unrecognized target entry type!", targetDotName); return null; } } @@ -109,8 +107,8 @@ private Result findClassBounds(DecompiledClassSource sour lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin )) .>map(Result::ok) - .orElseGet(() -> Result.err("No class open curly brace range!"))) - .orElseGet(() -> Result.err("No class open curly brace!")) + .orElseGet(() -> Result.err("no class open curly brace range!"))) + .orElseGet(() -> Result.err("no class open curly brace!")) ) .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) ); @@ -137,23 +135,23 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } + // TODO lambdas? private Result findMethodBounds(DecompiledClassSource source, MethodEntry target) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); final Token targetToken = source.getIndex().getDeclarationToken(target); - return findDeclaration(source, targetToken, MethodDeclaration.class, lineIndexer) - .andThen(declaration -> { - final Range range = declaration.getRange().orElseThrow(); - - return declaration - .getBody() - .map(body -> body.getRange() - .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) - .>map(Result::ok) - .orElseGet(() -> Result.err("No method body range!")) - ) - // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); - }); + return findDeclaration(source, targetToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> { + final Range range = declaration.getRange().orElseThrow(); + + return declaration + .getBody() + .map(body -> body.getRange() + .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) + .>map(Result::ok) + .orElseGet(() -> Result.err("no method body range!")) + ) + // no body: abstract + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); + }); } private Result findFieldBounds(DecompiledClassSource source, FieldEntry target) { @@ -168,7 +166,7 @@ private Result findFieldBounds(DecompiledClassSource sour return findEnumConstantBounds(source, targetToken, lineIndexer); } else { if (targetDef.getAccess().isStatic()) { - // don't check whether it's a record component if it's static + // not a record component if it's static return findRegularFieldBounds(source, targetToken, lineIndexer); } else { return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) @@ -179,7 +177,7 @@ private Result findFieldBounds(DecompiledClassSource sour return findRegularFieldBounds(source, targetToken, lineIndexer); } }) - .orElseGet(() -> Result.err("No field parent definition!")); + .orElseGet(() -> Result.err("no field parent definition!")); } } }) @@ -198,7 +196,7 @@ private Result findRecordComponent( .findFirst() .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) .>map(Result::ok) - .orElseGet(() -> Result.err("Could not find record component!")) + .orElseGet(() -> Result.err("could not find record component!")) ); } @@ -212,26 +210,25 @@ private static Result findEnumConstantBounds( private static Result findRegularFieldBounds( DecompiledClassSource source, Token target, LineIndexer lineIndexer ) { - return findDeclaration(source, target, FieldDeclaration.class, lineIndexer) - .andThen(declaration -> declaration - .getTokenRange() - .map(tokenRange -> { - final Range range = declaration.getRange().orElseThrow(); - return declaration.getVariables().stream() - .filter(variable -> rangeContains(lineIndexer, variable, target)) - .findFirst() - .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) - .map(terminator -> toTrimmedBounds( - lineIndexer, range.begin, terminator.getRange().orElseThrow().begin - )) - .>map(Result::ok) - // no assignment in field declaration - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) - ) - .orElseGet(() -> Result.err("No matching variable declarator!")); - }) - .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) - ); + return findDeclaration(source, target, FieldDeclaration.class, lineIndexer).andThen(declaration -> declaration + .getTokenRange() + .map(tokenRange -> { + final Range range = declaration.getRange().orElseThrow(); + return declaration.getVariables().stream() + .filter(variable -> rangeContains(lineIndexer, variable, target)) + .findFirst() + .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) + .map(terminator -> toTrimmedBounds( + lineIndexer, range.begin, terminator.getRange().orElseThrow().begin + )) + .>map(Result::ok) + // no assignment in field declaration + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) + ) + .orElseGet(() -> Result.err("no matching variable declarator!")); + }) + .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) + ); } /** @@ -251,8 +248,8 @@ private static > Result findDeclaration( // deepest .min(comparingInt(declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end))) .>map(Result::ok) - .orElseGet(() -> Result.err("Not found in parsed source!"))) - .orElseGet(() -> Result.err("Failed to parse source: " + parseResult.getProblems())); + .orElseGet(() -> Result.err("not found in parsed source!"))) + .orElseGet(() -> Result.err("failed to parse source: " + parseResult.getProblems())); } private static Optional findFirstToken(TokenRange range, Predicate predicate) { From 612adc317144327fed82fd1134f00450d92c9bff Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 18:25:52 -0700 Subject: [PATCH 022/109] fail early when target declaration token is missing add local source trimming --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 11 + .../enigma/gui/panel/TooltipEditorPanel.java | 296 +++++++++++++----- 2 files changed, 232 insertions(+), 75 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 2a6a212f5..bc006274d 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 @@ -44,6 +44,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.Box; import javax.swing.BoxLayout; @@ -61,6 +62,7 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; + private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -149,6 +151,9 @@ public void focusLost(FocusEvent e) { Toolkit.getDefaultToolkit().addAWTEventListener( e -> { // TODO configurably allow clicking tooltip + // - update tooltip with clicked entry declaration + // - add a "bread crumbs" back button + // - open entry tab on ctrl-click or "Got to source" button click if (e.getID() == MouseEvent.MOUSE_PRESSED) { this.closeTooltip(); } @@ -286,6 +291,7 @@ private void updateTooltip(Entry target) { final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); + // TODO show parent name instead tooltipContent.add(new JLabel(deobfTarget.getFullName())); if (target instanceof ParentedEntry parentedTarget) { final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); @@ -381,6 +387,11 @@ private static void consumeMousePositionIn( } } + // TODO use for tooltip parent label/link + private static String getFullDotName(Entry entry) { + return CLASS_PUNCTUATION.matcher(entry.getFullName()).replaceAll("."); + } + public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index cc68a819a..abcbf3191 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -17,7 +17,14 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.RecordDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.nodeTypes.NodeWithRange; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.Statement; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.source.DecompiledClassSource; @@ -36,14 +43,14 @@ import org.tinylog.Logger; import javax.swing.JViewport; +import java.util.Comparator; +import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; -import java.util.regex.Pattern; import static java.util.Comparator.comparingInt; public class TooltipEditorPanel extends BaseEditorPanel { - private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); private static final String NO_ENTRY_DEFINITION = "no entry definition!"; private static final String NO_TOKEN_RANGE = "no token range!"; @@ -67,38 +74,118 @@ public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHa }); this.getEditor().setEditable(false); - final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); - this.setClassHandle(targetTopClassHandle, source -> this.createTrimmedBounds(source, target, deobfTarget)); + this.setClassHandle(targetTopClassHandle, source -> this.createTrimmedBounds(source, target)); } - private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target, Entry deobfTarget) { - final String targetDotName = CLASS_PUNCTUATION.matcher(deobfTarget.getFullName()).replaceAll("."); + private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target) { + final Token targetToken = Objects.requireNonNull( + source.getIndex().getDeclarationToken(target), + () -> "Error trimming tooltip for '%s': no declaration token!" + .formatted(this.getFullDeobfuscatedName(target)) + ); + final Result bounds; if (target instanceof ClassEntry targetClass) { - return unwrapTooltipBoundsOrNull(this.findClassBounds(source, targetClass), targetDotName); - } else if (target instanceof MethodEntry targetMethod) { - return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetMethod), targetDotName); + bounds = this.findClassBounds(source, targetToken, targetClass); + } else if (target instanceof MethodEntry) { + bounds = this.findMethodBounds(source, targetToken); } else if (target instanceof FieldEntry targetField) { - return unwrapTooltipBoundsOrNull(this.findFieldBounds(source, targetField), targetDotName); + bounds = this.findFieldBounds(source, targetToken, targetField); } else if (target instanceof LocalVariableEntry targetLocal) { - if (targetLocal.isArgument()) { - return unwrapTooltipBoundsOrNull(this.findMethodBounds(source, targetLocal.getParent()), targetDotName); - } else { - // TODO show local declaration - return null; - } + bounds = this.getVariableBounds(source, targetToken, targetLocal); } else { // this should never be reached - Logger.error("Error trimming tooltip for '{}': unrecognized target entry type!", targetDotName); + Logger.error( + "Error trimming tooltip for '{}': unrecognized target entry type!", + this.getFullDeobfuscatedName(target) + ); return null; } + + return bounds.unwrapOrElse(error -> { + Logger.error( + "Error finding declaration of '{}' for tooltip: {}", + this.getFullDeobfuscatedName(target), + error + ); + return null; + }); + } + + private Result getVariableBounds( + DecompiledClassSource source, Token target, LocalVariableEntry targetEntry + ) { + final MethodEntry parent = targetEntry.getParent(); + final Token parentToken = source.getIndex().getDeclarationToken(parent); + if (parentToken == null) { + return this.findLambdaVariable(source, target, targetEntry, parent); + } else { + if (targetEntry.isArgument()) { + return this.findMethodBounds(source, parentToken); + } else { + return this.findLocalBounds(source, parentToken, target); + } + } + } + + private Result findLambdaVariable( + DecompiledClassSource source, Token target, LocalVariableEntry targetEntry, MethodEntry parent + ) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + + final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); + return Optional.ofNullable(entryIndex.getDefinition(parent)) + .map(parentDef -> { + if (parentDef.getAccess().isSynthetic()) { + return parse(source).andThen(unit -> unit + .findAll(LambdaExpr.class, lambda -> rangeContains(lineIndexer, lambda, target)) + .stream() + .max(depthComparatorOf(lineIndexer)) + .map(parentLambda -> { + if (targetEntry.isArgument()) { + return parentLambda + .getBegin() + .map(parentBegin -> parentLambda + .getBody() + .getRange() + .map(bodyRange -> toTrimmedBounds(lineIndexer, parentBegin, bodyRange.begin)) + .>map(Result::ok) + .orElseGet(() -> Result.err("no parent lambda body range!"))) + .orElseGet(() -> Result.err("no parent lambda begin")); + } else { + final Statement parentBody = parentLambda.getBody(); + return parentBody.toBlockStmt() + .map(parentBlock -> findLocalBounds(target, parentBlock, lineIndexer, "lambda")) + .orElseGet(() -> parentBody.asExpressionStmt() + .getExpression() + .toVariableDeclarationExpr() + .map(variableExpr -> + findVariableExpressionBounds(target, variableExpr, lineIndexer) + ) + .orElseGet(() -> Result.err("local declared in non-declaration expression!")) + ); + } + }) + .orElseGet(() -> Result.err("failed to find local's parent lambda!"))); + } else { + return Result.err("no parent token for non-synthetic parent!"); + } + }) + .orElseGet(() -> Result.err("no parent definition!")); } - private Result findClassBounds(DecompiledClassSource source, ClassEntry target) { - return this.getNodeType(target).andThen(nodeType -> { + private String getFullDeobfuscatedName(Entry entry) { + return this.gui.getController().getProject().getRemapper() + .deobfuscate(entry) + .getFullName(); + } + + private Result findClassBounds( + DecompiledClassSource source, Token target, ClassEntry targetEntry + ) { + return this.getNodeType(targetEntry).andThen(nodeType -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - final Token targetToken = source.getIndex().getDeclarationToken(target); - return findDeclaration(source, targetToken, nodeType, lineIndexer).andThen(declaration -> declaration + return findDeclaration(source, target, nodeType, lineIndexer).andThen(declaration -> declaration .getTokenRange() .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) .map(openCurlyBrace -> openCurlyBrace @@ -135,46 +222,47 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - // TODO lambdas? - private Result findMethodBounds(DecompiledClassSource source, MethodEntry target) { + // TODO check issue with (record?) constructors + private Result findMethodBounds(DecompiledClassSource source, Token targetToken) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - final Token targetToken = source.getIndex().getDeclarationToken(target); + return findDeclaration(source, targetToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); - return declaration - .getBody() - .map(body -> body.getRange() - .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) - .>map(Result::ok) - .orElseGet(() -> Result.err("no method body range!")) - ) - // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); + final Result methodBody = getMethodBody(declaration); + return methodBody.isErr() + // no body: abstract + ? Result.ok(toTrimmedBounds(lineIndexer, range)) + : methodBody + .andThen(body -> body.getRange() + .>map(Result::ok) + .orElseGet(() -> Result.err("no method body range!")) + ) + .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)); }); } - private Result findFieldBounds(DecompiledClassSource source, FieldEntry target) { - final Token targetToken = source.getIndex().getDeclarationToken(target); - + private Result findFieldBounds( + DecompiledClassSource source, Token target, FieldEntry targetEntry + ) { final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); - return Optional.ofNullable(entryIndex.getDefinition(target)) + return Optional.ofNullable(entryIndex.getDefinition(targetEntry)) .map(targetDef -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); if (targetDef.getAccess().isEnum()) { - return findEnumConstantBounds(source, targetToken, lineIndexer); + return findEnumConstantBounds(source, target, lineIndexer); } else { if (targetDef.getAccess().isStatic()) { // not a record component if it's static - return findRegularFieldBounds(source, targetToken, lineIndexer); + return findRegularFieldBounds(source, target, lineIndexer); } else { return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) .map(parent -> { if (parent.isRecord()) { - return this.findRecordComponent(source, targetToken, parent, lineIndexer); + return this.findRecordComponent(source, target, parent, lineIndexer); } else { - return findRegularFieldBounds(source, targetToken, lineIndexer); + return findRegularFieldBounds(source, target, lineIndexer); } }) .orElseGet(() -> Result.err("no field parent definition!")); @@ -217,20 +305,55 @@ private static Result findRegularFieldBounds( return declaration.getVariables().stream() .filter(variable -> rangeContains(lineIndexer, variable, target)) .findFirst() - .map(variable -> findFirstToken(tokenRange, token -> token.asString().equals("=")) - .map(terminator -> toTrimmedBounds( - lineIndexer, range.begin, terminator.getRange().orElseThrow().begin - )) - .>map(Result::ok) - // no assignment in field declaration - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))) - ) - .orElseGet(() -> Result.err("no matching variable declarator!")); + .map(variable -> toDeclaratorBounds(range, variable, lineIndexer)) + .orElseGet(() -> Result.err("no matching field declarator!")); }) .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) ); } + private Result findLocalBounds( + DecompiledClassSource source, Token parentToken, Token targetToken + ) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); + + return findDeclaration(source, parentToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> + getMethodBody(declaration) + .andThen(parentBody -> findLocalBounds(targetToken, parentBody, lineIndexer, "method")) + ); + } + + private static Result findLocalBounds( + Token target, BlockStmt parentBody, LineIndexer lineIndexer, String parentType + ) { + return parentBody + .getStatements() + .stream() + .map(Statement::toExpressionStmt) + .flatMap(Optional::stream) + .map(ExpressionStmt::getExpression) + .map(Expression::toVariableDeclarationExpr) + .flatMap(Optional::stream) + .filter(variableExpr -> rangeContains(lineIndexer, variableExpr, target)) + .max(depthComparatorOf(lineIndexer)) + .map(variableExpr -> findVariableExpressionBounds(target, variableExpr, lineIndexer)) + .orElseGet(() -> Result.err("failed to find local in parent %s!".formatted(parentType))); + } + + private static Result findVariableExpressionBounds( + Token targetToken, VariableDeclarationExpr variableExpr, LineIndexer lineIndexer + ) { + return variableExpr + .getVariables() + .stream() + .filter(variable -> rangeContains(lineIndexer, variable, targetToken)) + .findFirst() + .map(targetVariable -> + toDeclaratorBounds(variableExpr.getRange().orElseThrow(), targetVariable, lineIndexer) + ) + .orElseGet(() -> Result.err("failed to find local in variable expression!")); + } + /** * @return an {@linkplain Result#ok(Object) ok result} containing the declaration representing the passed * {@code token}, or an {@linkplain Result#err(Object) error result} if it could not be found; @@ -239,17 +362,55 @@ private static Result findRegularFieldBounds( private static > Result findDeclaration( DecompiledClassSource source, Token target, Class nodeType, LineIndexer lineIndexer ) { - final ParseResult parseResult = parse(source.toString()); + return parse(source).andThen(unit -> unit + .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) + .stream() + .max(depthComparatorOf(lineIndexer)) + .>map(Result::ok) + .orElseGet(() -> Result.err("not found in parsed source!")) + ); + } + + private static Comparator> depthComparatorOf(LineIndexer lineIndexer) { + return comparingInt(declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().begin)); + } + + private static Result parse(DecompiledClassSource source) { + final ParserConfiguration config = new ParserConfiguration() + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + + final ParseResult parseResult = new JavaParser(config).parse(source.toString()); return parseResult - .getResult() - .map(unit -> unit - .findAll(nodeType, declaration -> rangeContains(lineIndexer, declaration, target)) - .stream() - // deepest - .min(comparingInt(declaration -> lineIndexer.getIndex(declaration.getRange().orElseThrow().end))) - .>map(Result::ok) - .orElseGet(() -> Result.err("not found in parsed source!"))) - .orElseGet(() -> Result.err("failed to parse source: " + parseResult.getProblems())); + .getResult() + .>map(Result::ok) + .orElseGet(() -> Result.err("failed to parse source: " + parseResult.getProblems())); + } + + private static Result getMethodBody(MethodDeclaration declaration) { + return declaration + .getBody() + .>map(Result::ok) + .orElseGet(() -> Result.err("no method body!")); + } + + private static Result toDeclaratorBounds( + Range outerRange, VariableDeclarator variable, LineIndexer lineIndexer + ) { + if (outerRange.begin.line == outerRange.end.line) { + return Result.ok(toTrimmedBounds(lineIndexer, outerRange)); + } else { + return variable + .getTokenRange() + .map(variableRange -> findFirstToken(variableRange, token -> token.asString().equals("=")) + .map(assignment -> assignment.getRange().orElseThrow().begin) + .>map(Result::ok) + // no assignment + .orElse(Result.ok(outerRange.end)) + ) + .orElseGet(() -> Result.err("no variable token range!")) + .map(end -> toTrimmedBounds(lineIndexer, outerRange.begin, end)); + } } private static Optional findFirstToken(TokenRange range, Predicate predicate) { @@ -287,19 +448,4 @@ private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position s return new TrimmedBounds(start, end + 1); } - - private static TrimmedBounds unwrapTooltipBoundsOrNull(Result bounds, String targetName) { - return bounds.unwrapOrElse(error -> { - Logger.error("Error finding declaration of '{}' for tooltip: {}", targetName, error); - return null; - }); - } - - private static ParseResult parse(String source) { - final ParserConfiguration config = new ParserConfiguration() - .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - - return new JavaParser(config).parse(source); - } } From 12488475bf4e7188510c4e9639e9c4eeaf94a5f6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 19:12:24 -0700 Subject: [PATCH 023/109] fix tooltip source trimming for constructors --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 1 + .../enigma/gui/panel/TooltipEditorPanel.java | 39 ++++++++++++++----- 2 files changed, 31 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 bc006274d..b3d106fba 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 @@ -66,6 +66,7 @@ public class EditorPanel extends BaseEditorPanel { private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); + // TODO restore right click functionality! private final EditorPopupMenu popupMenu; // DIY tooltip because JToolTip can't be moved or resized diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index abcbf3191..08c421189 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -10,7 +10,9 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.AnnotationDeclaration; import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.FieldDeclaration; @@ -46,6 +48,7 @@ import java.util.Comparator; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; import static java.util.Comparator.comparingInt; @@ -53,6 +56,9 @@ public class TooltipEditorPanel extends BaseEditorPanel { 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 + private static final String METHOD = "method"; + private static final String LAMBDA = "lambda"; public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { super(gui); @@ -87,8 +93,8 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry final Result bounds; if (target instanceof ClassEntry targetClass) { bounds = this.findClassBounds(source, targetToken, targetClass); - } else if (target instanceof MethodEntry) { - bounds = this.findMethodBounds(source, targetToken); + } else if (target instanceof MethodEntry targetMethod) { + bounds = this.findMethodBounds(source, targetToken, targetMethod); } else if (target instanceof FieldEntry targetField) { bounds = this.findFieldBounds(source, targetToken, targetField); } else if (target instanceof LocalVariableEntry targetLocal) { @@ -116,12 +122,16 @@ private Result getVariableBounds( DecompiledClassSource source, Token target, LocalVariableEntry targetEntry ) { final MethodEntry parent = targetEntry.getParent(); + if (parent == null) { + return Result.err("variable parent is null!"); + } + final Token parentToken = source.getIndex().getDeclarationToken(parent); if (parentToken == null) { return this.findLambdaVariable(source, target, targetEntry, parent); } else { if (targetEntry.isArgument()) { - return this.findMethodBounds(source, parentToken); + return this.findMethodBounds(source, parentToken, parent); } else { return this.findLocalBounds(source, parentToken, target); } @@ -155,7 +165,7 @@ private Result findLambdaVariable( } else { final Statement parentBody = parentLambda.getBody(); return parentBody.toBlockStmt() - .map(parentBlock -> findLocalBounds(target, parentBlock, lineIndexer, "lambda")) + .map(parentBlock -> findLocalBounds(target, parentBlock, lineIndexer, LAMBDA)) .orElseGet(() -> parentBody.asExpressionStmt() .getExpression() .toVariableDeclarationExpr() @@ -222,14 +232,25 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - // TODO check issue with (record?) constructors - private Result findMethodBounds(DecompiledClassSource source, Token targetToken) { + private Result findMethodBounds( + DecompiledClassSource source, Token target, MethodEntry targetEntry + ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, targetToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> { + final Class> nodeType; + final Function, Result> bodyGetter; + if (targetEntry.isConstructor()) { + nodeType = ConstructorDeclaration.class; + bodyGetter = declaration -> Result.ok(((ConstructorDeclaration) declaration).getBody()); + } else { + nodeType = MethodDeclaration.class; + bodyGetter = declaration -> getMethodBody((MethodDeclaration) declaration); + } + + return findDeclaration(source, target, nodeType, lineIndexer).andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); - final Result methodBody = getMethodBody(declaration); + final Result methodBody = bodyGetter.apply(declaration); return methodBody.isErr() // no body: abstract ? Result.ok(toTrimmedBounds(lineIndexer, range)) @@ -319,7 +340,7 @@ private Result findLocalBounds( return findDeclaration(source, parentToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> getMethodBody(declaration) - .andThen(parentBody -> findLocalBounds(targetToken, parentBody, lineIndexer, "method")) + .andThen(parentBody -> findLocalBounds(targetToken, parentBody, lineIndexer, METHOD)) ); } From 6c32ab3c28d8b4fbe588e198aece2747da34c054 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 2 Oct 2025 19:32:23 -0700 Subject: [PATCH 024/109] minor refactor --- .../enigma/gui/panel/TooltipEditorPanel.java | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index 08c421189..3c5cb46c3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -238,28 +238,27 @@ private Result findMethodBounds( final LineIndexer lineIndexer = new LineIndexer(source.toString()); final Class> nodeType; - final Function, Result> bodyGetter; + final Function, Optional> bodyGetter; if (targetEntry.isConstructor()) { nodeType = ConstructorDeclaration.class; - bodyGetter = declaration -> Result.ok(((ConstructorDeclaration) declaration).getBody()); + bodyGetter = declaration -> Optional.of(((ConstructorDeclaration) declaration).getBody()); } else { nodeType = MethodDeclaration.class; - bodyGetter = declaration -> getMethodBody((MethodDeclaration) declaration); + bodyGetter = declaration -> ((MethodDeclaration) declaration).getBody(); } return findDeclaration(source, target, nodeType, lineIndexer).andThen(declaration -> { final Range range = declaration.getRange().orElseThrow(); - final Result methodBody = bodyGetter.apply(declaration); - return methodBody.isErr() - // no body: abstract - ? Result.ok(toTrimmedBounds(lineIndexer, range)) - : methodBody - .andThen(body -> body.getRange() - .>map(Result::ok) - .orElseGet(() -> Result.err("no method body range!")) - ) - .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)); + return bodyGetter.apply(declaration) + .map(methodBody -> methodBody + .getRange() + .>map(Result::ok) + .orElseGet(() -> Result.err("no method body range!")) + .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) + ) + // no body: abstract + .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); }); } @@ -338,10 +337,13 @@ private Result findLocalBounds( ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, parentToken, MethodDeclaration.class, lineIndexer).andThen(declaration -> - getMethodBody(declaration) + return findDeclaration(source, parentToken, MethodDeclaration.class, lineIndexer) + .andThen(declaration -> declaration + .getBody() + .>map(Result::ok) + .orElseGet(() -> Result.err("no method body!")) .andThen(parentBody -> findLocalBounds(targetToken, parentBody, lineIndexer, METHOD)) - ); + ); } private static Result findLocalBounds( @@ -408,13 +410,6 @@ private static Result parse(DecompiledClassSource sourc .orElseGet(() -> Result.err("failed to parse source: " + parseResult.getProblems())); } - private static Result getMethodBody(MethodDeclaration declaration) { - return declaration - .getBody() - .>map(Result::ok) - .orElseGet(() -> Result.err("no method body!")); - } - private static Result toDeclaratorBounds( Range outerRange, VariableDeclarator variable, LineIndexer lineIndexer ) { From afb4302dbaa8fff82194da624cb9688783571418 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 3 Oct 2025 11:54:33 -0700 Subject: [PATCH 025/109] show record component parent instead of just component; matches param showing parent method --- .../enigma/gui/panel/TooltipEditorPanel.java | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index 3c5cb46c3..c13e3fb90 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -60,6 +60,8 @@ public class TooltipEditorPanel extends BaseEditorPanel { private static final String METHOD = "method"; private static final String LAMBDA = "lambda"; + private static final int IMPLEMENTS_OFFSET = -" implements ".length(); + public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { super(gui); @@ -195,23 +197,30 @@ private Result findClassBounds( ) { return this.getNodeType(targetEntry).andThen(nodeType -> { final LineIndexer lineIndexer = new LineIndexer(source.toString()); - return findDeclaration(source, target, nodeType, lineIndexer).andThen(declaration -> declaration - .getTokenRange() - .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) - .map(openCurlyBrace -> openCurlyBrace - .getRange() - .map(openRange -> toTrimmedBounds( - lineIndexer, declaration.getRange().orElseThrow().begin, openRange.begin - )) - .>map(Result::ok) - .orElseGet(() -> Result.err("no class open curly brace range!"))) - .orElseGet(() -> Result.err("no class open curly brace!")) - ) - .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) - ); + return findDeclaration(source, target, nodeType, lineIndexer) + .andThen(declaration -> findTypeDeclarationBounds(declaration, lineIndexer)); }); } + private static Result findTypeDeclarationBounds( + TypeDeclaration declaration, LineIndexer lineIndexer + ) { + return declaration + .getTokenRange() + .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) + .map(openCurlyBrace -> openCurlyBrace + .getRange() + .map(openCurlyRange -> openCurlyRange.begin) + .map(openCurlyPos -> toTrimmedBounds( + lineIndexer, declaration.getBegin().orElseThrow(), openCurlyPos + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("no class open curly brace range!"))) + .orElseGet(() -> Result.err("no class open curly brace!")) + ) + .orElseGet(() -> Result.err(NO_TOKEN_RANGE)); + } + private Result>, String> getNodeType(ClassEntry targetClass) { final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); @@ -232,6 +241,7 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } + // TODO test record component getters private Result findMethodBounds( DecompiledClassSource source, Token target, MethodEntry targetEntry ) { @@ -269,55 +279,52 @@ private Result findFieldBounds( return Optional.ofNullable(entryIndex.getDefinition(targetEntry)) .map(targetDef -> { - final LineIndexer lineIndexer = new LineIndexer(source.toString()); if (targetDef.getAccess().isEnum()) { - return findEnumConstantBounds(source, target, lineIndexer); + return findEnumConstantBounds(source, target); } else { - if (targetDef.getAccess().isStatic()) { - // not a record component if it's static - return findRegularFieldBounds(source, target, lineIndexer); - } else { - return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) - .map(parent -> { - if (parent.isRecord()) { - return this.findRecordComponent(source, target, parent, lineIndexer); - } else { - return findRegularFieldBounds(source, target, lineIndexer); - } - }) - .orElseGet(() -> Result.err("no field parent definition!")); - } + return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) + .map(parent -> parent.isRecord() && !targetDef.getAccess().isStatic() + ? this.findComponentParent(source, parent) + : findRegularFieldBounds(source, target) + ) + .orElseGet(() -> Result.err("no field parent definition!")); } }) .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - private Result findRecordComponent( - DecompiledClassSource source, Token target, ClassDefEntry parent, LineIndexer lineIndexer - ) { + 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) .andThen(parentDeclaration -> parentDeclaration - .getParameters().stream() - .filter(component -> rangeContains(lineIndexer, component, target)) - .findFirst() - .map(targetComponent -> toTrimmedBounds(lineIndexer, targetComponent.getRange().orElseThrow())) - .>map(Result::ok) - .orElseGet(() -> Result.err("could not find record component!")) + .getImplementedTypes() + .getFirst() + // exclude implemented types if present + .map(implemented -> implemented + .getBegin() + .map(firstImplementedBegin -> toTrimmedBounds( + lineIndexer, + parentDeclaration.getBegin().orElseThrow(), + firstImplementedBegin.right(IMPLEMENTS_OFFSET) + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("no parent record implemented type range!")) + ) + // no implemented types + .orElseGet(() -> findTypeDeclarationBounds(parentDeclaration, lineIndexer)) ); } - private static Result findEnumConstantBounds( - DecompiledClassSource source, Token target, LineIndexer lineIndexer - ) { + private static Result findEnumConstantBounds(DecompiledClassSource source, Token target) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); return findDeclaration(source, target, EnumConstantDeclaration.class, lineIndexer) .andThen(declaration -> Result.ok(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); } - private static Result findRegularFieldBounds( - DecompiledClassSource source, Token target, LineIndexer lineIndexer - ) { + private static Result findRegularFieldBounds(DecompiledClassSource source, Token target) { + final LineIndexer lineIndexer = new LineIndexer(source.toString()); return findDeclaration(source, target, FieldDeclaration.class, lineIndexer).andThen(declaration -> declaration .getTokenRange() .map(tokenRange -> { @@ -418,6 +425,7 @@ private static Result toDeclaratorBounds( } else { return variable .getTokenRange() + // if it's not all on one line, try excluding assignment .map(variableRange -> findFirstToken(variableRange, token -> token.asString().equals("=")) .map(assignment -> assignment.getRange().orElseThrow().begin) .>map(Result::ok) From e62e9e4744092b210b782212b117ffb83fdd2a76 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 3 Oct 2025 12:21:52 -0700 Subject: [PATCH 026/109] when excluding implements from record component parent, find token instead of assuming offset --- .../enigma/gui/panel/TooltipEditorPanel.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index c13e3fb90..a8316465f 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -60,8 +60,6 @@ public class TooltipEditorPanel extends BaseEditorPanel { private static final String METHOD = "method"; private static final String LAMBDA = "lambda"; - private static final int IMPLEMENTS_OFFSET = -" implements ".length(); - public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { super(gui); @@ -207,7 +205,7 @@ private static Result findTypeDeclarationBounds( ) { return declaration .getTokenRange() - .map(tokenRange -> findFirstToken(tokenRange, token -> token.asString().equals("{")) + .map(tokenRange -> findFirstToken(tokenRange, "{") .map(openCurlyBrace -> openCurlyBrace .getRange() .map(openCurlyRange -> openCurlyRange.begin) @@ -302,15 +300,23 @@ private Result findComponentParent(DecompiledClassSource .getImplementedTypes() .getFirst() // exclude implemented types if present - .map(implemented -> implemented - .getBegin() - .map(firstImplementedBegin -> toTrimmedBounds( - lineIndexer, - parentDeclaration.getBegin().orElseThrow(), - firstImplementedBegin.right(IMPLEMENTS_OFFSET) - )) - .>map(Result::ok) - .orElseGet(() -> Result.err("no parent record implemented type range!")) + .map(ignored -> parentDeclaration + .getTokenRange() + .map(parentTokenRange -> findFirstToken(parentTokenRange, "implements") + .map(implToken -> implToken + .getRange() + .map(implRange -> implRange.begin.right(-1)) + .map(beforeImpl -> toTrimmedBounds( + lineIndexer, + parentDeclaration.getBegin().orElseThrow(), + beforeImpl + )) + .>map(Result::ok) + .orElseGet(() -> Result.err("no parent record implements token range!")) + ) + .orElseGet(() -> Result.err("record implements types but has no implements token!")) + ) + .orElseGet(() -> Result.err("no parent record token range!")) ) // no implemented types .orElseGet(() -> findTypeDeclarationBounds(parentDeclaration, lineIndexer)) @@ -426,7 +432,7 @@ private static Result toDeclaratorBounds( return variable .getTokenRange() // if it's not all on one line, try excluding assignment - .map(variableRange -> findFirstToken(variableRange, token -> token.asString().equals("=")) + .map(variableRange -> findFirstToken(variableRange, "=") .map(assignment -> assignment.getRange().orElseThrow().begin) .>map(Result::ok) // no assignment @@ -437,6 +443,10 @@ private static Result toDeclaratorBounds( } } + private static Optional findFirstToken(TokenRange range, String token) { + return findFirstToken(range, javaToken -> javaToken.asString().equals(token)); + } + private static Optional findFirstToken(TokenRange range, Predicate predicate) { for (final JavaToken token : range) { if (predicate.test(token)) { From 4c93021b153ea1b6d228a829f5fc3385c557a964 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 3 Oct 2025 17:44:08 -0700 Subject: [PATCH 027/109] close and destroy tooltip when source changes --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 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 b3d106fba..a54d408c5 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 @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.class_handle.ClassHandleError; import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; @@ -19,6 +20,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; +import org.quiltmc.enigma.util.Result; import java.awt.BorderLayout; import java.awt.Component; @@ -279,11 +281,11 @@ public void keyTyped(KeyEvent event) { } private void closeTooltip() { - EditorPanel.this.tooltip.setVisible(false); - EditorPanel.this.lastMouseTargetToken = null; - EditorPanel.this.mouseStoppedMovingTimer.stop(); - EditorPanel.this.showTokenTooltipTimer.stop(); - EditorPanel.this.hideTokenTooltipTimer.stop(); + this.tooltip.setVisible(false); + this.lastMouseTargetToken = null; + this.mouseStoppedMovingTimer.stop(); + this.showTokenTooltipTimer.stop(); + this.hideTokenTooltipTimer.stop(); } private void updateTooltip(Entry target) { @@ -298,12 +300,21 @@ private void updateTooltip(Entry target) { final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); final ClassHandle targetTopClassHandle = targetTopClass.equals(this.getSource().getEntry()) - ? this.classHandle + ? this.classHandle.copy() : this.gui.getController().getClassHandleProvider().openClass(targetTopClass); if (targetTopClassHandle != null) { final TooltipEditorPanel tooltipEditor = new TooltipEditorPanel(this.gui, target, targetTopClassHandle); + this.classHandle.addListener(new ClassHandleListener() { + @Override + public void onMappedSourceChanged(ClassHandle h, Result res) { + EditorPanel.this.closeTooltip(); + tooltipEditor.destroy(); + EditorPanel.this.classHandle.removeListener(this); + } + }); + tooltipEditor.addSourceSetListener(source -> this.tooltip.pack()); tooltipContent.add(tooltipEditor.ui); From 89b6a485a199a611053e79fb451a4c965c707438 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 3 Oct 2025 17:55:34 -0700 Subject: [PATCH 028/109] silently ignore missing declaration tokens (until #252 is fixed) --- .../enigma/gui/panel/TooltipEditorPanel.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java index a8316465f..8c7b3adce 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java @@ -46,7 +46,6 @@ import javax.swing.JViewport; import java.util.Comparator; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -84,11 +83,13 @@ public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHa } private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target) { - final Token targetToken = Objects.requireNonNull( - source.getIndex().getDeclarationToken(target), - () -> "Error trimming tooltip for '%s': no declaration token!" - .formatted(this.getFullDeobfuscatedName(target)) - ); + final Token targetToken = source.getIndex().getDeclarationToken(target); + + if (targetToken == null) { + // This can happen as a result of #252: Issue with lost parameter connection. + // Once #252 is fixed, an error should be logged here. + return null; + } final Result bounds; if (target instanceof ClassEntry targetClass) { From 80e84deff38664cf237111f87f99e78d7912fe99 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 4 Oct 2025 14:32:05 -0700 Subject: [PATCH 029/109] restore quickFindToolbar to ui stop closing tooltip's class handles as tabs may still need them; remove ClassHandleListener instead rename TooltipEditorPanel -> DeclarationSnippetPanel extract EditorTooltip add test input for easier manual tooltip testing --- .../enigma/gui/panel/BaseEditorPanel.java | 61 ++++++++---- ...anel.java => DeclarationSnippetPanel.java} | 8 +- .../quiltmc/enigma/gui/panel/EditorPanel.java | 95 ++++++------------- .../enigma/gui/panel/EditorTooltip.java | 91 ++++++++++++++++++ .../enigma/input/tooltip/Constructors.java | 15 +++ .../quiltmc/enigma/input/tooltip/Enums.java | 23 +++++ .../quiltmc/enigma/input/tooltip/Fields.java | 21 ++++ .../quiltmc/enigma/input/tooltip/Lambdas.java | 34 +++++++ .../quiltmc/enigma/input/tooltip/Methods.java | 18 ++++ .../quiltmc/enigma/input/tooltip/Records.java | 27 ++++++ 10 files changed, 303 insertions(+), 90 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/{TooltipEditorPanel.java => DeclarationSnippetPanel.java} (98%) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.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 05e2ce02c..e93ca2baa 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 @@ -89,7 +89,7 @@ public class BaseEditorPanel { private final BoxHighlightPainter debugPainter; private final BoxHighlightPainter fallbackPainter; - protected ClassHandle classHandle; + protected ClassHandler classHandler; private DecompiledClassSource source; private SourceBounds sourceBounds = new DefaultBounds(); protected boolean settingSource; @@ -125,16 +125,19 @@ public BaseEditorPanel(Gui gui) { } public void setClassHandle(ClassHandle handle) { - this.setClassHandle(handle, null); + this.setClassHandle(handle, true, null); } protected void setClassHandle( - ClassHandle handle, @Nullable Function trimFactory + ClassHandle handle, boolean closeOldHandle, + @Nullable Function trimFactory ) { ClassEntry old = null; - if (this.classHandle != null) { - old = this.classHandle.getRef(); - this.classHandle.close(); + if (this.classHandler != null) { + old = this.classHandler.getHandle().getRef(); + if (closeOldHandle) { + this.classHandler.getHandle().close(); + } } this.setClassHandleImpl(old, handle, trimFactory); @@ -147,7 +150,7 @@ protected void setClassHandleImpl( this.setDisplayMode(DisplayMode.IN_PROGRESS); this.setCursorReference(null); - handle.addListener(new ClassHandleListener() { + this.classHandler = ClassHandler.of(handle, new ClassHandleListener() { @Override public void onMappedSourceChanged(ClassHandle h, Result res) { BaseEditorPanel.this.handleDecompilerResult(res, trimFactory); @@ -167,17 +170,15 @@ public void onInvalidate(ClassHandle h, InvalidationType t) { res -> BaseEditorPanel.this.handleDecompilerResult(res, trimFactory), SwingUtilities::invokeLater ); - - this.classHandle = handle; } public void destroy() { - this.classHandle.close(); + this.classHandler.getHandle().close(); } private void redecompileClass() { - if (this.classHandle != null) { - this.classHandle.invalidate(); + if (this.classHandler != null) { + this.classHandler.getHandle().invalidate(); } } @@ -448,7 +449,7 @@ private void showReferenceImpl(EntryReference, Entry> reference) { List tokens = this.controller.getTokensForReference(this.source, reference); if (tokens.isEmpty()) { // DEBUG - Logger.debug("No tokens found for {} in {}", reference, this.classHandle.getRef()); + Logger.debug("No tokens found for {} in {}", reference, this.classHandler.getHandle().getRef()); } else { this.gui.showTokens(this, tokens); } @@ -502,8 +503,8 @@ protected void navigateToToken(Token token, HighlightPainter highlightPainter) { public void actionPerformed(ActionEvent event) { if (this.counter % 2 == 0) { try { - // final int offsetEnd = token.end - BaseEditorPanel.this.sourceBounds.start(); - this.highlight = BaseEditorPanel.this.editor.getHighlighter().addHighlight(offsetToken.start, offsetToken.end, highlightPainter); + this.highlight = BaseEditorPanel.this.editor.getHighlighter() + .addHighlight(offsetToken.start, offsetToken.end, highlightPainter); } catch (BadLocationException ex) { // don't care } @@ -534,7 +535,7 @@ public DecompiledClassSource getSource() { } public ClassHandle getClassHandle() { - return this.classHandle; + return this.classHandler == null ? null : this.classHandler.getHandle(); } public String getSimpleClassName() { @@ -546,8 +547,8 @@ public String getFullClassName() { } private ClassEntry getDeobfOrObfHandleRef() { - final ClassEntry deobfRef = this.classHandle.getDeobfRef(); - return deobfRef == null ? this.classHandle.getRef() : deobfRef; + final ClassEntry deobfRef = this.classHandler.handle.getDeobfRef(); + return deobfRef == null ? this.classHandler.handle.getRef() : deobfRef; } protected sealed interface SourceBounds { @@ -605,4 +606,28 @@ private enum DisplayMode { SUCCESS, ERRORED, } + + public static final class ClassHandler { + public static ClassHandler of(ClassHandle handle, ClassHandleListener listener) { + handle.addListener(listener); + + return new ClassHandler(handle, listener); + } + + private final ClassHandle handle; + private final ClassHandleListener listener; + + private ClassHandler(ClassHandle handle, ClassHandleListener listener) { + this.handle = handle; + this.listener = listener; + } + + public ClassHandle getHandle() { + return this.handle; + } + + public void removeListener() { + this.handle.removeListener(this.listener); + } + } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java similarity index 98% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java index 8c7b3adce..3f1a081bc 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/TooltipEditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -52,14 +52,14 @@ import static java.util.Comparator.comparingInt; -public class TooltipEditorPanel extends BaseEditorPanel { +public class DeclarationSnippetPanel extends BaseEditorPanel { 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 private static final String METHOD = "method"; private static final String LAMBDA = "lambda"; - public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { + public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopClassHandle) { super(gui); Optional.ofNullable(this.editorScrollPane.getRowHeader()) @@ -79,7 +79,7 @@ public TooltipEditorPanel(Gui gui, Entry target, ClassHandle targetTopClassHa }); this.getEditor().setEditable(false); - this.setClassHandle(targetTopClassHandle, source -> this.createTrimmedBounds(source, target)); + this.setClassHandle(targetTopClassHandle, false, source -> this.createTrimmedBounds(source, target)); } private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target) { @@ -87,7 +87,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry if (targetToken == null) { // This can happen as a result of #252: Issue with lost parameter connection. - // Once #252 is fixed, an error should be logged here. + // TODO once #252 is fixed, an error should be logged here. return null; } 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 a54d408c5..51969e884 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 @@ -4,11 +4,9 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; -import org.quiltmc.enigma.api.class_handle.ClassHandleError; import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; import org.quiltmc.enigma.gui.dialog.EnigmaQuickFindToolBar; @@ -21,10 +19,9 @@ import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; import org.quiltmc.enigma.util.Result; +import org.quiltmc.enigma.gui.event.EditorActionListener; -import java.awt.BorderLayout; import java.awt.Component; -import java.awt.Container; import java.awt.GridBagConstraints; import java.awt.Insets; import java.awt.MouseInfo; @@ -48,12 +45,8 @@ import java.util.function.Function; import java.util.regex.Pattern; import javax.annotation.Nullable; -import javax.swing.Box; -import javax.swing.BoxLayout; import javax.swing.JComponent; -import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JWindow; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.ToolTipManager; @@ -72,7 +65,7 @@ public class EditorPanel extends BaseEditorPanel { private final EditorPopupMenu popupMenu; // DIY tooltip because JToolTip can't be moved or resized - private final JWindow tooltip = new JWindow(); + private final EditorTooltip tooltip = new EditorTooltip(this.gui); @Nullable private Token lastMouseTargetToken; @@ -87,7 +80,7 @@ public class EditorPanel extends BaseEditorPanel { if (!targetToken.equals(this.lastMouseTargetToken)) { this.lastMouseTargetToken = targetToken; - this.updateTooltip(targetEntry); + this.openTooltip(targetEntry); } } else { this.lastMouseTargetToken = targetToken; @@ -106,7 +99,7 @@ public class EditorPanel extends BaseEditorPanel { this.hideTokenTooltipTimer.restart(); if (targetToken.equals(this.lastMouseTargetToken)) { this.tooltip.setVisible(true); - this.updateTooltip(targetEntry); + this.openTooltip(targetEntry); } }); } @@ -215,10 +208,6 @@ public void focusLost(FocusEvent e) { this.hideTokenTooltipTimer.setRepeats(false); this.tooltip.setVisible(false); - this.tooltip.setAlwaysOnTop(true); - this.tooltip.setType(Window.Type.POPUP); - this.tooltip.setLayout(new BorderLayout()); - this.tooltip.setContentPane(new Box(BoxLayout.PAGE_AXIS)); this.tooltip.addMouseListener(new MouseAdapter() { @Override @@ -281,53 +270,15 @@ public void keyTyped(KeyEvent event) { } private void closeTooltip() { - this.tooltip.setVisible(false); + this.tooltip.close(); this.lastMouseTargetToken = null; this.mouseStoppedMovingTimer.stop(); this.showTokenTooltipTimer.stop(); this.hideTokenTooltipTimer.stop(); } - private void updateTooltip(Entry target) { - final Container tooltipContent = this.tooltip.getContentPane(); - tooltipContent.removeAll(); - - final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); - - // TODO show parent name instead - tooltipContent.add(new JLabel(deobfTarget.getFullName())); - if (target instanceof ParentedEntry parentedTarget) { - final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); - - final ClassHandle targetTopClassHandle = targetTopClass.equals(this.getSource().getEntry()) - ? this.classHandle.copy() - : this.gui.getController().getClassHandleProvider().openClass(targetTopClass); - - if (targetTopClassHandle != null) { - final TooltipEditorPanel tooltipEditor = new TooltipEditorPanel(this.gui, target, targetTopClassHandle); - - this.classHandle.addListener(new ClassHandleListener() { - @Override - public void onMappedSourceChanged(ClassHandle h, Result res) { - EditorPanel.this.closeTooltip(); - tooltipEditor.destroy(); - EditorPanel.this.classHandle.removeListener(this); - } - }); - - tooltipEditor.addSourceSetListener(source -> this.tooltip.pack()); - - tooltipContent.add(tooltipEditor.ui); - } else { - tooltipContent.add(new JLabel("No source available")); - } - } - - // TODO offset from cursor slightly + ensure on-screen - this.tooltip.setLocation(MouseInfo.getPointerInfo().getLocation()); - - // TODO clamp size - this.tooltip.pack(); + private void openTooltip(Entry target) { + this.tooltip.open(target); } /** @@ -413,18 +364,26 @@ public void onRename(boolean isNewMapping) { @Override protected void initEditorPane(JPanel editorPane) { - final GridBagConstraints constraints = new GridBagConstraints(); - constraints.gridx = 0; - constraints.gridy = 0; - constraints.weightx = 1.0; - constraints.weighty = 1.0; - constraints.anchor = GridBagConstraints.FIRST_LINE_END; - constraints.insets = new Insets(32, 32, 32, 32); - constraints.ipadx = 16; - constraints.ipady = 16; - editorPane.add(this.navigatorPanel, constraints); + final GridBagConstraints navigatorConstraints = new GridBagConstraints(); + navigatorConstraints.gridx = 0; + navigatorConstraints.gridy = 0; + navigatorConstraints.weightx = 1.0; + navigatorConstraints.weighty = 1.0; + navigatorConstraints.anchor = GridBagConstraints.FIRST_LINE_END; + navigatorConstraints.insets = new Insets(32, 32, 32, 32); + navigatorConstraints.ipadx = 16; + navigatorConstraints.ipady = 16; + editorPane.add(this.navigatorPanel, navigatorConstraints); super.initEditorPane(editorPane); + + final var quickFindConstraints = new GridBagConstraints(); + quickFindConstraints.gridx = 0; + quickFindConstraints.weightx = 1.0; + quickFindConstraints.weighty = 0; + quickFindConstraints.anchor = GridBagConstraints.PAGE_END; + quickFindConstraints.fill = GridBagConstraints.HORIZONTAL; + editorPane.add(this.quickFindToolBar, quickFindConstraints); } @Nullable @@ -511,8 +470,8 @@ public void retranslateUi() { public void reloadKeyBinds() { putKeyBindAction(KeyBinds.EDITOR_RELOAD_CLASS, this.editor, e -> { - if (this.classHandle != null) { - this.classHandle.invalidate(); + if (this.classHandler != null) { + this.classHandler.getHandle().invalidate(); } }); putKeyBindAction(KeyBinds.EDITOR_ZOOM_IN, this.editor, e -> this.offsetEditorZoom(2)); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java new file mode 100644 index 000000000..0a90eb424 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -0,0 +1,91 @@ +package org.quiltmc.enigma.gui.panel; + +import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; +import org.quiltmc.enigma.gui.Gui; + +import javax.annotation.Nullable; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JWindow; +import java.awt.BorderLayout; +import java.awt.MouseInfo; +import java.awt.Window; + +public class EditorTooltip extends JWindow { + private final Gui gui; + private final Box content; + + @Nullable + private DeclarationSnippetPanel declarationSnippet; + + public EditorTooltip(Gui gui) { + super(); + + this.gui = gui; + this.content = new Box(BoxLayout.PAGE_AXIS); + + this.setAlwaysOnTop(true); + this.setType(Window.Type.POPUP); + this.setLayout(new BorderLayout()); + this.setContentPane(this.content); + } + + /** + * Opens this tooltip and populates it with information about the passed {@code target}. + * + * @param target the entry whose information will be displayed + */ + public void open(Entry target) { + this.content.removeAll(); + + final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); + + // TODO show parent name instead + this.content.add(new JLabel(deobfTarget.getFullName())); + + if (this.declarationSnippet != null) { + this.declarationSnippet.classHandler.removeListener(); + this.declarationSnippet = null; + } + + if (target instanceof ParentedEntry parentedTarget) { + final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); + + final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() + .openClass(targetTopClass); + + if (targetTopClassHandle != null) { + this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); + + // TODO create method that packs and adjusts position as necessary + this.declarationSnippet.addSourceSetListener(source -> this.pack()); + + this.content.add(this.declarationSnippet.ui); + } else { + this.content.add(new JLabel("No source available")); + } + } + + // TODO offset from cursor slightly + ensure on-screen + this.setLocation(MouseInfo.getPointerInfo().getLocation()); + + // TODO clamp size + // TODO create method that packs and adjusts position as necessary + this.pack(); + + this.setVisible(true); + } + + public void close() { + this.setVisible(false); + this.content.removeAll(); + + if (this.declarationSnippet != null) { + this.declarationSnippet.classHandler.removeListener(); + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java new file mode 100644 index 000000000..7773109ce --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java @@ -0,0 +1,15 @@ +package org.quiltmc.enigma.input.tooltip; + +public class Constructors { + public Constructors(String outerArg) { + new Constructors("outer arg"); + + new Methods() { + @Override + void abstraction() { + // tests #252 + System.out.println(outerArg); + } + }; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java new file mode 100644 index 000000000..8b89275ab --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java @@ -0,0 +1,23 @@ +package org.quiltmc.enigma.input.tooltip; + +public enum Enums { + FIRST, SECOND(2), + THIRD(3), FOURTH(4); + + static { + System.out.println(FIRST); + System.out.println(SECOND); + System.out.println(THIRD); + System.out.println(FOURTH); + } + + final int index; + + Enums() { + this(1); + } + + Enums(int index) { + this.index = index; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java new file mode 100644 index 000000000..d818046d6 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java @@ -0,0 +1,21 @@ +package org.quiltmc.enigma.input.tooltip; + +public class Fields { + static final String STATIC_FIELD = "static field"; + + final int initialized = 0; + + Object uninitialized; + + Runnable multiLineField = () -> { + System.out.println("hello"); + System.out.println("good bye"); + }; + + { + System.out.println(STATIC_FIELD); + System.out.println(this.initialized); + System.out.println(this.uninitialized); + System.out.println(this.multiLineField); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java new file mode 100644 index 000000000..24f00e469 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java @@ -0,0 +1,34 @@ +package org.quiltmc.enigma.input.tooltip; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class Lambdas { + private List children = new ArrayList<>(); + + public Stream stream() { + return Stream.concat(Stream.of(this), this.children.stream().flatMap(Lambdas::stream)); + } + + public Stream crazyStream() { + return get(() -> Stream.concat(Stream.of(this), this.children.stream().flatMap(Lambdas::crazyStream))); + } + + public static Stream get(Supplier> provider) { + return provider.get(); + } + + public static void foo() { + consume(s -> consume(s1 -> { + System.out.println(s); + consume(System.out::println); + })); + } + + public static void consume(Consumer consumer) { + consumer.accept("Foo"); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java new file mode 100644 index 000000000..7dedc397f --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java @@ -0,0 +1,18 @@ +package org.quiltmc.enigma.input.tooltip; + +public abstract class Methods { + private void parameterized(int i, Boolean z, Methods methods) { } + + @Override + public boolean equals(Object o) { + return false; + } + + abstract void abstraction(); + + static void referencer(Methods methods) { + methods.parameterized(0, null, null); + methods.equals(null); + methods.abstraction(); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java new file mode 100644 index 000000000..37a6ce35b --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java @@ -0,0 +1,27 @@ +package org.quiltmc.enigma.input.tooltip; + +public record Records() { + public record WithStaticField(Boolean truth) { + static final String NON_COMPONENT = "not a component"; + + @Override + public Boolean truth() { + return new WithStaticField(true).truth; + } + } + + public record Implementing(int component) implements Runnable { + @Override + public void run() { + System.out.println(new Implementing().component()); + } + + public Implementing() { + this(0); + } + + public Implementing(int component) { + this.component = component + 1; + } + } +} From 135f5f9e596a30985bc0bf4e3743201c907af6df Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 4 Oct 2025 14:45:50 -0700 Subject: [PATCH 030/109] restore EditorPanel popupMenu functionality --- .../src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 51969e884..57c19b827 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 @@ -61,7 +61,6 @@ public class EditorPanel extends BaseEditorPanel { private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); - // TODO restore right click functionality! private final EditorPopupMenu popupMenu; // DIY tooltip because JToolTip can't be moved or resized @@ -142,6 +141,7 @@ public void focusLost(FocusEvent e) { this.quickFindToolBar.setVisible(false); // init editor popup menu this.popupMenu = new EditorPopupMenu(this, gui); + this.editor.setComponentPopupMenu(this.popupMenu.getUi()); // global listener so tooltip hides even if clicking outside editor Toolkit.getDefaultToolkit().addAWTEventListener( From 2247f1c52f40aa7538bdb602efcf5896a9eaca65 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 4 Oct 2025 15:31:30 -0700 Subject: [PATCH 031/109] improve caret pos logic in BaseEditorPanel::setSource --- .../enigma/gui/panel/BaseEditorPanel.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 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 e93ca2baa..24328848f 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 @@ -306,7 +306,10 @@ protected void setSource( @Nullable Function trimFactory ) { this.setDisplayMode(DisplayMode.SUCCESS); - if (source == null) return; + if (source == null) { + return; + } + try { this.settingSource = true; @@ -334,24 +337,22 @@ protected void setSource( } this.source = source; - this.editor.setText(source.toString()); + this.editor.getHighlighter().removeAllHighlights(); + + this.editor.setText(this.source.toString()); final TrimmedBounds trimmedBounds = trimFactory == null ? null : trimFactory.apply(this.source); if (trimmedBounds == null) { this.sourceBounds = new DefaultBounds(); } else { this.sourceBounds = trimmedBounds; - this.trimSource(trimmedBounds); + newCaretPos = this.trimSource(trimmedBounds, newCaretPos); } - this.editor.getHighlighter().removeAllHighlights(); - this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); - if (this.source != null) { - this.editor.setCaretPosition(newCaretPos); + this.editor.setCaretPosition(newCaretPos); - for (final Consumer listener : this.sourceSetListeners) { - listener.accept(this.source); - } + for (final Consumer listener : this.sourceSetListeners) { + listener.accept(this.source); } this.setCursorReference(this.getReference(this.getToken(this.editor.getCaretPosition()))); @@ -366,15 +367,11 @@ protected void setSource( } // TODO strip indent - private void trimSource(TrimmedBounds bounds) { - final long oldCaretPos = this.editor.getCaretPosition(); - + private int trimSource(TrimmedBounds bounds, int originalCaretPos) { final String sourceString = this.source.toString(); this.sourceBounds = new TrimmedBounds(bounds.start(), Math.min(bounds.end(), sourceString.length())); this.editor.setText(sourceString.substring(this.sourceBounds.start(), this.sourceBounds.end())); - this.editor.setCaretPosition( - Utils.clamp(oldCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()) - ); + return Utils.clamp((long) originalCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()); } protected void addSourceSetListener(Consumer listener) { From 45e5a302020be3b2738102388e00b090f2126284 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 4 Oct 2025 18:43:13 -0700 Subject: [PATCH 032/109] replace source with message if the declaration token could be found --- .../enigma/gui/panel/BaseEditorPanel.java | 23 +++++++++++-------- .../gui/panel/DeclarationSnippetPanel.java | 10 ++++---- 2 files changed, 20 insertions(+), 13 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 24328848f..43a99e8a9 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 @@ -460,11 +460,14 @@ public void navigateToToken(Token token) { this.navigateToToken(token, SelectionHighlightPainter.INSTANCE); } - protected void navigateToToken(Token token, HighlightPainter highlightPainter) { + /** + * @return {@code true} if navigation was successful, or {@code false} otherwise + */ + protected boolean navigateToToken(@Nullable Token token, HighlightPainter highlightPainter) { final Token offsetToken = this.sourceBounds.offsetOf(token).orElse(null); if (offsetToken == null) { // token out of bounds - return; + return false; } // set the caret position to the token @@ -476,7 +479,7 @@ protected void navigateToToken(Token token, HighlightPainter highlightPainter) { Rectangle2D start = this.editor.modelToView2D(offsetToken.start); Rectangle2D end = this.editor.modelToView2D(offsetToken.start); if (start == null || end == null) { - return; + return false; } Rectangle show = new Rectangle(); @@ -487,7 +490,7 @@ protected void navigateToToken(Token token, HighlightPainter highlightPainter) { if (!this.settingSource) { throw new RuntimeException(ex); } else { - return; + return false; } } @@ -517,6 +520,8 @@ public void actionPerformed(ActionEvent event) { }); timer.start(); + + return true; } public JPanel getUi() { @@ -561,10 +566,10 @@ default boolean contains(Token token) { return this.contains(token.start) && this.contains(token.end); } - default Optional offsetOf(Token token) { - return this.contains(token) - ? Optional.of(new Token(token.start - this.start(), token.end - this.start(), token.text)) - : Optional.empty(); + default Optional offsetOf(@Nullable Token token) { + return token == null || !this.contains(token) + ? Optional.empty() + : Optional.of(new Token(token.start - this.start(), token.end - this.start(), token.text)); } } @@ -593,7 +598,7 @@ public int end() { @Override public Optional offsetOf(Token token) { - return this.end() < token.end ? Optional.empty() : Optional.of(token); + return token == null || this.end() < token.end ? Optional.empty() : Optional.of(token); } } 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 3f1a081bc..edfa6eab8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java @@ -72,9 +72,11 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.addSourceSetListener(source -> { final Token declarationToken = source.getIndex().getDeclarationToken(target); - if (declarationToken != null) { - // TODO create custom highlighter - this.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE); + // TODO create custom highlighter + if (!this.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE)) { + // the source isn't very useful if it couldn't be trimmed and the declaration couldn't be navigated to + // set this text so it doesn't waste space or cause confusion + this.editor.setText("// Unable to locate declaration"); } }); @@ -87,7 +89,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry if (targetToken == null) { // This can happen as a result of #252: Issue with lost parameter connection. - // TODO once #252 is fixed, an error should be logged here. + // This can also happen when the token is from a library. return null; } From 1492eed6c95d0cf743e1d70401a1df30fd677e71 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 08:46:40 -0700 Subject: [PATCH 033/109] implement source snippet unindenting --- .../enigma/gui/panel/BaseEditorPanel.java | 162 ++++++++++++++---- .../gui/panel/DeclarationSnippetPanel.java | 98 +++++------ .../quiltmc/enigma/gui/panel/EditorPanel.java | 4 +- .../org/quiltmc/enigma/util/LineIndexer.java | 2 +- .../enigma/input/tooltip/Constructors.java | 2 + 5 files changed, 187 insertions(+), 81 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 43a99e8a9..08e4a77a6 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 @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.panel; +import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.class_handle.ClassHandleError; @@ -21,6 +22,7 @@ import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.ScaleUtil; import org.quiltmc.enigma.util.I18n; +import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.enigma.util.Utils; import org.tinylog.Logger; @@ -56,6 +58,7 @@ import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; +import java.util.regex.Matcher; public class BaseEditorPanel { protected final JPanel ui = new JPanel(); @@ -130,7 +133,7 @@ public void setClassHandle(ClassHandle handle) { protected void setClassHandle( ClassHandle handle, boolean closeOldHandle, - @Nullable Function trimFactory + @Nullable Function snippetFactory ) { ClassEntry old = null; if (this.classHandler != null) { @@ -140,12 +143,12 @@ protected void setClassHandle( } } - this.setClassHandleImpl(old, handle, trimFactory); + this.setClassHandleImpl(old, handle, snippetFactory); } protected void setClassHandleImpl( ClassEntry old, ClassHandle handle, - @Nullable Function trimFactory + @Nullable Function snippetFactory ) { this.setDisplayMode(DisplayMode.IN_PROGRESS); this.setCursorReference(null); @@ -153,7 +156,7 @@ protected void setClassHandleImpl( this.classHandler = ClassHandler.of(handle, new ClassHandleListener() { @Override public void onMappedSourceChanged(ClassHandle h, Result res) { - BaseEditorPanel.this.handleDecompilerResult(res, trimFactory); + BaseEditorPanel.this.handleDecompilerResult(res, snippetFactory); } @Override @@ -167,7 +170,7 @@ public void onInvalidate(ClassHandle h, InvalidationType t) { }); handle.getSource().thenAcceptAsync( - res -> BaseEditorPanel.this.handleDecompilerResult(res, trimFactory), + res -> BaseEditorPanel.this.handleDecompilerResult(res, snippetFactory), SwingUtilities::invokeLater ); } @@ -184,11 +187,11 @@ private void redecompileClass() { private void handleDecompilerResult( Result res, - @Nullable Function trimFactory + @Nullable Function snippetFactory ) { SwingUtilities.invokeLater(() -> { if (res.isOk()) { - this.setSource(res.unwrap(), trimFactory); + this.setSource(res.unwrap(), snippetFactory); } else { this.displayError(res.unwrapErr()); } @@ -303,7 +306,7 @@ public EntryReference, Entry> getReference(Token token) { protected void setSource( DecompiledClassSource source, - @Nullable Function trimFactory + @Nullable Function snippetFactor ) { this.setDisplayMode(DisplayMode.SUCCESS); if (source == null) { @@ -340,12 +343,11 @@ protected void setSource( this.editor.getHighlighter().removeAllHighlights(); this.editor.setText(this.source.toString()); - final TrimmedBounds trimmedBounds = trimFactory == null ? null : trimFactory.apply(this.source); - if (trimmedBounds == null) { + final Snippet snippet = snippetFactor == null ? null : snippetFactor.apply(this.source); + if (snippet == null) { this.sourceBounds = new DefaultBounds(); } else { - this.sourceBounds = trimmedBounds; - newCaretPos = this.trimSource(trimmedBounds, newCaretPos); + newCaretPos = this.trimSource(snippet, newCaretPos); } this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); @@ -366,11 +368,16 @@ protected void setSource( } } - // TODO strip indent - private int trimSource(TrimmedBounds bounds, int originalCaretPos) { + private int trimSource(Snippet snippet, int originalCaretPos) { final String sourceString = this.source.toString(); - this.sourceBounds = new TrimmedBounds(bounds.start(), Math.min(bounds.end(), sourceString.length())); - this.editor.setText(sourceString.substring(this.sourceBounds.start(), this.sourceBounds.end())); + + final int end = Math.min(sourceString.length(), snippet.end); + + final Unindented unindented = Unindented.of(sourceString, snippet.start, end); + + this.sourceBounds = new TrimmedBounds(snippet.start, end, unindented.indentOffsets); + this.editor.setText(unindented.snippet); + return Utils.clamp((long) originalCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()); } @@ -553,7 +560,103 @@ private ClassEntry getDeobfOrObfHandleRef() { return deobfRef == null ? this.classHandler.handle.getRef() : deobfRef; } - protected sealed interface SourceBounds { + public record Snippet(int start, int end) { + public Snippet { + if (start < 0) { + throw new IllegalArgumentException("start must not be negative!"); + } + + if (start > end) { + throw new IllegalArgumentException("start must not be greater than end!"); + } + } + } + + private record LineOffset(int sourceStart, int sourceEnd, int offset) { + boolean contains(Token token) { + return this.sourceStart <= token.start && this.sourceEnd >= token.end; + } + } + + /** + * An unindented snippet of source code along with the data required + * to map tokens from their original position in the source code. + * + * @param snippet the unindented code snippet + * @param indentOffsets the cumulative offset resulting from stripped indents for each line, if any + */ + private record Unindented(String snippet, ImmutableList indentOffsets) { + static Unindented ofNoIndent(String source, int start, int end) { + return new Unindented(source.substring(start, end), ImmutableList.of()); + } + + /** + * Gets the unindented snippet of the passed {@code source} between the passed {@code start} and {@code end}. + * + *

The amount of indent is determined by looking for spaces/tabs before the passed {@code start}. + * If the first character before {@code start} is a tab, {@code source} is considered to be tab-indented, + * likewise for a space. + * + *

If any line is less indented than the first line, the whole snippet is considered to have no + * indent; that doesn't fit expected formatting. + */ + static Unindented of(String source, int start, int end) { + if (start == 0) { + return ofNoIndent(source, start, end); + } else { + final char indentChar = source.charAt(start - 1); + if (indentChar == '\t' || indentChar == ' ') { + final int firstLineIndent; + { + int currentIndent = 1; + while (source.charAt(start - currentIndent - 1) == indentChar) { + currentIndent++; + } + + firstLineIndent = currentIndent; + } + + final Matcher lineMatcher = LineIndexer.LINE_END.matcher(source); + final var snippet = new StringBuilder(); + final ImmutableList.Builder indents = ImmutableList.builder(); + + int prevIndentEnd = start; + int prevSourceLineStart = start; + int indentOffset = 0; + while (lineMatcher.find(prevIndentEnd)) { + final int currentIndentEnd; + if (lineMatcher.end() < end) { + currentIndentEnd = lineMatcher.end() + firstLineIndent; + for (int i = lineMatcher.end(); i < end && i < currentIndentEnd; i++) { + if (source.charAt(i) != indentChar) { + // if any line is indented less than the first, no indent + return ofNoIndent(source, start, end); + } + } + } else { + snippet.append(source, prevIndentEnd, end); + indents.add(new LineOffset(prevSourceLineStart, end, indentOffset)); + + break; + } + + snippet.append(source, prevIndentEnd, lineMatcher.end()); + indents.add(new LineOffset(prevSourceLineStart, lineMatcher.end(), indentOffset)); + + prevIndentEnd = currentIndentEnd; + prevSourceLineStart = currentIndentEnd - firstLineIndent; + indentOffset += firstLineIndent; + } + + return new Unindented(snippet.toString(), indents.build()); + } else { + return ofNoIndent(source, start, end); + } + } + } + } + + private sealed interface SourceBounds { int start(); int end(); @@ -566,21 +669,22 @@ default boolean contains(Token token) { return this.contains(token.start) && this.contains(token.end); } - default Optional offsetOf(@Nullable Token token) { - return token == null || !this.contains(token) - ? Optional.empty() - : Optional.of(new Token(token.start - this.start(), token.end - this.start(), token.text)); - } + Optional offsetOf(@Nullable Token token); } - protected record TrimmedBounds(int start, int end) implements SourceBounds { - public TrimmedBounds { - if (start < 0) { - throw new IllegalArgumentException("start must not be negative!"); - } + private record TrimmedBounds(int start, int end, ImmutableList indentOffsets) implements SourceBounds { + @Override + public Optional offsetOf(@Nullable Token token) { + if (token == null || !this.contains(token)) { + return Optional.empty(); + } else { + final int offset = this.start() + this.indentOffsets().stream() + .filter(lineOffset -> lineOffset.contains(token)) + .findFirst() + .map(LineOffset::offset) + .orElse(0); - if (start > end) { - throw new IllegalArgumentException("start must not be greater than end!"); + return Optional.of(token.move(-offset)); } } } 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 edfa6eab8..aa59043bf 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 @@ -81,10 +81,10 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl }); this.getEditor().setEditable(false); - this.setClassHandle(targetTopClassHandle, false, source -> this.createTrimmedBounds(source, target)); + this.setClassHandle(targetTopClassHandle, false, source -> this.createSnippet(source, target)); } - private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry target) { + private Snippet createSnippet(DecompiledClassSource source, Entry target) { final Token targetToken = source.getIndex().getDeclarationToken(target); if (targetToken == null) { @@ -93,15 +93,15 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return null; } - final Result bounds; + final Result snippet; if (target instanceof ClassEntry targetClass) { - bounds = this.findClassBounds(source, targetToken, targetClass); + snippet = this.findClassSnippet(source, targetToken, targetClass); } else if (target instanceof MethodEntry targetMethod) { - bounds = this.findMethodBounds(source, targetToken, targetMethod); + snippet = this.findMethodSnippet(source, targetToken, targetMethod); } else if (target instanceof FieldEntry targetField) { - bounds = this.findFieldBounds(source, targetToken, targetField); + snippet = this.findFieldSnippet(source, targetToken, targetField); } else if (target instanceof LocalVariableEntry targetLocal) { - bounds = this.getVariableBounds(source, targetToken, targetLocal); + snippet = this.getVariableSnippet(source, targetToken, targetLocal); } else { // this should never be reached Logger.error( @@ -111,7 +111,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry return null; } - return bounds.unwrapOrElse(error -> { + return snippet.unwrapOrElse(error -> { Logger.error( "Error finding declaration of '{}' for tooltip: {}", this.getFullDeobfuscatedName(target), @@ -121,7 +121,7 @@ private TrimmedBounds createTrimmedBounds(DecompiledClassSource source, Entry }); } - private Result getVariableBounds( + private Result getVariableSnippet( DecompiledClassSource source, Token target, LocalVariableEntry targetEntry ) { final MethodEntry parent = targetEntry.getParent(); @@ -134,14 +134,14 @@ private Result getVariableBounds( return this.findLambdaVariable(source, target, targetEntry, parent); } else { if (targetEntry.isArgument()) { - return this.findMethodBounds(source, parentToken, parent); + return this.findMethodSnippet(source, parentToken, parent); } else { - return this.findLocalBounds(source, parentToken, target); + return this.findLocalSnippet(source, parentToken, target); } } } - private Result findLambdaVariable( + private Result findLambdaVariable( DecompiledClassSource source, Token target, LocalVariableEntry targetEntry, MethodEntry parent ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); @@ -161,19 +161,19 @@ private Result findLambdaVariable( .map(parentBegin -> parentLambda .getBody() .getRange() - .map(bodyRange -> toTrimmedBounds(lineIndexer, parentBegin, bodyRange.begin)) - .>map(Result::ok) + .map(bodyRange -> toSnippet(lineIndexer, parentBegin, bodyRange.begin)) + .>map(Result::ok) .orElseGet(() -> Result.err("no parent lambda body range!"))) .orElseGet(() -> Result.err("no parent lambda begin")); } else { final Statement parentBody = parentLambda.getBody(); return parentBody.toBlockStmt() - .map(parentBlock -> findLocalBounds(target, parentBlock, lineIndexer, LAMBDA)) + .map(parentBlock -> findLocalSnippet(target, parentBlock, lineIndexer, LAMBDA)) .orElseGet(() -> parentBody.asExpressionStmt() .getExpression() .toVariableDeclarationExpr() .map(variableExpr -> - findVariableExpressionBounds(target, variableExpr, lineIndexer) + findVariableExpressionSnippet(target, variableExpr, lineIndexer) ) .orElseGet(() -> Result.err("local declared in non-declaration expression!")) ); @@ -181,7 +181,7 @@ private Result findLambdaVariable( }) .orElseGet(() -> Result.err("failed to find local's parent lambda!"))); } else { - return Result.err("no parent token for non-synthetic parent!"); + return Result.err("no parent token for non-synthetic parent!"); } }) .orElseGet(() -> Result.err("no parent definition!")); @@ -193,17 +193,17 @@ private String getFullDeobfuscatedName(Entry entry) { .getFullName(); } - private Result findClassBounds( + 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 -> findTypeDeclarationBounds(declaration, lineIndexer)); + .andThen(declaration -> findTypeDeclarationSnippet(declaration, lineIndexer)); }); } - private static Result findTypeDeclarationBounds( + private static Result findTypeDeclarationSnippet( TypeDeclaration declaration, LineIndexer lineIndexer ) { return declaration @@ -212,10 +212,10 @@ private static Result findTypeDeclarationBounds( .map(openCurlyBrace -> openCurlyBrace .getRange() .map(openCurlyRange -> openCurlyRange.begin) - .map(openCurlyPos -> toTrimmedBounds( + .map(openCurlyPos -> toSnippet( lineIndexer, declaration.getBegin().orElseThrow(), openCurlyPos )) - .>map(Result::ok) + .>map(Result::ok) .orElseGet(() -> Result.err("no class open curly brace range!"))) .orElseGet(() -> Result.err("no class open curly brace!")) ) @@ -243,7 +243,7 @@ private Result>, String> getNodeType(ClassEnt } // TODO test record component getters - private Result findMethodBounds( + private Result findMethodSnippet( DecompiledClassSource source, Token target, MethodEntry targetEntry ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); @@ -266,14 +266,14 @@ private Result findMethodBounds( .getRange() .>map(Result::ok) .orElseGet(() -> Result.err("no method body range!")) - .map(bodyRange -> toTrimmedBounds(lineIndexer, range.begin, bodyRange.begin)) + .map(bodyRange -> toSnippet(lineIndexer, range.begin, bodyRange.begin)) ) // no body: abstract - .orElseGet(() -> Result.ok(toTrimmedBounds(lineIndexer, range))); + .orElseGet(() -> Result.ok(toSnippet(lineIndexer, range))); }); } - private Result findFieldBounds( + private Result findFieldSnippet( DecompiledClassSource source, Token target, FieldEntry targetEntry ) { final EntryIndex entryIndex = this.gui.getController().getProject().getJarIndex().getIndex(EntryIndex.class); @@ -281,12 +281,12 @@ private Result findFieldBounds( return Optional.ofNullable(entryIndex.getDefinition(targetEntry)) .map(targetDef -> { if (targetDef.getAccess().isEnum()) { - return findEnumConstantBounds(source, target); + return findEnumConstantSnippet(source, target); } else { return Optional.ofNullable(entryIndex.getDefinition(targetDef.getParent())) .map(parent -> parent.isRecord() && !targetDef.getAccess().isStatic() ? this.findComponentParent(source, parent) - : findRegularFieldBounds(source, target) + : findRegularFieldSnippet(source, target) ) .orElseGet(() -> Result.err("no field parent definition!")); } @@ -294,7 +294,7 @@ private Result findFieldBounds( .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - private Result findComponentParent(DecompiledClassSource source, ClassDefEntry parent) { + private Result findComponentParent(DecompiledClassSource source, ClassDefEntry parent) { final Token parentToken = source.getIndex().getDeclarationToken(parent); final LineIndexer lineIndexer = new LineIndexer(source.toString()); @@ -309,12 +309,12 @@ private Result findComponentParent(DecompiledClassSource .map(implToken -> implToken .getRange() .map(implRange -> implRange.begin.right(-1)) - .map(beforeImpl -> toTrimmedBounds( + .map(beforeImpl -> toSnippet( lineIndexer, parentDeclaration.getBegin().orElseThrow(), beforeImpl )) - .>map(Result::ok) + .>map(Result::ok) .orElseGet(() -> Result.err("no parent record implements token range!")) ) .orElseGet(() -> Result.err("record implements types but has no implements token!")) @@ -322,17 +322,17 @@ private Result findComponentParent(DecompiledClassSource .orElseGet(() -> Result.err("no parent record token range!")) ) // no implemented types - .orElseGet(() -> findTypeDeclarationBounds(parentDeclaration, lineIndexer)) + .orElseGet(() -> findTypeDeclarationSnippet(parentDeclaration, lineIndexer)) ); } - private static Result findEnumConstantBounds(DecompiledClassSource source, Token target) { + 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(toTrimmedBounds(lineIndexer, declaration.getRange().orElseThrow()))); + .andThen(declaration -> Result.ok(toSnippet(lineIndexer, declaration.getRange().orElseThrow()))); } - private static Result findRegularFieldBounds(DecompiledClassSource source, Token target) { + 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 .getTokenRange() @@ -341,14 +341,14 @@ private static Result findRegularFieldBounds(DecompiledCl return declaration.getVariables().stream() .filter(variable -> rangeContains(lineIndexer, variable, target)) .findFirst() - .map(variable -> toDeclaratorBounds(range, variable, lineIndexer)) + .map(variable -> toDeclaratorSnippet(range, variable, lineIndexer)) .orElseGet(() -> Result.err("no matching field declarator!")); }) .orElseGet(() -> Result.err(NO_TOKEN_RANGE)) ); } - private Result findLocalBounds( + private Result findLocalSnippet( DecompiledClassSource source, Token parentToken, Token targetToken ) { final LineIndexer lineIndexer = new LineIndexer(source.toString()); @@ -358,11 +358,11 @@ private Result findLocalBounds( .getBody() .>map(Result::ok) .orElseGet(() -> Result.err("no method body!")) - .andThen(parentBody -> findLocalBounds(targetToken, parentBody, lineIndexer, METHOD)) + .andThen(parentBody -> findLocalSnippet(targetToken, parentBody, lineIndexer, METHOD)) ); } - private static Result findLocalBounds( + private static Result findLocalSnippet( Token target, BlockStmt parentBody, LineIndexer lineIndexer, String parentType ) { return parentBody @@ -375,11 +375,11 @@ private static Result findLocalBounds( .flatMap(Optional::stream) .filter(variableExpr -> rangeContains(lineIndexer, variableExpr, target)) .max(depthComparatorOf(lineIndexer)) - .map(variableExpr -> findVariableExpressionBounds(target, variableExpr, lineIndexer)) + .map(variableExpr -> findVariableExpressionSnippet(target, variableExpr, lineIndexer)) .orElseGet(() -> Result.err("failed to find local in parent %s!".formatted(parentType))); } - private static Result findVariableExpressionBounds( + private static Result findVariableExpressionSnippet( Token targetToken, VariableDeclarationExpr variableExpr, LineIndexer lineIndexer ) { return variableExpr @@ -388,7 +388,7 @@ private static Result findVariableExpressionBounds( .filter(variable -> rangeContains(lineIndexer, variable, targetToken)) .findFirst() .map(targetVariable -> - toDeclaratorBounds(variableExpr.getRange().orElseThrow(), targetVariable, lineIndexer) + toDeclaratorSnippet(variableExpr.getRange().orElseThrow(), targetVariable, lineIndexer) ) .orElseGet(() -> Result.err("failed to find local in variable expression!")); } @@ -426,11 +426,11 @@ private static Result parse(DecompiledClassSource sourc .orElseGet(() -> Result.err("failed to parse source: " + parseResult.getProblems())); } - private static Result toDeclaratorBounds( + private static Result toDeclaratorSnippet( Range outerRange, VariableDeclarator variable, LineIndexer lineIndexer ) { if (outerRange.begin.line == outerRange.end.line) { - return Result.ok(toTrimmedBounds(lineIndexer, outerRange)); + return Result.ok(toSnippet(lineIndexer, outerRange)); } else { return variable .getTokenRange() @@ -442,7 +442,7 @@ private static Result toDeclaratorBounds( .orElse(Result.ok(outerRange.end)) ) .orElseGet(() -> Result.err("no variable token range!")) - .map(end -> toTrimmedBounds(lineIndexer, outerRange.begin, end)); + .map(end -> toSnippet(lineIndexer, outerRange.begin, end)); } } @@ -472,17 +472,17 @@ private static > boolean rangeContains(LineIndexer li } } - private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Range range) { - return toTrimmedBounds(lineIndexer, range.begin, range.end); + private static Snippet toSnippet(LineIndexer lineIndexer, Range range) { + return toSnippet(lineIndexer, range.begin, range.end); } - private static TrimmedBounds toTrimmedBounds(LineIndexer lineIndexer, Position startPos, Position endPos) { + private static Snippet toSnippet(LineIndexer lineIndexer, Position startPos, Position endPos) { final int start = lineIndexer.getIndex(startPos); int end = lineIndexer.getIndex(endPos); while (Character.isWhitespace(lineIndexer.getString().charAt(end))) { end--; } - return new TrimmedBounds(start, end + 1); + return new Snippet(start, end + 1); } } 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 57c19b827..d6a92e5e3 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 @@ -405,9 +405,9 @@ public NavigatorPanel getNavigatorPanel() { @Override protected void setClassHandleImpl( ClassEntry old, ClassHandle handle, - @Nullable Function trimFactory + @Nullable Function snippetFactory ) { - super.setClassHandleImpl(old, handle, trimFactory); + super.setClassHandleImpl(old, handle, snippetFactory); handle.addListener(new ClassHandleListener() { @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java index 700270f74..40ce61f1a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -8,7 +8,7 @@ import java.util.regex.Pattern; public class LineIndexer { - private static final Pattern LINE_END = Pattern.compile("\\r\\n?|\\n"); + public static final Pattern LINE_END = Pattern.compile("\\r\\n?|\\n"); private final List indexesByLine = new ArrayList<>(); private final Matcher lineEndMatcher; diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java index 7773109ce..f9372cbf3 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java @@ -5,6 +5,8 @@ public Constructors(String outerArg) { new Constructors("outer arg"); new Methods() { + // multiline declaration to test unindenting + @Deprecated @Override void abstraction() { // tests #252 From b89575133849db40e41aa5e2ad476f038f503712 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 10:20:38 -0700 Subject: [PATCH 034/109] replace entry simple name label with formatted parent name label --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 7 -- .../enigma/gui/panel/EditorTooltip.java | 82 +++++++++++++++---- .../representation/entry/ClassEntry.java | 2 + 3 files changed, 66 insertions(+), 25 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 d6a92e5e3..1fb5a1d2f 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 @@ -43,7 +43,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.swing.JComponent; import javax.swing.JPanel; @@ -57,7 +56,6 @@ public class EditorPanel extends BaseEditorPanel { private static final int MOUSE_STOPPED_MOVING_DELAY = 100; - private static final Pattern CLASS_PUNCTUATION = Pattern.compile("[/\\$]"); private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); @@ -350,11 +348,6 @@ private static void consumeMousePositionIn( } } - // TODO use for tooltip parent label/link - private static String getFullDotName(Entry entry) { - return CLASS_PUNCTUATION.matcher(entry.getFullName()).replaceAll("."); - } - public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 0a90eb424..5a74575cb 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -1,9 +1,11 @@ package org.quiltmc.enigma.gui.panel; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.translation.representation.AccessFlags; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; import org.quiltmc.enigma.gui.Gui; import javax.annotation.Nullable; @@ -14,6 +16,7 @@ import java.awt.BorderLayout; import java.awt.MouseInfo; import java.awt.Window; +import java.util.Optional; public class EditorTooltip extends JWindow { private final Gui gui; @@ -42,32 +45,27 @@ public EditorTooltip(Gui gui) { public void open(Entry target) { this.content.removeAll(); - final Entry deobfTarget = this.gui.getController().getProject().getRemapper().deobfuscate(target); - - // TODO show parent name instead - this.content.add(new JLabel(deobfTarget.getFullName())); - if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); this.declarationSnippet = null; } - if (target instanceof ParentedEntry parentedTarget) { - final ClassEntry targetTopClass = parentedTarget.getTopLevelClass(); + this.content.add(new JLabel(this.getParentName(target).orElse("From un-packaged class"))); - final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() - .openClass(targetTopClass); + final ClassEntry targetTopClass = target.getTopLevelClass(); - if (targetTopClassHandle != null) { - this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); + final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() + .openClass(targetTopClass); - // TODO create method that packs and adjusts position as necessary - this.declarationSnippet.addSourceSetListener(source -> this.pack()); + if (targetTopClassHandle != null) { + this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); - this.content.add(this.declarationSnippet.ui); - } else { - this.content.add(new JLabel("No source available")); - } + // TODO create method that packs and adjusts position as necessary + this.declarationSnippet.addSourceSetListener(source -> this.pack()); + + this.content.add(this.declarationSnippet.ui); + } else { + this.content.add(new JLabel("No source available")); } // TODO offset from cursor slightly + ensure on-screen @@ -88,4 +86,52 @@ public void close() { this.declarationSnippet.classHandler.removeListener(); } } + + private Optional getParentName(Entry entry) { + final var builder = new StringBuilder(); + + Entry parent = entry.getParent(); + if (parent != null) { + while (true) { + if (!builder.isEmpty()) { + builder.insert(0, '.'); + } + + builder.insert(0, this.getSimpleName(parent)); + + final Entry nextParent = parent.getParent(); + if (nextParent == null) { + if (parent instanceof ClassEntry parentClass) { + final String parentPackage = parentClass.getPackageName(); + if (parentPackage != null) { + final String dotPackage = parentPackage.replace('/', '.'); + builder.insert(0, dotPackage); + } + } + + break; + } else { + parent = nextParent; + } + } + } + + return builder.isEmpty() ? Optional.empty() : Optional.of(builder.toString()); + } + + private String getSimpleName(Entry entry) { + final EnigmaProject project = this.gui.getController().getProject(); + + final String simpleObfName = entry.getSimpleName(); + if (!simpleObfName.isEmpty() && Character.isJavaIdentifierStart(simpleObfName.charAt(0))) { + final AccessFlags access = project.getJarIndex().getIndex(EntryIndex.class).getEntryAccess(entry); + if (access == null || !(access.isSynthetic())) { + return project.getRemapper().deobfuscate(entry).getSimpleName(); + } else { + return ""; + } + } + + return ""; + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java index 44da2495d..7248d9463 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java @@ -134,6 +134,7 @@ public String toString() { return this.getFullName(); } + @Nullable public String getPackageName() { return getParentPackage(this.fullName); } @@ -183,6 +184,7 @@ public boolean isJre() { return packageName != null && (packageName.startsWith("java/") || packageName.startsWith("javax/")); } + @Nullable public static String getParentPackage(String name) { int pos = name.lastIndexOf('/'); if (pos > 0) { From 07c0ce2e99b581a38db4e79be0dbd387f36fdff3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 10:38:41 -0700 Subject: [PATCH 035/109] add 'From: ' prefix to parent label left-align tooltip rows --- .../enigma/gui/panel/EditorTooltip.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 5a74575cb..a8e571bb7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -14,6 +14,7 @@ import javax.swing.JLabel; import javax.swing.JWindow; import java.awt.BorderLayout; +import java.awt.Component; import java.awt.MouseInfo; import java.awt.Window; import java.util.Optional; @@ -50,22 +51,34 @@ public void open(Entry target) { this.declarationSnippet = null; } - this.content.add(new JLabel(this.getParentName(target).orElse("From un-packaged class"))); + { + final Box fromRow = Box.createHorizontalBox(); + fromRow.add(new JLabel("From: ")); + fromRow.add(new JLabel(this.getParentName(target).orElse("un-packaged class"))); + fromRow.add(Box.createHorizontalGlue()); + this.content.add(fromRow); + } - final ClassEntry targetTopClass = target.getTopLevelClass(); + { + final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() + .openClass(target.getTopLevelClass()); - final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() - .openClass(targetTopClass); + final Component sourceInfo; + if (targetTopClassHandle != null) { + this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); - if (targetTopClassHandle != null) { - this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); + // TODO create method that packs and adjusts position as necessary + this.declarationSnippet.addSourceSetListener(source -> this.pack()); - // TODO create method that packs and adjusts position as necessary - this.declarationSnippet.addSourceSetListener(source -> this.pack()); + sourceInfo = this.declarationSnippet.ui; + } else { + sourceInfo = new JLabel("No source available"); + } - this.content.add(this.declarationSnippet.ui); - } else { - this.content.add(new JLabel("No source available")); + final Box sourceRow = Box.createHorizontalBox(); + sourceRow.add(sourceInfo); + sourceRow.add(Box.createHorizontalGlue()); + this.content.add(sourceRow); } // TODO offset from cursor slightly + ensure on-screen From 9b1da08cb285036c07ff3dfdab58fffabdec0c08 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 11:26:23 -0700 Subject: [PATCH 036/109] add javadocs to tooltip --- .../enigma/gui/panel/EditorTooltip.java | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index a8e571bb7..8b7eb2e05 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -7,17 +7,23 @@ import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.gui.config.Config; import javax.annotation.Nullable; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; +import javax.swing.JSeparator; +import javax.swing.JTextArea; import javax.swing.JWindow; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.Component; +import java.awt.Font; import java.awt.MouseInfo; import java.awt.Window; import java.util.Optional; +import java.util.function.Consumer; public class EditorTooltip extends JWindow { private final Gui gui; @@ -51,12 +57,20 @@ public void open(Entry target) { this.declarationSnippet = null; } - { - final Box fromRow = Box.createHorizontalBox(); - fromRow.add(new JLabel("From: ")); - fromRow.add(new JLabel(this.getParentName(target).orElse("un-packaged class"))); - fromRow.add(Box.createHorizontalGlue()); - this.content.add(fromRow); + this.addRow(new JLabel("From: "), new JLabel(this.getParentName(target).orElse("un-packaged class"))); + + final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); + if (javadoc != null) { + this.add(new JSeparator()); + + final JTextArea javadocComponent = new JTextArea(javadoc); + javadocComponent.setLineWrap(true); + javadocComponent.setWrapStyleWord(true); + javadocComponent.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); + javadocComponent.setFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); + javadocComponent.setBackground(new Color(0, 0, 0, 0)); + + this.addRow(javadocComponent); } { @@ -75,10 +89,7 @@ public void open(Entry target) { sourceInfo = new JLabel("No source available"); } - final Box sourceRow = Box.createHorizontalBox(); - sourceRow.add(sourceInfo); - sourceRow.add(Box.createHorizontalGlue()); - this.content.add(sourceRow); + this.addRow(sourceInfo); } // TODO offset from cursor slightly + ensure on-screen @@ -91,6 +102,21 @@ public void open(Entry target) { this.setVisible(true); } + private void addRow(Component... components) { + this.addRow(row -> { + for (final Component component : components) { + row.add(component); + } + }); + } + + private void addRow(Consumer rowInitializer) { + final Box row = Box.createHorizontalBox(); + rowInitializer.accept(row); + row.add(Box.createHorizontalGlue()); + this.add(row); + } + public void close() { this.setVisible(false); this.content.removeAll(); From 5f5167392756ff141a5cace5793f581b36a9472e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 11:47:53 -0700 Subject: [PATCH 037/109] allow disabling tooltip --- .../org/quiltmc/enigma/gui/config/Config.java | 5 +++ .../enigma/gui/config/EditorConfig.java | 12 ++++++ .../gui/config/EditorTooltipSection.java | 13 +++++++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 39 +++++++++++-------- .../enigma/gui/panel/EditorTooltip.java | 3 +- 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index d0d0038e1..f0768a2e5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -52,6 +52,7 @@ public final class Config extends ReflectiveConfig { private static final NetConfig NET = ConfigFactory.create(ENVIRONMENT, FAMILY, "net", NetConfig.class); private static final DockerConfig DOCKER = ConfigFactory.create(ENVIRONMENT, FAMILY, "docker", DockerConfig.class); private static final DecompilerConfig DECOMPILER = ConfigFactory.create(ENVIRONMENT, FAMILY, "decompiler", DecompilerConfig.class); + private static final EditorConfig EDITOR = ConfigFactory.create(ENVIRONMENT, FAMILY, "editor", EditorConfig.class); @Comment("The currently assigned UI language. This will be an ISO-639 two-letter language code, followed by an underscore and an ISO 3166-1 alpha-2 two-letter country code.") @Processor("grabPossibleLanguages") @@ -131,6 +132,10 @@ public static DecompilerConfig decompiler() { return DECOMPILER; } + public static EditorConfig editor() { + return EDITOR; + } + public static Theme currentTheme() { return activeThemeChoice.theme; } 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 new file mode 100644 index 000000000..e2247dbbf --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorConfig.java @@ -0,0 +1,12 @@ +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; + +@SerializedNameConvention(NamingSchemes.SNAKE_CASE) +public class EditorConfig extends ReflectiveConfig { + @Comment("The settings for the editor tooltip.") + public final EditorTooltipSection tooltip = new EditorTooltipSection(); +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java new file mode 100644 index 000000000..601c16f00 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java @@ -0,0 +1,13 @@ +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 EditorTooltipSection extends ReflectiveConfig.Section { + @Comment("Whether tooltips are enabled.") + public final TrackedValue enable = this.value(true); +} 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 1fb5a1d2f..83bd9000c 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 @@ -8,6 +8,7 @@ import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; 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.dialog.EnigmaQuickFindToolBar; import org.quiltmc.enigma.gui.element.EditorPopupMenu; @@ -69,27 +70,30 @@ public class EditorPanel extends BaseEditorPanel { // avoid finding the mouse entry every mouse movement update private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - this.consumeEditorMouseTarget( - (targetToken, targetEntry) -> { - this.hideTokenTooltipTimer.restart(); - if (this.tooltip.isVisible()) { - this.showTokenTooltipTimer.stop(); - - if (!targetToken.equals(this.lastMouseTargetToken)) { + if (Config.editor().tooltip.enable.value()) { + this.consumeEditorMouseTarget( + (targetToken, targetEntry) -> { + this.hideTokenTooltipTimer.restart(); + if (this.tooltip.isVisible()) { + this.showTokenTooltipTimer.stop(); + + if (!targetToken.equals(this.lastMouseTargetToken)) { + this.lastMouseTargetToken = targetToken; + this.openTooltip(targetEntry); + } + } else { this.lastMouseTargetToken = targetToken; - this.openTooltip(targetEntry); + this.showTokenTooltipTimer.start(); } - } else { - this.lastMouseTargetToken = targetToken; - this.showTokenTooltipTimer.start(); + }, + () -> { + this.lastMouseTargetToken = null; + this.showTokenTooltipTimer.stop(); } - }, - () -> { - this.lastMouseTargetToken = null; - this.showTokenTooltipTimer.stop(); - } - ); + ); + } }); + private final Timer showTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { this.consumeEditorMouseTarget((targetToken, targetEntry) -> { @@ -101,6 +105,7 @@ public class EditorPanel extends BaseEditorPanel { }); } ); + // TODO stop hide timer when mouse is over tooltip or target token // TODO tooltip re-shows after short delay after hiding private final Timer hideTokenTooltipTimer = new Timer( diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 8b7eb2e05..31c040696 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -143,8 +143,7 @@ private Optional getParentName(Entry entry) { if (parent instanceof ClassEntry parentClass) { final String parentPackage = parentClass.getPackageName(); if (parentPackage != null) { - final String dotPackage = parentPackage.replace('/', '.'); - builder.insert(0, dotPackage); + builder.insert(0, parentPackage.replace('/', '.')); } } From a7040f065199ae4d3e81355b44907af099ff9e2e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 13:31:11 -0700 Subject: [PATCH 038/109] make tooltip optionally interactable implement dragging to move tooltip --- .../gui/config/EditorTooltipSection.java | 3 + .../gui/panel/DeclarationSnippetPanel.java | 4 ++ .../quiltmc/enigma/gui/panel/EditorPanel.java | 17 +++-- .../enigma/gui/panel/EditorTooltip.java | 66 +++++++++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java index 601c16f00..118ba9b8a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java @@ -10,4 +10,7 @@ public class EditorTooltipSection extends ReflectiveConfig.Section { @Comment("Whether tooltips are enabled.") public final TrackedValue enable = this.value(true); + + @Comment("Whether tooltips can be clicked and interacted with to navigate their content.") + public final TrackedValue interactable = this.value(true); } 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 aa59043bf..a6e09b8aa 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,7 @@ import org.tinylog.Logger; import javax.swing.JViewport; +import java.awt.Color; import java.util.Comparator; import java.util.Optional; import java.util.function.Function; @@ -82,6 +83,9 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.getEditor().setEditable(false); this.setClassHandle(targetTopClassHandle, false, source -> this.createSnippet(source, target)); + + this.editor.setCaretColor(new Color(0, 0, 0, 0)); + this.editor.getCaret().setSelectionVisible(true); } private Snippet createSnippet(DecompiledClassSource source, Entry target) { 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 83bd9000c..a412ad530 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 @@ -149,12 +149,10 @@ public void focusLost(FocusEvent e) { // global listener so tooltip hides even if clicking outside editor Toolkit.getDefaultToolkit().addAWTEventListener( e -> { - // TODO configurably allow clicking tooltip - // - update tooltip with clicked entry declaration - // - add a "bread crumbs" back button - // - open entry tab on ctrl-click or "Got to source" button click - if (e.getID() == MouseEvent.MOUSE_PRESSED) { - this.closeTooltip(); + if (e instanceof MouseEvent mouseEvent && mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { + if (this.tooltip.isVisible()) { + consumeMousePositionOut(this.tooltip.getContentPane(), absolute -> this.closeTooltip()); + } } }, MouseEvent.MOUSE_PRESSED @@ -326,6 +324,13 @@ private static void consumeMousePositionIn(Component component, BiConsumer { }); } + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + private static void consumeMousePositionOut(Component component, Consumer outAction) { + consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); + } + /** * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse * position and its position relative to the passed {@code component} to the passed {@code inAction}.
diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 31c040696..204764c4c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -21,7 +21,11 @@ import java.awt.Component; import java.awt.Font; import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.Optional; import java.util.function.Consumer; @@ -29,6 +33,9 @@ public class EditorTooltip extends JWindow { private final Gui gui; private final Box content; + @Nullable + private Point dragStart; + @Nullable private DeclarationSnippetPanel declarationSnippet; @@ -42,6 +49,46 @@ public EditorTooltip(Gui gui) { this.setType(Window.Type.POPUP); this.setLayout(new BorderLayout()); this.setContentPane(this.content); + + Toolkit.getDefaultToolkit().addAWTEventListener( + e -> { + if (e instanceof MouseEvent mouseEvent && mouseEvent.getID() == MouseEvent.MOUSE_RELEASED) { + EditorTooltip.this.dragStart = null; + } + }, + MouseEvent.MOUSE_RELEASED + ); + + // TODO + // - update tooltip with clicked entry declaration + // - add a "bread crumbs" back button + // - open entry tab on ctrl-click or "Got to source" button click + this.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (Config.editor().tooltip.interactable.value()) { + EditorTooltip.this.dragStart = e.getButton() == MouseEvent.BUTTON1 + ? new Point(e.getX(), e.getY()) + : null; + + e.consume(); + } else { + EditorTooltip.this.close(); + } + } + }); + + this.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + final Point dragStart = EditorTooltip.this.dragStart; + if (dragStart != null) { + final Point location = EditorTooltip.this.getLocation(); + location.translate(e.getX() - dragStart.x, e.getY() - dragStart.y); + EditorTooltip.this.setLocation(location); + } + } + }); } /** @@ -52,6 +99,15 @@ public EditorTooltip(Gui gui) { public void open(Entry target) { this.content.removeAll(); + @Nullable + final MouseAdapter stopInteraction = Config.editor().tooltip.interactable.value() ? null : new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + EditorTooltip.this.close(); + e.consume(); + } + }; + if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); this.declarationSnippet = null; @@ -59,6 +115,7 @@ public void open(Entry target) { this.addRow(new JLabel("From: "), new JLabel(this.getParentName(target).orElse("un-packaged class"))); + // TODO add param javadocs for methods (and component javadocs for records) final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); if (javadoc != null) { this.add(new JSeparator()); @@ -69,6 +126,11 @@ public void open(Entry target) { javadocComponent.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); javadocComponent.setFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); javadocComponent.setBackground(new Color(0, 0, 0, 0)); + javadocComponent.setCaretColor(new Color(0, 0, 0, 0)); + javadocComponent.getCaret().setSelectionVisible(true); + if (stopInteraction != null) { + javadocComponent.addMouseListener(stopInteraction); + } this.addRow(javadocComponent); } @@ -84,6 +146,10 @@ public void open(Entry target) { // TODO create method that packs and adjusts position as necessary this.declarationSnippet.addSourceSetListener(source -> this.pack()); + if (stopInteraction != null) { + this.declarationSnippet.editor.addMouseListener(stopInteraction); + } + sourceInfo = this.declarationSnippet.ui; } else { sourceInfo = new JLabel("No source available"); From 4e20fa1ca0b68fb4d0d3f65b792f0644247e0f2f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 14:48:41 -0700 Subject: [PATCH 039/109] prevent hiding tooltip when cursor is over a token or the tooltip --- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 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 a412ad530..8a0b53219 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 @@ -73,7 +73,7 @@ public class EditorPanel extends BaseEditorPanel { if (Config.editor().tooltip.enable.value()) { this.consumeEditorMouseTarget( (targetToken, targetEntry) -> { - this.hideTokenTooltipTimer.restart(); + this.hideTokenTooltipTimer.stop(); if (this.tooltip.isVisible()) { this.showTokenTooltipTimer.stop(); @@ -89,6 +89,7 @@ public class EditorPanel extends BaseEditorPanel { () -> { this.lastMouseTargetToken = null; this.showTokenTooltipTimer.stop(); + this.hideTokenTooltipTimer.start(); } ); } @@ -97,7 +98,6 @@ public class EditorPanel extends BaseEditorPanel { private final Timer showTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { this.consumeEditorMouseTarget((targetToken, targetEntry) -> { - this.hideTokenTooltipTimer.restart(); if (targetToken.equals(this.lastMouseTargetToken)) { this.tooltip.setVisible(true); this.openTooltip(targetEntry); @@ -106,8 +106,6 @@ public class EditorPanel extends BaseEditorPanel { } ); - // TODO stop hide timer when mouse is over tooltip or target token - // TODO tooltip re-shows after short delay after hiding private final Timer hideTokenTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> this.closeTooltip() @@ -233,6 +231,15 @@ public void mousePressed(MouseEvent e) { } }); + this.tooltip.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (Config.editor().tooltip.interactable.value()) { + EditorPanel.this.hideTokenTooltipTimer.stop(); + } + } + }); + this.editor.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent event) { From 881e93033e1d8f57b945153c2c2ede2f37fd97c6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 18:31:23 -0700 Subject: [PATCH 040/109] fix parent label for top-level classes and add missing dot separator for package format labels use GridBagLayout for param javadocs (currently takes up extra space) --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 39 ++-- .../enigma/gui/panel/EditorTooltip.java | 180 ++++++++++++++---- 2 files changed, 160 insertions(+), 59 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 8a0b53219..e631c3efd 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 @@ -147,10 +147,8 @@ public void focusLost(FocusEvent e) { // global listener so tooltip hides even if clicking outside editor Toolkit.getDefaultToolkit().addAWTEventListener( e -> { - if (e instanceof MouseEvent mouseEvent && mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { - if (this.tooltip.isVisible()) { - consumeMousePositionOut(this.tooltip.getContentPane(), absolute -> this.closeTooltip()); - } + if (e.getID() == MouseEvent.MOUSE_PRESSED && this.tooltip.isVisible()) { + consumeMousePositionOut(this.tooltip.getContentPane(), absolute -> this.closeTooltip()); } }, MouseEvent.MOUSE_PRESSED @@ -211,23 +209,26 @@ public void focusLost(FocusEvent e) { this.tooltip.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - consumeMousePositionIn(EditorPanel.this.editor, (absolutMousePosition, editorMousePosition) -> { - final MouseEvent editorMouseEvent = new MouseEvent( - EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), - editorMousePosition.x, editorMousePosition.y, - absolutMousePosition.x, absolutMousePosition.y, - e.getClickCount(), e.isPopupTrigger(), e.getButton() - ); - - for (final MouseListener listener : EditorPanel.this.editor.getMouseListeners()) { - listener.mousePressed(editorMouseEvent); - if (editorMouseEvent.isConsumed()) { - break; + if (!Config.editor().tooltip.interactable.value()) { + // if not interactable, forward event to editor + consumeMousePositionIn(EditorPanel.this.editor, (absolutMousePosition, editorMousePosition) -> { + final MouseEvent editorMouseEvent = new MouseEvent( + EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), + editorMousePosition.x, editorMousePosition.y, + absolutMousePosition.x, absolutMousePosition.y, + e.getClickCount(), e.isPopupTrigger(), e.getButton() + ); + + for (final MouseListener listener : EditorPanel.this.editor.getMouseListeners()) { + listener.mousePressed(editorMouseEvent); + if (editorMouseEvent.isConsumed()) { + break; + } } - } - }); + }); - e.consume(); + e.consume(); + } } }); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 204764c4c..658ba57ce 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -1,18 +1,24 @@ package org.quiltmc.enigma.gui.panel; +import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.AccessFlags; -import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import javax.annotation.Nullable; +import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; +import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.JWindow; @@ -20,6 +26,8 @@ import java.awt.Color; import java.awt.Component; import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Toolkit; @@ -29,6 +37,8 @@ import java.util.Optional; import java.util.function.Consumer; +import static com.google.common.collect.ImmutableList.toImmutableList; + public class EditorTooltip extends JWindow { private final Gui gui; private final Box content; @@ -113,26 +123,49 @@ public void mousePressed(MouseEvent e) { this.declarationSnippet = null; } - this.addRow(new JLabel("From: "), new JLabel(this.getParentName(target).orElse("un-packaged class"))); + final Font editorFont = Config.currentFonts().editor.value(); + final Font italEditorFont = editorFont.deriveFont(Font.ITALIC); + + this.add(rowOf(row -> { + final JLabel from = labelOf("from", italEditorFont); + // the italics cause it to overlap with the colon if it has no right padding + from.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 1)); + row.add(from); + row.add(colonLabelOf("")); + row.add(labelOf(this.getParentName(target).orElse("un-packaged class"), editorFont)); + })); - // TODO add param javadocs for methods (and component javadocs for records) final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); - if (javadoc != null) { + final ImmutableList paramJavadocs = this.paramJavadocsOf(target, italEditorFont, stopInteraction); + if (javadoc != null || !paramJavadocs.isEmpty()) { this.add(new JSeparator()); - final JTextArea javadocComponent = new JTextArea(javadoc); - javadocComponent.setLineWrap(true); - javadocComponent.setWrapStyleWord(true); - javadocComponent.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); - javadocComponent.setFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); - javadocComponent.setBackground(new Color(0, 0, 0, 0)); - javadocComponent.setCaretColor(new Color(0, 0, 0, 0)); - javadocComponent.getCaret().setSelectionVisible(true); - if (stopInteraction != null) { - javadocComponent.addMouseListener(stopInteraction); + if (javadoc != null) { + this.add(rowOf(javadocOf(javadoc, italEditorFont, stopInteraction))); } - this.addRow(javadocComponent); + if (!paramJavadocs.isEmpty()) { + // TODO for some reason the param grid has extra space above and below it + final JPanel params = new JPanel(new GridBagLayout()); + params.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + + final GridBagConstraints nameConstraints = new GridBagConstraints(); + nameConstraints.gridx = 0; + nameConstraints.anchor = GridBagConstraints.FIRST_LINE_END; + + final GridBagConstraints javadocConstraints = new GridBagConstraints(); + javadocConstraints.gridx = 1; + javadocConstraints.fill = GridBagConstraints.HORIZONTAL; + javadocConstraints.weightx = 1; + javadocConstraints.anchor = GridBagConstraints.LINE_START; + + for (final ParamJavadoc paramJavadoc : paramJavadocs) { + params.add(paramJavadoc.name, nameConstraints); + params.add(paramJavadoc.javadoc, javadocConstraints); + } + + this.add(params); + } } { @@ -152,10 +185,10 @@ public void mousePressed(MouseEvent e) { sourceInfo = this.declarationSnippet.ui; } else { - sourceInfo = new JLabel("No source available"); + sourceInfo = labelOf("No source available", italEditorFont); } - this.addRow(sourceInfo); + this.add(rowOf(sourceInfo)); } // TODO offset from cursor slightly + ensure on-screen @@ -168,19 +201,87 @@ public void mousePressed(MouseEvent e) { this.setVisible(true); } - private void addRow(Component... components) { - this.addRow(row -> { + private static JLabel colonLabelOf(String text) { + final JLabel label = labelOf(text + ":", Config.currentFonts().editor.value()); + label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2)); + + return label; + } + + private static JLabel labelOf(String text, Font font) { + final JLabel label = new JLabel(text); + label.setFont(font); + + return label; + } + + // TODO for some reason sometimes there's extra space below the text, about one line's worth + private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopInteraction) { + final JTextArea text = new JTextArea(javadoc); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setForeground(Config.getCurrentSyntaxPaneColors().comment.value()); + text.setFont(font); + text.setBackground(invisibleColorOf()); + text.setCaretColor(invisibleColorOf()); + text.getCaret().setSelectionVisible(true); + text.setBorder(BorderFactory.createEmptyBorder()); + + if (stopInteraction != null) { + text.addMouseListener(stopInteraction); + } + + return text; + } + + private static Color invisibleColorOf() { + return new Color(0, 0, 0, 0); + } + + private ImmutableList paramJavadocsOf(Entry target, Font font, MouseAdapter stopInteraction) { + final EnigmaProject project = this.gui.getController().getProject(); + final EntryIndex entryIndex = project.getJarIndex().getIndex(EntryIndex.class); + + if (target instanceof MethodEntry targetMethod) { + final MethodDefEntry methodDef = entryIndex.getDefinition(targetMethod); + if (methodDef == null) { + return ImmutableList.of(); + } else { + final EntryRemapper remapper = project.getRemapper(); + + return methodDef.getParameters(entryIndex).stream() + .mapMulti((param, add) -> { + final EntryMapping mapping = remapper.getMapping(param); + if (mapping.javadoc() != null) { + final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName()); + final JTextArea javadoc = javadocOf(mapping.javadoc(), font, stopInteraction); + + add.accept(new ParamJavadoc(name, javadoc)); + } + }) + .collect(toImmutableList()); + } + } else { + // TODO if target is a record, add its components' javadocs + return ImmutableList.of(); + } + } + + private static Box rowOf(Component... components) { + return rowOf(row -> { for (final Component component : components) { row.add(component); } }); } - private void addRow(Consumer rowInitializer) { + private static Box rowOf(Consumer rowInitializer) { final Box row = Box.createHorizontalBox(); rowInitializer.accept(row); row.add(Box.createHorizontalGlue()); - this.add(row); + row.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + + return row; } public void close() { @@ -195,29 +296,26 @@ public void close() { private Optional getParentName(Entry entry) { final var builder = new StringBuilder(); + final Runnable tryDot = () -> { + if (!builder.isEmpty()) { + builder.insert(0, '.'); + } + }; + Entry parent = entry.getParent(); - if (parent != null) { - while (true) { - if (!builder.isEmpty()) { - builder.insert(0, '.'); - } + while (parent != null) { + tryDot.run(); - builder.insert(0, this.getSimpleName(parent)); + builder.insert(0, this.getSimpleName(parent)); - final Entry nextParent = parent.getParent(); - if (nextParent == null) { - if (parent instanceof ClassEntry parentClass) { - final String parentPackage = parentClass.getPackageName(); - if (parentPackage != null) { - builder.insert(0, parentPackage.replace('/', '.')); - } - } + parent = parent.getParent(); + } - break; - } else { - parent = nextParent; - } - } + final String packageName = entry.getTopLevelClass().getPackageName(); + if (packageName != null) { + tryDot.run(); + + builder.insert(0, packageName.replace('/', '.')); } return builder.isEmpty() ? Optional.empty() : Optional.of(builder.toString()); @@ -238,4 +336,6 @@ private String getSimpleName(Entry entry) { return ""; } + + private record ParamJavadoc(JLabel name, JTextArea javadoc) { } } From e73e1653ac1bdd3e6747e8955819f9fe999997e1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 19:18:41 -0700 Subject: [PATCH 041/109] allow clicking entries withing tooltip to populate tooltip with that entry's information --- .../enigma/gui/panel/BaseEditorPanel.java | 118 ++++++++++++++++-- .../quiltmc/enigma/gui/panel/EditorPanel.java | 82 ------------ .../enigma/gui/panel/EditorTooltip.java | 19 ++- 3 files changed, 125 insertions(+), 94 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 08e4a77a6..f80e54ecc 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 @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.panel; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.class_handle.ClassHandleError; @@ -42,11 +43,14 @@ import javax.swing.text.BadLocationException; import javax.swing.text.Highlighter.HighlightPainter; import java.awt.Color; +import java.awt.Component; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; +import java.awt.MouseInfo; +import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -56,6 +60,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -260,6 +266,82 @@ public boolean isOptimizedDrawingEnabled() { this.mode = mode; } + /** + * @see #consumeEditorMouseTarget(BiConsumer, Runnable) + */ + protected void consumeEditorMouseTarget(BiConsumer> action) { + this.consumeEditorMouseTarget(action, Runnables.doNothing()); + } + + /** + * If the mouse is currently over a {@link Token} in the {@link #editor} that resolves to an {@link Entry}, passes + * the token and entry to the passed {@code action}.
+ * Otherwise, calls the passed {@code onNoTarget}. + * + * @param action the action to run when the mouse is over a token that resolves to an entry + * @param onNoTarget the action to run when the mouse is not over a token that resolves to an entry + */ + protected void consumeEditorMouseTarget(BiConsumer> action, Runnable onNoTarget) { + BaseEditorPanel.consumeMousePositionIn(this.editor, + (absoluteMouse, relativeMouse) -> Optional.of(relativeMouse) + .map(this.editor::viewToModel2D) + .filter(textPos -> textPos >= 0) + .map(this::getToken) + .ifPresentOrElse( + token -> Optional.of(token) + .map(this::getReference) + .map(reference -> reference.entry) + .ifPresentOrElse( + entry -> action.accept(token, entry), + onNoTarget + ), + onNoTarget + ), + ignored -> onNoTarget.run() + ); + } + + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + protected static void consumeMousePositionIn(Component component, BiConsumer inAction) { + BaseEditorPanel.consumeMousePositionIn(component, inAction, pos -> { }); + } + + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + protected static void consumeMousePositionOut(Component component, Consumer outAction) { + BaseEditorPanel.consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); + } + + /** + * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse + * position and its position relative to the passed {@code component} to the passed {@code inAction}.
+ * Otherwise, passes the absolute mouse position to the passed {@code outAction}. + * + * @param component the component which may contain the mouse pointer + * @param inAction the action to run if the mouse is inside the passed {@code component}; + * receives the mouse's absolute position and its position relative to the component + * @param outAction the action to run if the mouse is outside the passed {@code component}; + * receives the mouse's absolute position + */ + private static void consumeMousePositionIn( + Component component, BiConsumer inAction, Consumer outAction + ) { + final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); + + final Point componentPos = component.getLocationOnScreen(); + final Point relativePos = new Point(absolutePos); + relativePos.translate(-componentPos.x, -componentPos.y); + + if (component.contains(relativePos)) { + inAction.accept(absolutePos, relativePos); + } else { + outAction.accept(absolutePos); + } + } + protected void initEditorPane(JPanel editorPane) { final GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; @@ -292,7 +374,7 @@ public Token getToken(int pos) { return null; } - return this.source.getIndex().getReferenceToken(pos); + return this.source.getIndex().getReferenceToken(this.sourceBounds.offsetOf(pos)); } @Nullable @@ -573,6 +655,10 @@ public record Snippet(int start, int end) { } private record LineOffset(int sourceStart, int sourceEnd, int offset) { + boolean contains(int pos) { + return this.sourceStart <= pos && this.sourceEnd >= pos; + } + boolean contains(Token token) { return this.sourceStart <= token.start && this.sourceEnd >= token.end; } @@ -666,27 +752,36 @@ default boolean contains(int pos) { } default boolean contains(Token token) { - return this.contains(token.start) && this.contains(token.end); + return token.start >= this.start() && token.end <= this.end(); } + int offsetOf(int pos); + Optional offsetOf(@Nullable Token token); } private record TrimmedBounds(int start, int end, ImmutableList indentOffsets) implements SourceBounds { + @Override + public int offsetOf(int pos) { + return pos + this.getOffset(pos, LineOffset::contains); + } + @Override public Optional offsetOf(@Nullable Token token) { if (token == null || !this.contains(token)) { return Optional.empty(); } else { - final int offset = this.start() + this.indentOffsets().stream() - .filter(lineOffset -> lineOffset.contains(token)) - .findFirst() - .map(LineOffset::offset) - .orElse(0); - - return Optional.of(token.move(-offset)); + return Optional.of(token.move(-this.getOffset(token, LineOffset::contains))); } } + + private int getOffset(T t, BiPredicate predicate) { + return this.start() + this.indentOffsets().stream() + .filter(lineOffset -> predicate.test(lineOffset, t)) + .findFirst() + .map(LineOffset::offset) + .orElse(0); + } } private final class DefaultBounds implements SourceBounds { @@ -700,6 +795,11 @@ public int end() { return BaseEditorPanel.this.source.toString().length(); } + @Override + public int offsetOf(int pos) { + return pos; + } + @Override public Optional offsetOf(Token token) { return token == null || this.end() < token.end ? Optional.empty() : Optional.of(token); 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 e631c3efd..0a5f0ff5e 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,5 @@ package org.quiltmc.enigma.gui.panel; -import com.google.common.util.concurrent.Runnables; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.class_handle.ClassHandle; @@ -25,8 +24,6 @@ import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.Insets; -import java.awt.MouseInfo; -import java.awt.Point; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; @@ -40,9 +37,6 @@ import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nullable; import javax.swing.JComponent; @@ -290,82 +284,6 @@ private void openTooltip(Entry target) { this.tooltip.open(target); } - /** - * @see #consumeEditorMouseTarget(BiConsumer, Runnable) - */ - private void consumeEditorMouseTarget(BiConsumer> action) { - this.consumeEditorMouseTarget(action, Runnables.doNothing()); - } - - /** - * If the mouse is currently over a {@link Token} in the {@link #editor} that resolves to an {@link Entry}, passes - * the token and entry to the passed {@code action}.
- * Otherwise, calls the passed {@code onNoTarget}. - * - * @param action the action to run when the mouse is over a token that resolves to an entry - * @param onNoTarget the action to run when the mouse is not over a token that resolves to an entry - */ - private void consumeEditorMouseTarget(BiConsumer> action, Runnable onNoTarget) { - consumeMousePositionIn(this.editor, - (absoluteMouse, relativeMouse) -> Optional.of(relativeMouse) - .map(this.editor::viewToModel2D) - .filter(textPos -> textPos >= 0) - .map(this::getToken) - .ifPresentOrElse( - token -> Optional.of(token) - .map(this::getReference) - .map(reference -> reference.entry) - .ifPresentOrElse( - entry -> action.accept(token, entry), - onNoTarget - ), - onNoTarget - ), - ignored -> onNoTarget.run() - ); - } - - /** - * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) - */ - private static void consumeMousePositionIn(Component component, BiConsumer inAction) { - consumeMousePositionIn(component, inAction, pos -> { }); - } - - /** - * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) - */ - private static void consumeMousePositionOut(Component component, Consumer outAction) { - consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); - } - - /** - * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse - * position and its position relative to the passed {@code component} to the passed {@code inAction}.
- * Otherwise, passes the absolute mouse position to the passed {@code outAction}. - * - * @param component the component which may contain the mouse pointer - * @param inAction the action to run if the mouse is inside the passed {@code component}; - * receives the mouse's absolute position and its position relative to the component - * @param outAction the action to run if the mouse is outside the passed {@code component}; - * receives the mouse's absolute position - */ - private static void consumeMousePositionIn( - Component component, BiConsumer inAction, Consumer outAction - ) { - final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); - - final Point componentPos = component.getLocationOnScreen(); - final Point relativePos = new Point(absolutePos); - relativePos.translate(-componentPos.x, -componentPos.y); - - if (component.contains(relativePos)) { - inAction.accept(absolutePos, relativePos); - } else { - outAction.accept(absolutePos); - } - } - public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 658ba57ce..dc547deb4 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -107,6 +107,13 @@ public void mouseDragged(MouseEvent e) { * @param target the entry whose information will be displayed */ public void open(Entry target) { + this.openImpl(target); + + // TODO offset from cursor slightly + ensure on-screen + this.setLocation(MouseInfo.getPointerInfo().getLocation()); + } + + private void openImpl(Entry target) { this.content.removeAll(); @Nullable @@ -176,6 +183,15 @@ public void mousePressed(MouseEvent e) { if (targetTopClassHandle != null) { this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); + this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { + EditorTooltip.this.openImpl(entry); + }); + } + }); + // TODO create method that packs and adjusts position as necessary this.declarationSnippet.addSourceSetListener(source -> this.pack()); @@ -191,9 +207,6 @@ public void mousePressed(MouseEvent e) { this.add(rowOf(sourceInfo)); } - // TODO offset from cursor slightly + ensure on-screen - this.setLocation(MouseInfo.getPointerInfo().getLocation()); - // TODO clamp size // TODO create method that packs and adjusts position as necessary this.pack(); From a13af94a361abab663f22cc9edf5fc922a968a81 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 5 Oct 2025 21:18:59 -0700 Subject: [PATCH 042/109] fix calculation for offsetting multiline snippet pos to un-trimmed pos allow clicking parent entries in tooltips to populate tooltip when ctrl+clicking entries in tooltips, open entry tab --- .../enigma/gui/panel/BaseEditorPanel.java | 66 ++++++++++++------- .../quiltmc/enigma/gui/panel/EditorPanel.java | 12 +--- .../enigma/gui/panel/EditorTooltip.java | 59 +++++++++++++++-- 3 files changed, 97 insertions(+), 40 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 f80e54ecc..806cb0c03 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 @@ -10,6 +10,8 @@ import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.source.TokenStore; import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; +import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.gui.BrowserCaret; @@ -61,10 +63,10 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; -import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; +import java.util.stream.Stream; public class BaseEditorPanel { protected final JPanel ui = new JPanel(); @@ -365,6 +367,18 @@ public void resetEditorZoom() { this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); } + protected Entry resolveReference(EntryReference, Entry> reference) { + final Entry navigationEntry; + if (reference.context == null) { + final EntryResolver resolver = this.controller.getProject().getRemapper().getObfResolver(); + navigationEntry = resolver.resolveFirstEntry(reference.entry, ResolutionStrategy.RESOLVE_ROOT); + } else { + navigationEntry = reference.entry; + } + + return navigationEntry; + } + protected void setCursorReference(EntryReference, Entry> ref) { this.cursorReference = ref; } @@ -374,6 +388,7 @@ public Token getToken(int pos) { return null; } + // TODO offset is wrong for Constructors::abstraction (multi-line) return this.source.getIndex().getReferenceToken(this.sourceBounds.offsetOf(pos)); } @@ -656,11 +671,11 @@ public record Snippet(int start, int end) { private record LineOffset(int sourceStart, int sourceEnd, int offset) { boolean contains(int pos) { - return this.sourceStart <= pos && this.sourceEnd >= pos; + return this.sourceStart <= pos && pos < this.sourceEnd; } boolean contains(Token token) { - return this.sourceStart <= token.start && this.sourceEnd >= token.end; + return this.sourceStart <= token.start && token.end < this.sourceEnd; } } @@ -752,35 +767,40 @@ default boolean contains(int pos) { } default boolean contains(Token token) { - return token.start >= this.start() && token.end <= this.end(); + return this.start() <= token.start && token.end <= this.end(); } - int offsetOf(int pos); + int offsetOf(int unBoundedPos); - Optional offsetOf(@Nullable Token token); + Optional offsetOf(@Nullable Token boundedToken); } private record TrimmedBounds(int start, int end, ImmutableList indentOffsets) implements SourceBounds { @Override - public int offsetOf(int pos) { - return pos + this.getOffset(pos, LineOffset::contains); + public int offsetOf(int boundedPos) { + final int searchStart = boundedPos + this.start; + return this.indentOffsets().reverse().stream() + .flatMap(indentOffset -> { + final int potentialPos = searchStart + indentOffset.offset; + return indentOffset.contains(potentialPos) ? Stream.of(potentialPos) : Stream.empty(); + }) + .findFirst() + .orElse(searchStart); } @Override - public Optional offsetOf(@Nullable Token token) { - if (token == null || !this.contains(token)) { + public Optional offsetOf(@Nullable Token unBoundedToken) { + if (unBoundedToken == null || !this.contains(unBoundedToken)) { return Optional.empty(); } else { - return Optional.of(token.move(-this.getOffset(token, LineOffset::contains))); - } - } + final int offset = this.start() + this.indentOffsets().stream() + .filter(lineOffset -> lineOffset.contains(unBoundedToken)) + .findFirst() + .map(LineOffset::offset) + .orElse(0); - private int getOffset(T t, BiPredicate predicate) { - return this.start() + this.indentOffsets().stream() - .filter(lineOffset -> predicate.test(lineOffset, t)) - .findFirst() - .map(LineOffset::offset) - .orElse(0); + return Optional.of(unBoundedToken.move(-offset)); + } } } @@ -796,13 +816,13 @@ public int end() { } @Override - public int offsetOf(int pos) { - return pos; + public int offsetOf(int unBoundedPos) { + return unBoundedPos; } @Override - public Optional offsetOf(Token token) { - return token == null || this.end() < token.end ? Optional.empty() : Optional.of(token); + public Optional offsetOf(Token boundedToken) { + return boundedToken == null || this.end() < boundedToken.end ? Optional.empty() : Optional.of(boundedToken); } } 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 0a5f0ff5e..22bcb12c3 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 @@ -5,7 +5,6 @@ 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.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.config.keybind.KeyBinds; @@ -18,14 +17,11 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; -import org.quiltmc.enigma.util.Result; -import org.quiltmc.enigma.gui.event.EditorActionListener; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.Insets; import java.awt.Toolkit; -import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -365,13 +361,7 @@ private void onCaretMove(int pos) { private void navigateToCursorReference() { if (this.cursorReference != null) { - final Entry referenceEntry = this.cursorReference.entry; - final Entry navigationEntry = this.cursorReference.context == null - ? this.controller.getProject().getRemapper().getObfResolver() - .resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT) - : referenceEntry; - - this.controller.navigateTo(navigationEntry); + this.controller.navigateTo(this.resolveReference(this.cursorReference)); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index dc547deb4..f2329b0f5 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; @@ -34,6 +35,9 @@ import java.awt.Window; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.font.TextAttribute; +import java.util.Map; import java.util.Optional; import java.util.function.Consumer; @@ -139,7 +143,35 @@ public void mousePressed(MouseEvent e) { from.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 1)); row.add(from); row.add(colonLabelOf("")); - row.add(labelOf(this.getParentName(target).orElse("un-packaged class"), editorFont)); + final Font parentFont; + @Nullable + final MouseListener parentClicked; + final Entry parent = target.getParent(); + if (stopInteraction == null && parent != null) { + @SuppressWarnings("rawtypes") + final Map attributes = editorFont.getAttributes(); + //noinspection unchecked + attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + //noinspection unchecked + parentFont = editorFont.deriveFont(attributes); + parentClicked = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorTooltip.this.navigateOnClick(parent, e.getModifiersEx()); + } + }; + } else { + parentFont = editorFont; + parentClicked = null; + } + + final JLabel parentLabel = labelOf(this.getParentName(target).orElse("un-packaged class"), parentFont); + + if (parentClicked != null) { + parentLabel.addMouseListener(parentClicked); + } + + row.add(parentLabel); })); final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); @@ -186,13 +218,20 @@ public void mousePressed(MouseEvent e) { this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - EditorTooltip.this.openImpl(entry); - }); + if (e.getButton() == MouseEvent.BUTTON1) { + EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { + final EntryReference, Entry> reference = + EditorTooltip.this.declarationSnippet.getReference(token); + final Entry target = reference == null + ? entry + : EditorTooltip.this.declarationSnippet.resolveReference(reference); + + EditorTooltip.this.navigateOnClick(target, e.getModifiersEx()); + }); + } } }); - // TODO create method that packs and adjusts position as necessary this.declarationSnippet.addSourceSetListener(source -> this.pack()); if (stopInteraction != null) { @@ -208,12 +247,20 @@ public void mouseClicked(MouseEvent e) { } // TODO clamp size - // TODO create method that packs and adjusts position as necessary this.pack(); this.setVisible(true); } + private void navigateOnClick(Entry entry, int modifiers) { + if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { + this.close(); + this.gui.getController().navigateTo(entry); + } else { + this.openImpl(entry); + } + } + private static JLabel colonLabelOf(String text) { final JLabel label = labelOf(text + ":", Config.currentFonts().editor.value()); label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2)); From 375833f1dfd34bf46df80c8ba8025dd209120987 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 6 Oct 2025 10:05:56 -0700 Subject: [PATCH 043/109] minor improvements, add TODOs --- .../java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java | 2 +- .../java/org/quiltmc/enigma/gui/panel/EditorTooltip.java | 6 +++++- .../java/org/quiltmc/enigma/input/tooltip/Constructors.java | 4 ++++ 3 files changed, 10 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 806cb0c03..67700075f 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 @@ -779,7 +779,7 @@ private record TrimmedBounds(int start, int end, ImmutableList inden @Override public int offsetOf(int boundedPos) { final int searchStart = boundedPos + this.start; - return this.indentOffsets().reverse().stream() + return this.indentOffsets().stream() .flatMap(indentOffset -> { final int potentialPos = searchStart + indentOffset.offset; return indentOffset.contains(potentialPos) ? Stream.of(potentialPos) : Stream.empty(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index f2329b0f5..bc553bd41 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -138,6 +138,8 @@ public void mousePressed(MouseEvent e) { final Font italEditorFont = editorFont.deriveFont(Font.ITALIC); this.add(rowOf(row -> { + // TODO add stat icon if enabled + // TODO add class/record/enum/method icon final JLabel from = labelOf("from", italEditorFont); // the italics cause it to overlap with the colon if it has no right padding from.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 1)); @@ -165,7 +167,7 @@ public void mouseClicked(MouseEvent e) { parentClicked = null; } - final JLabel parentLabel = labelOf(this.getParentName(target).orElse("un-packaged class"), parentFont); + final JLabel parentLabel = labelOf(this.getParentName(target).orElse(""), parentFont); if (parentClicked != null) { parentLabel.addMouseListener(parentClicked); @@ -174,6 +176,7 @@ public void mouseClicked(MouseEvent e) { row.add(parentLabel); })); + // TODO make javadocs and snippet copyable final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { @@ -213,6 +216,7 @@ public void mouseClicked(MouseEvent e) { final Component sourceInfo; if (targetTopClassHandle != null) { + // TODO expand right to fill width this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java index f9372cbf3..7462fb0d7 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java @@ -11,6 +11,10 @@ public Constructors(String outerArg) { void abstraction() { // tests #252 System.out.println(outerArg); + + if (this == null) { + this.abstraction(); + } } }; } From eec035ad8914b07fcb929554e92f0b15cf32350c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 6 Oct 2025 13:19:53 -0700 Subject: [PATCH 044/109] resolve references when finding tooltip targets --- .../enigma/gui/panel/BaseEditorPanel.java | 3 +- .../quiltmc/enigma/gui/panel/EditorPanel.java | 40 +++++++++---------- .../enigma/gui/panel/EditorTooltip.java | 10 +---- 3 files changed, 23 insertions(+), 30 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 67700075f..7294818ff 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 @@ -292,7 +292,7 @@ protected void consumeEditorMouseTarget(BiConsumer> action, Runn .ifPresentOrElse( token -> Optional.of(token) .map(this::getReference) - .map(reference -> reference.entry) + .map(this::resolveReference) .ifPresentOrElse( entry -> action.accept(token, entry), onNoTarget @@ -388,7 +388,6 @@ public Token getToken(int pos) { return null; } - // TODO offset is wrong for Constructors::abstraction (multi-line) return this.source.getIndex().getReferenceToken(this.sourceBounds.offsetOf(pos)); } 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 22bcb12c3..a5eddb828 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 @@ -62,41 +62,41 @@ public class EditorPanel extends BaseEditorPanel { private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { if (Config.editor().tooltip.enable.value()) { this.consumeEditorMouseTarget( - (targetToken, targetEntry) -> { - this.hideTokenTooltipTimer.stop(); + (token, entry) -> { + this.hideTooltipTimer.stop(); if (this.tooltip.isVisible()) { - this.showTokenTooltipTimer.stop(); + this.showTooltipTimer.stop(); - if (!targetToken.equals(this.lastMouseTargetToken)) { - this.lastMouseTargetToken = targetToken; - this.openTooltip(targetEntry); + if (!token.equals(this.lastMouseTargetToken)) { + this.lastMouseTargetToken = token; + this.openTooltip(entry); } } else { - this.lastMouseTargetToken = targetToken; - this.showTokenTooltipTimer.start(); + this.lastMouseTargetToken = token; + this.showTooltipTimer.start(); } }, () -> { this.lastMouseTargetToken = null; - this.showTokenTooltipTimer.stop(); - this.hideTokenTooltipTimer.start(); + this.showTooltipTimer.stop(); + this.hideTooltipTimer.start(); } ); } }); - private final Timer showTokenTooltipTimer = new Timer( + private final Timer showTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { - this.consumeEditorMouseTarget((targetToken, targetEntry) -> { - if (targetToken.equals(this.lastMouseTargetToken)) { + this.consumeEditorMouseTarget((token, entry) -> { + if (token.equals(this.lastMouseTargetToken)) { this.tooltip.setVisible(true); - this.openTooltip(targetEntry); + this.openTooltip(entry); } }); } ); - private final Timer hideTokenTooltipTimer = new Timer( + private final Timer hideTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> this.closeTooltip() ); @@ -191,8 +191,8 @@ public void focusLost(FocusEvent e) { this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); this.mouseStoppedMovingTimer.setRepeats(false); - this.showTokenTooltipTimer.setRepeats(false); - this.hideTokenTooltipTimer.setRepeats(false); + this.showTooltipTimer.setRepeats(false); + this.hideTooltipTimer.setRepeats(false); this.tooltip.setVisible(false); @@ -226,7 +226,7 @@ public void mousePressed(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { if (Config.editor().tooltip.interactable.value()) { - EditorPanel.this.hideTokenTooltipTimer.stop(); + EditorPanel.this.hideTooltipTimer.stop(); } } }); @@ -272,8 +272,8 @@ private void closeTooltip() { this.tooltip.close(); this.lastMouseTargetToken = null; this.mouseStoppedMovingTimer.stop(); - this.showTokenTooltipTimer.stop(); - this.hideTokenTooltipTimer.stop(); + this.showTooltipTimer.stop(); + this.hideTooltipTimer.stop(); } private void openTooltip(Entry target) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index bc553bd41..99d916890 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.EnigmaProject; -import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; @@ -57,6 +56,7 @@ public EditorTooltip(Gui gui) { super(); this.gui = gui; + // TODO add insets this.content = new Box(BoxLayout.PAGE_AXIS); this.setAlwaysOnTop(true); @@ -224,13 +224,7 @@ public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - final EntryReference, Entry> reference = - EditorTooltip.this.declarationSnippet.getReference(token); - final Entry target = reference == null - ? entry - : EditorTooltip.this.declarationSnippet.resolveReference(reference); - - EditorTooltip.this.navigateOnClick(target, e.getModifiersEx()); + EditorTooltip.this.navigateOnClick(entry, e.getModifiersEx()); }); } } From d1d4888f60d1780959614182e5f52ef4291e4649 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 6 Oct 2025 14:27:53 -0700 Subject: [PATCH 045/109] customize tooltip target highlighting --- .../enigma/gui/panel/BaseEditorPanel.java | 96 +++++++++++-------- .../gui/panel/DeclarationSnippetPanel.java | 14 ++- 2 files changed, 65 insertions(+), 45 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 7294818ff..0fed5701f 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 @@ -555,34 +555,66 @@ private void showReferenceImpl(EntryReference, Entry> reference) { } } - public void navigateToToken(Token token) { - if (token == null) { - throw new IllegalArgumentException("Token cannot be null!"); + /** + * Attempts navigating to and momentarily highlighting the passed {@code token}. + * + * @param token the token to navigate to, in {@linkplain #sourceBounds bounded} space + */ + public void navigateToToken(@Nullable Token token) { + final Token unBoundedToken = this.navigateToTokenImpl(token); + if (unBoundedToken == null) { + return; } - this.navigateToToken(token, SelectionHighlightPainter.INSTANCE); + // 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(unBoundedToken, SelectionHighlightPainter.INSTANCE); + } else if (this.highlight != null) { + BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); + } + + if (this.counter++ > 6) { + timer.stop(); + } + } + }); + + timer.start(); } /** - * @return {@code true} if navigation was successful, or {@code false} otherwise + * @return a token equivalent to the passed {@code boundedToken} with its position shifted so it aligns with the + * un-bounded source if navigation was successful, or {@code null} otherwise */ - protected boolean navigateToToken(@Nullable Token token, HighlightPainter highlightPainter) { - final Token offsetToken = this.sourceBounds.offsetOf(token).orElse(null); - if (offsetToken == null) { + @Nullable + protected Token navigateToTokenImpl(@Nullable Token boundedToken) { + if (boundedToken == null) { + return null; + } + + final Token unBoundedToken = this.sourceBounds.offsetOf(boundedToken).orElse(null); + if (unBoundedToken == null) { // token out of bounds - return false; + return null; } // set the caret position to the token - this.editor.setCaretPosition(offsetToken.start); + this.editor.setCaretPosition(unBoundedToken.start); this.editor.grabFocus(); try { // make sure the token is visible in the scroll window - Rectangle2D start = this.editor.modelToView2D(offsetToken.start); - Rectangle2D end = this.editor.modelToView2D(offsetToken.start); + Rectangle2D start = this.editor.modelToView2D(unBoundedToken.start); + Rectangle2D end = this.editor.modelToView2D(unBoundedToken.start); if (start == null || end == null) { - return false; + return null; } Rectangle show = new Rectangle(); @@ -593,38 +625,20 @@ protected boolean navigateToToken(@Nullable Token token, HighlightPainter highli if (!this.settingSource) { throw new RuntimeException(ex); } else { - return false; + return null; } } - // highlight the token momentarily - Timer timer = new Timer(200, new ActionListener() { - private int counter = 0; - private Object highlight = null; - - @Override - public void actionPerformed(ActionEvent event) { - if (this.counter % 2 == 0) { - try { - this.highlight = BaseEditorPanel.this.editor.getHighlighter() - .addHighlight(offsetToken.start, offsetToken.end, highlightPainter); - } catch (BadLocationException ex) { - // don't care - } - } else if (this.highlight != null) { - BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); - } - - if (this.counter++ > 6) { - Timer timer = (Timer) event.getSource(); - timer.stop(); - } - } - }); - - timer.start(); + return unBoundedToken; + } - return true; + protected Object addHighlight(Token token, HighlightPainter highlightPainter) { + try { + return BaseEditorPanel.this.editor.getHighlighter() + .addHighlight(token.start, token.end, highlightPainter); + } catch (BadLocationException ex) { + return null; + } } public JPanel getUi() { 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 a6e09b8aa..2b15e4b8a 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 @@ -38,7 +38,8 @@ import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.Gui; -import org.quiltmc.enigma.gui.highlight.SelectionHighlightPainter; +import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.LineNumbersRuler; @@ -72,12 +73,17 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl .ifPresent(lineNumbers -> lineNumbers.deinstall(this.editor)); this.addSourceSetListener(source -> { - final Token declarationToken = source.getIndex().getDeclarationToken(target); - // TODO create custom highlighter - if (!this.navigateToToken(declarationToken, SelectionHighlightPainter.INSTANCE)) { + final Token unBoundedToken = this.navigateToTokenImpl(source.getIndex().getDeclarationToken(target)); + if (unBoundedToken == null) { // the source isn't very useful if it couldn't be trimmed and the declaration couldn't be navigated to // set this text so it doesn't waste space or cause confusion this.editor.setText("// Unable to locate declaration"); + this.editor.getHighlighter().removeAllHighlights(); + } else { + this.addHighlight(unBoundedToken, BoxHighlightPainter.create( + new Color(0, 0, 0, 0), + Config.getCurrentSyntaxPaneColors().selectionHighlight.value() + )); } }); From 59b93a0e7e0298ef5bc0dffbefa6430cd10c5e46 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 6 Oct 2025 14:38:09 -0700 Subject: [PATCH 046/109] update TODOs regarding records --- .../org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java | 2 +- .../main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java | 2 +- 2 files changed, 2 insertions(+), 2 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 2b15e4b8a..1bd3467a0 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 @@ -252,7 +252,7 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - // TODO test record component getters + // TODO fix this for record component getters once there's a RecordIndex private Result findMethodSnippet( DecompiledClassSource source, Token target, MethodEntry targetEntry ) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 99d916890..dbc5d9916 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -320,7 +320,7 @@ private ImmutableList paramJavadocsOf(Entry target, Font font, .collect(toImmutableList()); } } else { - // TODO if target is a record, add its components' javadocs + // TODO add record component javadocs once there's a RecordIndex return ImmutableList.of(); } } From 3d622b4179c13721cf00c6ef3dea36006cbd0d7c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 6 Oct 2025 19:22:24 -0700 Subject: [PATCH 047/109] polish tooltip positioning fix exception when consumeMousePosition... methods where passed a component that wasn't showing fix several cases where the tooltip would hide when it shouldn't or hiden't when it should --- .../enigma/gui/panel/BaseEditorPanel.java | 26 +-- .../quiltmc/enigma/gui/panel/EditorPanel.java | 33 +++- .../enigma/gui/panel/EditorTooltip.java | 165 ++++++++++++++++-- 3 files changed, 190 insertions(+), 34 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 0fed5701f..6aefb1d72 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 @@ -307,14 +307,14 @@ protected void consumeEditorMouseTarget(BiConsumer> action, Runn * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) */ protected static void consumeMousePositionIn(Component component, BiConsumer inAction) { - BaseEditorPanel.consumeMousePositionIn(component, inAction, pos -> { }); + consumeMousePositionIn(component, inAction, pos -> { }); } /** * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) */ protected static void consumeMousePositionOut(Component component, Consumer outAction) { - BaseEditorPanel.consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); + consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); } /** @@ -328,20 +328,22 @@ protected static void consumeMousePositionOut(Component component, Consumer inAction, Consumer outAction ) { final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); - - final Point componentPos = component.getLocationOnScreen(); - final Point relativePos = new Point(absolutePos); - relativePos.translate(-componentPos.x, -componentPos.y); - - if (component.contains(relativePos)) { - inAction.accept(absolutePos, relativePos); - } else { - outAction.accept(absolutePos); + if (component.isShowing()) { + final Point componentPos = component.getLocationOnScreen(); + final Point relativePos = new Point(absolutePos); + relativePos.translate(-componentPos.x, -componentPos.y); + + if (component.contains(relativePos)) { + inAction.accept(absolutePos, relativePos); + return; + } } + + outAction.accept(absolutePos); } protected void initEditorPane(JPanel editorPane) { 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 a5eddb828..1ae2cbc86 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 @@ -76,11 +76,15 @@ public class EditorPanel extends BaseEditorPanel { this.showTooltipTimer.start(); } }, - () -> { - this.lastMouseTargetToken = null; - this.showTooltipTimer.stop(); - this.hideTooltipTimer.start(); - } + () -> consumeMousePositionIn( + this.tooltip.getContentPane(), + (absolute, relative) -> this.hideTooltipTimer.stop(), + absolute -> { + this.lastMouseTargetToken = null; + this.showTooltipTimer.stop(); + this.hideTooltipTimer.start(); + } + ) ); } }); @@ -179,10 +183,21 @@ public void mouseMoved(MouseEvent e) { } }; + this.tooltip.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (e.getOppositeComponent() != EditorPanel.this.editor) { + EditorPanel.this.closeTooltip(); + } + } + }); + this.editor.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { - EditorPanel.this.closeTooltip(); + if (e.getOppositeComponent() != EditorPanel.this.tooltip) { + EditorPanel.this.closeTooltip(); + } } }); @@ -190,6 +205,8 @@ public void focusLost(FocusEvent e) { this.editor.addMouseMotionListener(editorMouseAdapter); this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); + this.editorScrollPane.getViewport().addChangeListener(e -> this.closeTooltip()); + this.mouseStoppedMovingTimer.setRepeats(false); this.showTooltipTimer.setRepeats(false); this.hideTooltipTimer.setRepeats(false); @@ -218,6 +235,9 @@ public void mousePressed(MouseEvent e) { }); e.consume(); + } else { + EditorPanel.this.mouseStoppedMovingTimer.stop(); + EditorPanel.this.hideTooltipTimer.stop(); } } }); @@ -226,6 +246,7 @@ public void mousePressed(MouseEvent e) { @Override public void mouseMoved(MouseEvent e) { if (Config.editor().tooltip.interactable.value()) { + EditorPanel.this.mouseStoppedMovingTimer.stop(); EditorPanel.this.hideTooltipTimer.stop(); } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index dbc5d9916..d64cad39e 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -25,6 +25,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -43,6 +44,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList; public class EditorTooltip extends JWindow { + private static final int MOUSE_PAD = 5; + private static final int SMALL_MOVE_THRESHOLD = 10; + private final Gui gui; private final Box content; @@ -52,6 +56,7 @@ public class EditorTooltip extends JWindow { @Nullable private DeclarationSnippetPanel declarationSnippet; + // TODO clamp size public EditorTooltip(Gui gui) { super(); @@ -73,10 +78,6 @@ public EditorTooltip(Gui gui) { MouseEvent.MOUSE_RELEASED ); - // TODO - // - update tooltip with clicked entry declaration - // - add a "bread crumbs" back button - // - open entry tab on ctrl-click or "Got to source" button click this.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { @@ -97,9 +98,9 @@ public void mousePressed(MouseEvent e) { public void mouseDragged(MouseEvent e) { final Point dragStart = EditorTooltip.this.dragStart; if (dragStart != null) { - final Point location = EditorTooltip.this.getLocation(); - location.translate(e.getX() - dragStart.x, e.getY() - dragStart.y); - EditorTooltip.this.setLocation(location); + final Point pos = EditorTooltip.this.getLocation(); + pos.translate(e.getX() - dragStart.x, e.getY() - dragStart.y); + EditorTooltip.this.setLocation(pos); } } }); @@ -111,13 +112,11 @@ public void mouseDragged(MouseEvent e) { * @param target the entry whose information will be displayed */ public void open(Entry target) { - this.openImpl(target); - - // TODO offset from cursor slightly + ensure on-screen - this.setLocation(MouseInfo.getPointerInfo().getLocation()); + this.populateWith(target, true); + this.setVisible(true); } - private void openImpl(Entry target) { + private void populateWith(Entry target, boolean opening) { this.content.removeAll(); @Nullable @@ -230,7 +229,25 @@ public void mouseClicked(MouseEvent e) { } }); - this.declarationSnippet.addSourceSetListener(source -> this.pack()); + { + final Dimension oldSize = opening ? null : this.getSize(); + final Point oldMousePos = MouseInfo.getPointerInfo().getLocation(); + this.declarationSnippet.addSourceSetListener(source -> { + this.pack(); + + if (oldSize == null) { + // opening + if (oldMousePos.distance(MouseInfo.getPointerInfo().getLocation()) < SMALL_MOVE_THRESHOLD) { + this.moveNearCursor(); + } else { + this.moveOnScreen(); + } + } else { + // not opening + this.moveMaintainingAnchor(oldMousePos, oldSize); + } + }); + } if (stopInteraction != null) { this.declarationSnippet.editor.addMouseListener(stopInteraction); @@ -244,10 +261,111 @@ public void mouseClicked(MouseEvent e) { this.add(rowOf(sourceInfo)); } - // TODO clamp size this.pack(); - this.setVisible(true); + if (opening) { + this.moveNearCursor(); + } else { + this.moveOnScreen(); + } + } + + /** + * Moves this so it's near but not under the cursor, favoring the bottom right. + * + *

Also ensures this is entirely on-screen. + */ + private void moveNearCursor() { + if (!this.isShowing()) { + return; + } + + final Dimension size = this.getSize(); + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final Point mousePos = MouseInfo.getPointerInfo().getLocation(); + + final int x = findCoordinateSpace( + size.width, screenSize.width, + mousePos.x - MOUSE_PAD, mousePos.x + MOUSE_PAD + ); + + final int y = findCoordinateSpace( + size.height, screenSize.height, + mousePos.y - MOUSE_PAD, mousePos.y + MOUSE_PAD + ); + + this.setLocation(x, y); + } + + /** + * After resizing, moves this so that the old distance between the cursor and the closest corner remains the same. + * + *

Also ensures this is entirely on-screen. + */ + private void moveMaintainingAnchor(Point oldMousePos, Dimension oldSize) { + if (!this.isShowing()) { + return; + } + + final Point pos = this.getLocationOnScreen(); + + final int oldLeft = oldMousePos.x - pos.x; + final int oldRight = pos.x + oldSize.width - oldMousePos.x; + final boolean anchorRight = oldLeft >= oldRight; + + final int oldTop = oldMousePos.y - pos.y; + final int oldBottom = pos.y + oldSize.height - oldMousePos.y; + final boolean anchorBottom = oldTop >= oldBottom; + + if (anchorRight || anchorBottom) { + final Dimension newSize = this.getSize(); + + final int x; + if (anchorRight) { + final int widthDiff = oldSize.width - newSize.width; + x = pos.x + widthDiff; + } else { + x = pos.x; + } + + final int y; + if (anchorBottom) { + final int heightDiff = oldSize.height - newSize.height; + y = pos.y + heightDiff; + } else { + y = pos.y; + } + + this.setLocation(x, y); + } + + this.moveOnScreen(); + } + + /** + * Ensures this is entirely on-screen. + */ + private void moveOnScreen() { + if (!this.isShowing()) { + return; + } + + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final Dimension size = this.getSize(); + final Point pos = this.getLocationOnScreen(); + + final int xOffScreen = pos.x + size.width - screenSize.width; + final int yOffScreen = pos.y + size.height - screenSize.height; + + final boolean moveX = xOffScreen > 0; + final boolean moveY = yOffScreen > 0; + + if (moveX || moveY) { + final int x = pos.x - (moveX ? xOffScreen : 0); + final int y = pos.y - (moveY ? yOffScreen : 0); + + this.setLocation(x, y); + } } private void navigateOnClick(Entry entry, int modifiers) { @@ -255,7 +373,22 @@ private void navigateOnClick(Entry entry, int modifiers) { this.close(); this.gui.getController().navigateTo(entry); } else { - this.openImpl(entry); + this.populateWith(entry, false); + } + } + + private static int findCoordinateSpace(int size, int screenSize, int mouseMin, int mouseMax) { + final double spaceAfter = screenSize - mouseMax; + if (spaceAfter >= size) { + return mouseMax; + } else { + final int spaceBefore = mouseMin - size; + if (spaceBefore >= 0) { + return spaceBefore; + } else { + // doesn't fit before or after; align with screen edge that gives more space + return spaceAfter < spaceBefore ? 0 : screenSize - size; + } } } From 6ac519ea4b3bb7eb28f2677dac3275743950f64a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 7 Oct 2025 07:34:19 -0700 Subject: [PATCH 048/109] increase padding at outer edges of tooltip rows add tooltip border --- .../enigma/gui/panel/EditorTooltip.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index d64cad39e..8fe47c6da 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -14,7 +14,6 @@ import org.quiltmc.enigma.gui.config.Config; import javax.annotation.Nullable; -import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; @@ -22,6 +21,7 @@ import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.JWindow; +import javax.swing.border.Border; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -42,11 +42,24 @@ import java.util.function.Consumer; import static com.google.common.collect.ImmutableList.toImmutableList; +import static javax.swing.BorderFactory.createEmptyBorder; +import static javax.swing.BorderFactory.createLineBorder; public class EditorTooltip extends JWindow { private static final int MOUSE_PAD = 5; private static final int SMALL_MOVE_THRESHOLD = 10; + private static final int OUTER_ROW_PAD = 8; + private static final int INNER_ROW_PAD = 2; + + private static Border topRowInsetsOf() { + return createEmptyBorder(OUTER_ROW_PAD, OUTER_ROW_PAD, INNER_ROW_PAD, OUTER_ROW_PAD); + } + + private static Border innerRowInsetsOf() { + return createEmptyBorder(INNER_ROW_PAD, OUTER_ROW_PAD, INNER_ROW_PAD, OUTER_ROW_PAD); + } + private final Gui gui; private final Box content; @@ -61,13 +74,14 @@ public EditorTooltip(Gui gui) { super(); this.gui = gui; - // TODO add insets this.content = new Box(BoxLayout.PAGE_AXIS); this.setAlwaysOnTop(true); this.setType(Window.Type.POPUP); this.setLayout(new BorderLayout()); + this.setContentPane(this.content); + this.content.setBorder(createLineBorder(Config.getCurrentSyntaxPaneColors().lineNumbersSelected.value())); Toolkit.getDefaultToolkit().addAWTEventListener( e -> { @@ -128,20 +142,16 @@ public void mousePressed(MouseEvent e) { } }; - if (this.declarationSnippet != null) { - this.declarationSnippet.classHandler.removeListener(); - this.declarationSnippet = null; - } - final Font editorFont = Config.currentFonts().editor.value(); final Font italEditorFont = editorFont.deriveFont(Font.ITALIC); this.add(rowOf(row -> { + row.setBorder(topRowInsetsOf()); // TODO add stat icon if enabled // TODO add class/record/enum/method icon final JLabel from = labelOf("from", italEditorFont); // the italics cause it to overlap with the colon if it has no right padding - from.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 1)); + from.setBorder(createEmptyBorder(0, 0, 0, 1)); row.add(from); row.add(colonLabelOf("")); final Font parentFont; @@ -182,13 +192,16 @@ public void mouseClicked(MouseEvent e) { this.add(new JSeparator()); if (javadoc != null) { - this.add(rowOf(javadocOf(javadoc, italEditorFont, stopInteraction))); + this.add(rowOf(row -> { + row.setBorder(innerRowInsetsOf()); + row.add(javadocOf(javadoc, italEditorFont, stopInteraction)); + })); } if (!paramJavadocs.isEmpty()) { // TODO for some reason the param grid has extra space above and below it final JPanel params = new JPanel(new GridBagLayout()); - params.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + params.setBorder(createEmptyBorder(2, 2, 2, 2)); final GridBagConstraints nameConstraints = new GridBagConstraints(); nameConstraints.gridx = 0; @@ -205,10 +218,18 @@ public void mouseClicked(MouseEvent e) { params.add(paramJavadoc.javadoc, javadocConstraints); } - this.add(params); + this.add(rowOf(row -> { + row.setBorder(innerRowInsetsOf()); + row.add(params); + })); } } + if (this.declarationSnippet != null) { + this.declarationSnippet.classHandler.removeListener(); + this.declarationSnippet = null; + } + { final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() .openClass(target.getTopLevelClass()); @@ -394,7 +415,7 @@ private static int findCoordinateSpace(int size, int screenSize, int mouseMin, i private static JLabel colonLabelOf(String text) { final JLabel label = labelOf(text + ":", Config.currentFonts().editor.value()); - label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 2)); + label.setBorder(createEmptyBorder(0, 0, 0, 2)); return label; } @@ -416,7 +437,7 @@ private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopI text.setBackground(invisibleColorOf()); text.setCaretColor(invisibleColorOf()); text.getCaret().setSelectionVisible(true); - text.setBorder(BorderFactory.createEmptyBorder()); + text.setBorder(createEmptyBorder()); if (stopInteraction != null) { text.addMouseListener(stopInteraction); @@ -470,7 +491,7 @@ private static Box rowOf(Consumer rowInitializer) { final Box row = Box.createHorizontalBox(); rowInitializer.accept(row); row.add(Box.createHorizontalGlue()); - row.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + // row.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); return row; } From 998b0a347d94db2b861baf4db8af85acacb4e89f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 8 Oct 2025 14:41:41 -0700 Subject: [PATCH 049/109] use a JPanel with a GridBagLayout instead of a Box as the content pane for EditorTooltip; allows elements to expand to fill their space double-pack in EditorTooltip's declarationSnippet source set listener to eliminate extra space, because swing { - row.setBorder(topRowInsetsOf()); - // TODO add stat icon if enabled - // TODO add class/record/enum/method icon - final JLabel from = labelOf("from", italEditorFont); - // the italics cause it to overlap with the colon if it has no right padding - from.setBorder(createEmptyBorder(0, 0, 0, 1)); - row.add(from); - row.add(colonLabelOf("")); - final Font parentFont; - @Nullable - final MouseListener parentClicked; - final Entry parent = target.getParent(); - if (stopInteraction == null && parent != null) { - @SuppressWarnings("rawtypes") - final Map attributes = editorFont.getAttributes(); - //noinspection unchecked - attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); - //noinspection unchecked - parentFont = editorFont.deriveFont(attributes); - parentClicked = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - EditorTooltip.this.navigateOnClick(parent, e.getModifiersEx()); - } - }; - } else { - parentFont = editorFont; - parentClicked = null; - } + final AtomicInteger gridY = new AtomicInteger(0); + + // from: ... label + this.addRow( + constraints -> { + setTopRowInsets(constraints); + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }, + row -> { + // TODO add stat icon if enabled + // TODO add class/record/enum/method icon + final JLabel from = labelOf("from", italEditorFont); + // the italics cause it to overlap with the colon if it has no right padding + from.setBorder(createEmptyBorder(0, 0, 0, 1)); + row.add(from); + row.add(colonLabelOf("")); + final Font parentFont; + @Nullable + final MouseListener parentClicked; + final Entry parent = target.getParent(); + if (stopInteraction == null && parent != null) { + @SuppressWarnings("rawtypes") + final Map attributes = editorFont.getAttributes(); + //noinspection unchecked + attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + //noinspection unchecked + parentFont = editorFont.deriveFont(attributes); + parentClicked = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorTooltip.this.navigateOnClick(parent, e.getModifiersEx()); + } + }; + } else { + // TODO navigate to to parent package in a classes docker on ctrl+click + parentFont = editorFont; + parentClicked = null; + } - final JLabel parentLabel = labelOf(this.getParentName(target).orElse(""), parentFont); + final JLabel parentLabel = labelOf(this.getParentName(target).orElse(""), parentFont); - if (parentClicked != null) { - parentLabel.addMouseListener(parentClicked); - } + if (parentClicked != null) { + parentLabel.addMouseListener(parentClicked); + } - row.add(parentLabel); - })); + row.add(parentLabel); + row.add(Box.createHorizontalGlue()); + } + ); // TODO make javadocs and snippet copyable final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - this.add(new JSeparator()); + this.addRow(new JSeparator(), constraints -> { + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }); if (javadoc != null) { - this.add(rowOf(row -> { - row.setBorder(innerRowInsetsOf()); - row.add(javadocOf(javadoc, italEditorFont, stopInteraction)); - })); + this.addRow(javadocOf(javadoc, italEditorFont, stopInteraction), constraints -> { + setInnerRowInsets(constraints); + constraints.anchor = GridBagConstraints.LINE_START; + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }); } if (!paramJavadocs.isEmpty()) { - // TODO for some reason the param grid has extra space above and below it final JPanel params = new JPanel(new GridBagLayout()); - params.setBorder(createEmptyBorder(2, 2, 2, 2)); - - final GridBagConstraints nameConstraints = new GridBagConstraints(); - nameConstraints.gridx = 0; - nameConstraints.anchor = GridBagConstraints.FIRST_LINE_END; - - final GridBagConstraints javadocConstraints = new GridBagConstraints(); - javadocConstraints.gridx = 1; - javadocConstraints.fill = GridBagConstraints.HORIZONTAL; - javadocConstraints.weightx = 1; - javadocConstraints.anchor = GridBagConstraints.LINE_START; + final AtomicInteger paramsGridY = new AtomicInteger(0); for (final ParamJavadoc paramJavadoc : paramJavadocs) { - params.add(paramJavadoc.name, nameConstraints); - params.add(paramJavadoc.javadoc, javadocConstraints); + params.add(paramJavadoc.name, createConstraints(constraints -> { + constraints.gridx = 0; + constraints.gridy = paramsGridY.get(); + constraints.anchor = GridBagConstraints.FIRST_LINE_END; + })); + + params.add(paramJavadoc.javadoc, createConstraints(constraints -> { + constraints.gridx = 1; + constraints.gridy = paramsGridY.getAndIncrement(); + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.anchor = GridBagConstraints.LINE_START; + })); } - this.add(rowOf(row -> { - row.setBorder(innerRowInsetsOf()); - row.add(params); + this.add(params, createConstraints(constraints -> { + setInnerRowInsets(constraints); + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; })); } } @@ -236,7 +266,6 @@ public void mouseClicked(MouseEvent e) { final Component sourceInfo; if (targetTopClassHandle != null) { - // TODO expand right to fill width this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { @@ -255,6 +284,9 @@ public void mouseClicked(MouseEvent e) { final Point oldMousePos = MouseInfo.getPointerInfo().getLocation(); this.declarationSnippet.addSourceSetListener(source -> { this.pack(); + // swing { + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }); } this.pack(); @@ -427,7 +465,6 @@ private static JLabel labelOf(String text, Font font) { return label; } - // TODO for some reason sometimes there's extra space below the text, about one line's worth private static JTextArea javadocOf(String javadoc, Font font, MouseAdapter stopInteraction) { final JTextArea text = new JTextArea(javadoc); text.setLineWrap(true); @@ -479,21 +516,27 @@ private ImmutableList paramJavadocsOf(Entry target, Font font, } } - private static Box rowOf(Component... components) { - return rowOf(row -> { - for (final Component component : components) { - row.add(component); - } - }); + private void addRow(Consumer constrainer, Consumer initializer) { + this.addRow(Box::createHorizontalBox, constrainer, initializer); + } + + private void addRow( + Supplier factory, Consumer constrainer, Consumer initializer + ) { + final C component = factory.get(); + initializer.accept(component); + + this.addRow(component, constrainer); } - private static Box rowOf(Consumer rowInitializer) { - final Box row = Box.createHorizontalBox(); - rowInitializer.accept(row); - row.add(Box.createHorizontalGlue()); - // row.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + private void addRow(Component component, Consumer constrainer) { + this.add(component, createConstraints(constrainer)); + } - return row; + private static GridBagConstraints createConstraints(Consumer initializer) { + final GridBagConstraints constraints = new GridBagConstraints(); + initializer.accept(constraints); + return constraints; } public void close() { From 0dce574e6288ef51e9873375acae4bb3dbd90fd0 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 8 Oct 2025 15:44:47 -0700 Subject: [PATCH 050/109] reject some TODOs --- .../main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 1d814c509..cccd3b3c6 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -77,7 +77,6 @@ private static void setInnerRowInsets(GridBagConstraints constraints) { @Nullable private DeclarationSnippetPanel declarationSnippet; - // TODO clamp size public EditorTooltip(Gui gui) { super(); @@ -164,8 +163,6 @@ public void mousePressed(MouseEvent e) { constraints.gridy = gridY.getAndIncrement(); }, row -> { - // TODO add stat icon if enabled - // TODO add class/record/enum/method icon final JLabel from = labelOf("from", italEditorFont); // the italics cause it to overlap with the colon if it has no right padding from.setBorder(createEmptyBorder(0, 0, 0, 1)); From e12ee8a8123bc14b6ad68aaa47c546afcde90bc6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 8 Oct 2025 18:10:23 -0700 Subject: [PATCH 051/109] ctrl+click on parent package label to naviage to package --- .../quiltmc/enigma/gui/NestedPackages.java | 12 +- .../enigma/gui/panel/EditorTooltip.java | 186 ++++++++++++------ 2 files changed, 140 insertions(+), 58 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NestedPackages.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NestedPackages.java index 27b0514f3..f243743b1 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NestedPackages.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/NestedPackages.java @@ -12,6 +12,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class NestedPackages { private final SortedMutableTreeNode root; @@ -75,9 +76,16 @@ public SortedMutableTreeNode getRoot() { return this.root; } + /** + * @return the path to the passed {@code packageName} if present, or the path the {@link #root} otherwise + */ public TreePath getPackagePath(String packageName) { - var node = this.packageToNode.getOrDefault(packageName, this.root); - return new TreePath(node.getPath()); + return this.getPackagePathOrEmpty(packageName).orElseGet(() -> new TreePath(this.root.getPath())); + } + + public Optional getPackagePathOrEmpty(String packageName) { + return Optional.ofNullable(this.packageToNode.get(packageName)) + .map(node -> new TreePath(node.getPath())); } public ClassSelectorClassNode getClassNode(ClassEntry entry) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index cccd3b3c6..b0bb18425 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -7,11 +7,18 @@ import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.AccessFlags; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.gui.ClassSelector; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; +import org.quiltmc.enigma.gui.docker.AllClassesDocker; +import org.quiltmc.enigma.gui.docker.ClassesDocker; +import org.quiltmc.enigma.gui.docker.DeobfuscatedClassesDocker; +import org.quiltmc.enigma.gui.docker.Docker; +import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; import javax.annotation.Nullable; import javax.swing.Box; @@ -20,6 +27,7 @@ import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.JWindow; +import javax.swing.tree.TreePath; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -35,11 +43,14 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.font.TextAttribute; +import java.util.HashSet; +import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; import static javax.swing.BorderFactory.createEmptyBorder; @@ -154,52 +165,24 @@ public void mousePressed(MouseEvent e) { final AtomicInteger gridY = new AtomicInteger(0); - // from: ... label + // from: label this.addRow( - constraints -> { - setTopRowInsets(constraints); - constraints.anchor = GridBagConstraints.LINE_START; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }, - row -> { - final JLabel from = labelOf("from", italEditorFont); - // the italics cause it to overlap with the colon if it has no right padding - from.setBorder(createEmptyBorder(0, 0, 0, 1)); - row.add(from); - row.add(colonLabelOf("")); - final Font parentFont; - @Nullable - final MouseListener parentClicked; - final Entry parent = target.getParent(); - if (stopInteraction == null && parent != null) { - @SuppressWarnings("rawtypes") - final Map attributes = editorFont.getAttributes(); - //noinspection unchecked - attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); - //noinspection unchecked - parentFont = editorFont.deriveFont(attributes); - parentClicked = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - EditorTooltip.this.navigateOnClick(parent, e.getModifiersEx()); - } - }; - } else { - // TODO navigate to to parent package in a classes docker on ctrl+click - parentFont = editorFont; - parentClicked = null; - } - - final JLabel parentLabel = labelOf(this.getParentName(target).orElse(""), parentFont); - - if (parentClicked != null) { - parentLabel.addMouseListener(parentClicked); + constraints -> { + setTopRowInsets(constraints); + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }, + row -> { + final JLabel from = labelOf("from", italEditorFont); + // the italics cause it to overlap with the colon if it has no right padding + from.setBorder(createEmptyBorder(0, 0, 0, 1)); + row.add(from); + row.add(colonLabelOf("")); + + row.add(this.parentLabelOf(target, editorFont, stopInteraction)); + row.add(Box.createHorizontalGlue()); } - - row.add(parentLabel); - row.add(Box.createHorizontalGlue()); - } ); // TODO make javadocs and snippet copyable @@ -270,7 +253,7 @@ public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - EditorTooltip.this.navigateOnClick(entry, e.getModifiersEx()); + EditorTooltip.this.onEntryClick(entry, e.getModifiersEx()); }); } } @@ -424,7 +407,7 @@ private void moveOnScreen() { } } - private void navigateOnClick(Entry entry, int modifiers) { + private void onEntryClick(Entry entry, int modifiers) { if ((modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { this.close(); this.gui.getController().navigateTo(entry); @@ -545,32 +528,123 @@ public void close() { } } - private Optional getParentName(Entry entry) { - final var builder = new StringBuilder(); + private JLabel parentLabelOf(Entry entry, Font font, @Nullable MouseAdapter stopInteraction) { + final var nameBuilder = new StringBuilder(); final Runnable tryDot = () -> { - if (!builder.isEmpty()) { - builder.insert(0, '.'); + if (!nameBuilder.isEmpty()) { + nameBuilder.insert(0, '.'); } }; - Entry parent = entry.getParent(); + final Entry immediateParent = entry.getParent(); + Entry parent = immediateParent; while (parent != null) { tryDot.run(); - builder.insert(0, this.getSimpleName(parent)); + nameBuilder.insert(0, this.getSimpleName(parent)); parent = parent.getParent(); } - final String packageName = entry.getTopLevelClass().getPackageName(); + final ClassEntry topClass = entry.getTopLevelClass(); + + final String packageName = topClass.getPackageName(); if (packageName != null) { tryDot.run(); - builder.insert(0, packageName.replace('/', '.')); + nameBuilder.insert(0, packageName.replace('/', '.')); + } + + @Nullable + final MouseListener parentClicked; + if (stopInteraction == null && immediateParent != null) { + parentClicked = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorTooltip.this.onEntryClick(immediateParent, e.getModifiersEx()); + } + }; + } else { + parentClicked = packageName == null ? null : this.createPackagedClickedListener(topClass); } - return builder.isEmpty() ? Optional.empty() : Optional.of(builder.toString()); + final JLabel parentLabel = new JLabel(nameBuilder.isEmpty() ? "" : nameBuilder.toString()); + + final Font parentFont; + if (parentClicked != null) { + parentLabel.addMouseListener(parentClicked); + + @SuppressWarnings("rawtypes") + final Map attributes = font.getAttributes(); + //noinspection unchecked + attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + //noinspection unchecked + parentFont = font.deriveFont(attributes); + } else { + parentFont = font; + } + + parentLabel.setFont(parentFont); + + return parentLabel; + } + + @Nullable + private MouseListener createPackagedClickedListener(ClassEntry topClass) { + final List dockers = Stream + .of(AllClassesDocker.class, DeobfuscatedClassesDocker.class, ObfuscatedClassesDocker.class) + .mapMulti((dockerClass, keep) -> { + final ClassesDocker docker = EditorTooltip.this.gui.getDockerManager().getDocker(dockerClass); + + if (docker.getClassSelector().getPackageManager().getClassNode(topClass) != null) { + keep.accept(docker); + } + }) + .toList(); + + if (dockers.isEmpty()) { + return null; + } + + return new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) { + final Set activeDockers = new HashSet<>( + EditorTooltip.this.gui.getDockerManager().getActiveDockers().values() + ); + + final List sortedDockers = dockers.stream() + // active first + .sorted((left, right) -> { + final boolean leftActive = activeDockers.contains(left); + final boolean rightActive = activeDockers.contains(right); + + if (leftActive == rightActive) { + return 0; + } else { + return leftActive ? -1 : 1; + } + }) + .toList(); + + for (final ClassesDocker docker : sortedDockers) { + final ClassSelector selector = docker.getClassSelector(); + final TreePath path = selector.getPackageManager() + .getPackagePathOrEmpty(topClass.getPackageName()) + .orElse(null); + if (path != null) { + selector.setSelectionPath(path); + EditorTooltip.this.gui.openDocker(docker.getClass()); + EditorTooltip.this.close(); + + return; + } + } + } + } + }; } private String getSimpleName(Entry entry) { From 409cc510dda439c392b9ace39baed61ab4c34de1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 8 Oct 2025 18:27:00 -0700 Subject: [PATCH 052/109] fix tooltip separators improve "No source available" message formatting --- .../enigma/gui/panel/EditorTooltip.java | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index b0bb18425..6687cf137 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -79,6 +79,15 @@ private static void setInnerRowInsets(GridBagConstraints constraints) { constraints.insets.bottom = INNER_ROW_PAD; } + private static void setBottomRowInsets(GridBagConstraints constraints) { + constraints.insets.left = OUTER_ROW_PAD; + constraints.insets.right = OUTER_ROW_PAD; + constraints.insets.bottom = OUTER_ROW_PAD; + + constraints.insets.top = INNER_ROW_PAD; + } + + private final Gui gui; private final JPanel content; @@ -189,10 +198,7 @@ public void mousePressed(MouseEvent e) { final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - this.addRow(new JSeparator(), constraints -> { - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }); + this.addSeparator(gridY.getAndIncrement()); if (javadoc != null) { this.addRow(javadocOf(javadoc, italEditorFont, stopInteraction), constraints -> { @@ -244,7 +250,6 @@ public void mousePressed(MouseEvent e) { final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider() .openClass(target.getTopLevelClass()); - final Component sourceInfo; if (targetTopClassHandle != null) { this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); @@ -286,18 +291,25 @@ public void mouseClicked(MouseEvent e) { this.declarationSnippet.editor.addMouseListener(stopInteraction); } - sourceInfo = this.declarationSnippet.ui; + this.addRow(this.declarationSnippet.ui, constraints -> { + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + }); } else { - sourceInfo = labelOf("No source available", italEditorFont); - } + this.addSeparator(gridY.getAndIncrement()); - this.addRow(sourceInfo, constraints -> { - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - constraints.anchor = GridBagConstraints.LINE_START; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }); + this.addRow(labelOf("No source available", italEditorFont), constraints -> { + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridx = 0; + constraints.gridy = gridY.getAndIncrement(); + setInnerRowInsets(constraints); + }); + } } this.pack(); @@ -309,6 +321,15 @@ public void mouseClicked(MouseEvent e) { } } + private void addSeparator(int gridY) { + this.addRow(new JSeparator(), constraints -> { + constraints.gridx = 0; + constraints.gridy = gridY; + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + }); + } + /** * Moves this so it's near but not under the cursor, favoring the bottom right. * From ee995dcfb2539dd2c06438db756aa309927df0e2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 9 Oct 2025 07:17:34 -0700 Subject: [PATCH 053/109] respect tooltip ineractable config for parent package clicking add z_ prefix to tooltip test input to avoid changing SearchMappingsTest --- .../enigma/gui/panel/EditorTooltip.java | 35 ++++++++----------- .../{tooltip => z_tooltip}/Constructors.java | 2 +- .../input/{tooltip => z_tooltip}/Enums.java | 2 +- .../input/{tooltip => z_tooltip}/Fields.java | 2 +- .../input/{tooltip => z_tooltip}/Lambdas.java | 2 +- .../input/{tooltip => z_tooltip}/Methods.java | 2 +- .../input/{tooltip => z_tooltip}/Records.java | 2 +- 7 files changed, 21 insertions(+), 26 deletions(-) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Constructors.java (88%) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Enums.java (86%) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Fields.java (90%) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Lambdas.java (94%) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Methods.java (88%) rename enigma/src/test/java/org/quiltmc/enigma/input/{tooltip => z_tooltip}/Records.java (92%) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java index 6687cf137..3864f9bce 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java @@ -79,15 +79,6 @@ private static void setInnerRowInsets(GridBagConstraints constraints) { constraints.insets.bottom = INNER_ROW_PAD; } - private static void setBottomRowInsets(GridBagConstraints constraints) { - constraints.insets.left = OUTER_ROW_PAD; - constraints.insets.right = OUTER_ROW_PAD; - constraints.insets.bottom = OUTER_ROW_PAD; - - constraints.insets.top = INNER_ROW_PAD; - } - - private final Gui gui; private final JPanel content; @@ -579,21 +570,27 @@ private JLabel parentLabelOf(Entry entry, Font font, @Nullable MouseAdapter s @Nullable final MouseListener parentClicked; - if (stopInteraction == null && immediateParent != null) { - parentClicked = new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - EditorTooltip.this.onEntryClick(immediateParent, e.getModifiersEx()); - } - }; + if (stopInteraction == null) { + if (immediateParent != null) { + parentClicked = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + EditorTooltip.this.onEntryClick(immediateParent, e.getModifiersEx()); + } + }; + } else { + parentClicked = packageName == null ? null : this.createPackagedClickedListener(topClass); + } } else { - parentClicked = packageName == null ? null : this.createPackagedClickedListener(topClass); + parentClicked = null; } final JLabel parentLabel = new JLabel(nameBuilder.isEmpty() ? "" : nameBuilder.toString()); final Font parentFont; - if (parentClicked != null) { + if (parentClicked == null) { + parentFont = font; + } else { parentLabel.addMouseListener(parentClicked); @SuppressWarnings("rawtypes") @@ -602,8 +599,6 @@ public void mouseClicked(MouseEvent e) { attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); //noinspection unchecked parentFont = font.deriveFont(attributes); - } else { - parentFont = font; } parentLabel.setFont(parentFont); diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java similarity index 88% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java index 7462fb0d7..4651b542f 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; public class Constructors { public Constructors(String outerArg) { diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java similarity index 86% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java index 8b89275ab..a35871652 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; public enum Enums { FIRST, SECOND(2), diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java similarity index 90% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java index d818046d6..18d8fb91d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; public class Fields { static final String STATIC_FIELD = "static field"; diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java similarity index 94% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java index 24f00e469..fd1f86b77 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; import java.util.ArrayList; import java.util.List; diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java similarity index 88% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java index 7dedc397f..7547a0623 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; public abstract class Methods { private void parameterized(int i, Boolean z, Methods methods) { } diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java similarity index 92% rename from enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java rename to enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java index 37a6ce35b..5164bae71 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.tooltip; +package org.quiltmc.enigma.input.z_tooltip; public record Records() { public record WithStaticField(Boolean truth) { From 27308426c7ade2324b753a13ede38f47ffa5a168 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 9 Oct 2025 12:34:57 -0700 Subject: [PATCH 054/109] use global focus listener to ensure tooltip is closed when neither it nor its editor has focus --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 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 1ae2cbc86..1d3353570 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 @@ -148,6 +148,21 @@ public void focusLost(FocusEvent e) { MouseEvent.MOUSE_PRESSED ); + Toolkit.getDefaultToolkit().addAWTEventListener( + e -> { + if (e instanceof FocusEvent focusEvent) { + final Component gainer = focusEvent.getID() == FocusEvent.FOCUS_GAINED + ? focusEvent.getComponent() + : focusEvent.getOppositeComponent(); + + if (gainer == null || !SwingUtilities.isDescendingFrom(gainer, this.ui)) { + this.closeTooltip(); + } + } + }, + FocusEvent.FOCUS_EVENT_MASK + ); + final MouseAdapter editorMouseAdapter = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { @@ -161,8 +176,8 @@ public void mouseClicked(MouseEvent e) { public void mousePressed(MouseEvent mouseEvent) { EditorPanel.this.tooltip.setVisible(false); EditorPanel.this.mouseStoppedMovingTimer.stop(); - EditorPanel.this.showTokenTooltipTimer.stop(); - EditorPanel.this.hideTokenTooltipTimer.stop(); + EditorPanel.this.showTooltipTimer.stop(); + EditorPanel.this.hideTooltipTimer.stop(); } @Override @@ -183,24 +198,6 @@ public void mouseMoved(MouseEvent e) { } }; - this.tooltip.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - if (e.getOppositeComponent() != EditorPanel.this.editor) { - EditorPanel.this.closeTooltip(); - } - } - }); - - this.editor.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - if (e.getOppositeComponent() != EditorPanel.this.tooltip) { - EditorPanel.this.closeTooltip(); - } - } - }); - this.editor.addMouseListener(editorMouseAdapter); this.editor.addMouseMotionListener(editorMouseAdapter); this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); From 156b84fc3f92f8916fcceb2001c6eed2aa8023f7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 11 Oct 2025 11:44:37 -0700 Subject: [PATCH 055/109] move consumeMousePosition... methods to GuiUtil --- .../enigma/gui/panel/BaseEditorPanel.java | 51 ++----------------- .../quiltmc/enigma/gui/panel/EditorPanel.java | 2 + .../org/quiltmc/enigma/gui/util/GuiUtil.java | 47 +++++++++++++++++ 3 files changed, 53 insertions(+), 47 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 6aefb1d72..56d67ece4 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 @@ -45,14 +45,11 @@ import javax.swing.text.BadLocationException; import javax.swing.text.Highlighter.HighlightPainter; import java.awt.Color; -import java.awt.Component; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; -import java.awt.MouseInfo; -import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -68,6 +65,8 @@ import java.util.regex.Matcher; import java.util.stream.Stream; +import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; + public class BaseEditorPanel { protected final JPanel ui = new JPanel(); protected final JEditorPane editor = new JEditorPane(); @@ -284,7 +283,8 @@ protected void consumeEditorMouseTarget(BiConsumer> action) { * @param onNoTarget the action to run when the mouse is not over a token that resolves to an entry */ protected void consumeEditorMouseTarget(BiConsumer> action, Runnable onNoTarget) { - BaseEditorPanel.consumeMousePositionIn(this.editor, + consumeMousePositionIn( + this.editor, (absoluteMouse, relativeMouse) -> Optional.of(relativeMouse) .map(this.editor::viewToModel2D) .filter(textPos -> textPos >= 0) @@ -303,49 +303,6 @@ protected void consumeEditorMouseTarget(BiConsumer> action, Runn ); } - /** - * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) - */ - protected static void consumeMousePositionIn(Component component, BiConsumer inAction) { - consumeMousePositionIn(component, inAction, pos -> { }); - } - - /** - * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) - */ - protected static void consumeMousePositionOut(Component component, Consumer outAction) { - consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); - } - - /** - * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse - * position and its position relative to the passed {@code component} to the passed {@code inAction}.
- * Otherwise, passes the absolute mouse position to the passed {@code outAction}. - * - * @param component the component which may contain the mouse pointer - * @param inAction the action to run if the mouse is inside the passed {@code component}; - * receives the mouse's absolute position and its position relative to the component - * @param outAction the action to run if the mouse is outside the passed {@code component}; - * receives the mouse's absolute position - */ - protected static void consumeMousePositionIn( - Component component, BiConsumer inAction, Consumer outAction - ) { - final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); - if (component.isShowing()) { - final Point componentPos = component.getLocationOnScreen(); - final Point relativePos = new Point(absolutePos); - relativePos.translate(-componentPos.x, -componentPos.y); - - if (component.contains(relativePos)) { - inAction.accept(absolutePos, relativePos); - return; - } - } - - outAction.accept(absolutePos); - } - protected void initEditorPane(JPanel editorPane) { final GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index 1d3353570..bb2d992e6 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 @@ -42,6 +42,8 @@ import javax.swing.ToolTipManager; import javax.swing.text.JTextComponent; +import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; +import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionOut; import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; import static java.awt.event.InputEvent.CTRL_DOWN_MASK; 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 09c93bb77..c342e622e 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 @@ -30,9 +30,12 @@ import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.Color; +import java.awt.Component; import java.awt.Cursor; import java.awt.Desktop; import java.awt.Font; +import java.awt.MouseInfo; +import java.awt.Point; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; @@ -57,6 +60,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.BiConsumer; import java.util.function.Consumer; public final class GuiUtil { @@ -330,6 +334,49 @@ public static void putKeyBindAction( } } + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + public static void consumeMousePositionIn(Component component, BiConsumer inAction) { + consumeMousePositionIn(component, inAction, pos -> { }); + } + + /** + * @see #consumeMousePositionIn(Component, BiConsumer, Consumer) + */ + public static void consumeMousePositionOut(Component component, Consumer outAction) { + consumeMousePositionIn(component, (absolut, relative) -> { }, outAction); + } + + /** + * If the passed {@code component} {@link Component#contains(Point) contains} the mouse, passes the absolute mouse + * position and its position relative to the passed {@code component} to the passed {@code inAction}.
+ * Otherwise, passes the absolute mouse position to the passed {@code outAction}. + * + * @param component the component which may contain the mouse pointer + * @param inAction the action to run if the mouse is inside the passed {@code component}; + * receives the mouse's absolute position and its position relative to the component + * @param outAction the action to run if the mouse is outside the passed {@code component}; + * receives the mouse's absolute position + */ + public static void consumeMousePositionIn( + Component component, BiConsumer inAction, Consumer outAction + ) { + final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); + if (component.isShowing()) { + final Point componentPos = component.getLocationOnScreen(); + final Point relativePos = new Point(absolutePos); + relativePos.translate(-componentPos.x, -componentPos.y); + + if (component.contains(relativePos)) { + inAction.accept(absolutePos, relativePos); + return; + } + } + + outAction.accept(absolutePos); + } + public enum FocusCondition { /** * @see JComponent#WHEN_IN_FOCUSED_WINDOW From 3c6c9e14d0bf6b2020c7303d2fdaf01de6fdbc71 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 09:38:54 -0700 Subject: [PATCH 056/109] rename EditorTooltip -> EntryTooltip use GridBagConstraintsBuilder in EntryTooltip respect scale config in EntryTooltip --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 38 ++- .../{EditorTooltip.java => EntryTooltip.java} | 249 ++++++++---------- 2 files changed, 137 insertions(+), 150 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/{EditorTooltip.java => EntryTooltip.java} (73%) 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 bb2d992e6..38de8cfcf 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 @@ -55,7 +55,7 @@ public class EditorPanel extends BaseEditorPanel { private final EditorPopupMenu popupMenu; // DIY tooltip because JToolTip can't be moved or resized - private final EditorTooltip tooltip = new EditorTooltip(this.gui); + private final EntryTooltip entryTooltip = new EntryTooltip(this.gui); @Nullable private Token lastMouseTargetToken; @@ -66,7 +66,7 @@ public class EditorPanel extends BaseEditorPanel { this.consumeEditorMouseTarget( (token, entry) -> { this.hideTooltipTimer.stop(); - if (this.tooltip.isVisible()) { + if (this.entryTooltip.isVisible()) { this.showTooltipTimer.stop(); if (!token.equals(this.lastMouseTargetToken)) { @@ -79,7 +79,7 @@ public class EditorPanel extends BaseEditorPanel { } }, () -> consumeMousePositionIn( - this.tooltip.getContentPane(), + this.entryTooltip.getContentPane(), (absolute, relative) -> this.hideTooltipTimer.stop(), absolute -> { this.lastMouseTargetToken = null; @@ -95,7 +95,7 @@ public class EditorPanel extends BaseEditorPanel { ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { this.consumeEditorMouseTarget((token, entry) -> { if (token.equals(this.lastMouseTargetToken)) { - this.tooltip.setVisible(true); + this.entryTooltip.setVisible(true); this.openTooltip(entry); } }); @@ -143,8 +143,8 @@ public void focusLost(FocusEvent e) { // global listener so tooltip hides even if clicking outside editor Toolkit.getDefaultToolkit().addAWTEventListener( e -> { - if (e.getID() == MouseEvent.MOUSE_PRESSED && this.tooltip.isVisible()) { - consumeMousePositionOut(this.tooltip.getContentPane(), absolute -> this.closeTooltip()); + if (e.getID() == MouseEvent.MOUSE_PRESSED && this.entryTooltip.isVisible()) { + consumeMousePositionOut(this.entryTooltip.getContentPane(), absolute -> this.closeTooltip()); } }, MouseEvent.MOUSE_PRESSED @@ -176,7 +176,7 @@ public void mouseClicked(MouseEvent e) { @Override public void mousePressed(MouseEvent mouseEvent) { - EditorPanel.this.tooltip.setVisible(false); + EditorPanel.this.entryTooltip.setVisible(false); EditorPanel.this.mouseStoppedMovingTimer.stop(); EditorPanel.this.showTooltipTimer.stop(); EditorPanel.this.hideTooltipTimer.stop(); @@ -210,9 +210,9 @@ public void mouseMoved(MouseEvent e) { this.showTooltipTimer.setRepeats(false); this.hideTooltipTimer.setRepeats(false); - this.tooltip.setVisible(false); + this.entryTooltip.setVisible(false); - this.tooltip.addMouseListener(new MouseAdapter() { + this.entryTooltip.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (!Config.editor().tooltip.interactable.value()) { @@ -241,7 +241,7 @@ public void mousePressed(MouseEvent e) { } }); - this.tooltip.addMouseMotionListener(new MouseAdapter() { + this.entryTooltip.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { if (Config.editor().tooltip.interactable.value()) { @@ -289,7 +289,7 @@ public void keyTyped(KeyEvent event) { } private void closeTooltip() { - this.tooltip.close(); + this.entryTooltip.close(); this.lastMouseTargetToken = null; this.mouseStoppedMovingTimer.stop(); this.showTooltipTimer.stop(); @@ -297,7 +297,7 @@ private void closeTooltip() { } private void openTooltip(Entry target) { - this.tooltip.open(target); + this.entryTooltip.open(target); } public void onRename(boolean isNewMapping) { @@ -358,7 +358,7 @@ protected void setClassHandleImpl( @Override public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { SwingUtilities.invokeLater(() -> EditorPanel.this.listeners.forEach(l -> l - .onTitleChanged(EditorPanel.this, EditorPanel.this.getSimpleClassName())) + .onTitleChanged(EditorPanel.this, EditorPanel.this.getSimpleClassName())) ); } @@ -394,6 +394,18 @@ protected void setCursorReference(EntryReference, Entry> ref) { this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref)); } + @Override + public void offsetEditorZoom(int zoomAmount) { + super.offsetEditorZoom(zoomAmount); + this.entryTooltip.setZoom(zoomAmount); + } + + @Override + public void resetEditorZoom() { + super.resetEditorZoom(); + this.entryTooltip.resetZoom(); + } + public void addListener(EditorActionListener listener) { this.listeners.add(listener); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java similarity index 73% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 3864f9bce..96b209add 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorTooltip.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java @@ -19,6 +19,8 @@ import org.quiltmc.enigma.gui.docker.DeobfuscatedClassesDocker; import org.quiltmc.enigma.gui.docker.Docker; import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; +import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; +import org.quiltmc.enigma.gui.util.ScaleUtil; import javax.annotation.Nullable; import javax.swing.Box; @@ -30,7 +32,6 @@ import javax.swing.tree.TreePath; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; @@ -48,47 +49,31 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.BorderFactory.createLineBorder; -public class EditorTooltip extends JWindow { +public class EntryTooltip extends JWindow { private static final int MOUSE_PAD = 5; private static final int SMALL_MOVE_THRESHOLD = 10; - private static final int OUTER_ROW_PAD = 8; - private static final int INNER_ROW_PAD = 2; - - private static void setTopRowInsets(GridBagConstraints constraints) { - constraints.insets.left = OUTER_ROW_PAD; - constraints.insets.right = OUTER_ROW_PAD; - constraints.insets.top = OUTER_ROW_PAD; - - constraints.insets.bottom = INNER_ROW_PAD; - } - - private static void setInnerRowInsets(GridBagConstraints constraints) { - constraints.insets.left = OUTER_ROW_PAD; - constraints.insets.right = OUTER_ROW_PAD; - - constraints.insets.top = INNER_ROW_PAD; - constraints.insets.bottom = INNER_ROW_PAD; - } + private static final int ROW_OUTER_INSET = 8; + private static final int ROW_INNER_INSET = 2; private final Gui gui; private final JPanel content; + private int zoomAmount; + @Nullable private Point dragStart; @Nullable private DeclarationSnippetPanel declarationSnippet; - public EditorTooltip(Gui gui) { + public EntryTooltip(Gui gui) { super(); this.gui = gui; @@ -104,7 +89,7 @@ public EditorTooltip(Gui gui) { Toolkit.getDefaultToolkit().addAWTEventListener( e -> { if (e instanceof MouseEvent mouseEvent && mouseEvent.getID() == MouseEvent.MOUSE_RELEASED) { - EditorTooltip.this.dragStart = null; + EntryTooltip.this.dragStart = null; } }, MouseEvent.MOUSE_RELEASED @@ -114,13 +99,13 @@ public EditorTooltip(Gui gui) { @Override public void mousePressed(MouseEvent e) { if (Config.editor().tooltip.interactable.value()) { - EditorTooltip.this.dragStart = e.getButton() == MouseEvent.BUTTON1 + EntryTooltip.this.dragStart = e.getButton() == MouseEvent.BUTTON1 ? new Point(e.getX(), e.getY()) : null; e.consume(); } else { - EditorTooltip.this.close(); + EntryTooltip.this.close(); } } }); @@ -128,11 +113,11 @@ public void mousePressed(MouseEvent e) { this.addMouseMotionListener(new MouseAdapter() { @Override public void mouseDragged(MouseEvent e) { - final Point dragStart = EditorTooltip.this.dragStart; + final Point dragStart = EntryTooltip.this.dragStart; if (dragStart != null) { - final Point pos = EditorTooltip.this.getLocation(); + final Point pos = EntryTooltip.this.getLocation(); pos.translate(e.getX() - dragStart.x, e.getY() - dragStart.y); - EditorTooltip.this.setLocation(pos); + EntryTooltip.this.setLocation(pos); } } }); @@ -155,51 +140,52 @@ private void populateWith(Entry target, boolean opening) { final MouseAdapter stopInteraction = Config.editor().tooltip.interactable.value() ? null : new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - EditorTooltip.this.close(); + EntryTooltip.this.close(); e.consume(); } }; - final Font editorFont = Config.currentFonts().editor.value(); - final Font italEditorFont = editorFont.deriveFont(Font.ITALIC); + final Font editorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value()); + final Font italEditorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); final AtomicInteger gridY = new AtomicInteger(0); - // from: label - this.addRow( - constraints -> { - setTopRowInsets(constraints); - constraints.anchor = GridBagConstraints.LINE_START; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }, - row -> { - final JLabel from = labelOf("from", italEditorFont); - // the italics cause it to overlap with the colon if it has no right padding - from.setBorder(createEmptyBorder(0, 0, 0, 1)); - row.add(from); - row.add(colonLabelOf("")); - - row.add(this.parentLabelOf(target, editorFont, stopInteraction)); - row.add(Box.createHorizontalGlue()); - } - ); + { + final Box parentLabelRow = Box.createHorizontalBox(); + + final JLabel from = labelOf("from", italEditorFont); + // the italics cause it to overlap with the colon if it has no right padding + from.setBorder(createEmptyBorder(0, 0, 0, 1)); + parentLabelRow.add(from); + parentLabelRow.add(colonLabelOf("", editorFont)); + + parentLabelRow.add(this.parentLabelOf(target, editorFont, stopInteraction)); + parentLabelRow.add(Box.createHorizontalGlue()); + + this.add(parentLabelRow, GridBagConstraintsBuilder.create() + .pos(0, gridY.getAndIncrement()) + .insets(ROW_OUTER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET) + .anchor(GridBagConstraints.LINE_START) + .build() + ); + } // TODO make javadocs and snippet copyable final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); - final ImmutableList paramJavadocs = this.paramJavadocsOf(target, italEditorFont, stopInteraction); + final ImmutableList paramJavadocs = + this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { this.addSeparator(gridY.getAndIncrement()); if (javadoc != null) { - this.addRow(javadocOf(javadoc, italEditorFont, stopInteraction), constraints -> { - setInnerRowInsets(constraints); - constraints.anchor = GridBagConstraints.LINE_START; - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }); + this.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() + .pos(0, gridY.getAndIncrement()) + .insets(ROW_INNER_INSET, ROW_OUTER_INSET) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .anchor(GridBagConstraints.LINE_START) + .build() + ); } if (!paramJavadocs.isEmpty()) { @@ -207,28 +193,28 @@ public void mousePressed(MouseEvent e) { final AtomicInteger paramsGridY = new AtomicInteger(0); for (final ParamJavadoc paramJavadoc : paramJavadocs) { - params.add(paramJavadoc.name, createConstraints(constraints -> { - constraints.gridx = 0; - constraints.gridy = paramsGridY.get(); - constraints.anchor = GridBagConstraints.FIRST_LINE_END; - })); - - params.add(paramJavadoc.javadoc, createConstraints(constraints -> { - constraints.gridx = 1; - constraints.gridy = paramsGridY.getAndIncrement(); - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - constraints.anchor = GridBagConstraints.LINE_START; - })); + params.add(paramJavadoc.name, GridBagConstraintsBuilder.create() + .pos(0, paramsGridY.get()) + .anchor(GridBagConstraints.FIRST_LINE_END) + .build() + ); + + params.add(paramJavadoc.javadoc, GridBagConstraintsBuilder.create() + .pos(1, paramsGridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .anchor(GridBagConstraints.LINE_START) + .build() + ); } - this.add(params, createConstraints(constraints -> { - setInnerRowInsets(constraints); - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - })); + this.add(params, GridBagConstraintsBuilder.create() + .insets(ROW_INNER_INSET, ROW_OUTER_INSET) + .pos(0, gridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build() + ); } } @@ -244,12 +230,14 @@ public void mousePressed(MouseEvent e) { if (targetTopClassHandle != null) { this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle); + this.declarationSnippet.offsetEditorZoom(this.zoomAmount); + this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { - EditorTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - EditorTooltip.this.onEntryClick(entry, e.getModifiersEx()); + EntryTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { + EntryTooltip.this.onEntryClick(entry, e.getModifiersEx()); }); } } @@ -282,24 +270,24 @@ public void mouseClicked(MouseEvent e) { this.declarationSnippet.editor.addMouseListener(stopInteraction); } - this.addRow(this.declarationSnippet.ui, constraints -> { - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - constraints.anchor = GridBagConstraints.LINE_START; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - }); + this.add(this.declarationSnippet.ui, GridBagConstraintsBuilder.create() + .pos(0, gridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .anchor(GridBagConstraints.LINE_START) + .build() + ); } else { this.addSeparator(gridY.getAndIncrement()); - this.addRow(labelOf("No source available", italEditorFont), constraints -> { - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - constraints.anchor = GridBagConstraints.LINE_START; - constraints.gridx = 0; - constraints.gridy = gridY.getAndIncrement(); - setInnerRowInsets(constraints); - }); + this.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() + .pos(0, gridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .anchor(GridBagConstraints.LINE_START) + .insets(ROW_INNER_INSET, ROW_OUTER_INSET) + .build() + ); } } @@ -312,15 +300,6 @@ public void mouseClicked(MouseEvent e) { } } - private void addSeparator(int gridY) { - this.addRow(new JSeparator(), constraints -> { - constraints.gridx = 0; - constraints.gridy = gridY; - constraints.weightx = 1; - constraints.fill = GridBagConstraints.HORIZONTAL; - }); - } - /** * Moves this so it's near but not under the cursor, favoring the bottom right. * @@ -443,8 +422,8 @@ private static int findCoordinateSpace(int size, int screenSize, int mouseMin, i } } - private static JLabel colonLabelOf(String text) { - final JLabel label = labelOf(text + ":", Config.currentFonts().editor.value()); + private static JLabel colonLabelOf(String text, Font font) { + final JLabel label = labelOf(text + ":", font); label.setBorder(createEmptyBorder(0, 0, 0, 2)); return label; @@ -479,7 +458,9 @@ private static Color invisibleColorOf() { return new Color(0, 0, 0, 0); } - private ImmutableList paramJavadocsOf(Entry target, Font font, MouseAdapter stopInteraction) { + private ImmutableList paramJavadocsOf( + Entry target, Font nameFont, Font javadocFont, MouseAdapter stopInteraction + ) { final EnigmaProject project = this.gui.getController().getProject(); final EntryIndex entryIndex = project.getJarIndex().getIndex(EntryIndex.class); @@ -494,8 +475,8 @@ private ImmutableList paramJavadocsOf(Entry target, Font font, .mapMulti((param, add) -> { final EntryMapping mapping = remapper.getMapping(param); if (mapping.javadoc() != null) { - final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName()); - final JTextArea javadoc = javadocOf(mapping.javadoc(), font, stopInteraction); + final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName(), nameFont); + final JTextArea javadoc = javadocOf(mapping.javadoc(), javadocFont, stopInteraction); add.accept(new ParamJavadoc(name, javadoc)); } @@ -508,27 +489,12 @@ private ImmutableList paramJavadocsOf(Entry target, Font font, } } - private void addRow(Consumer constrainer, Consumer initializer) { - this.addRow(Box::createHorizontalBox, constrainer, initializer); - } - - private void addRow( - Supplier factory, Consumer constrainer, Consumer initializer - ) { - final C component = factory.get(); - initializer.accept(component); - - this.addRow(component, constrainer); - } - - private void addRow(Component component, Consumer constrainer) { - this.add(component, createConstraints(constrainer)); - } - - private static GridBagConstraints createConstraints(Consumer initializer) { - final GridBagConstraints constraints = new GridBagConstraints(); - initializer.accept(constraints); - return constraints; + private void addSeparator(int gridY) { + this.add(new JSeparator(), GridBagConstraintsBuilder.create() + .pos(0, gridY) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build()); } public void close() { @@ -537,6 +503,7 @@ public void close() { if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); + this.declarationSnippet = null; } } @@ -575,7 +542,7 @@ private JLabel parentLabelOf(Entry entry, Font font, @Nullable MouseAdapter s parentClicked = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - EditorTooltip.this.onEntryClick(immediateParent, e.getModifiersEx()); + EntryTooltip.this.onEntryClick(immediateParent, e.getModifiersEx()); } }; } else { @@ -611,7 +578,7 @@ private MouseListener createPackagedClickedListener(ClassEntry topClass) { final List dockers = Stream .of(AllClassesDocker.class, DeobfuscatedClassesDocker.class, ObfuscatedClassesDocker.class) .mapMulti((dockerClass, keep) -> { - final ClassesDocker docker = EditorTooltip.this.gui.getDockerManager().getDocker(dockerClass); + final ClassesDocker docker = EntryTooltip.this.gui.getDockerManager().getDocker(dockerClass); if (docker.getClassSelector().getPackageManager().getClassNode(topClass) != null) { keep.accept(docker); @@ -628,7 +595,7 @@ private MouseListener createPackagedClickedListener(ClassEntry topClass) { public void mouseClicked(MouseEvent e) { if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) { final Set activeDockers = new HashSet<>( - EditorTooltip.this.gui.getDockerManager().getActiveDockers().values() + EntryTooltip.this.gui.getDockerManager().getActiveDockers().values() ); final List sortedDockers = dockers.stream() @@ -652,8 +619,8 @@ public void mouseClicked(MouseEvent e) { .orElse(null); if (path != null) { selector.setSelectionPath(path); - EditorTooltip.this.gui.openDocker(docker.getClass()); - EditorTooltip.this.close(); + EntryTooltip.this.gui.openDocker(docker.getClass()); + EntryTooltip.this.close(); return; } @@ -679,5 +646,13 @@ private String getSimpleName(Entry entry) { return ""; } + public void setZoom(int amount) { + this.zoomAmount = amount; + } + + public void resetZoom() { + this.zoomAmount = 0; + } + private record ParamJavadoc(JLabel name, JTextArea javadoc) { } } From d67ca608a20cdfbe40555c3225b5f937ec6c96ab Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 12:09:21 -0700 Subject: [PATCH 057/109] use GridBagConstraintsBuilder in initEditorPane --- .../enigma/gui/panel/BaseEditorPanel.java | 13 ++++--- .../quiltmc/enigma/gui/panel/EditorPanel.java | 34 +++++++++---------- 2 files changed, 22 insertions(+), 25 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 56d67ece4..1ab1064cc 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 @@ -304,13 +304,12 @@ protected void consumeEditorMouseTarget(BiConsumer> action, Runn } protected void initEditorPane(JPanel editorPane) { - final GridBagConstraints constraints = new GridBagConstraints(); - constraints.gridx = 0; - constraints.gridy = 0; - constraints.weightx = 1.0; - constraints.weighty = 1.0; - constraints.fill = GridBagConstraints.BOTH; - editorPane.add(this.editorScrollPane, constraints); + editorPane.add(this.editorScrollPane, GridBagConstraintsBuilder.create() + .pos(0, 0) + .weight(1, 1) + .fill(GridBagConstraints.BOTH) + .build() + ); } public void offsetEditorZoom(int zoomAmount) { 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 38de8cfcf..8ef373a08 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 @@ -15,12 +15,12 @@ import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.syntaxpain.DefaultSyntaxAction; import org.quiltmc.syntaxpain.SyntaxDocument; import java.awt.Component; import java.awt.GridBagConstraints; -import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; @@ -309,26 +309,24 @@ public void onRename(boolean isNewMapping) { @Override protected void initEditorPane(JPanel editorPane) { - final GridBagConstraints navigatorConstraints = new GridBagConstraints(); - navigatorConstraints.gridx = 0; - navigatorConstraints.gridy = 0; - navigatorConstraints.weightx = 1.0; - navigatorConstraints.weighty = 1.0; - navigatorConstraints.anchor = GridBagConstraints.FIRST_LINE_END; - navigatorConstraints.insets = new Insets(32, 32, 32, 32); - navigatorConstraints.ipadx = 16; - navigatorConstraints.ipady = 16; - editorPane.add(this.navigatorPanel, navigatorConstraints); + editorPane.add(this.navigatorPanel, GridBagConstraintsBuilder.create() + .pos(0, 0) + .weight(1, 1) + .anchor(GridBagConstraints.FIRST_LINE_END) + .insets(32) + .padding(16) + .build() + ); super.initEditorPane(editorPane); - final var quickFindConstraints = new GridBagConstraints(); - quickFindConstraints.gridx = 0; - quickFindConstraints.weightx = 1.0; - quickFindConstraints.weighty = 0; - quickFindConstraints.anchor = GridBagConstraints.PAGE_END; - quickFindConstraints.fill = GridBagConstraints.HORIZONTAL; - editorPane.add(this.quickFindToolBar, quickFindConstraints); + editorPane.add(this.quickFindToolBar, GridBagConstraintsBuilder.create() + .pos(0, 1) + .weightX(1) + .anchor(GridBagConstraints.PAGE_END) + .fill(GridBagConstraints.HORIZONTAL) + .build() + ); } @Nullable From 8d58d8383cab6de4a208fd0632f6d6a524f7f6aa Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 14:12:17 -0700 Subject: [PATCH 058/109] make tooltip focusable; enables copying tooltip content --- .../main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java | 5 ++++- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 2 +- 2 files changed, 5 insertions(+), 2 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 8ef373a08..dabec06ca 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 @@ -157,7 +157,10 @@ public void focusLost(FocusEvent e) { ? focusEvent.getComponent() : focusEvent.getOppositeComponent(); - if (gainer == null || !SwingUtilities.isDescendingFrom(gainer, this.ui)) { + if (gainer == null || !( + SwingUtilities.isDescendingFrom(gainer, this.ui) + || SwingUtilities.isDescendingFrom(gainer, this.entryTooltip.getContentPane()) + )) { this.closeTooltip(); } } 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 96b209add..2e966de07 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 @@ -74,7 +74,7 @@ public class EntryTooltip extends JWindow { private DeclarationSnippetPanel declarationSnippet; public EntryTooltip(Gui gui) { - super(); + super(gui.getFrame()); this.gui = gui; this.content = new JPanel(new GridBagLayout()); From 693a430fd964dc1837de638af6e724e6adf8150a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 15:06:36 -0700 Subject: [PATCH 059/109] fix hiding tooltip immediately after re-populating it simplify tooltip focus and hiding logic --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 55 +++++-------------- .../enigma/gui/panel/EntryTooltip.java | 24 +++++++- 2 files changed, 35 insertions(+), 44 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 dabec06ca..5b688d815 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 @@ -21,7 +21,6 @@ import java.awt.Component; import java.awt.GridBagConstraints; -import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -43,7 +42,6 @@ import javax.swing.text.JTextComponent; import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; -import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionOut; import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; import static java.awt.event.InputEvent.CTRL_DOWN_MASK; @@ -104,7 +102,7 @@ public class EditorPanel extends BaseEditorPanel { private final Timer hideTooltipTimer = new Timer( ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, - e -> this.closeTooltip() + e -> this.entryTooltip.close() ); private final List listeners = new ArrayList<>(); @@ -140,38 +138,12 @@ public void focusLost(FocusEvent e) { this.popupMenu = new EditorPopupMenu(this, gui); this.editor.setComponentPopupMenu(this.popupMenu.getUi()); - // global listener so tooltip hides even if clicking outside editor - Toolkit.getDefaultToolkit().addAWTEventListener( - e -> { - if (e.getID() == MouseEvent.MOUSE_PRESSED && this.entryTooltip.isVisible()) { - consumeMousePositionOut(this.entryTooltip.getContentPane(), absolute -> this.closeTooltip()); - } - }, - MouseEvent.MOUSE_PRESSED - ); - - Toolkit.getDefaultToolkit().addAWTEventListener( - e -> { - if (e instanceof FocusEvent focusEvent) { - final Component gainer = focusEvent.getID() == FocusEvent.FOCUS_GAINED - ? focusEvent.getComponent() - : focusEvent.getOppositeComponent(); - - if (gainer == null || !( - SwingUtilities.isDescendingFrom(gainer, this.ui) - || SwingUtilities.isDescendingFrom(gainer, this.entryTooltip.getContentPane()) - )) { - this.closeTooltip(); - } - } - }, - FocusEvent.FOCUS_EVENT_MASK - ); + this.entryTooltip.addCloseListener(this::onTooltipClose); - final MouseAdapter editorMouseAdapter = new MouseAdapter() { + this.editor.addMouseListener(new MouseAdapter() { @Override - public void mouseClicked(MouseEvent e) { - if ((e.getModifiersEx() & CTRL_DOWN_MASK) != 0 && e.getButton() == MouseEvent.BUTTON1) { + public void mouseClicked(MouseEvent e1) { + if ((e1.getModifiersEx() & CTRL_DOWN_MASK) != 0 && e1.getButton() == MouseEvent.BUTTON1) { // ctrl + left click EditorPanel.this.navigateToCursorReference(); } @@ -186,28 +158,28 @@ public void mousePressed(MouseEvent mouseEvent) { } @Override - public void mouseReleased(MouseEvent e) { - switch (e.getButton()) { + public void mouseReleased(MouseEvent e1) { + switch (e1.getButton()) { case MouseEvent.BUTTON3 -> // Right click - EditorPanel.this.editor.setCaretPosition(EditorPanel.this.editor.viewToModel2D(e.getPoint())); + EditorPanel.this.editor.setCaretPosition(EditorPanel.this.editor.viewToModel2D(e1.getPoint())); case 4 -> // Back navigation gui.getController().openPreviousReference(); case 5 -> // Forward navigation gui.getController().openNextReference(); } } + }); + this.editor.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { EditorPanel.this.mouseStoppedMovingTimer.restart(); } - }; + }); - this.editor.addMouseListener(editorMouseAdapter); - this.editor.addMouseMotionListener(editorMouseAdapter); this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); - this.editorScrollPane.getViewport().addChangeListener(e -> this.closeTooltip()); + this.editorScrollPane.getViewport().addChangeListener(e -> this.entryTooltip.close()); this.mouseStoppedMovingTimer.setRepeats(false); this.showTooltipTimer.setRepeats(false); @@ -291,8 +263,7 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } - private void closeTooltip() { - this.entryTooltip.close(); + private void onTooltipClose() { this.lastMouseTargetToken = null; this.mouseStoppedMovingTimer.stop(); this.showTooltipTimer.stop(); 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 2e966de07..bbf0a19f6 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 @@ -43,6 +43,8 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.awt.font.TextAttribute; import java.util.HashSet; import java.util.List; @@ -65,6 +67,8 @@ public class EntryTooltip extends JWindow { private final Gui gui; private final JPanel content; + private final Set closeListeners = new HashSet<>(); + private int zoomAmount; @Nullable @@ -121,6 +125,13 @@ public void mouseDragged(MouseEvent e) { } } }); + + this.addWindowFocusListener(new WindowAdapter() { + @Override + public void windowLostFocus(WindowEvent e) { + EntryTooltip.this.close(); + } + }); } /** @@ -170,7 +181,6 @@ public void mousePressed(MouseEvent e) { ); } - // TODO make javadocs and snippet copyable final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); @@ -300,6 +310,14 @@ public void mouseClicked(MouseEvent e) { } } + public void addCloseListener(Runnable listener) { + this.closeListeners.add(listener); + } + + public void removeCloseListener(Runnable listener) { + this.closeListeners.remove(listener); + } + /** * Moves this so it's near but not under the cursor, favoring the bottom right. * @@ -499,12 +517,14 @@ private void addSeparator(int gridY) { public void close() { this.setVisible(false); - this.content.removeAll(); + // this.content.removeAll(); if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); this.declarationSnippet = null; } + + this.closeListeners.forEach(Runnable::run); } private JLabel parentLabelOf(Entry entry, Font font, @Nullable MouseAdapter stopInteraction) { From a749ee0c305fdd4a668ff801e5421dcdbc0f0fbc Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 15:32:27 -0700 Subject: [PATCH 060/109] remove tooltip to editor click forward as it didn't work consistently remove unecessary insteractable tooltip click listener --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 30 ------------------- .../enigma/gui/panel/EntryTooltip.java | 20 +++++++------ 2 files changed, 11 insertions(+), 39 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 5b688d815..299ecbf7e 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 @@ -29,7 +29,6 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -187,35 +186,6 @@ public void mouseMoved(MouseEvent e) { this.entryTooltip.setVisible(false); - this.entryTooltip.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - if (!Config.editor().tooltip.interactable.value()) { - // if not interactable, forward event to editor - consumeMousePositionIn(EditorPanel.this.editor, (absolutMousePosition, editorMousePosition) -> { - final MouseEvent editorMouseEvent = new MouseEvent( - EditorPanel.this.editor, e.getID(), e.getWhen(), e.getModifiersEx(), - editorMousePosition.x, editorMousePosition.y, - absolutMousePosition.x, absolutMousePosition.y, - e.getClickCount(), e.isPopupTrigger(), e.getButton() - ); - - for (final MouseListener listener : EditorPanel.this.editor.getMouseListeners()) { - listener.mousePressed(editorMouseEvent); - if (editorMouseEvent.isConsumed()) { - break; - } - } - }); - - e.consume(); - } else { - EditorPanel.this.mouseStoppedMovingTimer.stop(); - EditorPanel.this.hideTooltipTimer.stop(); - } - } - }); - this.entryTooltip.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { 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 bbf0a19f6..a1b821405 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 @@ -242,16 +242,18 @@ public void mousePressed(MouseEvent e) { this.declarationSnippet.offsetEditorZoom(this.zoomAmount); - this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) { - EntryTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - EntryTooltip.this.onEntryClick(entry, e.getModifiersEx()); - }); + if (stopInteraction == null) { + this.declarationSnippet.editor.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + EntryTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { + EntryTooltip.this.onEntryClick(entry, e.getModifiersEx()); + }); + } } - } - }); + }); + } { final Dimension oldSize = opening ? null : this.getSize(); From 484bbb49344338fa6ae039dd1c71b7fbc67a4025 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 16:51:04 -0700 Subject: [PATCH 061/109] add EntryTooltip.repopulated field for workaround --- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 4 +++- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 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 299ecbf7e..8094cb3af 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 @@ -172,7 +172,9 @@ public void mouseReleased(MouseEvent e1) { this.editor.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { - EditorPanel.this.mouseStoppedMovingTimer.restart(); + if (!EditorPanel.this.entryTooltip.hasRepopulated()) { + EditorPanel.this.mouseStoppedMovingTimer.restart(); + } } }); 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 a1b821405..3fcd28ee2 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 @@ -70,6 +70,7 @@ public class EntryTooltip extends JWindow { private final Set closeListeners = new HashSet<>(); private int zoomAmount; + private boolean repopulated; @Nullable private Point dragStart; @@ -134,6 +135,13 @@ public void windowLostFocus(WindowEvent e) { }); } + // Sometimes when re-populating and resizing+moving, the cursor may be briefly over the parent EditorPanel. + // This is used to stop EditorPanel from starting its mouseStoppedMovingTimer which may reset the tooltip to the + // token under the cursor, discarding the re-populated content. + public boolean hasRepopulated() { + return this.repopulated; + } + /** * Opens this tooltip and populates it with information about the passed {@code target}. * @@ -145,6 +153,7 @@ public void open(Entry target) { } private void populateWith(Entry target, boolean opening) { + this.repopulated = !opening; this.content.removeAll(); @Nullable @@ -518,8 +527,9 @@ private void addSeparator(int gridY) { } public void close() { + this.repopulated = false; this.setVisible(false); - // this.content.removeAll(); + this.content.removeAll(); if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); From cbcf0b4db17d33fb2af50cb6a93718b65d18b852 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 17:39:33 -0700 Subject: [PATCH 062/109] deobfuscate parent package --- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 3fcd28ee2..60d27ea43 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 @@ -560,7 +560,8 @@ private JLabel parentLabelOf(Entry entry, Font font, @Nullable MouseAdapter s final ClassEntry topClass = entry.getTopLevelClass(); - final String packageName = topClass.getPackageName(); + final String packageName = this.gui.getController().getProject().getRemapper() + .deobfuscate(topClass).getPackageName(); if (packageName != null) { tryDot.run(); @@ -644,15 +645,19 @@ public void mouseClicked(MouseEvent e) { }) .toList(); + final String packageName = EntryTooltip.this.gui.getController().getProject().getRemapper() + .deobfuscate(topClass) + .getPackageName(); + for (final ClassesDocker docker : sortedDockers) { final ClassSelector selector = docker.getClassSelector(); final TreePath path = selector.getPackageManager() - .getPackagePathOrEmpty(topClass.getPackageName()) + .getPackagePathOrEmpty(packageName) .orElse(null); if (path != null) { + EntryTooltip.this.close(); selector.setSelectionPath(path); EntryTooltip.this.gui.openDocker(docker.getClass()); - EntryTooltip.this.close(); return; } From 5a46dc29306fa191949b65745bf27b1ae2e808c3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 18:44:46 -0700 Subject: [PATCH 063/109] focus class selector when navigating to parent package from tooltip --- .../java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 60d27ea43..f5e2ea8ca 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 @@ -645,8 +645,8 @@ public void mouseClicked(MouseEvent e) { }) .toList(); - final String packageName = EntryTooltip.this.gui.getController().getProject().getRemapper() - .deobfuscate(topClass) + final String packageName = EntryTooltip.this.gui.getController().getProject().getRemapper() + .deobfuscate(topClass) .getPackageName(); for (final ClassesDocker docker : sortedDockers) { @@ -656,9 +656,11 @@ public void mouseClicked(MouseEvent e) { .orElse(null); if (path != null) { EntryTooltip.this.close(); - selector.setSelectionPath(path); EntryTooltip.this.gui.openDocker(docker.getClass()); + selector.setSelectionPath(path); + selector.requestFocus(); + return; } } From 7bfbb6e5783f8ddd3a1951c7c97f14b08c76a125 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 19:01:44 -0700 Subject: [PATCH 064/109] scroll to class selector path when navigating from entry tooltip parent --- .../src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 1 + 1 file changed, 1 insertion(+) 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 f5e2ea8ca..e4e8ab55c 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 @@ -659,6 +659,7 @@ public void mouseClicked(MouseEvent e) { EntryTooltip.this.gui.openDocker(docker.getClass()); selector.setSelectionPath(path); + selector.scrollPathToVisible(path); selector.requestFocus(); return; From c0e225a6a42325c9dde09d64c144e5933d298896 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 12 Oct 2025 19:32:32 -0700 Subject: [PATCH 065/109] move Config.persistentEditorQuickFind to EditorConfig.persistentQuickFind --- .../main/java/org/quiltmc/enigma/gui/config/Config.java | 3 --- .../java/org/quiltmc/enigma/gui/config/EditorConfig.java | 8 ++++++-- .../quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java | 8 ++++---- .../java/org/quiltmc/enigma/gui/panel/EditorPanel.java | 4 ++-- .../java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index f0768a2e5..72d5ba10d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -78,9 +78,6 @@ public final class Config extends ReflectiveConfig { @Comment("You shouldn't enable options in this section unless you know what you're doing!") public final DevSection development = new DevSection(); - @Comment("Whether editors' quick find toolbars should remain visible when they lose focus.") - public final TrackedValue persistentEditorQuickFind = this.value(true); - /** * The look and feel stored in the config: do not use this unless setting! Use {@link #activeThemeChoice} instead, * since look and feel is final once loaded. 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 e2247dbbf..1cb512c0b 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 @@ -4,9 +4,13 @@ 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 EditorConfig extends ReflectiveConfig { - @Comment("The settings for the editor tooltip.") - public final EditorTooltipSection tooltip = new EditorTooltipSection(); + @Comment("Whether editors' quick find toolbars should remain visible when they lose focus.") + public final TrackedValue persistentQuickFind = this.value(true); + + @Comment("The settings for the editor's entry tooltip.") + public final EditorTooltipSection entryTooltip = new EditorTooltipSection(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java index 131289707..e68d0969d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java @@ -64,15 +64,15 @@ public EnigmaQuickFindToolBar() { this.persistentCheckBox.addActionListener(this); this.persistentCheckBox.addItemListener(e -> { final boolean selected = this.persistentCheckBox.isSelected(); - if (selected != Config.main().persistentEditorQuickFind.value()) { - Config.main().persistentEditorQuickFind.setValue(selected); + if (selected != Config.editor().persistentQuickFind.value()) { + Config.editor().persistentQuickFind.setValue(selected); } // request focus so when it's lost this may be dismissed this.requestFocus(); }); - this.persistentCheckBox.setSelected(Config.main().persistentEditorQuickFind.value()); - Config.main().persistentEditorQuickFind.registerCallback(callback -> { + this.persistentCheckBox.setSelected(Config.editor().persistentQuickFind.value()); + Config.editor().persistentQuickFind.registerCallback(callback -> { final Boolean configured = callback.value(); if (this.persistentCheckBox.isSelected() != configured) { this.persistentCheckBox.setSelected(configured); 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 8094cb3af..1c9ffe03c 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 @@ -59,7 +59,7 @@ public class EditorPanel extends BaseEditorPanel { // avoid finding the mouse entry every mouse movement update private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - if (Config.editor().tooltip.enable.value()) { + if (Config.editor().entryTooltip.enable.value()) { this.consumeEditorMouseTarget( (token, entry) -> { this.hideTooltipTimer.stop(); @@ -191,7 +191,7 @@ public void mouseMoved(MouseEvent e) { this.entryTooltip.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { - if (Config.editor().tooltip.interactable.value()) { + if (Config.editor().entryTooltip.interactable.value()) { EditorPanel.this.mouseStoppedMovingTimer.stop(); EditorPanel.this.hideTooltipTimer.stop(); } 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 e4e8ab55c..39fe0e63f 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 @@ -103,7 +103,7 @@ public EntryTooltip(Gui gui) { this.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - if (Config.editor().tooltip.interactable.value()) { + if (Config.editor().entryTooltip.interactable.value()) { EntryTooltip.this.dragStart = e.getButton() == MouseEvent.BUTTON1 ? new Point(e.getX(), e.getY()) : null; @@ -157,7 +157,7 @@ private void populateWith(Entry target, boolean opening) { this.content.removeAll(); @Nullable - final MouseAdapter stopInteraction = Config.editor().tooltip.interactable.value() ? null : new MouseAdapter() { + final MouseAdapter stopInteraction = Config.editor().entryTooltip.interactable.value() ? null : new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { EntryTooltip.this.close(); From e573b6c768faf8bae6a314bbaa923f2c813b34c7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 13 Oct 2025 15:45:10 -0700 Subject: [PATCH 066/109] rename config getter methods to match their ids and update Config javadoc --- .../main/java/org/quiltmc/enigma/gui/Gui.java | 4 ++-- .../org/quiltmc/enigma/gui/config/Config.java | 19 +++++++++++++----- .../enigma/gui/config/keybind/KeyBinds.java | 4 ++-- .../org/quiltmc/enigma/gui/docker/Dock.java | 20 +++++++++---------- .../org/quiltmc/enigma/gui/docker/Docker.java | 2 +- .../gui/docker/component/DockerSelector.java | 2 +- 6 files changed, 30 insertions(+), 21 deletions(-) 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 588772bc5..76add4c4f 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 @@ -171,8 +171,8 @@ private void setupDockers() { this.dockerManager.registerDocker(new AllClassesDocker(this)); this.dockerManager.registerDocker(new DeobfuscatedClassesDocker(this)); - if (Config.dockers().buttonLocations.value().isEmpty()) { - Config.dockers().updateButtonLocations(this.dockerManager); + if (Config.docker().buttonLocations.value().isEmpty()) { + Config.docker().updateButtonLocations(this.dockerManager); } // set default docker sizes diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index 72d5ba10d..3210180aa 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -34,9 +34,18 @@ import java.nio.file.Paths; /** - * The Enigma config is separated into five different files: {@link Config the main config (this one)}, - * {@link NetConfig the networking configuration}, {@link KeyBindConfig the keybinding configuration}, - * {@link DockerConfig the docker configuration}, and {@link DecompilerConfig the decompiler configuration}. + * Enigma config is separated into several {@value #FORMAT} files with names matching the methods used to access them: + *

    + *
  • {@link #main()} (this one) + *
  • {@link #net()} (networking) + *
  • {@link #keybind()} + *
  • {@link #docker()} + *
  • {@link #decompiler()} + *
  • {@link #editor()} + *
+ * + * {@value #THEME_FAMILY} also holds a config file for each theme; + * the active theme is accessible via {@link #currentTheme()}. */ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) @Processor("processChange") @@ -113,11 +122,11 @@ public static StatsSection stats() { return main().stats; } - public static DockerConfig dockers() { + public static DockerConfig docker() { return DOCKER; } - public static KeyBindConfig keyBinds() { + public static KeyBindConfig keybind() { return KEYBIND; } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/keybind/KeyBinds.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/keybind/KeyBinds.java index 036ecd838..9ff85d63a 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/keybind/KeyBinds.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/keybind/KeyBinds.java @@ -95,7 +95,7 @@ public static Map> getEditableKeyBindsByCategory() { public static void loadConfig() { for (KeyBind keyBind : CONFIGURABLE_KEY_BINDS) { - keyBind.deserializeCombinations(Config.keyBinds().getKeyCodes(keyBind)); + keyBind.deserializeCombinations(Config.keybind().getKeyCodes(keyBind)); } resetEditableKeyBinds(); @@ -107,7 +107,7 @@ public static void saveConfig() { KeyBind editedKeyBind = editableKeyBinds.get(i); if (!editedKeyBind.equals(keyBind)) { keyBind.setFrom(editedKeyBind); - Config.keyBinds().setBind(editedKeyBind); + Config.keybind().setBind(editedKeyBind); } } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Dock.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Dock.java index 61a287885..f9552ab80 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Dock.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Dock.java @@ -69,7 +69,7 @@ public Dock(Gui gui, Docker.Side side) { */ public void restoreState(DockerManager manager) { // restore docker state - DockerConfig.SelectedDockers hostedDockers = Config.dockers().getSelectedDockers(this.side); + DockerConfig.SelectedDockers hostedDockers = Config.docker().getSelectedDockers(this.side); hostedDockers.asMap().forEach((id, location) -> this.host(manager.getDocker(id), location)); this.restoreDividerState(true); @@ -82,18 +82,18 @@ public void restoreState(DockerManager manager) { public void restoreDividerState(boolean init) { // restore vertical divider state if (this.isSplit) { - this.splitPane.setDividerLocation(Config.dockers().getVerticalDividerLocation(this.side)); + this.splitPane.setDividerLocation(Config.docker().getVerticalDividerLocation(this.side)); } // restore horizontal divider state JSplitPane parentSplitPane = this.getParentSplitPane(); - int location = Config.dockers().getHorizontalDividerLocation(this.side); + int location = Config.docker().getHorizontalDividerLocation(this.side); // hack fix: if the right dock is closed while the left dock is open, the divider location is saved as if the left dock is open, // thereby offsetting the divider location by the width of the left dock. which means, if the right dock is reopened while the left dock is closed, // the divider location is too far to the left by the width of the left dock. so here we offset the location to avoid that. - if (init && this.side == Docker.Side.RIGHT && !this.gui.getSplitLeft().getLeftComponent().isVisible() && Config.dockers().savedWithLeftDockerOpen.value()) { - location += Config.dockers().getHorizontalDividerLocation(Docker.Side.LEFT); + if (init && this.side == Docker.Side.RIGHT && !this.gui.getSplitLeft().getLeftComponent().isVisible() && Config.docker().savedWithLeftDockerOpen.value()) { + location += Config.docker().getHorizontalDividerLocation(Docker.Side.LEFT); } parentSplitPane.setDividerLocation(location); @@ -103,16 +103,16 @@ public void saveDividerState() { if (this.isVisible()) { // save vertical divider state if (this.isSplit) { - Config.dockers().setVerticalDividerLocation(this.side, this.splitPane.getDividerLocation()); + Config.docker().setVerticalDividerLocation(this.side, this.splitPane.getDividerLocation()); } // save horizontal divider state JSplitPane parentSplitPane = this.getParentSplitPane(); - Config.dockers().setHorizontalDividerLocation(this.side, parentSplitPane.getDividerLocation()); + Config.docker().setHorizontalDividerLocation(this.side, parentSplitPane.getDividerLocation()); // hack if (this.side == Docker.Side.RIGHT) { - Config.dockers().savedWithLeftDockerOpen.setValue(this.gui.getSplitLeft().getLeftComponent().isVisible(), true); + Config.docker().savedWithLeftDockerOpen.setValue(this.gui.getSplitLeft().getLeftComponent().isVisible(), true); } } } @@ -141,7 +141,7 @@ public void host(Docker docker, Docker.VerticalLocation verticalLocation) { } public void host(Docker docker, Docker.VerticalLocation verticalLocation, boolean avoidEmptySpace) { - Config.dockers().getSelectedDockers(this.side).add(docker.getId(), verticalLocation); + Config.docker().getSelectedDockers(this.side).add(docker.getId(), verticalLocation); Dock dock = Util.findDock(docker); if (dock != null) { @@ -183,7 +183,7 @@ public void host(Docker docker, Docker.VerticalLocation verticalLocation, boolea parent.remove(button); (verticalLocation == Docker.VerticalLocation.TOP ? selector.getTopSelector() : selector.getBottomSelector()).add(button); button.setSide(this.side); - Config.dockers().putButtonLocation(docker, this.side, verticalLocation); + Config.docker().putButtonLocation(docker, this.side, verticalLocation); button.getParent().revalidate(); button.getParent().repaint(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Docker.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Docker.java index d9f63e6fc..4a1127b12 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Docker.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/Docker.java @@ -72,7 +72,7 @@ public DockerButton getButton() { * @return the position of the docker's button in the selector panels. this also represents where the docker will open when its button is clicked cannot use {@link Docker.VerticalLocation#FULL} */ public final Location getButtonLocation() { - Location savedLocation = Config.dockers().getButtonLocation(this.getId()); + Location savedLocation = Config.docker().getButtonLocation(this.getId()); return savedLocation == null ? this.getPreferredButtonLocation() : savedLocation; } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/component/DockerSelector.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/component/DockerSelector.java index 3b422b954..f1a89e076 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/component/DockerSelector.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/docker/component/DockerSelector.java @@ -97,7 +97,7 @@ private boolean dropButton(DockerButton button, MouseEvent event) { if (hoveredPanel != null) { hoveredPanel.add(button); button.setSide(this.side); - Config.dockers().putButtonLocation(button.getDocker(), this.side, hoveredPanel.equals(this.bottomSelector) ? Docker.VerticalLocation.BOTTOM : Docker.VerticalLocation.TOP); + Config.docker().putButtonLocation(button.getDocker(), this.side, hoveredPanel.equals(this.bottomSelector) ? Docker.VerticalLocation.BOTTOM : Docker.VerticalLocation.TOP); return true; } From f6d5eb1473c9d7b78856d75d2db6a9a8b4221259 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 12:38:33 -0700 Subject: [PATCH 067/109] fix another tooltip focus issue --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 18 ++++++++++++++++++ 1 file changed, 18 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 1c9ffe03c..d4b0f3d7d 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 @@ -29,6 +29,8 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -53,6 +55,7 @@ public class EditorPanel extends BaseEditorPanel { // DIY tooltip because JToolTip can't be moved or resized private final EntryTooltip entryTooltip = new EntryTooltip(this.gui); + private final WindowAdapter guiLostFocusListener; @Nullable private Token lastMouseTargetToken; @@ -138,6 +141,15 @@ public void focusLost(FocusEvent e) { this.editor.setComponentPopupMenu(this.popupMenu.getUi()); this.entryTooltip.addCloseListener(this::onTooltipClose); + this.guiLostFocusListener = new WindowAdapter() { + @Override + public void windowLostFocus(WindowEvent e) { + if (e.getOppositeWindow() != EditorPanel.this.entryTooltip) { + EditorPanel.this.entryTooltip.close(); + } + } + }; + this.gui.getFrame().addWindowFocusListener(this.guiLostFocusListener); this.editor.addMouseListener(new MouseAdapter() { @Override @@ -287,6 +299,12 @@ public static EditorPanel byUi(Component ui) { return null; } + @Override + public void destroy() { + super.destroy(); + this.gui.getFrame().removeWindowFocusListener(this.guiLostFocusListener); + } + public NavigatorPanel getNavigatorPanel() { return this.navigatorPanel; } From 5b3e86e3a4beec448ec0c9c8d283bfa725bb8e57 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 13:37:11 -0700 Subject: [PATCH 068/109] fix bug preventing RecordGetterFindingVisitor from finding inner records' getters make RecordGetterFindingVisitor's getter map accessible fix DeclarationSnippetPanel for record component getters --- .../gui/panel/DeclarationSnippetPanel.java | 80 ++++++++++++++----- .../enigma/impl/plugin/BuiltinPlugin.java | 10 ++- .../RecordComponentProposalService.java | 9 ++- .../plugin/RecordGetterFindingService.java | 40 ++++++++++ .../plugin/RecordGetterFindingVisitor.java | 16 ++-- 5 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java 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 1bd3467a0..c8a40dc29 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 @@ -27,8 +27,10 @@ import com.github.javaparser.ast.stmt.BlockStmt; import com.github.javaparser.ast.stmt.ExpressionStmt; import com.github.javaparser.ast.stmt.Statement; +import com.google.common.collect.BiMap; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; +import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; @@ -40,11 +42,13 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; +import org.quiltmc.enigma.impl.plugin.RecordGetterFindingService; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.LineNumbersRuler; import org.tinylog.Logger; +import javax.annotation.Nullable; import javax.swing.JViewport; import java.awt.Color; import java.util.Comparator; @@ -73,7 +77,11 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl .ifPresent(lineNumbers -> lineNumbers.deinstall(this.editor)); this.addSourceSetListener(source -> { - final Token unBoundedToken = this.navigateToTokenImpl(source.getIndex().getDeclarationToken(target)); + @Nullable + final Token unBoundedToken = this.resolveTarget(source, target) + .map(Target::token) + .map(this::navigateToTokenImpl) + .orElse(null); if (unBoundedToken == null) { // the source isn't very useful if it couldn't be trimmed and the declaration couldn't be navigated to // set this text so it doesn't waste space or cause confusion @@ -94,43 +102,47 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.editor.getCaret().setSelectionVisible(true); } - private Snippet createSnippet(DecompiledClassSource source, Entry target) { - final Token targetToken = source.getIndex().getDeclarationToken(target); - - if (targetToken == null) { - // This can happen as a result of #252: Issue with lost parameter connection. - // This can also happen when the token is from a library. - return null; - } + private Snippet createSnippet(DecompiledClassSource source, Entry targetEntry) { + return this.resolveTarget(source, targetEntry) + .map(target -> this.createSnippet(source, target.token, target.entry)) + .orElse(null); + } + private Snippet createSnippet(DecompiledClassSource source, Token targetToken, Entry targetEntry) { final Result snippet; - if (target instanceof ClassEntry targetClass) { + if (targetEntry instanceof ClassEntry targetClass) { snippet = this.findClassSnippet(source, targetToken, targetClass); - } else if (target instanceof MethodEntry targetMethod) { + } else if (targetEntry instanceof MethodEntry targetMethod) { snippet = this.findMethodSnippet(source, targetToken, targetMethod); - } else if (target instanceof FieldEntry targetField) { + } else if (targetEntry instanceof FieldEntry targetField) { snippet = this.findFieldSnippet(source, targetToken, targetField); - } else if (target instanceof LocalVariableEntry targetLocal) { + } else if (targetEntry instanceof LocalVariableEntry targetLocal) { snippet = this.getVariableSnippet(source, targetToken, targetLocal); } else { // this should never be reached Logger.error( "Error trimming tooltip for '{}': unrecognized target entry type!", - this.getFullDeobfuscatedName(target) + this.getFullDeobfuscatedName(targetEntry) ); + return null; } return snippet.unwrapOrElse(error -> { - Logger.error( - "Error finding declaration of '{}' for tooltip: {}", - this.getFullDeobfuscatedName(target), - error - ); + this.logDeclarationSearchError(targetEntry, error); + return null; }); } + private void logDeclarationSearchError(Entry targetEntry, String error) { + Logger.error( + "Error searching for declaration of '{}' for tooltip: {}", + this.getFullDeobfuscatedName(targetEntry), + error + ); + } + private Result getVariableSnippet( DecompiledClassSource source, Token target, LocalVariableEntry targetEntry ) { @@ -252,7 +264,6 @@ private Result>, String> getNodeType(ClassEnt .orElseGet(() -> Result.err(NO_ENTRY_DEFINITION)); } - // TODO fix this for record component getters once there's a RecordIndex private Result findMethodSnippet( DecompiledClassSource source, Token target, MethodEntry targetEntry ) { @@ -495,4 +506,33 @@ private static Snippet toSnippet(LineIndexer lineIndexer, Position startPos, Pos return new Snippet(start, end + 1); } + + private Optional resolveTarget(DecompiledClassSource source, Entry targetEntry) { + return Optional.ofNullable(source.getIndex().getDeclarationToken(targetEntry)) + .map(token -> new Target(token, targetEntry)) + .or(() -> { + if (targetEntry instanceof MethodEntry targetMethod) { + // try to find record component getter's corresponding field instead + return this.gui.getController() + .getProject() + .getEnigma() + .getService(JarIndexerService.TYPE, RecordGetterFindingService.ID) + .map(service -> (RecordGetterFindingService) service) + .map(RecordGetterFindingService::getGettersByField) + .map(BiMap::inverse) + .map(recordFieldByGetter -> recordFieldByGetter.get(targetMethod)) + .flatMap(recordField -> Optional + .ofNullable(source.getIndex().getDeclarationToken(recordField)) + .map(fieldToken -> new Target(fieldToken, recordField)) + ); + } else { + // This can happen as a result of #252: Issue with lost parameter connection. + // This can also happen when the token is from a library. + + return Optional.empty(); + } + }); + } + + private record Target(Token token, Entry entry) { } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java index b50a65143..cc6d4eeb8 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -1,5 +1,7 @@ package org.quiltmc.enigma.impl.plugin; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; import org.quiltmc.enigma.api.EnigmaPlugin; @@ -64,11 +66,11 @@ public String getId() { } private static void registerRecordNamingService(EnigmaPluginContext ctx) { - final Map fieldToGetter = new HashMap<>(); - final RecordGetterFindingVisitor visitor = new RecordGetterFindingVisitor(fieldToGetter); + final BiMap gettersByField = HashBiMap.create(); + final RecordGetterFindingVisitor visitor = new RecordGetterFindingVisitor(gettersByField); - ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:record_component_indexer")); - ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(fieldToGetter)); + ctx.registerService(JarIndexerService.TYPE, ctx1 -> new RecordGetterFindingService(visitor)); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(gettersByField)); } private static void registerSpecializedMethodNamingService(EnigmaPluginContext ctx) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java index 9b1f7b507..135e8a859 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.impl.plugin; +import com.google.common.collect.BiMap; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; @@ -18,7 +19,9 @@ import java.util.List; import java.util.Map; -public record RecordComponentProposalService(Map fieldToGetter) implements NameProposalService { +public record RecordComponentProposalService(BiMap gettersByField) implements NameProposalService { + public static final String ID = "enigma:record_component_proposer"; + @Nullable @Override public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex index) { @@ -88,12 +91,12 @@ public void validateProposedMapping(Entry entry, EntryMapping mapping, boolea } public boolean isGetter(FieldEntry obfFieldEntry, MethodEntry method) { - var getter = this.fieldToGetter.get(obfFieldEntry); + var getter = this.gettersByField.get(obfFieldEntry); return getter != null && getter.equals(method); } @Override public String getId() { - return "enigma:record_component_proposer"; + return ID; } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java new file mode 100644 index 000000000..a9a96310e --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java @@ -0,0 +1,40 @@ +package org.quiltmc.enigma.impl.plugin; + +import com.google.common.collect.BiMap; +import org.objectweb.asm.tree.ClassNode; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; + +import java.util.Set; + +public class RecordGetterFindingService implements JarIndexerService { + public static final String ID = "enigma:record_component_indexer"; + + private final RecordGetterFindingVisitor visitor; + + RecordGetterFindingService(RecordGetterFindingVisitor visitor) { + this.visitor = visitor; + } + + public BiMap getGettersByField() { + return this.visitor.getGettersByField(); + } + + @Override + public void acceptJar(Set scope, ProjectClassProvider classProvider, JarIndex jarIndex) { + for (String className : scope) { + ClassNode node = classProvider.get(className); + if (node != null) { + node.accept(this.visitor); + } + } + } + + @Override + public String getId() { + return ID; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java index e05bcae68..d14a4a2be 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.impl.plugin; +import com.google.common.collect.BiMap; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; @@ -18,19 +19,22 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import java.util.HashSet; -import java.util.Map; import java.util.Set; final class RecordGetterFindingVisitor extends ClassVisitor { private ClassEntry clazz; - private final Map fieldToMethod; + private final BiMap gettersByField; private final Set recordComponents = new HashSet<>(); private final Set fields = new HashSet<>(); private final Set methods = new HashSet<>(); - RecordGetterFindingVisitor(Map fieldToMethod) { + RecordGetterFindingVisitor(BiMap gettersByField) { super(Enigma.ASM_VERSION); - this.fieldToMethod = fieldToMethod; + this.gettersByField = gettersByField; + } + + public BiMap getGettersByField() { + return this.gettersByField; } @Override @@ -103,11 +107,11 @@ private void collectResults() { && instructions.get(2).getOpcode() == Opcodes.ALOAD && instructions.get(3) instanceof FieldInsnNode fieldInsn && fieldInsn.getOpcode() == Opcodes.GETFIELD - && fieldInsn.owner.equals(this.clazz.getName()) + && fieldInsn.owner.equals(this.clazz.getFullName()) && fieldInsn.desc.equals(field.desc) && fieldInsn.name.equals(field.name) && instructions.get(4).getOpcode() >= Opcodes.IRETURN && instructions.get(4).getOpcode() <= Opcodes.ARETURN) { - this.fieldToMethod.put(new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)), new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc))); + this.gettersByField.put(new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)), new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc))); } } } From ab6835871f8aac801f7634f4bf5a4514d0e04ddc Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 14:38:19 -0700 Subject: [PATCH 069/109] rename RecordGetterFindingVisitor -> RecordIndexingVisitor expand RecordGetterFindingVisitor to provide maps of record classes to their fields and methods make EntryTooltip show record field javadocs for record classes make EntryTooltip show record field javadocs for record getters if they don't have their own javadocs --- .../gui/panel/DeclarationSnippetPanel.java | 12 ++-- .../enigma/gui/panel/EntryTooltip.java | 70 +++++++++++++------ .../org/quiltmc/enigma/gui/util/GuiUtil.java | 11 +++ .../enigma/impl/plugin/BuiltinPlugin.java | 4 +- ...ervice.java => RecordIndexingService.java} | 16 ++++- ...isitor.java => RecordIndexingVisitor.java} | 53 ++++++++++---- 6 files changed, 116 insertions(+), 50 deletions(-) rename enigma/src/main/java/org/quiltmc/enigma/impl/plugin/{RecordGetterFindingService.java => RecordIndexingService.java} (66%) rename enigma/src/main/java/org/quiltmc/enigma/impl/plugin/{RecordGetterFindingVisitor.java => RecordIndexingVisitor.java} (71%) 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 c8a40dc29..e484119c0 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 @@ -30,7 +30,6 @@ import com.google.common.collect.BiMap; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; -import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; @@ -42,7 +41,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.impl.plugin.RecordGetterFindingService; +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.LineNumbersRuler; @@ -56,6 +55,7 @@ import java.util.function.Function; import java.util.function.Predicate; +import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; import static java.util.Comparator.comparingInt; public class DeclarationSnippetPanel extends BaseEditorPanel { @@ -513,12 +513,8 @@ private Optional resolveTarget(DecompiledClassSource source, Entry ta .or(() -> { if (targetEntry instanceof MethodEntry targetMethod) { // try to find record component getter's corresponding field instead - return this.gui.getController() - .getProject() - .getEnigma() - .getService(JarIndexerService.TYPE, RecordGetterFindingService.ID) - .map(service -> (RecordGetterFindingService) service) - .map(RecordGetterFindingService::getGettersByField) + return getRecordIndexingService(this.gui) + .map(RecordIndexingService::getGettersByField) .map(BiMap::inverse) .map(recordFieldByGetter -> recordFieldByGetter.get(targetMethod)) .flatMap(recordField -> Optional 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 39fe0e63f..5394362cb 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 @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.panel; +import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; @@ -9,7 +10,6 @@ import org.quiltmc.enigma.api.translation.representation.AccessFlags; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.ClassSelector; import org.quiltmc.enigma.gui.Gui; @@ -21,6 +21,7 @@ import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import javax.annotation.Nullable; import javax.swing.Box; @@ -46,14 +47,17 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.font.TextAttribute; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; +import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.BorderFactory.createLineBorder; @@ -190,7 +194,7 @@ public void mousePressed(MouseEvent e) { ); } - final String javadoc = this.gui.getController().getProject().getRemapper().getMapping(target).javadoc(); + final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { @@ -321,6 +325,22 @@ public void mouseClicked(MouseEvent e) { } } + private Optional getJavadoc(Entry target) { + final EntryRemapper remapper = this.gui.getController().getProject().getRemapper(); + return Optional + .ofNullable(remapper.getMapping(target).javadoc()) + .or(() -> target instanceof MethodEntry targetMethod + // try getting record field javadocs for record getters if the getter has no javadoc + ? getRecordIndexingService(this.gui) + .map(RecordIndexingService::getGettersByField) + .map(BiMap::inverse) + .map(fieldsByGetter -> fieldsByGetter.get(targetMethod)) + .map(remapper::getMapping) + .map(EntryMapping::javadoc) + : Optional.empty() + ); + } + public void addCloseListener(Runnable listener) { this.closeListeners.add(listener); } @@ -493,29 +513,35 @@ private ImmutableList paramJavadocsOf( final EnigmaProject project = this.gui.getController().getProject(); final EntryIndex entryIndex = project.getJarIndex().getIndex(EntryIndex.class); + final Stream> entries; if (target instanceof MethodEntry targetMethod) { - final MethodDefEntry methodDef = entryIndex.getDefinition(targetMethod); - if (methodDef == null) { - return ImmutableList.of(); - } else { - final EntryRemapper remapper = project.getRemapper(); - - return methodDef.getParameters(entryIndex).stream() - .mapMulti((param, add) -> { - final EntryMapping mapping = remapper.getMapping(param); - if (mapping.javadoc() != null) { - final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName(), nameFont); - final JTextArea javadoc = javadocOf(mapping.javadoc(), javadocFont, stopInteraction); - - add.accept(new ParamJavadoc(name, javadoc)); - } - }) - .collect(toImmutableList()); - } + entries = Optional + .ofNullable(entryIndex.getDefinition(targetMethod)) + .stream() + .flatMap(methodDef -> methodDef.getParameters(entryIndex).stream()); + } else if (target instanceof ClassEntry targetClass) { + entries = getRecordIndexingService(this.gui) + .map(RecordIndexingService::getFieldsByClass) + .map(fieldsByClass -> fieldsByClass.get(targetClass)) + .stream() + .flatMap(Collection::stream); } else { - // TODO add record component javadocs once there's a RecordIndex - return ImmutableList.of(); + entries = Stream.empty(); } + + final EntryRemapper remapper = project.getRemapper(); + + return entries + .mapMulti((param, add) -> { + final EntryMapping mapping = remapper.getMapping(param); + if (mapping.javadoc() != null) { + final JLabel name = colonLabelOf(remapper.deobfuscate(param).getSimpleName(), nameFont); + final JTextArea javadoc = javadocOf(mapping.javadoc(), javadocFont, stopInteraction); + + add.accept(new ParamJavadoc(name, javadoc)); + } + }) + .collect(toImmutableList()); } private void addSeparator(int gridY) { 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 c342e622e..1f0fd8983 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 @@ -2,6 +2,7 @@ import com.formdev.flatlaf.extras.FlatSVGIcon; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.api.stats.ProjectStatsResult; import org.quiltmc.enigma.api.translation.representation.AccessFlags; @@ -9,6 +10,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.config.keybind.KeyBind; import org.quiltmc.enigma.gui.config.theme.ThemeUtil; +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import org.quiltmc.enigma.util.Os; import javax.swing.Action; @@ -60,6 +62,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -377,6 +380,14 @@ public static void consumeMousePositionIn( outAction.accept(absolutePos); } + public static Optional getRecordIndexingService(Gui gui) { + return gui.getController() + .getProject() + .getEnigma() + .getService(JarIndexerService.TYPE, RecordIndexingService.ID) + .map(service -> (RecordIndexingService) service); + } + public enum FocusCondition { /** * @see JComponent#WHEN_IN_FOCUSED_WINDOW diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java index cc6d4eeb8..8e2aaed9a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -67,9 +67,9 @@ public String getId() { private static void registerRecordNamingService(EnigmaPluginContext ctx) { final BiMap gettersByField = HashBiMap.create(); - final RecordGetterFindingVisitor visitor = new RecordGetterFindingVisitor(gettersByField); + final RecordIndexingVisitor visitor = new RecordIndexingVisitor(gettersByField); - ctx.registerService(JarIndexerService.TYPE, ctx1 -> new RecordGetterFindingService(visitor)); + ctx.registerService(JarIndexerService.TYPE, ctx1 -> new RecordIndexingService(visitor)); ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(gettersByField)); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java similarity index 66% rename from enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java rename to enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java index a9a96310e..0c54821b3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java @@ -1,21 +1,23 @@ package org.quiltmc.enigma.impl.plugin; import com.google.common.collect.BiMap; +import com.google.common.collect.Multimap; import org.objectweb.asm.tree.ClassNode; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import java.util.Set; -public class RecordGetterFindingService implements JarIndexerService { +public class RecordIndexingService implements JarIndexerService { public static final String ID = "enigma:record_component_indexer"; - private final RecordGetterFindingVisitor visitor; + private final RecordIndexingVisitor visitor; - RecordGetterFindingService(RecordGetterFindingVisitor visitor) { + RecordIndexingService(RecordIndexingVisitor visitor) { this.visitor = visitor; } @@ -23,6 +25,14 @@ public BiMap getGettersByField() { return this.visitor.getGettersByField(); } + public Multimap getFieldsByClass() { + return this.visitor.getFieldsByClass(); + } + + public Multimap getMethodsByClass() { + return this.visitor.getMethodsByClass(); + } + @Override public void acceptJar(Set scope, ProjectClassProvider classProvider, JarIndex jarIndex) { for (String className : scope) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java similarity index 71% rename from enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java rename to enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index d14a4a2be..6c3e6b2ed 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -1,6 +1,8 @@ package org.quiltmc.enigma.impl.plugin; import com.google.common.collect.BiMap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; @@ -21,14 +23,17 @@ import java.util.HashSet; import java.util.Set; -final class RecordGetterFindingVisitor extends ClassVisitor { +final class RecordIndexingVisitor extends ClassVisitor { private ClassEntry clazz; - private final BiMap gettersByField; private final Set recordComponents = new HashSet<>(); private final Set fields = new HashSet<>(); private final Set methods = new HashSet<>(); - RecordGetterFindingVisitor(BiMap gettersByField) { + private final BiMap gettersByField; + private final Multimap fieldsByClass = HashMultimap.create(); + private final Multimap methodsByClass = HashMultimap.create(); + + RecordIndexingVisitor(BiMap gettersByField) { super(Enigma.ASM_VERSION); this.gettersByField = gettersByField; } @@ -37,6 +42,14 @@ public BiMap getGettersByField() { return this.gettersByField; } + public Multimap getFieldsByClass() { + return this.fieldsByClass; + } + + public Multimap getMethodsByClass() { + return this.methodsByClass; + } + @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); @@ -76,15 +89,17 @@ public MethodVisitor visitMethod(final int access, final String name, final Stri public void visitEnd() { super.visitEnd(); try { - if (this.clazz != null) { - this.collectResults(); - } + this.collectResults(); } catch (Exception ex) { throw new RuntimeException(ex); } } private void collectResults() { + if (this.clazz == null) { + return; + } + for (RecordComponentNode component : this.recordComponents) { FieldNode field = null; for (FieldNode node : this.fields) { @@ -103,15 +118,23 @@ private void collectResults() { // match bytecode to exact expected bytecode for a getter // only check important instructions (ignore new frame instructions, etc.) - if (instructions.size() == 6 - && instructions.get(2).getOpcode() == Opcodes.ALOAD - && instructions.get(3) instanceof FieldInsnNode fieldInsn - && fieldInsn.getOpcode() == Opcodes.GETFIELD - && fieldInsn.owner.equals(this.clazz.getFullName()) - && fieldInsn.desc.equals(field.desc) - && fieldInsn.name.equals(field.name) - && instructions.get(4).getOpcode() >= Opcodes.IRETURN && instructions.get(4).getOpcode() <= Opcodes.ARETURN) { - this.gettersByField.put(new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)), new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc))); + if ( + instructions.size() == 6 + && instructions.get(2).getOpcode() == Opcodes.ALOAD + && instructions.get(3) instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETFIELD + && fieldInsn.owner.equals(this.clazz.getFullName()) + && fieldInsn.desc.equals(field.desc) + && fieldInsn.name.equals(field.name) + && instructions.get(4).getOpcode() >= Opcodes.IRETURN + && instructions.get(4).getOpcode() <= Opcodes.ARETURN + ) { + final FieldEntry fieldEntry = new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)); + final MethodEntry methodEntry = new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc)); + + this.gettersByField.put(fieldEntry, methodEntry); + this.fieldsByClass.put(this.clazz, fieldEntry); + this.methodsByClass.put(this.clazz, methodEntry); } } } From 309cfbf192995056de56e432cc179408b9be70ac Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 14:43:12 -0700 Subject: [PATCH 070/109] clear things in visitEnd instead of visit clear fields and methods --- .../quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 6c3e6b2ed..0907cbf6b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -54,7 +54,6 @@ public Multimap getMethodsByClass() { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.clazz = (access & Opcodes.ACC_RECORD) != 0 ? new ClassEntry(name) : null; - this.recordComponents.clear(); } @Override @@ -90,6 +89,11 @@ public void visitEnd() { super.visitEnd(); try { this.collectResults(); + + this.clazz = null; + this.recordComponents.clear(); + this.fields.clear(); + this.methods.clear(); } catch (Exception ex) { throw new RuntimeException(ex); } From 626fe8e2175fbc7af18ce99f0827048e14881d6c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 14:54:28 -0700 Subject: [PATCH 071/109] fix for java 17 --- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 3 +++ 1 file changed, 3 insertions(+) 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 5394362cb..b291dd742 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 @@ -10,6 +10,7 @@ import org.quiltmc.enigma.api.translation.representation.AccessFlags; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.ClassSelector; import org.quiltmc.enigma.gui.Gui; @@ -335,6 +336,8 @@ private Optional getJavadoc(Entry target) { .map(RecordIndexingService::getGettersByField) .map(BiMap::inverse) .map(fieldsByGetter -> fieldsByGetter.get(targetMethod)) + // this cast is required on java 17 for some reason + .map(entry -> (FieldEntry) entry) .map(remapper::getMapping) .map(EntryMapping::javadoc) : Optional.empty() From f19d5dbe1257782e321aad4e61fc52f77845cd0c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 20:07:15 -0700 Subject: [PATCH 072/109] tighten RecordIndexingVisitor encapsulation --- .../gui/panel/DeclarationSnippetPanel.java | 12 +++------ .../enigma/gui/panel/EntryTooltip.java | 11 ++------ .../enigma/impl/plugin/BuiltinPlugin.java | 9 ++----- .../RecordComponentProposalService.java | 5 ++-- .../impl/plugin/RecordIndexingService.java | 22 ++++++++++------ .../impl/plugin/RecordIndexingVisitor.java | 25 +++++++++++++------ 6 files changed, 41 insertions(+), 43 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 e484119c0..6c166f461 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 @@ -27,7 +27,6 @@ import com.github.javaparser.ast.stmt.BlockStmt; import com.github.javaparser.ast.stmt.ExpressionStmt; import com.github.javaparser.ast.stmt.Statement; -import com.google.common.collect.BiMap; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.source.DecompiledClassSource; @@ -41,7 +40,6 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; -import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.LineNumbersRuler; @@ -514,12 +512,10 @@ private Optional resolveTarget(DecompiledClassSource source, Entry ta if (targetEntry instanceof MethodEntry targetMethod) { // try to find record component getter's corresponding field instead return getRecordIndexingService(this.gui) - .map(RecordIndexingService::getGettersByField) - .map(BiMap::inverse) - .map(recordFieldByGetter -> recordFieldByGetter.get(targetMethod)) - .flatMap(recordField -> Optional - .ofNullable(source.getIndex().getDeclarationToken(recordField)) - .map(fieldToken -> new Target(fieldToken, recordField)) + .map(service -> service.getComponentField(targetMethod)) + .flatMap(componentField -> Optional + .ofNullable(source.getIndex().getDeclarationToken(componentField)) + .map(fieldToken -> new Target(fieldToken, componentField)) ); } else { // This can happen as a result of #252: Issue with lost parameter connection. 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 b291dd742..1bab3613f 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 @@ -1,6 +1,5 @@ package org.quiltmc.enigma.gui.panel; -import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableList; import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; @@ -22,7 +21,6 @@ import org.quiltmc.enigma.gui.docker.ObfuscatedClassesDocker; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.ScaleUtil; -import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import javax.annotation.Nullable; import javax.swing.Box; @@ -48,7 +46,6 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.font.TextAttribute; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -333,9 +330,7 @@ private Optional getJavadoc(Entry target) { .or(() -> target instanceof MethodEntry targetMethod // try getting record field javadocs for record getters if the getter has no javadoc ? getRecordIndexingService(this.gui) - .map(RecordIndexingService::getGettersByField) - .map(BiMap::inverse) - .map(fieldsByGetter -> fieldsByGetter.get(targetMethod)) + .map(service -> service.getComponentField(targetMethod)) // this cast is required on java 17 for some reason .map(entry -> (FieldEntry) entry) .map(remapper::getMapping) @@ -524,10 +519,8 @@ private ImmutableList paramJavadocsOf( .flatMap(methodDef -> methodDef.getParameters(entryIndex).stream()); } else if (target instanceof ClassEntry targetClass) { entries = getRecordIndexingService(this.gui) - .map(RecordIndexingService::getFieldsByClass) - .map(fieldsByClass -> fieldsByClass.get(targetClass)) .stream() - .flatMap(Collection::stream); + .flatMap(service -> service.streamComponentFields(targetClass)); } else { entries = Stream.empty(); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java index 8e2aaed9a..63f591372 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -1,7 +1,5 @@ package org.quiltmc.enigma.impl.plugin; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; import org.quiltmc.enigma.api.EnigmaPlugin; @@ -16,8 +14,6 @@ import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import javax.annotation.Nullable; import java.util.HashMap; @@ -66,11 +62,10 @@ public String getId() { } private static void registerRecordNamingService(EnigmaPluginContext ctx) { - final BiMap gettersByField = HashBiMap.create(); - final RecordIndexingVisitor visitor = new RecordIndexingVisitor(gettersByField); + final RecordIndexingVisitor visitor = new RecordIndexingVisitor(); ctx.registerService(JarIndexerService.TYPE, ctx1 -> new RecordIndexingService(visitor)); - ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(gettersByField)); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(visitor)); } private static void registerSpecializedMethodNamingService(EnigmaPluginContext ctx) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java index 135e8a859..1e2ede19f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.impl.plugin; -import com.google.common.collect.BiMap; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; @@ -19,7 +18,7 @@ import java.util.List; import java.util.Map; -public record RecordComponentProposalService(BiMap gettersByField) implements NameProposalService { +public record RecordComponentProposalService(RecordIndexingVisitor visitor) implements NameProposalService { public static final String ID = "enigma:record_component_proposer"; @Nullable @@ -91,7 +90,7 @@ public void validateProposedMapping(Entry entry, EntryMapping mapping, boolea } public boolean isGetter(FieldEntry obfFieldEntry, MethodEntry method) { - var getter = this.gettersByField.get(obfFieldEntry); + final MethodEntry getter = this.visitor.getComponentGetter(obfFieldEntry); return getter != null && getter.equals(method); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java index 0c54821b3..222312c8e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java @@ -1,7 +1,5 @@ package org.quiltmc.enigma.impl.plugin; -import com.google.common.collect.BiMap; -import com.google.common.collect.Multimap; import org.objectweb.asm.tree.ClassNode; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; @@ -10,7 +8,9 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import javax.annotation.Nullable; import java.util.Set; +import java.util.stream.Stream; public class RecordIndexingService implements JarIndexerService { public static final String ID = "enigma:record_component_indexer"; @@ -21,16 +21,22 @@ public class RecordIndexingService implements JarIndexerService { this.visitor = visitor; } - public BiMap getGettersByField() { - return this.visitor.getGettersByField(); + @Nullable + public MethodEntry getComponentGetter(FieldEntry componentField) { + return this.visitor.getComponentGetter(componentField); } - public Multimap getFieldsByClass() { - return this.visitor.getFieldsByClass(); + @Nullable + public FieldEntry getComponentField(MethodEntry componentGetter) { + return this.visitor.getComponentField(componentGetter); } - public Multimap getMethodsByClass() { - return this.visitor.getMethodsByClass(); + public Stream streamComponentFields(ClassEntry recordEntry) { + return this.visitor.streamComponentFields(recordEntry); + } + + public Stream streamComponentMethods(ClassEntry recordEntry) { + return this.visitor.streamComponentMethods(recordEntry); } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 0907cbf6b..d33f295dc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.impl.plugin; import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import org.objectweb.asm.ClassVisitor; @@ -20,8 +21,10 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import javax.annotation.Nullable; import java.util.HashSet; import java.util.Set; +import java.util.stream.Stream; final class RecordIndexingVisitor extends ClassVisitor { private ClassEntry clazz; @@ -33,21 +36,27 @@ final class RecordIndexingVisitor extends ClassVisitor { private final Multimap fieldsByClass = HashMultimap.create(); private final Multimap methodsByClass = HashMultimap.create(); - RecordIndexingVisitor(BiMap gettersByField) { + RecordIndexingVisitor() { super(Enigma.ASM_VERSION); - this.gettersByField = gettersByField; + this.gettersByField = HashBiMap.create(); } - public BiMap getGettersByField() { - return this.gettersByField; + @Nullable + public MethodEntry getComponentGetter(FieldEntry componentField) { + return this.gettersByField.get(componentField); } - public Multimap getFieldsByClass() { - return this.fieldsByClass; + @Nullable + public FieldEntry getComponentField(MethodEntry componentGetter) { + return this.gettersByField.inverse().get(componentGetter); } - public Multimap getMethodsByClass() { - return this.methodsByClass; + public Stream streamComponentFields(ClassEntry recordEntry) { + return this.fieldsByClass.get(recordEntry).stream(); + } + + public Stream streamComponentMethods(ClassEntry recordEntry) { + return this.methodsByClass.get(recordEntry).stream(); } @Override From 96c7ed93abc171ba612e536516b91f96835f73e6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 14 Oct 2025 20:34:57 -0700 Subject: [PATCH 073/109] make navigating to a record component getter with no explicit declaration navigate to its field instead --- .../enigma/gui/panel/BaseEditorPanel.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 1ab1064cc..2eb606cf2 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 @@ -14,6 +14,7 @@ import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.gui.BrowserCaret; import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; @@ -66,6 +67,7 @@ import java.util.stream.Stream; import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; +import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; public class BaseEditorPanel { protected final JPanel ui = new JPanel(); @@ -504,7 +506,20 @@ private void showReferenceImpl(EntryReference, Entry> reference) { return; } - List tokens = this.controller.getTokensForReference(this.source, reference); + final List tokens = Optional.of(this.controller.getTokensForReference(this.source, reference)) + .filter(directTokens -> !directTokens.isEmpty()) + .or(() -> { + // record component getters often don't have a declaration token + // try to get the field declaration instead + return reference.entry instanceof MethodEntry method + ? getRecordIndexingService(this.gui) + .map(service -> service.getComponentField(method)) + .map(field -> EntryReference., Entry>declaration(field, field.getName())) + .map(fieldReference -> this.controller.getTokensForReference(this.source, fieldReference)) + : Optional.empty(); + }) + .orElse(List.of()); + if (tokens.isEmpty()) { // DEBUG Logger.debug("No tokens found for {} in {}", reference, this.classHandler.getHandle().getRef()); From 37fbaf3d49a14cf2e4b115d02a0a63808f75b351 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 07:42:57 -0700 Subject: [PATCH 074/109] add nullable annotation --- .../main/java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2eb606cf2..dd1b4cf23 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 @@ -807,7 +807,7 @@ public int offsetOf(int unBoundedPos) { } @Override - public Optional offsetOf(Token boundedToken) { + public Optional offsetOf(@Nullable Token boundedToken) { return boundedToken == null || this.end() < boundedToken.end ? Optional.empty() : Optional.of(boundedToken); } } From 8ac9726c7377587c885882e5f36cf9604ac880fe Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 07:55:01 -0700 Subject: [PATCH 075/109] only set BaseEditorPanel.editor text once --- .../enigma/gui/panel/BaseEditorPanel.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 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 dd1b4cf23..a5eb54e57 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 @@ -398,12 +398,21 @@ protected void setSource( this.source = source; this.editor.getHighlighter().removeAllHighlights(); - this.editor.setText(this.source.toString()); final Snippet snippet = snippetFactor == null ? null : snippetFactor.apply(this.source); if (snippet == null) { + this.editor.setText(this.source.toString()); this.sourceBounds = new DefaultBounds(); } else { - newCaretPos = this.trimSource(snippet, newCaretPos); + final String sourceString = this.source.toString(); + + final int end = Math.min(sourceString.length(), snippet.end); + + final Unindented unindented = Unindented.of(sourceString, snippet.start, end); + + this.sourceBounds = new TrimmedBounds(snippet.start, end, unindented.indentOffsets); + this.editor.setText(unindented.snippet); + + newCaretPos = Utils.clamp((long) newCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()); } this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); @@ -424,19 +433,6 @@ protected void setSource( } } - private int trimSource(Snippet snippet, int originalCaretPos) { - final String sourceString = this.source.toString(); - - final int end = Math.min(sourceString.length(), snippet.end); - - final Unindented unindented = Unindented.of(sourceString, snippet.start, end); - - this.sourceBounds = new TrimmedBounds(snippet.start, end, unindented.indentOffsets); - this.editor.setText(unindented.snippet); - - return Utils.clamp((long) originalCaretPos - this.sourceBounds.start(), 0, this.editor.getText().length()); - } - protected void addSourceSetListener(Consumer listener) { this.sourceSetListeners.add(listener); } From 470d0be2d8e69d2de8a605659c7bc9b80a11e799 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 07:56:03 -0700 Subject: [PATCH 076/109] fix typo --- .../java/org/quiltmc/enigma/gui/panel/BaseEditorPanel.java | 4 ++-- 1 file changed, 2 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 a5eb54e57..9e0dc6392 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 @@ -362,7 +362,7 @@ public EntryReference, Entry> getReference(Token token) { protected void setSource( DecompiledClassSource source, - @Nullable Function snippetFactor + @Nullable Function snippetFactory ) { this.setDisplayMode(DisplayMode.SUCCESS); if (source == null) { @@ -398,7 +398,7 @@ protected void setSource( this.source = source; this.editor.getHighlighter().removeAllHighlights(); - final Snippet snippet = snippetFactor == null ? null : snippetFactor.apply(this.source); + final Snippet snippet = snippetFactory == null ? null : snippetFactory.apply(this.source); if (snippet == null) { this.editor.setText(this.source.toString()); this.sourceBounds = new DefaultBounds(); From 393cf53b8bef66b0e1a0da890cd35ab85f67b158 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 11:19:05 -0700 Subject: [PATCH 077/109] tweak moveMaintainingAnchor --- .../enigma/gui/panel/EntryTooltip.java | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 1bab3613f..b7e3469f3 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 @@ -388,32 +388,19 @@ private void moveMaintainingAnchor(Point oldMousePos, Dimension oldSize) { final int oldLeft = oldMousePos.x - pos.x; final int oldRight = pos.x + oldSize.width - oldMousePos.x; - final boolean anchorRight = oldLeft >= oldRight; + final boolean anchorRight = oldLeft > oldRight; final int oldTop = oldMousePos.y - pos.y; final int oldBottom = pos.y + oldSize.height - oldMousePos.y; - final boolean anchorBottom = oldTop >= oldBottom; + final boolean anchorBottom = oldTop > oldBottom; if (anchorRight || anchorBottom) { final Dimension newSize = this.getSize(); - final int x; - if (anchorRight) { - final int widthDiff = oldSize.width - newSize.width; - x = pos.x + widthDiff; - } else { - x = pos.x; - } - - final int y; - if (anchorBottom) { - final int heightDiff = oldSize.height - newSize.height; - y = pos.y + heightDiff; - } else { - y = pos.y; - } + final int xDiff = anchorRight ? oldSize.width - newSize.width : 0; + final int yDiff = anchorBottom ? oldSize.height - newSize.height : 0; - this.setLocation(x, y); + this.setLocation(pos.x + xDiff, pos.y + yDiff); } this.moveOnScreen(); From 9857a471fd8e0f4c86565ac2fa7f093c50fba9c1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 11:54:01 -0700 Subject: [PATCH 078/109] close entry tooltip on key press --- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 b7e3469f3..286a0bfcb 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 @@ -30,6 +30,7 @@ import javax.swing.JTextArea; import javax.swing.JWindow; import javax.swing.tree.TreePath; +import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; @@ -40,6 +41,7 @@ import java.awt.Point; import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; @@ -129,6 +131,15 @@ public void mouseDragged(MouseEvent e) { } }); + Toolkit.getDefaultToolkit().addAWTEventListener( + e -> { + if (this.isShowing() && e.getID() == KeyEvent.KEY_PRESSED) { + this.close(); + } + }, + AWTEvent.KEY_EVENT_MASK + ); + this.addWindowFocusListener(new WindowAdapter() { @Override public void windowLostFocus(WindowEvent e) { From a9d6cb0a6691838a9c9146b419da86c7a263d2a8 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 15 Oct 2025 12:19:14 -0700 Subject: [PATCH 079/109] close tooltip on KEY_TYPED instead of KEY_PRESSED so ctrl+c can copy --- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 286a0bfcb..b983e49d5 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 @@ -133,7 +133,7 @@ public void mouseDragged(MouseEvent e) { Toolkit.getDefaultToolkit().addAWTEventListener( e -> { - if (this.isShowing() && e.getID() == KeyEvent.KEY_PRESSED) { + if (this.isShowing() && e.getID() == KeyEvent.KEY_TYPED) { this.close(); } }, From 92a8d9b5e18f29b09307de0370bbd4ab784b31e3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 17 Oct 2025 17:26:59 -0700 Subject: [PATCH 080/109] add javaparser to lib list in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 96af74eb2..9bba51301 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Enigma includes the following open-source libraries: - [FlatLaf](https://github.com/JFormDesigner/FlatLaf) (Apache-2.0) - [jopt-simple](https://github.com/jopt-simple/jopt-simple) (MIT) - [ASM](https://asm.ow2.io/) (BSD-3-Clause) + - [JavaParser](https://javaparser.org/) (Apache-2.0) ## Usage From ac7797ca9efaa18157271d4be8d85787af586df2 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 18 Oct 2025 18:42:11 -0700 Subject: [PATCH 081/109] cleanup createSnippet --- .../gui/panel/DeclarationSnippetPanel.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 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 6c166f461..c12a3b7f0 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 @@ -102,43 +102,32 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl private Snippet createSnippet(DecompiledClassSource source, Entry targetEntry) { return this.resolveTarget(source, targetEntry) - .map(target -> this.createSnippet(source, target.token, target.entry)) + .map(target -> this.findSnippet(source, target.token, target.entry)) + .map(snippet -> snippet.unwrapOrElse(error -> { + Logger.error( + "Error searching for declaration of '{}' for tooltip: {}", + this.getFullDeobfuscatedName(targetEntry), + error + ); + + return null; + })) .orElse(null); } - private Snippet createSnippet(DecompiledClassSource source, Token targetToken, Entry targetEntry) { - final Result snippet; + private Result findSnippet(DecompiledClassSource source, Token targetToken, Entry targetEntry) { if (targetEntry instanceof ClassEntry targetClass) { - snippet = this.findClassSnippet(source, targetToken, targetClass); + return this.findClassSnippet(source, targetToken, targetClass); } else if (targetEntry instanceof MethodEntry targetMethod) { - snippet = this.findMethodSnippet(source, targetToken, targetMethod); + return this.findMethodSnippet(source, targetToken, targetMethod); } else if (targetEntry instanceof FieldEntry targetField) { - snippet = this.findFieldSnippet(source, targetToken, targetField); + return this.findFieldSnippet(source, targetToken, targetField); } else if (targetEntry instanceof LocalVariableEntry targetLocal) { - snippet = this.getVariableSnippet(source, targetToken, targetLocal); + return this.getVariableSnippet(source, targetToken, targetLocal); } else { // this should never be reached - Logger.error( - "Error trimming tooltip for '{}': unrecognized target entry type!", - this.getFullDeobfuscatedName(targetEntry) - ); - - return null; + return Result.err("unrecognized target entry type!"); } - - return snippet.unwrapOrElse(error -> { - this.logDeclarationSearchError(targetEntry, error); - - return null; - }); - } - - private void logDeclarationSearchError(Entry targetEntry, String error) { - Logger.error( - "Error searching for declaration of '{}' for tooltip: {}", - this.getFullDeobfuscatedName(targetEntry), - error - ); } private Result getVariableSnippet( From 8751d3bb63d221e4144f8716874531eb6d8967c4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 18 Oct 2025 19:12:11 -0700 Subject: [PATCH 082/109] fix swapped un/bounded names --- .../enigma/gui/panel/BaseEditorPanel.java | 26 +++++++++---------- .../gui/panel/DeclarationSnippetPanel.java | 6 ++--- 2 files changed, 16 insertions(+), 16 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 9e0dc6392..4b5c78f80 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 @@ -530,8 +530,8 @@ private void showReferenceImpl(EntryReference, Entry> reference) { * @param token the token to navigate to, in {@linkplain #sourceBounds bounded} space */ public void navigateToToken(@Nullable Token token) { - final Token unBoundedToken = this.navigateToTokenImpl(token); - if (unBoundedToken == null) { + final Token boundedToken = this.navigateToTokenImpl(token); + if (boundedToken == null) { return; } @@ -544,7 +544,7 @@ public void navigateToToken(@Nullable Token token) { @Override public void actionPerformed(ActionEvent event) { if (this.counter % 2 == 0) { - this.highlight = BaseEditorPanel.this.addHighlight(unBoundedToken, SelectionHighlightPainter.INSTANCE); + this.highlight = BaseEditorPanel.this.addHighlight(boundedToken, SelectionHighlightPainter.INSTANCE); } else if (this.highlight != null) { BaseEditorPanel.this.editor.getHighlighter().removeHighlight(this.highlight); } @@ -559,29 +559,29 @@ public void actionPerformed(ActionEvent event) { } /** - * @return a token equivalent to the passed {@code boundedToken} with its position shifted so it aligns with the - * un-bounded source if navigation was successful, or {@code null} otherwise + * @return a token equivalent to the passed {@code unBoundedToken} with its position shifted so it aligns with the + * bounded source if navigation was successful, or {@code null} otherwise */ @Nullable - protected Token navigateToTokenImpl(@Nullable Token boundedToken) { - if (boundedToken == null) { + protected Token navigateToTokenImpl(@Nullable Token unBoundedToken) { + if (unBoundedToken == null) { return null; } - final Token unBoundedToken = this.sourceBounds.offsetOf(boundedToken).orElse(null); - if (unBoundedToken == null) { + final Token boundedToken = this.sourceBounds.offsetOf(unBoundedToken).orElse(null); + if (boundedToken == null) { // token out of bounds return null; } // set the caret position to the token - this.editor.setCaretPosition(unBoundedToken.start); + this.editor.setCaretPosition(boundedToken.start); this.editor.grabFocus(); try { // make sure the token is visible in the scroll window - Rectangle2D start = this.editor.modelToView2D(unBoundedToken.start); - Rectangle2D end = this.editor.modelToView2D(unBoundedToken.start); + Rectangle2D start = this.editor.modelToView2D(boundedToken.start); + Rectangle2D end = this.editor.modelToView2D(boundedToken.start); if (start == null || end == null) { return null; } @@ -598,7 +598,7 @@ protected Token navigateToTokenImpl(@Nullable Token boundedToken) { } } - return unBoundedToken; + return boundedToken; } protected Object addHighlight(Token token, HighlightPainter highlightPainter) { 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 c12a3b7f0..92a3faa2a 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 @@ -76,17 +76,17 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl this.addSourceSetListener(source -> { @Nullable - final Token unBoundedToken = this.resolveTarget(source, target) + final Token boundedToken = this.resolveTarget(source, target) .map(Target::token) .map(this::navigateToTokenImpl) .orElse(null); - if (unBoundedToken == null) { + if (boundedToken == null) { // the source isn't very useful if it couldn't be trimmed and the declaration couldn't be navigated to // set this text so it doesn't waste space or cause confusion this.editor.setText("// Unable to locate declaration"); this.editor.getHighlighter().removeAllHighlights(); } else { - this.addHighlight(unBoundedToken, BoxHighlightPainter.create( + this.addHighlight(boundedToken, BoxHighlightPainter.create( new Color(0, 0, 0, 0), Config.getCurrentSyntaxPaneColors().selectionHighlight.value() )); From 7e3539f70464fcf94c06d53fc3e75ae4333e8544 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 19 Oct 2025 08:22:41 -0700 Subject: [PATCH 083/109] make LineIndexer::getIndex return -1 for out-of-bounds positions, add LineIndexerTest --- .../org/quiltmc/enigma/util/LineIndexer.java | 8 +- .../quiltmc/enigma/util/LineIndexerTest.java | 92 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/util/LineIndexerTest.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java index 40ce61f1a..ccc24c3a0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -35,6 +35,12 @@ public int getStartIndex(int line) { public int getIndex(Position position) { final int lineIndex = this.getStartIndex(position.line - Position.FIRST_LINE); - return lineIndex < 0 ? lineIndex : lineIndex + position.column - Position.FIRST_COLUMN; + if (lineIndex < 0) { + return lineIndex; + } else { + final int index = lineIndex + position.column - Position.FIRST_COLUMN; + + return index < this.string.length() ? index : -1; + } } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/util/LineIndexerTest.java b/enigma/src/test/java/org/quiltmc/enigma/util/LineIndexerTest.java new file mode 100644 index 000000000..d8562dbfb --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/util/LineIndexerTest.java @@ -0,0 +1,92 @@ +package org.quiltmc.enigma.util; + +import com.github.javaparser.Position; +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LineIndexerTest { + private static final String SUBJECT = + """ + I + II + III + IV + V\ + """; + + private static final ImmutableList START_INDEX_EXPECTATIONS = ImmutableList.of(0, 2, 5, 9, 12, -1); + + private static LineIndexer createIndexer() { + return new LineIndexer(SUBJECT); + } + + @Test + void testGetStartIndex() { + final LineIndexer indexer = createIndexer(); + + for (int line = 0; line < START_INDEX_EXPECTATIONS.size(); line++) { + assertEquals(START_INDEX_EXPECTATIONS.get(line), indexer.getStartIndex(line)); + } + } + + // test backwards to make it find all start indexes first, then ensure it correctly uses cached results + @Test + void testGetStartIndexCaching() { + final LineIndexer indexer = createIndexer(); + + for (int line = START_INDEX_EXPECTATIONS.size() - 1; line >= 0; line--) { + assertEquals(START_INDEX_EXPECTATIONS.get(line), indexer.getStartIndex(line)); + } + } + + @Test + void testGetIndex() { + final LineIndexer indexer = createIndexer(); + + final Position firstLine = new Position(Position.FIRST_LINE, Position.FIRST_COLUMN); + final Position secondLine = firstLine.nextLine(); + final Position thirdLine = secondLine.nextLine(); + final Position fourthLine = thirdLine.nextLine(); + final Position fifthLine = fourthLine.nextLine(); + + final List positionsByExpectedIndex = List.of( + // I + firstLine, + firstLine.right(1), + // II + secondLine, + secondLine.right(1), + secondLine.right(2), + // III + thirdLine, + thirdLine.right(1), + thirdLine.right(2), + thirdLine.right(3), + // IV + fourthLine, + fourthLine.right(1), + fourthLine.right(2), + // V + fifthLine + // no character after V, so no right(1) expected + ); + + for (int expectedIndex = 0; expectedIndex < positionsByExpectedIndex.size(); expectedIndex++) { + final Position pos = positionsByExpectedIndex.get(expectedIndex); + final int index = indexer.getIndex(pos); + + final int finalExpectedIndex = expectedIndex; + assertEquals(expectedIndex, index, () -> + "expected pos [%s, %s] to have index %s, but had index %s!" + .formatted(pos.line, pos.column, finalExpectedIndex, index) + ); + } + + assertEquals(-1, indexer.getIndex(fifthLine.right(1))); + assertEquals(-1, indexer.getIndex(fifthLine.nextLine())); + } +} From 3e322511f7ee89d0642e1f69d13f7f7b3ead35d9 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 22 Oct 2025 14:06:45 -0700 Subject: [PATCH 084/109] rename EditorTooltipSection -> EntryTooltipSection --- .../main/java/org/quiltmc/enigma/gui/config/EditorConfig.java | 4 ++-- .../{EditorTooltipSection.java => EntryTooltipSection.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/{EditorTooltipSection.java => EntryTooltipSection.java} (90%) 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 1cb512c0b..061cebbe8 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 @@ -11,6 +11,6 @@ public class EditorConfig extends ReflectiveConfig { @Comment("Whether editors' quick find toolbars should remain visible when they lose focus.") public final TrackedValue persistentQuickFind = this.value(true); - @Comment("The settings for the editor's entry tooltip.") - public final EditorTooltipSection entryTooltip = new EditorTooltipSection(); + @Comment("Settings for the editor's entry tooltip.") + public final EntryTooltipSection entryTooltip = new EntryTooltipSection(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java similarity index 90% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java index 118ba9b8a..ac843c84c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EditorTooltipSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java @@ -7,7 +7,7 @@ import org.quiltmc.config.api.values.TrackedValue; @SerializedNameConvention(NamingSchemes.SNAKE_CASE) -public class EditorTooltipSection extends ReflectiveConfig.Section { +public class EntryTooltipSection extends ReflectiveConfig.Section { @Comment("Whether tooltips are enabled.") public final TrackedValue enable = this.value(true); From 0563d0fae9c1dddd4c3b8cacc859767fa0806b95 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 24 Oct 2025 13:35:38 -0700 Subject: [PATCH 085/109] limit EntryTooltip size --- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 b983e49d5..4e4303251 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 @@ -148,6 +148,17 @@ public void windowLostFocus(WindowEvent e) { }); } + // setting/overriding max size does not work; getMaximumSize is never called + @Override + public Dimension getPreferredSize() { + final Dimension superPreferred = super.getPreferredSize(); + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + return new Dimension( + Math.min(superPreferred.width, screenSize.width / 2), + Math.min(superPreferred.height, screenSize.height / 2) + ); + } + // Sometimes when re-populating and resizing+moving, the cursor may be briefly over the parent EditorPanel. // This is used to stop EditorPanel from starting its mouseStoppedMovingTimer which may reset the tooltip to the // token under the cursor, discarding the re-populated content. From 726bda5ce1e8f632fbb81f5aee0a63c5a1fb0620 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 24 Oct 2025 15:43:53 -0700 Subject: [PATCH 086/109] put main tooltip content in a scroll pane --- .../enigma/gui/panel/EntryTooltip.java | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 4e4303251..319a36ba9 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 @@ -26,6 +26,8 @@ import javax.swing.Box; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTextArea; import javax.swing.JWindow; @@ -214,15 +216,24 @@ public void mousePressed(MouseEvent e) { ); } + final var mainContent = new JPanel(new GridBagLayout()); + final var mainScroll = new JScrollPane(mainContent); + final var mainGridY = new AtomicInteger(0); + final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { - this.addSeparator(gridY.getAndIncrement()); + mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() + .pos(0, mainGridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build()); + if (javadoc != null) { - this.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() - .pos(0, gridY.getAndIncrement()) + mainContent.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() + .pos(0, mainGridY.getAndIncrement()) .insets(ROW_INNER_INSET, ROW_OUTER_INSET) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) @@ -251,9 +262,9 @@ public void mousePressed(MouseEvent e) { ); } - this.add(params, GridBagConstraintsBuilder.create() + mainContent.add(params, GridBagConstraintsBuilder.create() .insets(ROW_INNER_INSET, ROW_OUTER_INSET) - .pos(0, gridY.getAndIncrement()) + .pos(0, mainGridY.getAndIncrement()) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .build() @@ -297,6 +308,10 @@ public void mouseClicked(MouseEvent e) { // a second call is required to eliminate extra space this.pack(); + final JScrollBar vertical = mainScroll.getVerticalScrollBar(); + // scroll to bottom so declaration snippet is in view + vertical.setValue(vertical.getMaximum()); + if (oldSize == null) { // opening if (oldMousePos.distance(MouseInfo.getPointerInfo().getLocation()) < SMALL_MOVE_THRESHOLD) { @@ -315,18 +330,23 @@ public void mouseClicked(MouseEvent e) { this.declarationSnippet.editor.addMouseListener(stopInteraction); } - this.add(this.declarationSnippet.ui, GridBagConstraintsBuilder.create() - .pos(0, gridY.getAndIncrement()) + mainContent.add(this.declarationSnippet.ui, GridBagConstraintsBuilder.create() + .pos(0, mainGridY.getAndIncrement()) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .anchor(GridBagConstraints.LINE_START) .build() ); } else { - this.addSeparator(gridY.getAndIncrement()); + mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() + .pos(0, mainGridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build()); + - this.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() - .pos(0, gridY.getAndIncrement()) + mainContent.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() + .pos(0, mainGridY.getAndIncrement()) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .anchor(GridBagConstraints.LINE_START) @@ -336,6 +356,13 @@ public void mouseClicked(MouseEvent e) { } } + this.add(mainScroll, GridBagConstraintsBuilder.create() + .pos(0, gridY.getAndIncrement()) + .weight(1, 1) + .fill(GridBagConstraints.BOTH) + .build() + ); + this.pack(); if (opening) { @@ -549,14 +576,6 @@ private ImmutableList paramJavadocsOf( .collect(toImmutableList()); } - private void addSeparator(int gridY) { - this.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, gridY) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .build()); - } - public void close() { this.repopulated = false; this.setVisible(false); From 36a8e89f1508900d48846f8306fb43da85744e7b Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 24 Oct 2025 15:59:39 -0700 Subject: [PATCH 087/109] focus snippet editor ui on source set remove main content scroll pane border --- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 319a36ba9..736fa87fa 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 @@ -217,7 +217,12 @@ public void mousePressed(MouseEvent e) { } final var mainContent = new JPanel(new GridBagLayout()); + // Put all main content in one big scroll pane. + // Ideally there'd be separate javadoc and snippet scroll panes, but multiple scroll pane children + // of a grid bag parent don't play nice when space is limited. + // The snippet has its own scroll pane, but wrapping it in this one effectively disables its resizing. final var mainScroll = new JScrollPane(mainContent); + mainScroll.setBorder(createEmptyBorder()); final var mainGridY = new AtomicInteger(0); final String javadoc = this.getJavadoc(target).orElse(null); @@ -308,6 +313,12 @@ public void mouseClicked(MouseEvent e) { // a second call is required to eliminate extra space this.pack(); + if (this.declarationSnippet != null) { + // without this, the editor gets focus and has a blue border + // but only when it's in a scroll pane, for some reason + this.declarationSnippet.ui.requestFocus(); + } + final JScrollBar vertical = mainScroll.getVerticalScrollBar(); // scroll to bottom so declaration snippet is in view vertical.setValue(vertical.getMaximum()); From e923fd5442332290c3ef94ef7a5d40f19fe0fbee Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 24 Oct 2025 16:42:41 -0700 Subject: [PATCH 088/109] checkstyle --- .../enigma/gui/panel/EntryTooltip.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 736fa87fa..009b520ce 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 @@ -230,11 +230,11 @@ public void mousePressed(MouseEvent e) { this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .build()); - + .pos(0, mainGridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build() + ); if (javadoc != null) { mainContent.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() @@ -350,11 +350,11 @@ public void mouseClicked(MouseEvent e) { ); } else { mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .build()); - + .pos(0, mainGridY.getAndIncrement()) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .build() + ); mainContent.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() .pos(0, mainGridY.getAndIncrement()) From 18be34f805bd7d95d64d2ab12b0c46a05a7f5e78 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 24 Oct 2025 16:48:10 -0700 Subject: [PATCH 089/109] replace AtomicInteger gridY's with primitive ints --- .../enigma/gui/panel/EntryTooltip.java | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 009b520ce..3eff274a4 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 @@ -55,7 +55,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -194,7 +193,7 @@ public void mousePressed(MouseEvent e) { final Font editorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value()); final Font italEditorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); - final AtomicInteger gridY = new AtomicInteger(0); + int gridY = 0; { final Box parentLabelRow = Box.createHorizontalBox(); @@ -209,7 +208,7 @@ public void mousePressed(MouseEvent e) { parentLabelRow.add(Box.createHorizontalGlue()); this.add(parentLabelRow, GridBagConstraintsBuilder.create() - .pos(0, gridY.getAndIncrement()) + .pos(0, gridY++) .insets(ROW_OUTER_INSET, ROW_OUTER_INSET, ROW_INNER_INSET, ROW_OUTER_INSET) .anchor(GridBagConstraints.LINE_START) .build() @@ -223,14 +222,14 @@ public void mousePressed(MouseEvent e) { // The snippet has its own scroll pane, but wrapping it in this one effectively disables its resizing. final var mainScroll = new JScrollPane(mainContent); mainScroll.setBorder(createEmptyBorder()); - final var mainGridY = new AtomicInteger(0); + int mainGridY = 0; final String javadoc = this.getJavadoc(target).orElse(null); final ImmutableList paramJavadocs = this.paramJavadocsOf(target, editorFont, italEditorFont, stopInteraction); if (javadoc != null || !paramJavadocs.isEmpty()) { mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .build() @@ -238,7 +237,7 @@ public void mousePressed(MouseEvent e) { if (javadoc != null) { mainContent.add(javadocOf(javadoc, italEditorFont, stopInteraction), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .insets(ROW_INNER_INSET, ROW_OUTER_INSET) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) @@ -249,17 +248,17 @@ public void mousePressed(MouseEvent e) { if (!paramJavadocs.isEmpty()) { final JPanel params = new JPanel(new GridBagLayout()); - final AtomicInteger paramsGridY = new AtomicInteger(0); + int paramsGridY = 0; for (final ParamJavadoc paramJavadoc : paramJavadocs) { params.add(paramJavadoc.name, GridBagConstraintsBuilder.create() - .pos(0, paramsGridY.get()) + .pos(0, paramsGridY) .anchor(GridBagConstraints.FIRST_LINE_END) .build() ); params.add(paramJavadoc.javadoc, GridBagConstraintsBuilder.create() - .pos(1, paramsGridY.getAndIncrement()) + .pos(1, paramsGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .anchor(GridBagConstraints.LINE_START) @@ -269,7 +268,7 @@ public void mousePressed(MouseEvent e) { mainContent.add(params, GridBagConstraintsBuilder.create() .insets(ROW_INNER_INSET, ROW_OUTER_INSET) - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .build() @@ -342,7 +341,7 @@ public void mouseClicked(MouseEvent e) { } mainContent.add(this.declarationSnippet.ui, GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .anchor(GridBagConstraints.LINE_START) @@ -350,14 +349,14 @@ public void mouseClicked(MouseEvent e) { ); } else { mainContent.add(new JSeparator(), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .build() ); mainContent.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() - .pos(0, mainGridY.getAndIncrement()) + .pos(0, mainGridY++) .weightX(1) .fill(GridBagConstraints.HORIZONTAL) .anchor(GridBagConstraints.LINE_START) @@ -368,7 +367,7 @@ public void mouseClicked(MouseEvent e) { } this.add(mainScroll, GridBagConstraintsBuilder.create() - .pos(0, gridY.getAndIncrement()) + .pos(0, gridY++) .weight(1, 1) .fill(GridBagConstraints.BOTH) .build() From dd47e884e0cedfc800b8738a16872fdf27cc2e90 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 07:39:26 -0700 Subject: [PATCH 090/109] hard cap tooltip width at 600px (scaled) --- .../main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 3eff274a4..778c032ca 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 @@ -69,6 +69,8 @@ public class EntryTooltip extends JWindow { private static final int ROW_OUTER_INSET = 8; private static final int ROW_INNER_INSET = 2; + private static final int DEFAULT_MAX_WIDTH = 600; + private final Gui gui; private final JPanel content; @@ -155,7 +157,7 @@ public Dimension getPreferredSize() { final Dimension superPreferred = super.getPreferredSize(); final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); return new Dimension( - Math.min(superPreferred.width, screenSize.width / 2), + Math.min(Math.min(superPreferred.width, screenSize.width / 2), ScaleUtil.scale(DEFAULT_MAX_WIDTH)), Math.min(superPreferred.height, screenSize.height / 2) ); } From 55613484ffd1067e67fb253cd656dda93b2ceded Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 11:56:09 -0700 Subject: [PATCH 091/109] expand parent package path when navigating from tooltip --- .../src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 1 + 1 file changed, 1 insertion(+) 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 778c032ca..a126dbef8 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 @@ -721,6 +721,7 @@ public void mouseClicked(MouseEvent e) { EntryTooltip.this.gui.openDocker(docker.getClass()); selector.setSelectionPath(path); + selector.expandPath(path); selector.scrollPathToVisible(path); selector.requestFocus(); From 03c1015017f8985d3980121084bad2ebba9eb9c3 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 12:44:08 -0700 Subject: [PATCH 092/109] explicitly check if source is bounded when setting 'Unable to locate declaration' message show for constructor simple names --- .../enigma/gui/panel/BaseEditorPanel.java | 4 ++++ .../gui/panel/DeclarationSnippetPanel.java | 21 ++++++++----------- .../enigma/gui/panel/EntryTooltip.java | 16 ++++++++------ 3 files changed, 23 insertions(+), 18 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 4b5c78f80..1f253a53a 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 @@ -441,6 +441,10 @@ protected void removeSourceSetListener(Consumer listener) this.sourceSetListeners.remove(listener); } + protected boolean isBounded() { + return this.sourceBounds instanceof TrimmedBounds; + } + public void setHighlightedTokens(TokenStore tokenStore, Map> tokens) { // remove any old highlighters this.editor.getHighlighter().removeAllHighlights(); 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 92a3faa2a..47c87f8b4 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,7 +45,6 @@ import org.quiltmc.syntaxpain.LineNumbersRuler; import org.tinylog.Logger; -import javax.annotation.Nullable; import javax.swing.JViewport; import java.awt.Color; import java.util.Comparator; @@ -75,21 +74,19 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl .ifPresent(lineNumbers -> lineNumbers.deinstall(this.editor)); this.addSourceSetListener(source -> { - @Nullable - final Token boundedToken = this.resolveTarget(source, target) - .map(Target::token) - .map(this::navigateToTokenImpl) - .orElse(null); - if (boundedToken == null) { - // the source isn't very useful if it couldn't be trimmed and the declaration couldn't be navigated to + if (!this.isBounded()) { + // the source isn't very useful if it couldn't be trimmed // set this text so it doesn't waste space or cause confusion this.editor.setText("// Unable to locate declaration"); this.editor.getHighlighter().removeAllHighlights(); } else { - this.addHighlight(boundedToken, BoxHighlightPainter.create( - new Color(0, 0, 0, 0), - Config.getCurrentSyntaxPaneColors().selectionHighlight.value() - )); + this.resolveTarget(source, target) + .map(Target::token) + .map(this::navigateToTokenImpl) + .ifPresent(boundedToken -> this.addHighlight(boundedToken, BoxHighlightPainter.create( + new Color(0, 0, 0, 0), + Config.getCurrentSyntaxPaneColors().selectionHighlight.value() + ))); } }); 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 a126dbef8..2dfb805e5 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 @@ -737,12 +737,16 @@ private String getSimpleName(Entry entry) { final EnigmaProject project = this.gui.getController().getProject(); final String simpleObfName = entry.getSimpleName(); - if (!simpleObfName.isEmpty() && Character.isJavaIdentifierStart(simpleObfName.charAt(0))) { - final AccessFlags access = project.getJarIndex().getIndex(EntryIndex.class).getEntryAccess(entry); - if (access == null || !(access.isSynthetic())) { - return project.getRemapper().deobfuscate(entry).getSimpleName(); - } else { - return ""; + if (!simpleObfName.isEmpty()) { + if (Character.isJavaIdentifierStart(simpleObfName.charAt(0))) { + final AccessFlags access = project.getJarIndex().getIndex(EntryIndex.class).getEntryAccess(entry); + if (access == null || !(access.isSynthetic())) { + return project.getRemapper().deobfuscate(entry).getSimpleName(); + } else { + return ""; + } + } else if (simpleObfName.equals("")) { + return simpleObfName; } } From 9afcd612ec9e6a715005918581ad5ced8970a359 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 13:27:14 -0700 Subject: [PATCH 093/109] improve constructor name check --- .../java/org/quiltmc/enigma/gui/panel/EntryTooltip.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 2dfb805e5..c2757c54b 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 @@ -738,15 +738,15 @@ private String getSimpleName(Entry entry) { final String simpleObfName = entry.getSimpleName(); if (!simpleObfName.isEmpty()) { - if (Character.isJavaIdentifierStart(simpleObfName.charAt(0))) { + if (entry instanceof MethodEntry method && method.isConstructor()) { + return simpleObfName; + } else if (Character.isJavaIdentifierStart(simpleObfName.charAt(0))) { final AccessFlags access = project.getJarIndex().getIndex(EntryIndex.class).getEntryAccess(entry); if (access == null || !(access.isSynthetic())) { return project.getRemapper().deobfuscate(entry).getSimpleName(); } else { return ""; } - } else if (simpleObfName.equals("")) { - return simpleObfName; } } From fa6b96ffa440b4511c0919661303b7e13dc81001 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 16:41:27 -0700 Subject: [PATCH 094/109] use 'inherited from' label for tooltip labels for entries that resolved to a parent --- .../enigma/gui/panel/BaseEditorPanel.java | 21 ++++++++++++------- .../quiltmc/enigma/gui/panel/EditorPanel.java | 12 +++++------ .../enigma/gui/panel/EntryTooltip.java | 18 +++++++++------- 3 files changed, 30 insertions(+), 21 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 1f253a53a..538fc6746 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 @@ -60,7 +60,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -270,21 +269,22 @@ public boolean isOptimizedDrawingEnabled() { } /** - * @see #consumeEditorMouseTarget(BiConsumer, Runnable) + * @see #consumeEditorMouseTarget(MouseTargetAction, Runnable) */ - protected void consumeEditorMouseTarget(BiConsumer> action) { + protected void consumeEditorMouseTarget(MouseTargetAction action) { this.consumeEditorMouseTarget(action, Runnables.doNothing()); } /** * If the mouse is currently over a {@link Token} in the {@link #editor} that resolves to an {@link Entry}, passes - * the token and entry to the passed {@code action}.
+ * the token and entry to the passed {@code action}, + * along with whether the entry is a resolved parent of the targeted entry.
* Otherwise, calls the passed {@code onNoTarget}. * * @param action the action to run when the mouse is over a token that resolves to an entry * @param onNoTarget the action to run when the mouse is not over a token that resolves to an entry */ - protected void consumeEditorMouseTarget(BiConsumer> action, Runnable onNoTarget) { + protected void consumeEditorMouseTarget(MouseTargetAction action, Runnable onNoTarget) { consumeMousePositionIn( this.editor, (absoluteMouse, relativeMouse) -> Optional.of(relativeMouse) @@ -294,9 +294,11 @@ protected void consumeEditorMouseTarget(BiConsumer> action, Runn .ifPresentOrElse( token -> Optional.of(token) .map(this::getReference) - .map(this::resolveReference) .ifPresentOrElse( - entry -> action.accept(token, entry), + reference -> { + final Entry resolved = this.resolveReference(reference); + action.run(token, resolved, !resolved.equals(reference.entry)); + }, onNoTarget ), onNoTarget @@ -842,4 +844,9 @@ public void removeListener() { this.handle.removeListener(this.listener); } } + + @FunctionalInterface + protected interface MouseTargetAction { + void run(Token token, Entry entry, boolean resolvedParent); + } } 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 d4b0f3d7d..053ce4bb5 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 @@ -64,14 +64,14 @@ public class EditorPanel extends BaseEditorPanel { private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { if (Config.editor().entryTooltip.enable.value()) { this.consumeEditorMouseTarget( - (token, entry) -> { + (token, entry, resolvedParent) -> { this.hideTooltipTimer.stop(); if (this.entryTooltip.isVisible()) { this.showTooltipTimer.stop(); if (!token.equals(this.lastMouseTargetToken)) { this.lastMouseTargetToken = token; - this.openTooltip(entry); + this.openTooltip(entry, resolvedParent); } } else { this.lastMouseTargetToken = token; @@ -93,10 +93,10 @@ public class EditorPanel extends BaseEditorPanel { private final Timer showTooltipTimer = new Timer( ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { - this.consumeEditorMouseTarget((token, entry) -> { + this.consumeEditorMouseTarget((token, entry, resolvedParent) -> { if (token.equals(this.lastMouseTargetToken)) { this.entryTooltip.setVisible(true); - this.openTooltip(entry); + this.openTooltip(entry, resolvedParent); } }); } @@ -254,8 +254,8 @@ private void onTooltipClose() { this.hideTooltipTimer.stop(); } - private void openTooltip(Entry target) { - this.entryTooltip.open(target); + private void openTooltip(Entry target, boolean inherited) { + this.entryTooltip.open(target, inherited); } public void onRename(boolean isNewMapping) { 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 c2757c54b..d2421fbbd 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 @@ -174,12 +174,12 @@ public boolean hasRepopulated() { * * @param target the entry whose information will be displayed */ - public void open(Entry target) { - this.populateWith(target, true); + public void open(Entry target, boolean inherited) { + this.populateWith(target, inherited, true); this.setVisible(true); } - private void populateWith(Entry target, boolean opening) { + private void populateWith(Entry target, boolean inherited, boolean opening) { this.repopulated = !opening; this.content.removeAll(); @@ -200,7 +200,7 @@ public void mousePressed(MouseEvent e) { { final Box parentLabelRow = Box.createHorizontalBox(); - final JLabel from = labelOf("from", italEditorFont); + final JLabel from = labelOf(inherited ? "inherited from" : "from", italEditorFont); // the italics cause it to overlap with the colon if it has no right padding from.setBorder(createEmptyBorder(0, 0, 0, 1)); parentLabelRow.add(from); @@ -297,9 +297,11 @@ public void mousePressed(MouseEvent e) { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { - EntryTooltip.this.declarationSnippet.consumeEditorMouseTarget((token, entry) -> { - EntryTooltip.this.onEntryClick(entry, e.getModifiersEx()); - }); + EntryTooltip.this.declarationSnippet + .consumeEditorMouseTarget((token, entry, resolvedParent) -> { + EntryTooltip.this.onEntryClick(entry, e.getModifiersEx()); + } + ); } } }); @@ -498,7 +500,7 @@ private void onEntryClick(Entry entry, int modifiers) { this.close(); this.gui.getController().navigateTo(entry); } else { - this.populateWith(entry, false); + this.populateWith(entry, false, false); } } From 86cca4c405f9a0094ffb97fd3f717b6807f295e6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 18:33:18 -0700 Subject: [PATCH 095/109] dispatch key events that close the entry tooltip to the previously focused component --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 8 +++- .../enigma/gui/panel/EntryTooltip.java | 45 ++++++++++++++----- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 16 +++++-- 3 files changed, 55 insertions(+), 14 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 053ce4bb5..5205908d9 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 @@ -21,6 +21,7 @@ import java.awt.Component; import java.awt.GridBagConstraints; +import java.awt.KeyboardFocusManager; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -44,6 +45,7 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionIn; import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; +import static javax.swing.SwingUtilities.isDescendingFrom; import static java.awt.event.InputEvent.CTRL_DOWN_MASK; public class EditorPanel extends BaseEditorPanel { @@ -255,7 +257,11 @@ private void onTooltipClose() { } private void openTooltip(Entry target, boolean inherited) { - this.entryTooltip.open(target, inherited); + final Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + final Component eventReceiver = focusOwner != null && isDescendingFrom(focusOwner, this.gui.getFrame()) + ? focusOwner : null; + + this.entryTooltip.open(target, inherited, eventReceiver); } public void onRename(boolean isNewMapping) { 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 d2421fbbd..7423eb178 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 @@ -35,6 +35,7 @@ import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; @@ -85,6 +86,9 @@ public class EntryTooltip extends JWindow { @Nullable private DeclarationSnippetPanel declarationSnippet; + @Nullable + private Component eventReceiver; + public EntryTooltip(Gui gui) { super(gui.getFrame()); @@ -117,6 +121,7 @@ public void mousePressed(MouseEvent e) { e.consume(); } else { + // dispatching mouse events here causes cast exceptions when receivers get unexpected event sources EntryTooltip.this.close(); } } @@ -137,7 +142,7 @@ public void mouseDragged(MouseEvent e) { Toolkit.getDefaultToolkit().addAWTEventListener( e -> { if (this.isShowing() && e.getID() == KeyEvent.KEY_TYPED) { - this.close(); + this.closeAndDispatch(e); } }, AWTEvent.KEY_EVENT_MASK @@ -172,9 +177,13 @@ public boolean hasRepopulated() { /** * Opens this tooltip and populates it with information about the passed {@code target}. * - * @param target the entry whose information will be displayed + * @param target the entry whose information will be displayed + * @param inherited whether this tooltip is displaying information about the parent of another entry + * @param eventReceiver a component to receive events such as key presses that cause this tooltip to close; + * may be {@code null} */ - public void open(Entry target, boolean inherited) { + public void open(Entry target, boolean inherited, @Nullable Component eventReceiver) { + this.eventReceiver = eventReceiver; this.populateWith(target, inherited, true); this.setVisible(true); } @@ -184,13 +193,16 @@ private void populateWith(Entry target, boolean inherited, boolean opening) { this.content.removeAll(); @Nullable - final MouseAdapter stopInteraction = Config.editor().entryTooltip.interactable.value() ? null : new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - EntryTooltip.this.close(); - e.consume(); - } - }; + final MouseAdapter stopInteraction = Config.editor().entryTooltip.interactable.value() + ? null : new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + // dispatching mouse events here causes cast exceptions when + // receivers get unexpected event sources + EntryTooltip.this.close(); + e.consume(); + } + }; final Font editorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value()); final Font italEditorFont = ScaleUtil.scaleFont(Config.currentFonts().editor.value().deriveFont(Font.ITALIC)); @@ -591,10 +603,23 @@ private ImmutableList paramJavadocsOf( } public void close() { + this.closeAndDispatch(null); + } + + private void closeAndDispatch(@Nullable AWTEvent dispatching) { this.repopulated = false; this.setVisible(false); this.content.removeAll(); + if (this.eventReceiver != null) { + if (dispatching != null) { + // this must be dispatched after setting not visible to avoid a stack overflow in the event dispatching + this.eventReceiver.dispatchEvent(dispatching); + } + + this.eventReceiver = null; + } + if (this.declarationSnippet != null) { this.declarationSnippet.classHandler.removeListener(); this.declarationSnippet = null; 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 1f0fd8983..51f989d3c 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 @@ -367,9 +367,7 @@ public static void consumeMousePositionIn( ) { final Point absolutePos = MouseInfo.getPointerInfo().getLocation(); if (component.isShowing()) { - final Point componentPos = component.getLocationOnScreen(); - final Point relativePos = new Point(absolutePos); - relativePos.translate(-componentPos.x, -componentPos.y); + final Point relativePos = getRelativePos(component, absolutePos); if (component.contains(relativePos)) { inAction.accept(absolutePos, relativePos); @@ -380,6 +378,18 @@ public static void consumeMousePositionIn( outAction.accept(absolutePos); } + public static Point getRelativePos(Component component, Point absolutePos) { + return getRelativePos(component, absolutePos.x, absolutePos.y); + } + + public static Point getRelativePos(Component component, int absoluteX, int absoluteY) { + final Point componentPos = component.getLocationOnScreen(); + componentPos.setLocation(-componentPos.x, -componentPos.y); + componentPos.translate(absoluteX, absoluteY); + + return componentPos; + } + public static Optional getRecordIndexingService(Gui gui) { return gui.getController() .getProject() From fd800d80bedd6692d9cbdef293bf6fe0ca722efc Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 18:50:57 -0700 Subject: [PATCH 096/109] close entry tooltip with global mouse listener --- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 5 +---- .../quiltmc/enigma/gui/panel/EntryTooltip.java | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 7 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 5205908d9..785abd59f 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 @@ -164,10 +164,7 @@ public void mouseClicked(MouseEvent e1) { @Override public void mousePressed(MouseEvent mouseEvent) { - EditorPanel.this.entryTooltip.setVisible(false); - EditorPanel.this.mouseStoppedMovingTimer.stop(); - EditorPanel.this.showTooltipTimer.stop(); - EditorPanel.this.hideTooltipTimer.stop(); + EditorPanel.this.entryTooltip.close(); } @Override 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 7423eb178..af76d4301 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 @@ -59,6 +59,7 @@ import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; +import static org.quiltmc.enigma.gui.util.GuiUtil.consumeMousePositionOut; import static org.quiltmc.enigma.gui.util.GuiUtil.getRecordIndexingService; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.BorderFactory.createLineBorder; @@ -104,11 +105,19 @@ public EntryTooltip(Gui gui) { Toolkit.getDefaultToolkit().addAWTEventListener( e -> { - if (e instanceof MouseEvent mouseEvent && mouseEvent.getID() == MouseEvent.MOUSE_RELEASED) { - EntryTooltip.this.dragStart = null; + if (e instanceof MouseEvent mouseEvent) { + final int id = mouseEvent.getID(); + if (id == MouseEvent.MOUSE_RELEASED) { + EntryTooltip.this.dragStart = null; + } else if ( + this.isVisible() + && id == MouseEvent.MOUSE_PRESSED || id == MouseEvent.MOUSE_CLICKED + ) { + consumeMousePositionOut(this, ignored -> this.close()); + } } }, - MouseEvent.MOUSE_RELEASED + MouseEvent.MOUSE_EVENT_MASK ); this.addMouseListener(new MouseAdapter() { From ac2b2e741138e4dc510342baba945be6160c43b7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 19:56:47 -0700 Subject: [PATCH 097/109] add gui for entry tooltip config --- .../menu_bar/view/EntryTooltipMenu.java | 31 +++++++++++++++ .../gui/element/menu_bar/view/ViewMenu.java | 4 ++ .../org/quiltmc/enigma/gui/util/GuiUtil.java | 39 +++++++++++++++++++ enigma/src/main/resources/lang/en_us.json | 3 ++ 4 files changed, 77 insertions(+) create mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java new file mode 100644 index 000000000..e1b33cdf7 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java @@ -0,0 +1,31 @@ +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.element.menu_bar.AbstractEnigmaMenu; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JCheckBoxMenuItem; + +import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedCheckBox; + +public class EntryTooltipMenu extends AbstractEnigmaMenu { + private final JCheckBoxMenuItem enable = createSyncedCheckBox(Config.editor().entryTooltip.enable); + private final JCheckBoxMenuItem interactable = createSyncedCheckBox(Config.editor().entryTooltip.interactable); + + protected EntryTooltipMenu(Gui gui) { + super(gui); + + this.add(this.enable); + this.add(this.interactable); + + this.retranslate(); + } + + @Override + public void retranslate() { + this.setText(I18n.translate("menu.view.entry_tooltip")); + this.enable.setText(I18n.translate("menu.view.entry_tooltip.enable")); + this.interactable.setText(I18n.translate("menu.view.entry_tooltip.interactable")); + } +} 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 8f39a0373..965cb86bb 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 @@ -14,6 +14,7 @@ public class ViewMenu extends AbstractEnigmaMenu { private final LanguagesMenu languages; private final ThemesMenu themes; private final ScaleMenu scale; + private final EntryTooltipMenu entryTooltip; private final JMenuItem fontItem = new JMenuItem(); @@ -24,12 +25,14 @@ public ViewMenu(Gui gui) { this.languages = new LanguagesMenu(gui); this.themes = new ThemesMenu(gui); this.scale = new ScaleMenu(gui); + this.entryTooltip = new EntryTooltipMenu(gui); this.add(this.themes); this.add(this.languages); this.add(this.notifications); this.add(this.scale); this.add(this.stats); + this.add(this.entryTooltip); this.add(this.fontItem); this.fontItem.addActionListener(e -> this.onFontClicked(this.gui)); @@ -44,6 +47,7 @@ public void retranslate() { this.languages.retranslate(); this.scale.retranslate(); this.stats.retranslate(); + this.entryTooltip.retranslate(); this.fontItem.setText(I18n.translate("menu.view.font")); } 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 51f989d3c..ad2459bfb 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 @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.util; import com.formdev.flatlaf.extras.FlatSVGIcon; +import org.quiltmc.config.api.values.TrackedValue; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.gui.Gui; @@ -17,6 +18,7 @@ import javax.swing.ActionMap; import javax.swing.Icon; import javax.swing.InputMap; +import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; @@ -398,6 +400,43 @@ public static Optional getRecordIndexingService(Gui gui) .map(service -> (RecordIndexingService) service); } + /** + * Creates a {@link JCheckBoxMenuItem} that is kept in sync with the passed {@code config}. + * + * @see #syncStateWithConfig(JCheckBoxMenuItem, TrackedValue) + */ + public static JCheckBoxMenuItem createSyncedCheckBox(TrackedValue config) { + final var box = new JCheckBoxMenuItem(); + syncStateWithConfig(box, config); + + return box; + } + + /** + * Adds listeners to the passed {@code box} and {@code config} that keep the + * {@link JCheckBoxMenuItem#getState() state} of the {@code box} and the + * {@link TrackedValue#value() value} of the {@code config} in sync. + * + * @see #createSyncedCheckBox(TrackedValue) + */ + public 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); + } + }); + } + 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 166b24270..b981ea98e 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -82,6 +82,9 @@ "menu.view.stat_icons.count_fallback": "Count fallback-proposed names", "menu.view.stat_icons.included_types": "Included types", "menu.view.stat_icons.enable_icons": "Enable icons", + "menu.view.entry_tooltip": "Entry Tooltip", + "menu.view.entry_tooltip.enable": "Enable tooltip", + "menu.view.entry_tooltip.interactable": "Allow tooltip interaction", "menu.view.font": "Fonts...", "menu.view.change.title": "Changes", "menu.view.change.summary": "Changes will be applied after the next restart.", From 661a88050c66b8c2f416f3bb6c5a6b9e72ccd98e Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 20:18:40 -0700 Subject: [PATCH 098/109] deduplicate check box sync logic --- .../gui/dialog/EnigmaQuickFindToolBar.java | 25 +++-------- .../menu_bar/view/EntryTooltipMenu.java | 6 +-- .../org/quiltmc/enigma/gui/util/GuiUtil.java | 42 ++++++++++++++----- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java index e68d0969d..f94872929 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/dialog/EnigmaQuickFindToolBar.java @@ -25,14 +25,15 @@ import javax.swing.JCheckBox; import javax.swing.SwingConstants; +import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedCheckBox; import static org.quiltmc.enigma.gui.util.GuiUtil.putKeyBindAction; /** * Extension of {@link QuickFindToolBar} to allow using keybindings, and improve UI. */ public class EnigmaQuickFindToolBar extends QuickFindToolBar { - protected JCheckBox persistentCheckBox; - protected JButton closeButton; + protected final JCheckBox persistentCheckBox = createSyncedCheckBox(Config.editor().persistentQuickFind); + protected final JButton closeButton = new JButton(); public EnigmaQuickFindToolBar() { super(); @@ -56,33 +57,17 @@ public EnigmaQuickFindToolBar() { this.addSeparator(); - this.persistentCheckBox = new JCheckBox(); this.persistentCheckBox.setFocusable(false); this.persistentCheckBox.setOpaque(false); this.persistentCheckBox.setVerticalTextPosition(SwingConstants.BOTTOM); this.persistentCheckBox.setHorizontalTextPosition(SwingConstants.LEADING); this.persistentCheckBox.addActionListener(this); - this.persistentCheckBox.addItemListener(e -> { - final boolean selected = this.persistentCheckBox.isSelected(); - if (selected != Config.editor().persistentQuickFind.value()) { - Config.editor().persistentQuickFind.setValue(selected); - } - - // request focus so when it's lost this may be dismissed - this.requestFocus(); - }); - this.persistentCheckBox.setSelected(Config.editor().persistentQuickFind.value()); - Config.editor().persistentQuickFind.registerCallback(callback -> { - final Boolean configured = callback.value(); - if (this.persistentCheckBox.isSelected() != configured) { - this.persistentCheckBox.setSelected(configured); - } - }); + // request focus so when it's lost this may be dismissed + this.persistentCheckBox.addItemListener(e -> this.requestFocus()); this.add(this.persistentCheckBox); this.addSeparator(); - this.closeButton = new JButton(); this.closeButton.setIcon(GuiUtil.getCloseIcon()); this.closeButton.setFocusable(false); this.closeButton.setOpaque(false); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java index e1b33cdf7..6e9608610 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java @@ -7,11 +7,11 @@ import javax.swing.JCheckBoxMenuItem; -import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedCheckBox; +import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedMenuCheckBox; public class EntryTooltipMenu extends AbstractEnigmaMenu { - private final JCheckBoxMenuItem enable = createSyncedCheckBox(Config.editor().entryTooltip.enable); - private final JCheckBoxMenuItem interactable = createSyncedCheckBox(Config.editor().entryTooltip.interactable); + private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltip.enable); + private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltip.interactable); protected EntryTooltipMenu(Gui gui) { super(gui); 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 ad2459bfb..32f5a91f5 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 @@ -14,10 +14,12 @@ import org.quiltmc.enigma.impl.plugin.RecordIndexingService; import org.quiltmc.enigma.util.Os; +import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.Icon; import javax.swing.InputMap; +import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JLabel; @@ -67,6 +69,7 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; public final class GuiUtil { private GuiUtil() { @@ -405,34 +408,53 @@ public static Optional getRecordIndexingService(Gui gui) * * @see #syncStateWithConfig(JCheckBoxMenuItem, TrackedValue) */ - public static JCheckBoxMenuItem createSyncedCheckBox(TrackedValue config) { + public static JCheckBoxMenuItem createSyncedMenuCheckBox(TrackedValue config) { final var box = new JCheckBoxMenuItem(); syncStateWithConfig(box, config); return box; } + public static JCheckBox createSyncedCheckBox(TrackedValue config) { + final var box = new JCheckBox(); + syncStateWithConfig(box, config); + + return box; + } + /** * Adds listeners to the passed {@code box} and {@code config} that keep the * {@link JCheckBoxMenuItem#getState() state} of the {@code box} and the * {@link TrackedValue#value() value} of the {@code config} in sync. * - * @see #createSyncedCheckBox(TrackedValue) + * @see #createSyncedMenuCheckBox(TrackedValue) */ public static void syncStateWithConfig(JCheckBoxMenuItem box, TrackedValue config) { - box.setState(config.value()); + syncStateWithConfigImpl(box, box::setState, box::getState, config); + } + + public static void syncStateWithConfig(JCheckBox box, TrackedValue config) { + syncStateWithConfigImpl(box, box::setSelected, box::isSelected, config); + } + + private static void syncStateWithConfigImpl( + AbstractButton button, + Consumer buttonSetter, Supplier buttonGetter, + TrackedValue config + ) { + buttonSetter.accept(config.value()); - box.addActionListener(e -> { - final boolean checked = box.getState(); - if (checked != config.value()) { - config.setValue(checked); + button.addActionListener(e -> { + final boolean buttonValue = buttonGetter.get(); + if (buttonValue != config.value()) { + config.setValue(buttonValue); } }); config.registerCallback(updated -> { - final boolean configured = updated.value(); - if (configured != box.getState()) { - box.setState(configured); + final boolean configValue = updated.value(); + if (configValue != buttonGetter.get()) { + buttonSetter.accept(configValue); } }); } From 6cca386bbadcf755f188e8193476a06358ff4544 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 20:27:34 -0700 Subject: [PATCH 099/109] javadoc createSyncedCheckBox --- .../src/main/java/org/quiltmc/enigma/gui/util/GuiUtil.java | 5 +++++ 1 file changed, 5 insertions(+) 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 32f5a91f5..670c1321a 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 @@ -415,6 +415,11 @@ public static JCheckBoxMenuItem createSyncedMenuCheckBox(TrackedValue c return box; } + /** + * Creates a {@link JCheckBox} that is kept in sync with the passed {@code config}. + * + * @see #syncStateWithConfig(JCheckBox, TrackedValue) + */ public static JCheckBox createSyncedCheckBox(TrackedValue config) { final var box = new JCheckBox(); syncStateWithConfig(box, config); From 44b9fe955633fc12feb677af57edcbcbf3da7205 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 27 Oct 2025 21:06:30 -0700 Subject: [PATCH 100/109] javadoc syncStateWithConfig override --- .../main/java/org/quiltmc/enigma/gui/util/GuiUtil.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 670c1321a..dcadef2f4 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 @@ -429,7 +429,7 @@ public static JCheckBox createSyncedCheckBox(TrackedValue config) { /** * Adds listeners to the passed {@code box} and {@code config} that keep the - * {@link JCheckBoxMenuItem#getState() state} of the {@code box} and the + * {@link JCheckBoxMenuItem#getState() state} of the {@code box} and the * {@link TrackedValue#value() value} of the {@code config} in sync. * * @see #createSyncedMenuCheckBox(TrackedValue) @@ -438,6 +438,13 @@ public static void syncStateWithConfig(JCheckBoxMenuItem box, TrackedValue config) { syncStateWithConfigImpl(box, box::setSelected, box::isSelected, config); } From 93f49789519a771269000504b2c7bd3142d0969c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 13:31:03 -0700 Subject: [PATCH 101/109] move tooltip management to EnitorPanel.TooltipManager add/remove EntryTooltip's global listeners on open/close reset TooltipManager on key presses --- .../quiltmc/enigma/gui/panel/EditorPanel.java | 268 ++++++++++-------- .../enigma/gui/panel/EntryTooltip.java | 58 ++-- 2 files changed, 184 insertions(+), 142 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 785abd59f..53bbdfe53 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 @@ -22,6 +22,8 @@ import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.KeyboardFocusManager; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -49,65 +51,11 @@ import static java.awt.event.InputEvent.CTRL_DOWN_MASK; public class EditorPanel extends BaseEditorPanel { - private static final int MOUSE_STOPPED_MOVING_DELAY = 100; - private final NavigatorPanel navigatorPanel; private final EnigmaQuickFindToolBar quickFindToolBar = new EnigmaQuickFindToolBar(); private final EditorPopupMenu popupMenu; - // DIY tooltip because JToolTip can't be moved or resized - private final EntryTooltip entryTooltip = new EntryTooltip(this.gui); - private final WindowAdapter guiLostFocusListener; - - @Nullable - private Token lastMouseTargetToken; - - // avoid finding the mouse entry every mouse movement update - private final Timer mouseStoppedMovingTimer = new Timer(MOUSE_STOPPED_MOVING_DELAY, e -> { - if (Config.editor().entryTooltip.enable.value()) { - this.consumeEditorMouseTarget( - (token, entry, resolvedParent) -> { - this.hideTooltipTimer.stop(); - if (this.entryTooltip.isVisible()) { - this.showTooltipTimer.stop(); - - if (!token.equals(this.lastMouseTargetToken)) { - this.lastMouseTargetToken = token; - this.openTooltip(entry, resolvedParent); - } - } else { - this.lastMouseTargetToken = token; - this.showTooltipTimer.start(); - } - }, - () -> consumeMousePositionIn( - this.entryTooltip.getContentPane(), - (absolute, relative) -> this.hideTooltipTimer.stop(), - absolute -> { - this.lastMouseTargetToken = null; - this.showTooltipTimer.stop(); - this.hideTooltipTimer.start(); - } - ) - ); - } - }); - - private final Timer showTooltipTimer = new Timer( - ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { - this.consumeEditorMouseTarget((token, entry, resolvedParent) -> { - if (token.equals(this.lastMouseTargetToken)) { - this.entryTooltip.setVisible(true); - this.openTooltip(entry, resolvedParent); - } - }); - } - ); - - private final Timer hideTooltipTimer = new Timer( - ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, - e -> this.entryTooltip.close() - ); + private final TooltipManager tooltipManager = new TooltipManager(); private final List listeners = new ArrayList<>(); @@ -142,17 +90,6 @@ public void focusLost(FocusEvent e) { this.popupMenu = new EditorPopupMenu(this, gui); this.editor.setComponentPopupMenu(this.popupMenu.getUi()); - this.entryTooltip.addCloseListener(this::onTooltipClose); - this.guiLostFocusListener = new WindowAdapter() { - @Override - public void windowLostFocus(WindowEvent e) { - if (e.getOppositeWindow() != EditorPanel.this.entryTooltip) { - EditorPanel.this.entryTooltip.close(); - } - } - }; - this.gui.getFrame().addWindowFocusListener(this.guiLostFocusListener); - this.editor.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e1) { @@ -162,11 +99,6 @@ public void mouseClicked(MouseEvent e1) { } } - @Override - public void mousePressed(MouseEvent mouseEvent) { - EditorPanel.this.entryTooltip.close(); - } - @Override public void mouseReleased(MouseEvent e1) { switch (e1.getButton()) { @@ -180,35 +112,8 @@ public void mouseReleased(MouseEvent e1) { } }); - this.editor.addMouseMotionListener(new MouseAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - if (!EditorPanel.this.entryTooltip.hasRepopulated()) { - EditorPanel.this.mouseStoppedMovingTimer.restart(); - } - } - }); - this.editor.addCaretListener(event -> this.onCaretMove(event.getDot())); - this.editorScrollPane.getViewport().addChangeListener(e -> this.entryTooltip.close()); - - this.mouseStoppedMovingTimer.setRepeats(false); - this.showTooltipTimer.setRepeats(false); - this.hideTooltipTimer.setRepeats(false); - - this.entryTooltip.setVisible(false); - - this.entryTooltip.addMouseMotionListener(new MouseAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - if (Config.editor().entryTooltip.interactable.value()) { - EditorPanel.this.mouseStoppedMovingTimer.stop(); - EditorPanel.this.hideTooltipTimer.stop(); - } - } - }); - this.editor.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent event) { @@ -246,21 +151,6 @@ public void keyTyped(KeyEvent event) { this.ui.putClientProperty(EditorPanel.class, this); } - private void onTooltipClose() { - this.lastMouseTargetToken = null; - this.mouseStoppedMovingTimer.stop(); - this.showTooltipTimer.stop(); - this.hideTooltipTimer.stop(); - } - - private void openTooltip(Entry target, boolean inherited) { - final Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); - final Component eventReceiver = focusOwner != null && isDescendingFrom(focusOwner, this.gui.getFrame()) - ? focusOwner : null; - - this.entryTooltip.open(target, inherited, eventReceiver); - } - public void onRename(boolean isNewMapping) { this.navigatorPanel.updateAllTokenTypes(); if (isNewMapping) { @@ -305,7 +195,7 @@ public static EditorPanel byUi(Component ui) { @Override public void destroy() { super.destroy(); - this.gui.getFrame().removeWindowFocusListener(this.guiLostFocusListener); + this.tooltipManager.removeExternalListeners(); } public NavigatorPanel getNavigatorPanel() { @@ -362,13 +252,13 @@ protected void setCursorReference(EntryReference, Entry> ref) { @Override public void offsetEditorZoom(int zoomAmount) { super.offsetEditorZoom(zoomAmount); - this.entryTooltip.setZoom(zoomAmount); + this.tooltipManager.entryTooltip.setZoom(zoomAmount); } @Override public void resetEditorZoom() { super.resetEditorZoom(); - this.entryTooltip.resetZoom(); + this.tooltipManager.entryTooltip.resetZoom(); } public void addListener(EditorActionListener listener) { @@ -402,4 +292,150 @@ public void actionPerformed(JTextComponent target, SyntaxDocument sDoc, int dot, this.popupMenu.getButtonKeyBinds().forEach((key, button) -> putKeyBindAction(key, this.editor, e -> button.doClick())); } + + private class TooltipManager { + static final int MOUSE_STOPPED_MOVING_DELAY = 100; + + // DIY tooltip because JToolTip can't be moved or resized + private final EntryTooltip entryTooltip = new EntryTooltip(EditorPanel.this.gui); + + private final WindowAdapter guiFocusListener = new WindowAdapter() { + @Override + public void windowLostFocus(WindowEvent e) { + if (e.getOppositeWindow() != TooltipManager.this.entryTooltip) { + TooltipManager.this.entryTooltip.close(); + } + } + }; + + private final AWTEventListener globalKeyListener = e -> { + if (e.getID() == KeyEvent.KEY_TYPED || e.getID() == KeyEvent.KEY_PRESSED) { + this.reset(); + } + }; + + @Nullable + private Token lastMouseTargetToken; + + // Avoid finding the mouse entry every mouse movement update. + // 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().entryTooltip.enable.value()) { + EditorPanel.this.consumeEditorMouseTarget( + (token, entry, resolvedParent) -> { + this.hideTimer.stop(); + if (this.entryTooltip.isVisible()) { + this.showTimer.stop(); + + if (!token.equals(this.lastMouseTargetToken)) { + this.lastMouseTargetToken = token; + this.openTooltip(entry, resolvedParent); + } + } else { + this.lastMouseTargetToken = token; + this.showTimer.start(); + } + }, + () -> consumeMousePositionIn( + this.entryTooltip.getContentPane(), + (absolute, relative) -> this.hideTimer.stop(), + absolute -> { + this.lastMouseTargetToken = null; + this.showTimer.stop(); + this.hideTimer.start(); + } + ) + ); + } + }); + + final Timer showTimer = new Timer( + ToolTipManager.sharedInstance().getInitialDelay() - MOUSE_STOPPED_MOVING_DELAY, e -> { + EditorPanel.this.consumeEditorMouseTarget((token, entry, resolvedParent) -> { + if (token.equals(this.lastMouseTargetToken)) { + this.entryTooltip.setVisible(true); + this.openTooltip(entry, resolvedParent); + } + }); + } + ); + + final Timer hideTimer = new Timer( + ToolTipManager.sharedInstance().getDismissDelay() - MOUSE_STOPPED_MOVING_DELAY, + e -> this.entryTooltip.close() + ); + + TooltipManager() { + this.mouseStoppedMovingTimer.setRepeats(false); + this.showTimer.setRepeats(false); + this.hideTimer.setRepeats(false); + + this.entryTooltip.setVisible(false); + + this.entryTooltip.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (Config.editor().entryTooltip.interactable.value()) { + TooltipManager.this.mouseStoppedMovingTimer.stop(); + TooltipManager.this.hideTimer.stop(); + } + } + }); + + this.entryTooltip.addCloseListener(TooltipManager.this::reset); + + EditorPanel.this.editor.addKeyListener(new KeyAdapter() { + @Override + public void keyTyped(KeyEvent e) { + TooltipManager.this.reset(); + } + }); + + EditorPanel.this.editor.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) { + TooltipManager.this.entryTooltip.close(); + } + }); + + EditorPanel.this.editor.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (!TooltipManager.this.entryTooltip.hasRepopulated()) { + TooltipManager.this.mouseStoppedMovingTimer.restart(); + } + } + }); + + EditorPanel.this.editorScrollPane.getViewport().addChangeListener(e -> this.entryTooltip.close()); + + this.addExternalListeners(); + } + + void reset() { + this.lastMouseTargetToken = null; + this.mouseStoppedMovingTimer.stop(); + this.showTimer.stop(); + this.hideTimer.stop(); + } + + void openTooltip(Entry target, boolean inherited) { + final Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + final Component eventReceiver = focusOwner != null && isDescendingFrom(focusOwner, EditorPanel.this.gui.getFrame()) + ? focusOwner : null; + + this.entryTooltip.open(target, inherited, eventReceiver); + } + + void addExternalListeners() { + EditorPanel.this.gui.getFrame().addWindowFocusListener(this.guiFocusListener); + Toolkit.getDefaultToolkit().addAWTEventListener(this.globalKeyListener, KeyEvent.KEY_EVENT_MASK); + } + + void removeExternalListeners() { + EditorPanel.this.gui.getFrame().removeWindowFocusListener(this.guiFocusListener); + Toolkit.getDefaultToolkit().removeAWTEventListener(this.globalKeyListener); + } + } } 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 af76d4301..b052b91f4 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 @@ -44,6 +44,7 @@ import java.awt.Point; import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.AWTEventListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -76,6 +77,23 @@ public class EntryTooltip extends JWindow { private final Gui gui; private final JPanel content; + private final AWTEventListener globalMouseListener = e -> { + if (e instanceof MouseEvent mouseEvent) { + final int id = mouseEvent.getID(); + if (id == MouseEvent.MOUSE_RELEASED) { + EntryTooltip.this.dragStart = null; + } else if (this.isVisible() && id == MouseEvent.MOUSE_PRESSED || id == MouseEvent.MOUSE_CLICKED) { + consumeMousePositionOut(this, ignored -> this.close()); + } + } + }; + + private final AWTEventListener globalKeyListener = e -> { + if (this.isShowing() && e.getID() == KeyEvent.KEY_TYPED) { + this.closeAndDispatch(e); + } + }; + private final Set closeListeners = new HashSet<>(); private int zoomAmount; @@ -103,23 +121,6 @@ public EntryTooltip(Gui gui) { this.setContentPane(this.content); this.content.setBorder(createLineBorder(Config.getCurrentSyntaxPaneColors().lineNumbersSelected.value())); - Toolkit.getDefaultToolkit().addAWTEventListener( - e -> { - if (e instanceof MouseEvent mouseEvent) { - final int id = mouseEvent.getID(); - if (id == MouseEvent.MOUSE_RELEASED) { - EntryTooltip.this.dragStart = null; - } else if ( - this.isVisible() - && id == MouseEvent.MOUSE_PRESSED || id == MouseEvent.MOUSE_CLICKED - ) { - consumeMousePositionOut(this, ignored -> this.close()); - } - } - }, - MouseEvent.MOUSE_EVENT_MASK - ); - this.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { @@ -148,15 +149,6 @@ public void mouseDragged(MouseEvent e) { } }); - Toolkit.getDefaultToolkit().addAWTEventListener( - e -> { - if (this.isShowing() && e.getID() == KeyEvent.KEY_TYPED) { - this.closeAndDispatch(e); - } - }, - AWTEvent.KEY_EVENT_MASK - ); - this.addWindowFocusListener(new WindowAdapter() { @Override public void windowLostFocus(WindowEvent e) { @@ -195,6 +187,8 @@ public void open(Entry target, boolean inherited, @Nullable Component eventRe this.eventReceiver = eventReceiver; this.populateWith(target, inherited, true); this.setVisible(true); + + this.addExternalListeners(); } private void populateWith(Entry target, boolean inherited, boolean opening) { @@ -431,6 +425,16 @@ public void removeCloseListener(Runnable listener) { this.closeListeners.remove(listener); } + private void addExternalListeners() { + Toolkit.getDefaultToolkit().addAWTEventListener(this.globalMouseListener, MouseEvent.MOUSE_EVENT_MASK); + Toolkit.getDefaultToolkit().addAWTEventListener(this.globalKeyListener, AWTEvent.KEY_EVENT_MASK); + } + + private void removeExternalListeners() { + Toolkit.getDefaultToolkit().removeAWTEventListener(this.globalMouseListener); + Toolkit.getDefaultToolkit().removeAWTEventListener(this.globalKeyListener); + } + /** * Moves this so it's near but not under the cursor, favoring the bottom right. * @@ -616,6 +620,8 @@ public void close() { } private void closeAndDispatch(@Nullable AWTEvent dispatching) { + this.removeExternalListeners(); + this.repopulated = false; this.setVisible(false); this.content.removeAll(); From 77b4326cb0c228e596d3cc4bb192fb0186f1b284 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Wed, 29 Oct 2025 16:46:43 -0700 Subject: [PATCH 102/109] minor refactor --- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 53bbdfe53..7b3b3bad2 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 @@ -297,9 +297,9 @@ private class TooltipManager { static final int MOUSE_STOPPED_MOVING_DELAY = 100; // DIY tooltip because JToolTip can't be moved or resized - private final EntryTooltip entryTooltip = new EntryTooltip(EditorPanel.this.gui); + final EntryTooltip entryTooltip = new EntryTooltip(EditorPanel.this.gui); - private final WindowAdapter guiFocusListener = new WindowAdapter() { + final WindowAdapter guiFocusListener = new WindowAdapter() { @Override public void windowLostFocus(WindowEvent e) { if (e.getOppositeWindow() != TooltipManager.this.entryTooltip) { @@ -308,15 +308,12 @@ public void windowLostFocus(WindowEvent e) { } }; - private final AWTEventListener globalKeyListener = e -> { + final AWTEventListener globalKeyListener = e -> { if (e.getID() == KeyEvent.KEY_TYPED || e.getID() == KeyEvent.KEY_PRESSED) { this.reset(); } }; - @Nullable - private Token lastMouseTargetToken; - // Avoid finding the mouse entry every mouse movement update. // This also reduces the chances of accidentally updating the tooltip with // a new entry's content as you move your mouse to the tooltip. @@ -366,6 +363,9 @@ public void windowLostFocus(WindowEvent e) { e -> this.entryTooltip.close() ); + @Nullable + Token lastMouseTargetToken; + TooltipManager() { this.mouseStoppedMovingTimer.setRepeats(false); this.showTimer.setRepeats(false); From be44d7d8649f1bbda7a8359647fcba51c16b29e6 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 30 Oct 2025 11:17:30 -0700 Subject: [PATCH 103/109] make 'tooltips' consistently plural in user-facing strings --- .../quiltmc/enigma/gui/config/EditorConfig.java | 4 ++-- ...oltipSection.java => EntryTooltipsSection.java} | 2 +- ...ntryTooltipMenu.java => EntryTooltipsMenu.java} | 14 +++++++------- .../enigma/gui/element/menu_bar/view/ViewMenu.java | 8 ++++---- .../org/quiltmc/enigma/gui/panel/EditorPanel.java | 4 ++-- .../org/quiltmc/enigma/gui/panel/EntryTooltip.java | 4 ++-- enigma/src/main/resources/lang/en_us.json | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/{EntryTooltipSection.java => EntryTooltipsSection.java} (90%) rename enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/{EntryTooltipMenu.java => EntryTooltipsMenu.java} (66%) 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 061cebbe8..4e179bad7 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 @@ -11,6 +11,6 @@ public class EditorConfig extends ReflectiveConfig { @Comment("Whether editors' quick find toolbars should remain visible when they lose focus.") public final TrackedValue persistentQuickFind = this.value(true); - @Comment("Settings for the editor's entry tooltip.") - public final EntryTooltipSection entryTooltip = new EntryTooltipSection(); + @Comment("Settings for editors' entry tooltips.") + public final EntryTooltipsSection entryTooltips = new EntryTooltipsSection(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipsSection.java similarity index 90% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipsSection.java index ac843c84c..aedf5be46 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/EntryTooltipsSection.java @@ -7,7 +7,7 @@ import org.quiltmc.config.api.values.TrackedValue; @SerializedNameConvention(NamingSchemes.SNAKE_CASE) -public class EntryTooltipSection extends ReflectiveConfig.Section { +public class EntryTooltipsSection extends ReflectiveConfig.Section { @Comment("Whether tooltips are enabled.") public final TrackedValue enable = this.value(true); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java similarity index 66% rename from enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java rename to enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java index 6e9608610..1598cef8c 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/EntryTooltipsMenu.java @@ -9,11 +9,11 @@ import static org.quiltmc.enigma.gui.util.GuiUtil.createSyncedMenuCheckBox; -public class EntryTooltipMenu extends AbstractEnigmaMenu { - private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltip.enable); - private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltip.interactable); +public class EntryTooltipsMenu extends AbstractEnigmaMenu { + private final JCheckBoxMenuItem enable = createSyncedMenuCheckBox(Config.editor().entryTooltips.enable); + private final JCheckBoxMenuItem interactable = createSyncedMenuCheckBox(Config.editor().entryTooltips.interactable); - protected EntryTooltipMenu(Gui gui) { + protected EntryTooltipsMenu(Gui gui) { super(gui); this.add(this.enable); @@ -24,8 +24,8 @@ protected EntryTooltipMenu(Gui gui) { @Override public void retranslate() { - this.setText(I18n.translate("menu.view.entry_tooltip")); - this.enable.setText(I18n.translate("menu.view.entry_tooltip.enable")); - this.interactable.setText(I18n.translate("menu.view.entry_tooltip.interactable")); + this.setText(I18n.translate("menu.view.entry_tooltips")); + this.enable.setText(I18n.translate("menu.view.entry_tooltips.enable")); + this.interactable.setText(I18n.translate("menu.view.entry_tooltips.interactable")); } } 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 965cb86bb..a9471b18c 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 @@ -14,7 +14,7 @@ public class ViewMenu extends AbstractEnigmaMenu { private final LanguagesMenu languages; private final ThemesMenu themes; private final ScaleMenu scale; - private final EntryTooltipMenu entryTooltip; + private final EntryTooltipsMenu entryTooltips; private final JMenuItem fontItem = new JMenuItem(); @@ -25,14 +25,14 @@ public ViewMenu(Gui gui) { this.languages = new LanguagesMenu(gui); this.themes = new ThemesMenu(gui); this.scale = new ScaleMenu(gui); - this.entryTooltip = new EntryTooltipMenu(gui); + this.entryTooltips = new EntryTooltipsMenu(gui); this.add(this.themes); this.add(this.languages); this.add(this.notifications); this.add(this.scale); this.add(this.stats); - this.add(this.entryTooltip); + this.add(this.entryTooltips); this.add(this.fontItem); this.fontItem.addActionListener(e -> this.onFontClicked(this.gui)); @@ -47,7 +47,7 @@ public void retranslate() { this.languages.retranslate(); this.scale.retranslate(); this.stats.retranslate(); - this.entryTooltip.retranslate(); + this.entryTooltips.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 7b3b3bad2..f99191eb7 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 @@ -318,7 +318,7 @@ 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().entryTooltip.enable.value()) { + if (Config.editor().entryTooltips.enable.value()) { EditorPanel.this.consumeEditorMouseTarget( (token, entry, resolvedParent) -> { this.hideTimer.stop(); @@ -376,7 +376,7 @@ public void windowLostFocus(WindowEvent e) { this.entryTooltip.addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { - if (Config.editor().entryTooltip.interactable.value()) { + if (Config.editor().entryTooltips.interactable.value()) { TooltipManager.this.mouseStoppedMovingTimer.stop(); TooltipManager.this.hideTimer.stop(); } 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 b052b91f4..18c066622 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 @@ -124,7 +124,7 @@ public EntryTooltip(Gui gui) { this.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - if (Config.editor().entryTooltip.interactable.value()) { + if (Config.editor().entryTooltips.interactable.value()) { EntryTooltip.this.dragStart = e.getButton() == MouseEvent.BUTTON1 ? new Point(e.getX(), e.getY()) : null; @@ -196,7 +196,7 @@ private void populateWith(Entry target, boolean inherited, boolean opening) { this.content.removeAll(); @Nullable - final MouseAdapter stopInteraction = Config.editor().entryTooltip.interactable.value() + final MouseAdapter stopInteraction = Config.editor().entryTooltips.interactable.value() ? null : new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index b981ea98e..5d4fe5a91 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -82,9 +82,9 @@ "menu.view.stat_icons.count_fallback": "Count fallback-proposed names", "menu.view.stat_icons.included_types": "Included types", "menu.view.stat_icons.enable_icons": "Enable icons", - "menu.view.entry_tooltip": "Entry Tooltip", - "menu.view.entry_tooltip.enable": "Enable tooltip", - "menu.view.entry_tooltip.interactable": "Allow tooltip interaction", + "menu.view.entry_tooltips": "Entry Tooltips", + "menu.view.entry_tooltips.enable": "Enable tooltips", + "menu.view.entry_tooltips.interactable": "Allow tooltip interaction", "menu.view.font": "Fonts...", "menu.view.change.title": "Changes", "menu.view.change.summary": "Changes will be applied after the next restart.", From a1c460b75a5aa7d7c18d5cb3627dd091b43a23ea Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Thu, 30 Oct 2025 19:22:07 -0700 Subject: [PATCH 104/109] dispatch KEY_PRESSED events that close entry tooltips (with exception for ctrl+c) --- .../quiltmc/enigma/gui/panel/EntryTooltip.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 18c066622..86940f2af 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 @@ -45,6 +45,7 @@ import java.awt.Toolkit; import java.awt.Window; import java.awt.event.AWTEventListener; +import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -89,8 +90,21 @@ public class EntryTooltip extends JWindow { }; private final AWTEventListener globalKeyListener = e -> { - if (this.isShowing() && e.getID() == KeyEvent.KEY_TYPED) { - this.closeAndDispatch(e); + if (this.isShowing()) { + final int id = e.getID(); + if (id == KeyEvent.KEY_TYPED) { + this.closeAndDispatch(e); + } else if (id == KeyEvent.KEY_PRESSED && e instanceof KeyEvent keyEvent) { + final int modifiers = keyEvent.getModifiersEx(); + final int keyCode = keyEvent.getKeyCode(); + if ( + modifiers != 0 && keyCode != KeyEvent.VK_CONTROL + // special case ctrl+c so an editor's copy doesn't overwrite text copied by a tooltip's copy + && !(keyCode == KeyEvent.VK_C && modifiers == InputEvent.CTRL_DOWN_MASK) + ) { + this.closeAndDispatch(e); + } + } } }; From bd5d3a6b04d2d54afa55ae3e05ee32563987466f Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Fri, 31 Oct 2025 12:40:30 -0700 Subject: [PATCH 105/109] rework moveMaintainingAnchor when tooltip shrunk --- .../enigma/gui/panel/EntryTooltip.java | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index 86940f2af..cca32e353 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.ObfuscatedClassesDocker; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.util.Utils; import javax.annotation.Nullable; import javax.swing.Box; @@ -477,7 +478,16 @@ private void moveNearCursor() { } /** - * After resizing, moves this so that the old distance between the cursor and the closest corner remains the same. + * After resizing, moves this so that the cursor is at an 'equivalent' relative location. + * + *

The way the equivalent location is determined depends on whether this grew or shrunk, independently for the + * {@code x} and {@code y} axes: + *

    + *
  • if this grew in an axis, then the distance between the cursor and the closest edge perpendicular to + * that axis is maintained + *
  • if this shrunk in an axis, then the ratio of the distances between the cursor and the two edges + * perpendicular to that axis is maintained + *
* *

Also ensures this is entirely on-screen. */ @@ -488,24 +498,38 @@ private void moveMaintainingAnchor(Point oldMousePos, Dimension oldSize) { final Point pos = this.getLocationOnScreen(); - final int oldLeft = oldMousePos.x - pos.x; - final int oldRight = pos.x + oldSize.width - oldMousePos.x; - final boolean anchorRight = oldLeft > oldRight; - - final int oldTop = oldMousePos.y - pos.y; - final int oldBottom = pos.y + oldSize.height - oldMousePos.y; - final boolean anchorBottom = oldTop > oldBottom; + final int left = oldMousePos.x - pos.x; + final int top = oldMousePos.y - pos.y; - if (anchorRight || anchorBottom) { - final Dimension newSize = this.getSize(); + final Dimension newSize = this.getSize(); - final int xDiff = anchorRight ? oldSize.width - newSize.width : 0; - final int yDiff = anchorBottom ? oldSize.height - newSize.height : 0; + final int anchoredX; + if (oldSize.width > newSize.width) { + final int targetLeft = (int) ((double) (left * newSize.width) / oldSize.width); + anchoredX = pos.x + left - targetLeft; + } else { + final int oldRight = pos.x + oldSize.width - oldMousePos.x; + final int xDiff = left > oldRight ? oldSize.width - newSize.width : 0; + anchoredX = pos.x + xDiff; + } - this.setLocation(pos.x + xDiff, pos.y + yDiff); + final int anchoredY; + if (oldSize.height > newSize.height) { + final int targetTop = (int) ((double) (top * newSize.height) / oldSize.height); + anchoredY = pos.y + top - targetTop; + } else { + final int oldBottom = pos.y + oldSize.height - oldMousePos.y; + final int yDiff = top > oldBottom ? oldSize.height - newSize.height : 0; + anchoredY = pos.y + yDiff; } - this.moveOnScreen(); + final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + final int targetX = Utils.clamp(anchoredX, 0, screenSize.width - newSize.width); + final int targetY = Utils.clamp(anchoredY, 0, screenSize.height - newSize.height); + + if (targetX != pos.x || targetY != pos.y) { + this.setLocation(targetX, targetY); + } } /** From b03cf64e65d16760c903cd425b7245600f1fce08 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 4 Nov 2025 15:13:25 -0800 Subject: [PATCH 106/109] move FeaturesSection.enableClassTreeStatIcons to StatsSection move FeaturesSection.autoSaveMappings to EditorConfig eliminate FeaturesSection --- .../org/quiltmc/enigma/gui/GuiController.java | 4 ++-- .../org/quiltmc/enigma/gui/config/Config.java | 3 --- .../quiltmc/enigma/gui/config/EditorConfig.java | 3 +++ .../enigma/gui/config/FeaturesSection.java | 15 --------------- .../quiltmc/enigma/gui/config/StatsSection.java | 3 +++ .../enigma/gui/element/ClassTreeCellRenderer.java | 2 +- .../gui/element/menu_bar/file/FileMenu.java | 4 ++-- .../gui/element/menu_bar/view/StatsMenu.java | 4 ++-- .../enigma/gui/node/ClassSelectorClassNode.java | 2 +- .../quiltmc/enigma/gui/util/PackageRenamer.java | 2 +- 10 files changed, 15 insertions(+), 27 deletions(-) delete mode 100644 enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/FeaturesSection.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java index 390542675..9d17cb5ed 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java @@ -197,7 +197,7 @@ public void openMappings(EntryTree mappings) { } public void regenerateAndUpdateStatIcons() { - if (Config.main().features.enableClassTreeStatIcons.value()) { + if (Config.stats().enableClassTreeStatIcons.value()) { ProgressListener progressListener = ProgressListener.createEmpty(); this.gui.getMainWindow().getStatusBar().syncWith(progressListener); @@ -570,7 +570,7 @@ public void applyChange(ValidationContext vc, EntryChange change, boolean upd return; } - if (autosave && Config.main().features.autoSaveMappings.value() && this.gui.mappingsFileChooser.getSelectedFile() != null) { + if (autosave && Config.editor().autoSaveMappings.value() && this.gui.mappingsFileChooser.getSelectedFile() != null) { this.gui.getController().saveMappings(this.gui.mappingsFileChooser.getSelectedFile().toPath(), true); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java index 3210180aa..dac042820 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Config.java @@ -81,9 +81,6 @@ public final class Config extends ReflectiveConfig { @Comment("The settings for the statistics window.") public final StatsSection stats = new StatsSection(); - @Comment("Contains all features that can be toggled on or off.") - public final FeaturesSection features = new FeaturesSection(); - @Comment("You shouldn't enable options in this section unless you know what you're doing!") public final DevSection development = new DevSection(); 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 4e179bad7..eb4acd888 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 @@ -8,6 +8,9 @@ @SerializedNameConvention(NamingSchemes.SNAKE_CASE) public class EditorConfig extends ReflectiveConfig { + @Comment("Enables auto save functionality, which will automatically save mappings when a change is made.") + public final TrackedValue autoSaveMappings = this.value(false); + @Comment("Whether editors' quick find toolbars should remain visible when they lose focus.") public final TrackedValue persistentQuickFind = this.value(true); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/FeaturesSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/FeaturesSection.java deleted file mode 100644 index 6013e89d4..000000000 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/FeaturesSection.java +++ /dev/null @@ -1,15 +0,0 @@ -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 FeaturesSection extends ReflectiveConfig.Section { - @Comment("Enables statistic icons in the class tree. This has a major performance impact on JAR files with lots of classes.") - public final TrackedValue enableClassTreeStatIcons = this.value(true); - @Comment("Enables auto save functionality, which will automatically save mappings when a change is made.") - public final TrackedValue autoSaveMappings = this.value(false); -} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java index 02388d8bf..6f977c4bd 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/StatsSection.java @@ -22,6 +22,9 @@ public class StatsSection extends ReflectiveConfig.Section { public final TrackedValue shouldIncludeSyntheticParameters = this.value(false); public final TrackedValue shouldCountFallbackNames = this.value(false); + @Comment("Enables statistic icons in the class tree. This has a major performance impact on JAR files with lots of classes.") + public final TrackedValue enableClassTreeStatIcons = this.value(true); + public Set getIncludedTypesForIcons(Set editableTypes) { var types = new HashSet<>(editableTypes); types.removeIf(type -> !this.includedStatTypes.value().contains(type)); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java index 75266062d..c0b1a0658 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassTreeCellRenderer.java @@ -90,7 +90,7 @@ String getDisplayName() { JLabel nodeLabel = new JLabel(icon); panel.add(nodeLabel); - if (Config.main().features.enableClassTreeStatIcons.value()) { + if (Config.stats().enableClassTreeStatIcons.value()) { if (this.controller.getStatsGenerator() != null) { ProjectStatsResult stats = this.controller.getStatsGenerator().getResultNullable(Config.stats().createIconGenParameters(this.controller.getGui().getEditableStatTypes())); if (stats == null) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java index 44bb5a178..f0d092bac 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/file/FileMenu.java @@ -80,7 +80,7 @@ public FileMenu(Gui gui) { this.jarCloseItem.addActionListener(e -> this.gui.getController().closeJar()); this.maxRecentFilesItem.addActionListener(e -> this.onMaxRecentFilesClicked()); this.saveMappingsItem.addActionListener(e -> this.onSaveMappingsClicked()); - this.autoSaveMappingsItem.addActionListener(e -> Config.main().features.autoSaveMappings.setValue(this.autoSaveMappingsItem.getState())); + this.autoSaveMappingsItem.addActionListener(e -> Config.editor().autoSaveMappings.setValue(this.autoSaveMappingsItem.getState())); this.closeMappingsItem.addActionListener(e -> this.onCloseMappingsClicked()); this.dropMappingsItem.addActionListener(e -> this.gui.getController().dropMappings()); this.reloadMappingsItem.addActionListener(e -> this.onReloadMappingsClicked()); @@ -109,7 +109,7 @@ public void updateState(boolean jarOpen, ConnectionState state) { this.saveMappingsItem.setEnabled(jarOpen && this.gui.mappingsFileChooser.getSelectedFile() != null && this.gui.getConnectionState() != ConnectionState.CONNECTED); this.saveMappingsAs.updateState(); this.autoSaveMappingsItem.setEnabled(jarOpen); - this.autoSaveMappingsItem.setState(Config.main().features.autoSaveMappings.value()); + this.autoSaveMappingsItem.setState(Config.editor().autoSaveMappings.value()); this.closeMappingsItem.setEnabled(jarOpen); this.reloadMappingsItem.setEnabled(jarOpen); this.reloadAllItem.setEnabled(jarOpen); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java index f61cc7038..9390bead8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/menu_bar/view/StatsMenu.java @@ -58,7 +58,7 @@ public void retranslate() { @Override public void updateState(boolean jarOpen, ConnectionState state) { - this.enableIcons.setSelected(Config.main().features.enableClassTreeStatIcons.value()); + this.enableIcons.setSelected(Config.stats().enableClassTreeStatIcons.value()); this.includeSynthetic.setSelected(Config.main().stats.shouldIncludeSyntheticParameters.value()); this.countFallback.setSelected(Config.main().stats.shouldCountFallbackNames.value()); @@ -69,7 +69,7 @@ public void updateState(boolean jarOpen, ConnectionState state) { } private void onEnableIconsClicked() { - Config.main().features.enableClassTreeStatIcons.setValue(this.enableIcons.isSelected()); + Config.stats().enableClassTreeStatIcons.setValue(this.enableIcons.isSelected()); this.updateIconsLater(); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java index bda7a2d9c..578016553 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/node/ClassSelectorClassNode.java @@ -113,7 +113,7 @@ public void done() { } }; - if (Config.main().features.enableClassTreeStatIcons.value()) { + if (Config.stats().enableClassTreeStatIcons.value()) { SwingUtilities.invokeLater(iconUpdateWorker::execute); } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/PackageRenamer.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/PackageRenamer.java index bc5404c67..efef91929 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/PackageRenamer.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/PackageRenamer.java @@ -194,7 +194,7 @@ public CompletableFuture renamePackage(String path, String input) { classSelector.restoreExpansionState(entry.getValue()); } - if (Config.main().features.autoSaveMappings.value() && this.gui.mappingsFileChooser.getSelectedFile() != null) { + if (Config.editor().autoSaveMappings.value() && this.gui.mappingsFileChooser.getSelectedFile() != null) { this.gui.getController().saveMappings(this.gui.mappingsFileChooser.getSelectedFile().toPath(), true); } }); From 6ac3ff2e3de15fdcf281977e512a98060f2850ca Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 8 Nov 2025 19:36:36 -0800 Subject: [PATCH 107/109] ensure RecordIndexingVisitor is cleared in visitEnd --- .../quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index d33f295dc..ddb0822e2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -98,13 +98,13 @@ public void visitEnd() { super.visitEnd(); try { this.collectResults(); - + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { this.clazz = null; this.recordComponents.clear(); this.fields.clear(); this.methods.clear(); - } catch (Exception ex) { - throw new RuntimeException(ex); } } From f87d0ef38fa7ff37fdacc9cc8905432bc9904261 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 10:00:56 -0800 Subject: [PATCH 108/109] make several tooltip string translatable --- .../enigma/gui/panel/EntryTooltip.java | 22 ++++++++++++------- enigma/src/main/resources/lang/en_us.json | 3 +++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EntryTooltip.java index cca32e353..98d27881c 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.ObfuscatedClassesDocker; import org.quiltmc.enigma.gui.util.GridBagConstraintsBuilder; import org.quiltmc.enigma.gui.util.ScaleUtil; +import org.quiltmc.enigma.util.I18n; import org.quiltmc.enigma.util.Utils; import javax.annotation.Nullable; @@ -230,7 +231,10 @@ public void mousePressed(MouseEvent e) { { final Box parentLabelRow = Box.createHorizontalBox(); - final JLabel from = labelOf(inherited ? "inherited from" : "from", italEditorFont); + final JLabel from = labelOf( + I18n.translate(inherited ? "editor.tooltip.label.inherited_from" : "editor.tooltip.label.from"), + italEditorFont + ); // the italics cause it to overlap with the colon if it has no right padding from.setBorder(createEmptyBorder(0, 0, 0, 1)); parentLabelRow.add(from); @@ -389,13 +393,15 @@ public void mouseClicked(MouseEvent e) { .build() ); - mainContent.add(labelOf("No source available", italEditorFont), GridBagConstraintsBuilder.create() - .pos(0, mainGridY++) - .weightX(1) - .fill(GridBagConstraints.HORIZONTAL) - .anchor(GridBagConstraints.LINE_START) - .insets(ROW_INNER_INSET, ROW_OUTER_INSET) - .build() + mainContent.add( + labelOf(I18n.translate("editor.tooltip.message.no_source"), italEditorFont), + GridBagConstraintsBuilder.create() + .pos(0, mainGridY++) + .weightX(1) + .fill(GridBagConstraints.HORIZONTAL) + .anchor(GridBagConstraints.LINE_START) + .insets(ROW_INNER_INSET, ROW_OUTER_INSET) + .build() ); } } diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 5d4fe5a91..57dc81634 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -154,6 +154,9 @@ "editor.quick_find.wrap": "Wrap", "editor.quick_find.not_found": "Not found!", "editor.quick_find.persistent": "Persistent", + "editor.tooltip.label.from": "from", + "editor.tooltip.label.inherited_from": "inherited from", + "editor.tooltip.message.no_source": "No source available", "info_panel.identifier": "Identifier Info", "info_panel.identifier.none": "No identifier selected", From 6ce443c2b9f14979db1787429bc8a71998233142 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 10:28:23 -0800 Subject: [PATCH 109/109] make a snippet string translatable --- .../org/quiltmc/enigma/gui/panel/DeclarationSnippetPanel.java | 3 ++- enigma/src/main/resources/lang/en_us.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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 47c87f8b4..e5829650c 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.util.I18n; import org.quiltmc.enigma.util.LineIndexer; import org.quiltmc.enigma.util.Result; import org.quiltmc.syntaxpain.LineNumbersRuler; @@ -77,7 +78,7 @@ public DeclarationSnippetPanel(Gui gui, Entry target, ClassHandle targetTopCl if (!this.isBounded()) { // the source isn't very useful if it couldn't be trimmed // set this text so it doesn't waste space or cause confusion - this.editor.setText("// Unable to locate declaration"); + this.editor.setText("// " + I18n.translate("editor.snippet.message.no_declaration_found")); this.editor.getHighlighter().removeAllHighlights(); } else { this.resolveTarget(source, target) diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 57dc81634..c597bab44 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -157,6 +157,7 @@ "editor.tooltip.label.from": "from", "editor.tooltip.label.inherited_from": "inherited from", "editor.tooltip.message.no_source": "No source available", + "editor.snippet.message.no_declaration_found": "Unable to locate declaration", "info_panel.identifier": "Identifier Info", "info_panel.identifier.none": "No identifier selected",