summaryrefslogtreecommitdiff
path: root/enigma-swing
diff options
context:
space:
mode:
Diffstat (limited to 'enigma-swing')
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java6
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java27
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/config/LookAndFeel.java7
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java42
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/EditorPanel.java83
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java238
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java8
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java16
-rw-r--r--enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService1
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;
27import java.util.stream.Collectors; 27import java.util.stream.Collectors;
28import java.util.stream.Stream; 28import java.util.stream.Stream;
29 29
30import javax.swing.JEditorPane;
30import javax.swing.JFrame; 31import javax.swing.JFrame;
31import javax.swing.JOptionPane; 32import javax.swing.JOptionPane;
32import javax.swing.SwingUtilities; 33import javax.swing.SwingUtilities;
@@ -52,12 +53,15 @@ import cuchaz.enigma.api.service.ObfuscationTestService;
52import cuchaz.enigma.api.service.ProjectService; 53import cuchaz.enigma.api.service.ProjectService;
53import cuchaz.enigma.api.view.GuiView; 54import cuchaz.enigma.api.view.GuiView;
54import cuchaz.enigma.api.view.entry.EntryReferenceView; 55import cuchaz.enigma.api.view.entry.EntryReferenceView;
56import cuchaz.enigma.api.view.entry.EntryView;
55import cuchaz.enigma.classhandle.ClassHandle; 57import cuchaz.enigma.classhandle.ClassHandle;
56import cuchaz.enigma.classhandle.ClassHandleProvider; 58import cuchaz.enigma.classhandle.ClassHandleProvider;
59import cuchaz.enigma.gui.config.LookAndFeel;
57import cuchaz.enigma.gui.config.NetConfig; 60import cuchaz.enigma.gui.config.NetConfig;
58import cuchaz.enigma.gui.config.UiConfig; 61import cuchaz.enigma.gui.config.UiConfig;
59import cuchaz.enigma.gui.dialog.ProgressDialog; 62import cuchaz.enigma.gui.dialog.ProgressDialog;
60import cuchaz.enigma.gui.newabstraction.EntryValidation; 63import cuchaz.enigma.gui.newabstraction.EntryValidation;
64import cuchaz.enigma.gui.panels.EditorPanel;
61import cuchaz.enigma.gui.stats.StatsGenerator; 65import cuchaz.enigma.gui.stats.StatsGenerator;
62import cuchaz.enigma.gui.stats.StatsMember; 66import cuchaz.enigma.gui.stats.StatsMember;
63import cuchaz.enigma.gui.util.History; 67import 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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Cursor;
4import java.awt.event.ComponentAdapter;
5import java.awt.event.ComponentEvent;
6
7import javax.swing.JButton;
8
9import cuchaz.enigma.api.service.GuiService;
10import cuchaz.enigma.gui.util.EnigmaIconImpl;
11import cuchaz.enigma.gui.util.ScaleUtil;
12
13public 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;
15import java.awt.event.MouseEvent; 15import java.awt.event.MouseEvent;
16import java.util.ArrayList; 16import java.util.ArrayList;
17import java.util.Collection; 17import java.util.Collection;
18import java.util.Comparator;
18import java.util.List; 19import java.util.List;
19import java.util.Map; 20import java.util.Map;
20 21
@@ -35,10 +36,12 @@ import javax.swing.text.Highlighter.HighlightPainter;
35 36
36import de.sciss.syntaxpane.DefaultSyntaxKit; 37import de.sciss.syntaxpane.DefaultSyntaxKit;
37import de.sciss.syntaxpane.SyntaxDocument; 38import de.sciss.syntaxpane.SyntaxDocument;
39import de.sciss.syntaxpane.actions.ActionUtils;
38import org.jetbrains.annotations.Nullable; 40import org.jetbrains.annotations.Nullable;
39 41
40import cuchaz.enigma.EnigmaProject; 42import cuchaz.enigma.EnigmaProject;
41import cuchaz.enigma.analysis.EntryReference; 43import cuchaz.enigma.analysis.EntryReference;
44import cuchaz.enigma.api.service.GuiService;
42import cuchaz.enigma.classhandle.ClassHandle; 45import cuchaz.enigma.classhandle.ClassHandle;
43import cuchaz.enigma.classhandle.ClassHandleError; 46import cuchaz.enigma.classhandle.ClassHandleError;
44import cuchaz.enigma.events.ClassHandleListener; 47import cuchaz.enigma.events.ClassHandleListener;
@@ -50,14 +53,17 @@ import cuchaz.enigma.gui.config.LookAndFeel;
50import cuchaz.enigma.gui.config.Themes; 53import cuchaz.enigma.gui.config.Themes;
51import cuchaz.enigma.gui.config.UiConfig; 54import cuchaz.enigma.gui.config.UiConfig;
52import cuchaz.enigma.gui.elements.EditorPopupMenu; 55import cuchaz.enigma.gui.elements.EditorPopupMenu;
56import cuchaz.enigma.gui.elements.GutterIcon;
53import cuchaz.enigma.gui.events.EditorActionListener; 57import cuchaz.enigma.gui.events.EditorActionListener;
54import cuchaz.enigma.gui.events.ThemeChangeListener; 58import cuchaz.enigma.gui.events.ThemeChangeListener;
55import cuchaz.enigma.gui.highlight.BoxHighlightPainter; 59import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
56import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; 60import cuchaz.enigma.gui.highlight.SelectionHighlightPainter;
61import cuchaz.enigma.gui.util.EnigmaIconImpl;
57import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; 62import cuchaz.enigma.gui.util.GridBagConstraintsBuilder;
58import cuchaz.enigma.gui.util.ScaleUtil; 63import cuchaz.enigma.gui.util.ScaleUtil;
59import cuchaz.enigma.source.DecompiledClassSource; 64import cuchaz.enigma.source.DecompiledClassSource;
60import cuchaz.enigma.source.RenamableTokenType; 65import cuchaz.enigma.source.RenamableTokenType;
66import cuchaz.enigma.source.SourceIndex;
61import cuchaz.enigma.source.Token; 67import cuchaz.enigma.source.Token;
62import cuchaz.enigma.translation.mapping.EntryRemapper; 68import cuchaz.enigma.translation.mapping.EntryRemapper;
63import cuchaz.enigma.translation.mapping.EntryResolver; 69import cuchaz.enigma.translation.mapping.EntryResolver;
@@ -65,12 +71,14 @@ import cuchaz.enigma.translation.mapping.ResolutionStrategy;
65import cuchaz.enigma.translation.representation.entry.ClassEntry; 71import cuchaz.enigma.translation.representation.entry.ClassEntry;
66import cuchaz.enigma.translation.representation.entry.Entry; 72import cuchaz.enigma.translation.representation.entry.Entry;
67import cuchaz.enigma.utils.I18n; 73import cuchaz.enigma.utils.I18n;
74import cuchaz.enigma.utils.Pair;
68import cuchaz.enigma.utils.Result; 75import cuchaz.enigma.utils.Result;
69 76
70public class EditorPanel { 77public 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 @@
1package cuchaz.enigma.gui.panels;
2
3import java.awt.Color;
4import java.awt.Component;
5import java.awt.Container;
6import java.awt.Dimension;
7import java.awt.FlowLayout;
8import java.awt.FontMetrics;
9import java.awt.Graphics;
10import java.awt.Insets;
11import java.awt.LayoutManager2;
12import java.beans.PropertyChangeEvent;
13import java.beans.PropertyChangeListener;
14import java.util.HashMap;
15import java.util.Map;
16
17import javax.swing.BorderFactory;
18import javax.swing.JComponent;
19import javax.swing.JEditorPane;
20import javax.swing.JPanel;
21import javax.swing.SwingUtilities;
22import javax.swing.event.CaretEvent;
23import javax.swing.event.CaretListener;
24import javax.swing.event.DocumentEvent;
25import javax.swing.event.DocumentListener;
26import javax.swing.text.BadLocationException;
27
28import de.sciss.syntaxpane.actions.ActionUtils;
29
30import cuchaz.enigma.api.service.GuiService;
31import cuchaz.enigma.gui.config.UiConfig;
32
33public 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 @@
1package cuchaz.enigma.gui.util;
2
3import com.formdev.flatlaf.extras.FlatSVGIcon;
4
5import cuchaz.enigma.api.EnigmaIcon;
6
7public 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 @@
1package cuchaz.enigma.gui.util;
2
3import java.io.IOException;
4import java.io.InputStream;
5
6import com.formdev.flatlaf.extras.FlatSVGIcon;
7
8import cuchaz.enigma.api.EnigmaIcon;
9import cuchaz.enigma.utils.IconLoadingService;
10
11public 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