diff options
| author | 2025-10-18 16:38:40 +0100 | |
|---|---|---|
| committer | 2025-10-18 16:38:40 +0100 | |
| commit | 87581ffe8d23aaf8ad677ffbb9de1ecfec3cfe80 (patch) | |
| tree | f3191c08ee6be85c11d8f109e6923c320e74368e /enigma-swing | |
| parent | Fix class version check in AddFramesIfNecessaryClassProvider (#575) (diff) | |
| download | enigma-87581ffe8d23aaf8ad677ffbb9de1ecfec3cfe80.tar.gz enigma-87581ffe8d23aaf8ad677ffbb9de1ecfec3cfe80.tar.xz enigma-87581ffe8d23aaf8ad677ffbb9de1ecfec3cfe80.zip | |
Annotation editor support (#568)
* Add gutter markers
* Add more GUI APIs
* Use SVG icons for gutter markers
* Add a little more padding between the line numbers and the gutter markers
* Add API for creating an Enigma JEditorPane
* Add API to list all classes including library classes
* Add API to create a LocalVariableEntryView
* Expose BrdigeMethodIndex to API
* Require name to be passed to LocalVariableEntryView
* Make implementation of isCursorOnDeclaration more robust
* Checkstyle
* Replace isCursorOnDeclaration with getCursorDeclaration
* Checkstyle again
* Refactor EnigmaIcon as per Juuz's suggestions
* Add more @NonExtendable and add EnigmaIcon docs
Diffstat (limited to 'enigma-swing')
9 files changed, 419 insertions, 9 deletions
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java index 12877fed..3a0597e2 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java | |||
| @@ -350,6 +350,12 @@ public class Gui { | |||
| 350 | return activeEditor == null ? null : activeEditor.getCursorReference(); | 350 | return activeEditor == null ? null : activeEditor.getCursorReference(); |
| 351 | } | 351 | } |
| 352 | 352 | ||
| 353 | @Nullable | ||
| 354 | public Entry<?> getCursorDeclaration() { | ||
| 355 | EditorPanel activeEditor = this.editorTabbedPane.getActiveEditor(); | ||
| 356 | return activeEditor == null ? null : activeEditor.getCursorDeclaration(); | ||
| 357 | } | ||
| 358 | |||
| 353 | public void startDocChange(EditorPanel editor) { | 359 | public void startDocChange(EditorPanel editor) { |
| 354 | EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference(); | 360 | EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference(); |
| 355 | 361 | ||
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java index a2b3bd9d..2c27376d 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java | |||
| @@ -27,6 +27,7 @@ import java.util.concurrent.ExecutionException; | |||
| 27 | import java.util.stream.Collectors; | 27 | import java.util.stream.Collectors; |
| 28 | import java.util.stream.Stream; | 28 | import java.util.stream.Stream; |
| 29 | 29 | ||
| 30 | import javax.swing.JEditorPane; | ||
| 30 | import javax.swing.JFrame; | 31 | import javax.swing.JFrame; |
| 31 | import javax.swing.JOptionPane; | 32 | import javax.swing.JOptionPane; |
| 32 | import javax.swing.SwingUtilities; | 33 | import javax.swing.SwingUtilities; |
| @@ -52,12 +53,15 @@ import cuchaz.enigma.api.service.ObfuscationTestService; | |||
| 52 | import cuchaz.enigma.api.service.ProjectService; | 53 | import cuchaz.enigma.api.service.ProjectService; |
| 53 | import cuchaz.enigma.api.view.GuiView; | 54 | import cuchaz.enigma.api.view.GuiView; |
| 54 | import cuchaz.enigma.api.view.entry.EntryReferenceView; | 55 | import cuchaz.enigma.api.view.entry.EntryReferenceView; |
| 56 | import cuchaz.enigma.api.view.entry.EntryView; | ||
| 55 | import cuchaz.enigma.classhandle.ClassHandle; | 57 | import cuchaz.enigma.classhandle.ClassHandle; |
| 56 | import cuchaz.enigma.classhandle.ClassHandleProvider; | 58 | import cuchaz.enigma.classhandle.ClassHandleProvider; |
| 59 | import cuchaz.enigma.gui.config.LookAndFeel; | ||
| 57 | import cuchaz.enigma.gui.config.NetConfig; | 60 | import cuchaz.enigma.gui.config.NetConfig; |
| 58 | import cuchaz.enigma.gui.config.UiConfig; | 61 | import cuchaz.enigma.gui.config.UiConfig; |
| 59 | import cuchaz.enigma.gui.dialog.ProgressDialog; | 62 | import cuchaz.enigma.gui.dialog.ProgressDialog; |
| 60 | import cuchaz.enigma.gui.newabstraction.EntryValidation; | 63 | import cuchaz.enigma.gui.newabstraction.EntryValidation; |
| 64 | import cuchaz.enigma.gui.panels.EditorPanel; | ||
| 61 | import cuchaz.enigma.gui.stats.StatsGenerator; | 65 | import cuchaz.enigma.gui.stats.StatsGenerator; |
| 62 | import cuchaz.enigma.gui.stats.StatsMember; | 66 | import cuchaz.enigma.gui.stats.StatsMember; |
| 63 | import cuchaz.enigma.gui.util.History; | 67 | import cuchaz.enigma.gui.util.History; |
| @@ -130,6 +134,23 @@ public class GuiController implements ClientPacketHandler, GuiView, DataInvalida | |||
| 130 | return gui.getFrame(); | 134 | return gui.getFrame(); |
| 131 | } | 135 | } |
| 132 | 136 | ||
| 137 | @Override | ||
| 138 | public float getScale() { | ||
| 139 | return UiConfig.getActiveScaleFactor(); | ||
| 140 | } | ||
| 141 | |||
| 142 | @Override | ||
| 143 | public boolean isDarkTheme() { | ||
| 144 | return LookAndFeel.isDarkLaf(); | ||
| 145 | } | ||
| 146 | |||
| 147 | @Override | ||
| 148 | public JEditorPane createEditorPane() { | ||
| 149 | JEditorPane editor = new JEditorPane(); | ||
| 150 | EditorPanel.customizeEditor(editor); | ||
| 151 | return editor; | ||
| 152 | } | ||
| 153 | |||
| 133 | public boolean isDirty() { | 154 | public boolean isDirty() { |
| 134 | return project != null && project.getMapper().isDirty(); | 155 | return project != null && project.getMapper().isDirty(); |
| 135 | } | 156 | } |
| @@ -349,6 +370,12 @@ public class GuiController implements ClientPacketHandler, GuiView, DataInvalida | |||
| 349 | return gui.getCursorReference(); | 370 | return gui.getCursorReference(); |
| 350 | } | 371 | } |
| 351 | 372 | ||
| 373 | @Override | ||
| 374 | @Nullable | ||
| 375 | public EntryView getCursorDeclaration() { | ||
| 376 | return gui.getCursorDeclaration(); | ||
| 377 | } | ||
| 378 | |||
| 352 | /** | 379 | /** |
| 353 | * Navigates to the declaration with respect to navigation history. | 380 | * Navigates to the declaration with respect to navigation history. |
| 354 | * | 381 | * |
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/LookAndFeel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/LookAndFeel.java index 2088aac2..2ac5d748 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/LookAndFeel.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/LookAndFeel.java | |||
| @@ -22,6 +22,7 @@ public enum LookAndFeel { | |||
| 22 | // the "JVM default" look and feel, get it at the beginning and store it so we can set it later | 22 | // the "JVM default" look and feel, get it at the beginning and store it so we can set it later |
| 23 | private static final javax.swing.LookAndFeel NONE_LAF = UIManager.getLookAndFeel(); | 23 | private static final javax.swing.LookAndFeel NONE_LAF = UIManager.getLookAndFeel(); |
| 24 | private final boolean needsScaling; | 24 | private final boolean needsScaling; |
| 25 | private static Boolean isDarkLaf = null; | ||
| 25 | 26 | ||
| 26 | LookAndFeel(boolean needsScaling) { | 27 | LookAndFeel(boolean needsScaling) { |
| 27 | this.needsScaling = needsScaling; | 28 | this.needsScaling = needsScaling; |
| @@ -52,6 +53,10 @@ public enum LookAndFeel { | |||
| 52 | } | 53 | } |
| 53 | 54 | ||
| 54 | public static boolean isDarkLaf() { | 55 | public static boolean isDarkLaf() { |
| 56 | if (isDarkLaf != null) { | ||
| 57 | return isDarkLaf; | ||
| 58 | } | ||
| 59 | |||
| 55 | // a bit of a hack because swing doesn't give any API for that, and we need colors that aren't defined in look and feel | 60 | // a bit of a hack because swing doesn't give any API for that, and we need colors that aren't defined in look and feel |
| 56 | JPanel panel = new JPanel(); | 61 | JPanel panel = new JPanel(); |
| 57 | panel.setSize(new Dimension(10, 10)); | 62 | panel.setSize(new Dimension(10, 10)); |
| @@ -64,6 +69,6 @@ public enum LookAndFeel { | |||
| 64 | 69 | ||
| 65 | // convert the color we got to grayscale | 70 | // convert the color we got to grayscale |
| 66 | int b = (int) (0.3 * c.getRed() + 0.59 * c.getGreen() + 0.11 * c.getBlue()); | 71 | int b = (int) (0.3 * c.getRed() + 0.59 * c.getGreen() + 0.11 * c.getBlue()); |
| 67 | return b < 85; | 72 | return isDarkLaf = b < 85; |
| 68 | } | 73 | } |
| 69 | } | 74 | } |
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java new file mode 100644 index 00000000..946591a6 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java | |||
| @@ -0,0 +1,42 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | ||
| 2 | |||
| 3 | import java.awt.Cursor; | ||
| 4 | import java.awt.event.ComponentAdapter; | ||
| 5 | import java.awt.event.ComponentEvent; | ||
| 6 | |||
| 7 | import javax.swing.JButton; | ||
| 8 | |||
| 9 | import cuchaz.enigma.api.service.GuiService; | ||
| 10 | import cuchaz.enigma.gui.util.EnigmaIconImpl; | ||
| 11 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 12 | |||
| 13 | public class GutterIcon extends JButton implements GuiService.GutterMarkerBuilder { | ||
| 14 | private Runnable clickAction = () -> { }; | ||
| 15 | |||
| 16 | public GutterIcon(EnigmaIconImpl icon) { | ||
| 17 | super(icon.icon()); | ||
| 18 | setContentAreaFilled(false); | ||
| 19 | setCursor(Cursor.getDefaultCursor()); | ||
| 20 | addActionListener(e -> clickAction.run()); | ||
| 21 | |||
| 22 | addComponentListener(new ComponentAdapter() { | ||
| 23 | @Override | ||
| 24 | public void componentResized(ComponentEvent e) { | ||
| 25 | setIcon(icon.icon().derive(ScaleUtil.invert(getWidth()), ScaleUtil.invert(getHeight()))); | ||
| 26 | } | ||
| 27 | }); | ||
| 28 | } | ||
| 29 | |||
| 30 | @Override | ||
| 31 | public GuiService.GutterMarkerBuilder setClickAction(Runnable action) { | ||
| 32 | this.clickAction = action; | ||
| 33 | setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); | ||
| 34 | return this; | ||
| 35 | } | ||
| 36 | |||
| 37 | @Override | ||
| 38 | public GuiService.GutterMarkerBuilder setTooltip(String tooltip) { | ||
| 39 | setToolTipText(tooltip); | ||
| 40 | return this; | ||
| 41 | } | ||
| 42 | } | ||
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java index 6b341ee0..92d1bd7f 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java | |||
| @@ -15,6 +15,7 @@ import java.awt.event.MouseAdapter; | |||
| 15 | import java.awt.event.MouseEvent; | 15 | import java.awt.event.MouseEvent; |
| 16 | import java.util.ArrayList; | 16 | import java.util.ArrayList; |
| 17 | import java.util.Collection; | 17 | import java.util.Collection; |
| 18 | import java.util.Comparator; | ||
| 18 | import java.util.List; | 19 | import java.util.List; |
| 19 | import java.util.Map; | 20 | import java.util.Map; |
| 20 | 21 | ||
| @@ -35,10 +36,12 @@ import javax.swing.text.Highlighter.HighlightPainter; | |||
| 35 | 36 | ||
| 36 | import de.sciss.syntaxpane.DefaultSyntaxKit; | 37 | import de.sciss.syntaxpane.DefaultSyntaxKit; |
| 37 | import de.sciss.syntaxpane.SyntaxDocument; | 38 | import de.sciss.syntaxpane.SyntaxDocument; |
| 39 | import de.sciss.syntaxpane.actions.ActionUtils; | ||
| 38 | import org.jetbrains.annotations.Nullable; | 40 | import org.jetbrains.annotations.Nullable; |
| 39 | 41 | ||
| 40 | import cuchaz.enigma.EnigmaProject; | 42 | import cuchaz.enigma.EnigmaProject; |
| 41 | import cuchaz.enigma.analysis.EntryReference; | 43 | import cuchaz.enigma.analysis.EntryReference; |
| 44 | import cuchaz.enigma.api.service.GuiService; | ||
| 42 | import cuchaz.enigma.classhandle.ClassHandle; | 45 | import cuchaz.enigma.classhandle.ClassHandle; |
| 43 | import cuchaz.enigma.classhandle.ClassHandleError; | 46 | import cuchaz.enigma.classhandle.ClassHandleError; |
| 44 | import cuchaz.enigma.events.ClassHandleListener; | 47 | import cuchaz.enigma.events.ClassHandleListener; |
| @@ -50,14 +53,17 @@ import cuchaz.enigma.gui.config.LookAndFeel; | |||
| 50 | import cuchaz.enigma.gui.config.Themes; | 53 | import cuchaz.enigma.gui.config.Themes; |
| 51 | import cuchaz.enigma.gui.config.UiConfig; | 54 | import cuchaz.enigma.gui.config.UiConfig; |
| 52 | import cuchaz.enigma.gui.elements.EditorPopupMenu; | 55 | import cuchaz.enigma.gui.elements.EditorPopupMenu; |
| 56 | import cuchaz.enigma.gui.elements.GutterIcon; | ||
| 53 | import cuchaz.enigma.gui.events.EditorActionListener; | 57 | import cuchaz.enigma.gui.events.EditorActionListener; |
| 54 | import cuchaz.enigma.gui.events.ThemeChangeListener; | 58 | import cuchaz.enigma.gui.events.ThemeChangeListener; |
| 55 | import cuchaz.enigma.gui.highlight.BoxHighlightPainter; | 59 | import cuchaz.enigma.gui.highlight.BoxHighlightPainter; |
| 56 | import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; | 60 | import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; |
| 61 | import cuchaz.enigma.gui.util.EnigmaIconImpl; | ||
| 57 | import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; | 62 | import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; |
| 58 | import cuchaz.enigma.gui.util.ScaleUtil; | 63 | import cuchaz.enigma.gui.util.ScaleUtil; |
| 59 | import cuchaz.enigma.source.DecompiledClassSource; | 64 | import cuchaz.enigma.source.DecompiledClassSource; |
| 60 | import cuchaz.enigma.source.RenamableTokenType; | 65 | import cuchaz.enigma.source.RenamableTokenType; |
| 66 | import cuchaz.enigma.source.SourceIndex; | ||
| 61 | import cuchaz.enigma.source.Token; | 67 | import cuchaz.enigma.source.Token; |
| 62 | import cuchaz.enigma.translation.mapping.EntryRemapper; | 68 | import cuchaz.enigma.translation.mapping.EntryRemapper; |
| 63 | import cuchaz.enigma.translation.mapping.EntryResolver; | 69 | import cuchaz.enigma.translation.mapping.EntryResolver; |
| @@ -65,12 +71,14 @@ import cuchaz.enigma.translation.mapping.ResolutionStrategy; | |||
| 65 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 71 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 66 | import cuchaz.enigma.translation.representation.entry.Entry; | 72 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 67 | import cuchaz.enigma.utils.I18n; | 73 | import cuchaz.enigma.utils.I18n; |
| 74 | import cuchaz.enigma.utils.Pair; | ||
| 68 | import cuchaz.enigma.utils.Result; | 75 | import cuchaz.enigma.utils.Result; |
| 69 | 76 | ||
| 70 | public class EditorPanel { | 77 | public class EditorPanel { |
| 71 | private final JPanel ui = new JPanel(); | 78 | private final JPanel ui = new JPanel(); |
| 72 | private final JEditorPane editor = new JEditorPane(); | 79 | private final JEditorPane editor = new JEditorPane(); |
| 73 | private final JScrollPane editorScrollPane = new JScrollPane(this.editor); | 80 | private final JScrollPane editorScrollPane = new JScrollPane(this.editor); |
| 81 | private final GutterPanel gutterPanel; | ||
| 74 | private final EditorPopupMenu popupMenu; | 82 | private final EditorPopupMenu popupMenu; |
| 75 | 83 | ||
| 76 | // progress UI | 84 | // progress UI |
| @@ -109,20 +117,16 @@ public class EditorPanel { | |||
| 109 | this.gui = gui; | 117 | this.gui = gui; |
| 110 | this.controller = gui.getController(); | 118 | this.controller = gui.getController(); |
| 111 | 119 | ||
| 112 | this.editor.setEditable(false); | 120 | customizeEditor(this.editor); |
| 113 | this.editor.setSelectionColor(new Color(31, 46, 90)); | ||
| 114 | this.editor.setCaret(new BrowserCaret()); | ||
| 115 | this.editor.addCaretListener(event -> onCaretMove(event.getDot(), this.mouseIsPressed)); | 121 | this.editor.addCaretListener(event -> onCaretMove(event.getDot(), this.mouseIsPressed)); |
| 116 | this.editor.setCaretColor(UiConfig.getCaretColor()); | ||
| 117 | this.editor.setContentType("text/enigma-sources"); | ||
| 118 | this.editor.setBackground(UiConfig.getEditorBackgroundColor()); | ||
| 119 | DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit(); | ||
| 120 | kit.toggleComponent(this.editor, "de.sciss.syntaxpane.components.TokenMarker"); | ||
| 121 | 122 | ||
| 122 | // set unit increment to height of one line, the amount scrolled per | 123 | // set unit increment to height of one line, the amount scrolled per |
| 123 | // mouse wheel rotation is then controlled by OS settings | 124 | // mouse wheel rotation is then controlled by OS settings |
| 124 | this.editorScrollPane.getVerticalScrollBar().setUnitIncrement(this.editor.getFontMetrics(this.editor.getFont()).getHeight()); | 125 | this.editorScrollPane.getVerticalScrollBar().setUnitIncrement(this.editor.getFontMetrics(this.editor.getFont()).getHeight()); |
| 125 | 126 | ||
| 127 | this.gutterPanel = new GutterPanel(this.editor, (JComponent) this.editorScrollPane.getRowHeader().getView()); | ||
| 128 | this.editorScrollPane.setRowHeaderView(this.gutterPanel); | ||
| 129 | |||
| 126 | // init editor popup menu | 130 | // init editor popup menu |
| 127 | this.popupMenu = new EditorPopupMenu(this, gui); | 131 | this.popupMenu = new EditorPopupMenu(this, gui); |
| 128 | this.editor.setComponentPopupMenu(this.popupMenu.getUi()); | 132 | this.editor.setComponentPopupMenu(this.popupMenu.getUi()); |
| @@ -255,6 +259,17 @@ public class EditorPanel { | |||
| 255 | this.ui.putClientProperty(EditorPanel.class, this); | 259 | this.ui.putClientProperty(EditorPanel.class, this); |
| 256 | } | 260 | } |
| 257 | 261 | ||
| 262 | public static void customizeEditor(JEditorPane editor) { | ||
| 263 | editor.setEditable(false); | ||
| 264 | editor.setSelectionColor(new Color(31, 46, 90)); | ||
| 265 | editor.setCaret(new BrowserCaret()); | ||
| 266 | editor.setCaretColor(UiConfig.getCaretColor()); | ||
| 267 | editor.setContentType("text/enigma-sources"); | ||
| 268 | editor.setBackground(UiConfig.getEditorBackgroundColor()); | ||
| 269 | DefaultSyntaxKit kit = (DefaultSyntaxKit) editor.getEditorKit(); | ||
| 270 | kit.toggleComponent(editor, "de.sciss.syntaxpane.components.TokenMarker"); | ||
| 271 | } | ||
| 272 | |||
| 258 | @Nullable | 273 | @Nullable |
| 259 | public static EditorPanel byUi(Component ui) { | 274 | public static EditorPanel byUi(Component ui) { |
| 260 | if (ui instanceof JComponent) { | 275 | if (ui instanceof JComponent) { |
| @@ -512,6 +527,7 @@ public class EditorPanel { | |||
| 512 | this.editor.setCaretPosition(newCaretPos); | 527 | this.editor.setCaretPosition(newCaretPos); |
| 513 | } | 528 | } |
| 514 | 529 | ||
| 530 | addGutterMarkers(source.getIndex()); | ||
| 515 | setHighlightedTokens(source.getHighlightedTokens()); | 531 | setHighlightedTokens(source.getHighlightedTokens()); |
| 516 | setCursorReference(getReference(getToken(this.editor.getCaretPosition()))); | 532 | setCursorReference(getReference(getToken(this.editor.getCaretPosition()))); |
| 517 | } finally { | 533 | } finally { |
| @@ -524,6 +540,44 @@ public class EditorPanel { | |||
| 524 | } | 540 | } |
| 525 | } | 541 | } |
| 526 | 542 | ||
| 543 | private void addGutterMarkers(SourceIndex sourceIndex) { | ||
| 544 | List<GuiService> services = this.gui.getController().enigma.getServices().get(GuiService.TYPE); | ||
| 545 | |||
| 546 | if (services.isEmpty()) { | ||
| 547 | return; | ||
| 548 | } | ||
| 549 | |||
| 550 | this.gutterPanel.clearMarkers(); | ||
| 551 | |||
| 552 | List<Pair<Entry<?>, Token>> declarationTokens = new ArrayList<>(); | ||
| 553 | |||
| 554 | for (Entry<?> declaration : sourceIndex.declarations()) { | ||
| 555 | declarationTokens.add(new Pair<>(declaration, sourceIndex.getDeclarationToken(declaration))); | ||
| 556 | } | ||
| 557 | |||
| 558 | declarationTokens.sort(Comparator.comparing(pair -> pair.b)); | ||
| 559 | |||
| 560 | for (Pair<Entry<?>, Token> declaration : declarationTokens) { | ||
| 561 | int lineNumber; | ||
| 562 | |||
| 563 | try { | ||
| 564 | lineNumber = ActionUtils.getLineNumber(this.editor, declaration.b.start); | ||
| 565 | } catch (BadLocationException e) { | ||
| 566 | continue; | ||
| 567 | } | ||
| 568 | |||
| 569 | for (GuiService service : services) { | ||
| 570 | service.addGutterMarkers(this.gui.getController(), declaration.a, (icon, alignment) -> { | ||
| 571 | GutterIcon button = new GutterIcon((EnigmaIconImpl) icon); | ||
| 572 | this.gutterPanel.addMarker(lineNumber, alignment, button); | ||
| 573 | return button; | ||
| 574 | }); | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | this.editor.revalidate(); | ||
| 579 | } | ||
| 580 | |||
| 527 | public void setHighlightedTokens(Map<RenamableTokenType, ? extends Collection<Token>> tokens) { | 581 | public void setHighlightedTokens(Map<RenamableTokenType, ? extends Collection<Token>> tokens) { |
| 528 | // remove any old highlighters | 582 | // remove any old highlighters |
| 529 | this.editor.getHighlighter().removeAllHighlights(); | 583 | this.editor.getHighlighter().removeAllHighlights(); |
| @@ -565,10 +619,23 @@ public class EditorPanel { | |||
| 565 | } | 619 | } |
| 566 | } | 620 | } |
| 567 | 621 | ||
| 622 | @Nullable | ||
| 568 | public EntryReference<Entry<?>, Entry<?>> getCursorReference() { | 623 | public EntryReference<Entry<?>, Entry<?>> getCursorReference() { |
| 569 | return this.cursorReference; | 624 | return this.cursorReference; |
| 570 | } | 625 | } |
| 571 | 626 | ||
| 627 | @Nullable | ||
| 628 | public Entry<?> getCursorDeclaration() { | ||
| 629 | int pos = this.editor.getCaretPosition(); | ||
| 630 | Token token = getToken(pos); | ||
| 631 | |||
| 632 | if (token == null) { | ||
| 633 | return null; | ||
| 634 | } | ||
| 635 | |||
| 636 | return this.source.getIndex().getDeclaration(token); | ||
| 637 | } | ||
| 638 | |||
| 572 | public void showReference(EntryReference<Entry<?>, Entry<?>> reference) { | 639 | public void showReference(EntryReference<Entry<?>, Entry<?>> reference) { |
| 573 | if (this.mode == DisplayMode.SUCCESS) { | 640 | if (this.mode == DisplayMode.SUCCESS) { |
| 574 | showReference0(reference); | 641 | showReference0(reference); |
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java new file mode 100644 index 00000000..28636a18 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java | |||
| @@ -0,0 +1,238 @@ | |||
| 1 | package cuchaz.enigma.gui.panels; | ||
| 2 | |||
| 3 | import java.awt.Color; | ||
| 4 | import java.awt.Component; | ||
| 5 | import java.awt.Container; | ||
| 6 | import java.awt.Dimension; | ||
| 7 | import java.awt.FlowLayout; | ||
| 8 | import java.awt.FontMetrics; | ||
| 9 | import java.awt.Graphics; | ||
| 10 | import java.awt.Insets; | ||
| 11 | import java.awt.LayoutManager2; | ||
| 12 | import java.beans.PropertyChangeEvent; | ||
| 13 | import java.beans.PropertyChangeListener; | ||
| 14 | import java.util.HashMap; | ||
| 15 | import java.util.Map; | ||
| 16 | |||
| 17 | import javax.swing.BorderFactory; | ||
| 18 | import javax.swing.JComponent; | ||
| 19 | import javax.swing.JEditorPane; | ||
| 20 | import javax.swing.JPanel; | ||
| 21 | import javax.swing.SwingUtilities; | ||
| 22 | import javax.swing.event.CaretEvent; | ||
| 23 | import javax.swing.event.CaretListener; | ||
| 24 | import javax.swing.event.DocumentEvent; | ||
| 25 | import javax.swing.event.DocumentListener; | ||
| 26 | import javax.swing.text.BadLocationException; | ||
| 27 | |||
| 28 | import de.sciss.syntaxpane.actions.ActionUtils; | ||
| 29 | |||
| 30 | import cuchaz.enigma.api.service.GuiService; | ||
| 31 | import cuchaz.enigma.gui.config.UiConfig; | ||
| 32 | |||
| 33 | public class GutterPanel extends JPanel { | ||
| 34 | private final JPanel markerPanel; | ||
| 35 | |||
| 36 | public GutterPanel(JEditorPane editor, JComponent lineNumbers) { | ||
| 37 | markerPanel = new MarkerPanel(editor); | ||
| 38 | |||
| 39 | setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); | ||
| 40 | add(lineNumbers); | ||
| 41 | add(markerPanel); | ||
| 42 | |||
| 43 | setBackground(UiConfig.getLineNumbersBackgroundColor()); | ||
| 44 | setForeground(UiConfig.getLineNumbersForegroundColor()); | ||
| 45 | markerPanel.setBackground(UiConfig.getLineNumbersBackgroundColor()); | ||
| 46 | markerPanel.setForeground(UiConfig.getLineNumbersForegroundColor()); | ||
| 47 | setBorder(lineNumbers.getBorder()); | ||
| 48 | lineNumbers.setBorder(null); | ||
| 49 | } | ||
| 50 | |||
| 51 | public void clearMarkers() { | ||
| 52 | markerPanel.removeAll(); | ||
| 53 | } | ||
| 54 | |||
| 55 | public void addMarker(int line, GuiService.GutterMarkerAlignment alignment, Component marker) { | ||
| 56 | markerPanel.add(marker, new MarkerLayout.Constraint(line, alignment)); | ||
| 57 | } | ||
| 58 | |||
| 59 | private static class MarkerPanel extends JPanel implements CaretListener, DocumentListener, PropertyChangeListener { | ||
| 60 | private final JEditorPane editor; | ||
| 61 | private final Color currentLineColor = UiConfig.getLineNumbersSelectedColor(); | ||
| 62 | |||
| 63 | private MarkerPanel(JEditorPane editor) { | ||
| 64 | this.editor = editor; | ||
| 65 | setLayout(new MarkerLayout()); | ||
| 66 | |||
| 67 | Insets editorInsets = editor.getInsets(); | ||
| 68 | |||
| 69 | if (editorInsets.top != 0 || editorInsets.bottom != 0) { | ||
| 70 | setBorder(BorderFactory.createEmptyBorder(editorInsets.top, 0, editorInsets.bottom, 0)); | ||
| 71 | } | ||
| 72 | |||
| 73 | setFont(editor.getFont()); | ||
| 74 | |||
| 75 | editor.addCaretListener(this); | ||
| 76 | editor.getDocument().addDocumentListener(this); | ||
| 77 | editor.addPropertyChangeListener(this); | ||
| 78 | } | ||
| 79 | |||
| 80 | @Override | ||
| 81 | protected void paintComponent(Graphics g) { | ||
| 82 | super.paintComponent(g); | ||
| 83 | |||
| 84 | int currentLine; | ||
| 85 | |||
| 86 | try { | ||
| 87 | currentLine = ActionUtils.getLineNumber(editor, editor.getCaretPosition()); | ||
| 88 | } catch (BadLocationException ex) { | ||
| 89 | return; // no valid caret -> nothing to draw | ||
| 90 | } | ||
| 91 | |||
| 92 | FontMetrics fm = getFontMetrics(getFont()); | ||
| 93 | int lh = fm.getHeight(); | ||
| 94 | Insets insets = getInsets(); | ||
| 95 | |||
| 96 | int y = currentLine * lh; | ||
| 97 | |||
| 98 | g.setColor(currentLineColor); | ||
| 99 | g.fillRect(0, y, getWidth(), lh); | ||
| 100 | } | ||
| 101 | |||
| 102 | @Override | ||
| 103 | public void propertyChange(PropertyChangeEvent evt) { | ||
| 104 | if (evt.getPropertyName().equals("document")) { | ||
| 105 | repaint(); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | @Override | ||
| 110 | public void caretUpdate(CaretEvent e) { | ||
| 111 | repaint(); | ||
| 112 | } | ||
| 113 | |||
| 114 | @Override | ||
| 115 | public void insertUpdate(DocumentEvent e) { | ||
| 116 | documentChanged(); | ||
| 117 | } | ||
| 118 | |||
| 119 | @Override | ||
| 120 | public void removeUpdate(DocumentEvent e) { | ||
| 121 | documentChanged(); | ||
| 122 | } | ||
| 123 | |||
| 124 | @Override | ||
| 125 | public void changedUpdate(DocumentEvent e) { | ||
| 126 | documentChanged(); | ||
| 127 | } | ||
| 128 | |||
| 129 | private void documentChanged() { | ||
| 130 | SwingUtilities.invokeLater(this::repaint); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | private static class MarkerLayout implements LayoutManager2 { | ||
| 135 | private static final int GAP = 2; | ||
| 136 | private static final int HUGE_HEIGHT = 0x100000; // taken from LineNumbersRuler | ||
| 137 | |||
| 138 | private final Map<Component, Constraint> constraints = new HashMap<>(); | ||
| 139 | |||
| 140 | @Override | ||
| 141 | public void addLayoutComponent(Component comp, Object constraints) { | ||
| 142 | this.constraints.put(comp, (Constraint) constraints); | ||
| 143 | } | ||
| 144 | |||
| 145 | @Override | ||
| 146 | public Dimension maximumLayoutSize(Container target) { | ||
| 147 | return preferredLayoutSize(target); | ||
| 148 | } | ||
| 149 | |||
| 150 | @Override | ||
| 151 | public float getLayoutAlignmentX(Container target) { | ||
| 152 | return 0.5f; | ||
| 153 | } | ||
| 154 | |||
| 155 | @Override | ||
| 156 | public float getLayoutAlignmentY(Container target) { | ||
| 157 | return 0.5f; | ||
| 158 | } | ||
| 159 | |||
| 160 | @Override | ||
| 161 | public void invalidateLayout(Container target) { | ||
| 162 | } | ||
| 163 | |||
| 164 | @Override | ||
| 165 | public void addLayoutComponent(String name, Component comp) { | ||
| 166 | } | ||
| 167 | |||
| 168 | @Override | ||
| 169 | public void removeLayoutComponent(Component comp) { | ||
| 170 | constraints.remove(comp); | ||
| 171 | } | ||
| 172 | |||
| 173 | @Override | ||
| 174 | public Dimension preferredLayoutSize(Container parent) { | ||
| 175 | synchronized (parent.getTreeLock()) { | ||
| 176 | Insets insets = parent.getInsets(); | ||
| 177 | |||
| 178 | if (constraints.isEmpty()) { | ||
| 179 | return new Dimension(insets.left + insets.right, HUGE_HEIGHT); | ||
| 180 | } | ||
| 181 | |||
| 182 | Map<Integer, Integer> leftCount = new HashMap<>(); | ||
| 183 | Map<Integer, Integer> rightCount = new HashMap<>(); | ||
| 184 | |||
| 185 | for (Constraint constraint : constraints.values()) { | ||
| 186 | switch (constraint.alignment) { | ||
| 187 | case LEFT -> leftCount.merge(constraint.line, 1, Integer::sum); | ||
| 188 | case RIGHT -> rightCount.merge(constraint.line, 1, Integer::sum); | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 192 | int maxLeft = leftCount.values().stream().mapToInt(Integer::intValue).max().orElse(0); | ||
| 193 | int maxRight = rightCount.values().stream().mapToInt(Integer::intValue).max().orElse(0); | ||
| 194 | |||
| 195 | int lineHeight = parent.getFontMetrics(parent.getFont()).getHeight(); | ||
| 196 | return new Dimension( | ||
| 197 | GAP + insets.left + insets.right + (maxLeft + maxRight) * lineHeight, | ||
| 198 | HUGE_HEIGHT | ||
| 199 | ); | ||
| 200 | } | ||
| 201 | } | ||
| 202 | |||
| 203 | @Override | ||
| 204 | public Dimension minimumLayoutSize(Container parent) { | ||
| 205 | return preferredLayoutSize(parent); | ||
| 206 | } | ||
| 207 | |||
| 208 | @Override | ||
| 209 | public void layoutContainer(Container parent) { | ||
| 210 | synchronized (parent.getTreeLock()) { | ||
| 211 | Map<Integer, Integer> leftCount = new HashMap<>(); | ||
| 212 | Map<Integer, Integer> rightCount = new HashMap<>(); | ||
| 213 | |||
| 214 | int lineHeight = parent.getFontMetrics(parent.getFont()).getHeight(); | ||
| 215 | |||
| 216 | int numComponents = parent.getComponentCount(); | ||
| 217 | |||
| 218 | for (int i = 0; i < numComponents; i++) { | ||
| 219 | Component comp = parent.getComponent(i); | ||
| 220 | Constraint constraint = constraints.get(comp); | ||
| 221 | |||
| 222 | if (constraint == null) { | ||
| 223 | continue; | ||
| 224 | } | ||
| 225 | |||
| 226 | int left = switch (constraint.alignment) { | ||
| 227 | case LEFT -> GAP + (leftCount.merge(constraint.line, 1, Integer::sum) - 1) * lineHeight + GAP / 2; | ||
| 228 | case RIGHT -> parent.getWidth() - rightCount.merge(constraint.line, 1, Integer::sum) * lineHeight + GAP / 2; | ||
| 229 | }; | ||
| 230 | comp.setBounds(left, constraint.line * lineHeight + GAP / 2, lineHeight - GAP, lineHeight - GAP); | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 | private record Constraint(int line, GuiService.GutterMarkerAlignment alignment) { | ||
| 236 | } | ||
| 237 | } | ||
| 238 | } | ||
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java new file mode 100644 index 00000000..8e07df3c --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; | ||
| 4 | |||
| 5 | import cuchaz.enigma.api.EnigmaIcon; | ||
| 6 | |||
| 7 | public record EnigmaIconImpl(FlatSVGIcon icon) implements EnigmaIcon { | ||
| 8 | } | ||
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java new file mode 100644 index 00000000..e948f5a1 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import java.io.IOException; | ||
| 4 | import java.io.InputStream; | ||
| 5 | |||
| 6 | import com.formdev.flatlaf.extras.FlatSVGIcon; | ||
| 7 | |||
| 8 | import cuchaz.enigma.api.EnigmaIcon; | ||
| 9 | import cuchaz.enigma.utils.IconLoadingService; | ||
| 10 | |||
| 11 | public class IconLoadingServiceImpl implements IconLoadingService { | ||
| 12 | @Override | ||
| 13 | public EnigmaIcon loadIcon(InputStream in) throws IOException { | ||
| 14 | return new EnigmaIconImpl(new FlatSVGIcon(in)); | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService b/enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService new file mode 100644 index 00000000..649dfd02 --- /dev/null +++ b/enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService | |||
| @@ -0,0 +1 @@ | |||
| cuchaz.enigma.gui.util.IconLoadingServiceImpl | |||