From 87581ffe8d23aaf8ad677ffbb9de1ecfec3cfe80 Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sat, 18 Oct 2025 16:38:40 +0100 Subject: 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--- .../src/main/java/cuchaz/enigma/gui/Gui.java | 6 + .../main/java/cuchaz/enigma/gui/GuiController.java | 27 +++ .../java/cuchaz/enigma/gui/config/LookAndFeel.java | 7 +- .../cuchaz/enigma/gui/elements/GutterIcon.java | 42 ++++ .../java/cuchaz/enigma/gui/panels/EditorPanel.java | 83 ++++++- .../java/cuchaz/enigma/gui/panels/GutterPanel.java | 238 +++++++++++++++++++++ .../cuchaz/enigma/gui/util/EnigmaIconImpl.java | 8 + .../enigma/gui/util/IconLoadingServiceImpl.java | 16 ++ .../cuchaz.enigma.utils.IconLoadingService | 1 + 9 files changed, 419 insertions(+), 9 deletions(-) create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java create mode 100644 enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService (limited to 'enigma-swing') 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 12877fe..3a0597e 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 { return activeEditor == null ? null : activeEditor.getCursorReference(); } + @Nullable + public Entry getCursorDeclaration() { + EditorPanel activeEditor = this.editorTabbedPane.getActiveEditor(); + return activeEditor == null ? null : activeEditor.getCursorDeclaration(); + } + public void startDocChange(EditorPanel editor) { EntryReference, Entry> cursorReference = editor.getCursorReference(); 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 a2b3bd9..2c27376 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; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; @@ -52,12 +53,15 @@ import cuchaz.enigma.api.service.ObfuscationTestService; import cuchaz.enigma.api.service.ProjectService; import cuchaz.enigma.api.view.GuiView; import cuchaz.enigma.api.view.entry.EntryReferenceView; +import cuchaz.enigma.api.view.entry.EntryView; import cuchaz.enigma.classhandle.ClassHandle; import cuchaz.enigma.classhandle.ClassHandleProvider; +import cuchaz.enigma.gui.config.LookAndFeel; import cuchaz.enigma.gui.config.NetConfig; import cuchaz.enigma.gui.config.UiConfig; import cuchaz.enigma.gui.dialog.ProgressDialog; import cuchaz.enigma.gui.newabstraction.EntryValidation; +import cuchaz.enigma.gui.panels.EditorPanel; import cuchaz.enigma.gui.stats.StatsGenerator; import cuchaz.enigma.gui.stats.StatsMember; import cuchaz.enigma.gui.util.History; @@ -130,6 +134,23 @@ public class GuiController implements ClientPacketHandler, GuiView, DataInvalida return gui.getFrame(); } + @Override + public float getScale() { + return UiConfig.getActiveScaleFactor(); + } + + @Override + public boolean isDarkTheme() { + return LookAndFeel.isDarkLaf(); + } + + @Override + public JEditorPane createEditorPane() { + JEditorPane editor = new JEditorPane(); + EditorPanel.customizeEditor(editor); + return editor; + } + public boolean isDirty() { return project != null && project.getMapper().isDirty(); } @@ -349,6 +370,12 @@ public class GuiController implements ClientPacketHandler, GuiView, DataInvalida return gui.getCursorReference(); } + @Override + @Nullable + public EntryView getCursorDeclaration() { + return gui.getCursorDeclaration(); + } + /** * Navigates to the declaration with respect to navigation history. * 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 2088aac..2ac5d74 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 { // the "JVM default" look and feel, get it at the beginning and store it so we can set it later private static final javax.swing.LookAndFeel NONE_LAF = UIManager.getLookAndFeel(); private final boolean needsScaling; + private static Boolean isDarkLaf = null; LookAndFeel(boolean needsScaling) { this.needsScaling = needsScaling; @@ -52,6 +53,10 @@ public enum LookAndFeel { } public static boolean isDarkLaf() { + if (isDarkLaf != null) { + return isDarkLaf; + } + // 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 JPanel panel = new JPanel(); panel.setSize(new Dimension(10, 10)); @@ -64,6 +69,6 @@ public enum LookAndFeel { // convert the color we got to grayscale int b = (int) (0.3 * c.getRed() + 0.59 * c.getGreen() + 0.11 * c.getBlue()); - return b < 85; + return isDarkLaf = b < 85; } } 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 0000000..946591a --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/GutterIcon.java @@ -0,0 +1,42 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Cursor; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +import javax.swing.JButton; + +import cuchaz.enigma.api.service.GuiService; +import cuchaz.enigma.gui.util.EnigmaIconImpl; +import cuchaz.enigma.gui.util.ScaleUtil; + +public class GutterIcon extends JButton implements GuiService.GutterMarkerBuilder { + private Runnable clickAction = () -> { }; + + public GutterIcon(EnigmaIconImpl icon) { + super(icon.icon()); + setContentAreaFilled(false); + setCursor(Cursor.getDefaultCursor()); + addActionListener(e -> clickAction.run()); + + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + setIcon(icon.icon().derive(ScaleUtil.invert(getWidth()), ScaleUtil.invert(getHeight()))); + } + }); + } + + @Override + public GuiService.GutterMarkerBuilder setClickAction(Runnable action) { + this.clickAction = action; + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + return this; + } + + @Override + public GuiService.GutterMarkerBuilder setTooltip(String tooltip) { + setToolTipText(tooltip); + return this; + } +} 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 6b341ee..92d1bd7 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; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -35,10 +36,12 @@ import javax.swing.text.Highlighter.HighlightPainter; import de.sciss.syntaxpane.DefaultSyntaxKit; import de.sciss.syntaxpane.SyntaxDocument; +import de.sciss.syntaxpane.actions.ActionUtils; import org.jetbrains.annotations.Nullable; import cuchaz.enigma.EnigmaProject; import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.api.service.GuiService; import cuchaz.enigma.classhandle.ClassHandle; import cuchaz.enigma.classhandle.ClassHandleError; import cuchaz.enigma.events.ClassHandleListener; @@ -50,14 +53,17 @@ import cuchaz.enigma.gui.config.LookAndFeel; import cuchaz.enigma.gui.config.Themes; import cuchaz.enigma.gui.config.UiConfig; import cuchaz.enigma.gui.elements.EditorPopupMenu; +import cuchaz.enigma.gui.elements.GutterIcon; import cuchaz.enigma.gui.events.EditorActionListener; import cuchaz.enigma.gui.events.ThemeChangeListener; import cuchaz.enigma.gui.highlight.BoxHighlightPainter; import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; +import cuchaz.enigma.gui.util.EnigmaIconImpl; import cuchaz.enigma.gui.util.GridBagConstraintsBuilder; import cuchaz.enigma.gui.util.ScaleUtil; import cuchaz.enigma.source.DecompiledClassSource; import cuchaz.enigma.source.RenamableTokenType; +import cuchaz.enigma.source.SourceIndex; import cuchaz.enigma.source.Token; import cuchaz.enigma.translation.mapping.EntryRemapper; import cuchaz.enigma.translation.mapping.EntryResolver; @@ -65,12 +71,14 @@ import cuchaz.enigma.translation.mapping.ResolutionStrategy; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.Pair; import cuchaz.enigma.utils.Result; public class EditorPanel { private final JPanel ui = new JPanel(); private final JEditorPane editor = new JEditorPane(); private final JScrollPane editorScrollPane = new JScrollPane(this.editor); + private final GutterPanel gutterPanel; private final EditorPopupMenu popupMenu; // progress UI @@ -109,20 +117,16 @@ public class EditorPanel { this.gui = gui; this.controller = gui.getController(); - this.editor.setEditable(false); - this.editor.setSelectionColor(new Color(31, 46, 90)); - this.editor.setCaret(new BrowserCaret()); + customizeEditor(this.editor); this.editor.addCaretListener(event -> onCaretMove(event.getDot(), this.mouseIsPressed)); - this.editor.setCaretColor(UiConfig.getCaretColor()); - this.editor.setContentType("text/enigma-sources"); - this.editor.setBackground(UiConfig.getEditorBackgroundColor()); - DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit(); - kit.toggleComponent(this.editor, "de.sciss.syntaxpane.components.TokenMarker"); // 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.gutterPanel = new GutterPanel(this.editor, (JComponent) this.editorScrollPane.getRowHeader().getView()); + this.editorScrollPane.setRowHeaderView(this.gutterPanel); + // init editor popup menu this.popupMenu = new EditorPopupMenu(this, gui); this.editor.setComponentPopupMenu(this.popupMenu.getUi()); @@ -255,6 +259,17 @@ public class EditorPanel { this.ui.putClientProperty(EditorPanel.class, this); } + public static void customizeEditor(JEditorPane editor) { + editor.setEditable(false); + editor.setSelectionColor(new Color(31, 46, 90)); + editor.setCaret(new BrowserCaret()); + editor.setCaretColor(UiConfig.getCaretColor()); + editor.setContentType("text/enigma-sources"); + editor.setBackground(UiConfig.getEditorBackgroundColor()); + DefaultSyntaxKit kit = (DefaultSyntaxKit) editor.getEditorKit(); + kit.toggleComponent(editor, "de.sciss.syntaxpane.components.TokenMarker"); + } + @Nullable public static EditorPanel byUi(Component ui) { if (ui instanceof JComponent) { @@ -512,6 +527,7 @@ public class EditorPanel { this.editor.setCaretPosition(newCaretPos); } + addGutterMarkers(source.getIndex()); setHighlightedTokens(source.getHighlightedTokens()); setCursorReference(getReference(getToken(this.editor.getCaretPosition()))); } finally { @@ -524,6 +540,44 @@ public class EditorPanel { } } + private void addGutterMarkers(SourceIndex sourceIndex) { + List services = this.gui.getController().enigma.getServices().get(GuiService.TYPE); + + if (services.isEmpty()) { + return; + } + + this.gutterPanel.clearMarkers(); + + List, Token>> declarationTokens = new ArrayList<>(); + + for (Entry declaration : sourceIndex.declarations()) { + declarationTokens.add(new Pair<>(declaration, sourceIndex.getDeclarationToken(declaration))); + } + + declarationTokens.sort(Comparator.comparing(pair -> pair.b)); + + for (Pair, Token> declaration : declarationTokens) { + int lineNumber; + + try { + lineNumber = ActionUtils.getLineNumber(this.editor, declaration.b.start); + } catch (BadLocationException e) { + continue; + } + + for (GuiService service : services) { + service.addGutterMarkers(this.gui.getController(), declaration.a, (icon, alignment) -> { + GutterIcon button = new GutterIcon((EnigmaIconImpl) icon); + this.gutterPanel.addMarker(lineNumber, alignment, button); + return button; + }); + } + } + + this.editor.revalidate(); + } + public void setHighlightedTokens(Map> tokens) { // remove any old highlighters this.editor.getHighlighter().removeAllHighlights(); @@ -565,10 +619,23 @@ public class EditorPanel { } } + @Nullable public EntryReference, Entry> getCursorReference() { return this.cursorReference; } + @Nullable + public Entry getCursorDeclaration() { + int pos = this.editor.getCaretPosition(); + Token token = getToken(pos); + + if (token == null) { + return null; + } + + return this.source.getIndex().getDeclaration(token); + } + public void showReference(EntryReference, Entry> reference) { if (this.mode == DisplayMode.SUCCESS) { 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 0000000..28636a1 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/GutterPanel.java @@ -0,0 +1,238 @@ +package cuchaz.enigma.gui.panels; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.LayoutManager2; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JEditorPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; + +import de.sciss.syntaxpane.actions.ActionUtils; + +import cuchaz.enigma.api.service.GuiService; +import cuchaz.enigma.gui.config.UiConfig; + +public class GutterPanel extends JPanel { + private final JPanel markerPanel; + + public GutterPanel(JEditorPane editor, JComponent lineNumbers) { + markerPanel = new MarkerPanel(editor); + + setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + add(lineNumbers); + add(markerPanel); + + setBackground(UiConfig.getLineNumbersBackgroundColor()); + setForeground(UiConfig.getLineNumbersForegroundColor()); + markerPanel.setBackground(UiConfig.getLineNumbersBackgroundColor()); + markerPanel.setForeground(UiConfig.getLineNumbersForegroundColor()); + setBorder(lineNumbers.getBorder()); + lineNumbers.setBorder(null); + } + + public void clearMarkers() { + markerPanel.removeAll(); + } + + public void addMarker(int line, GuiService.GutterMarkerAlignment alignment, Component marker) { + markerPanel.add(marker, new MarkerLayout.Constraint(line, alignment)); + } + + private static class MarkerPanel extends JPanel implements CaretListener, DocumentListener, PropertyChangeListener { + private final JEditorPane editor; + private final Color currentLineColor = UiConfig.getLineNumbersSelectedColor(); + + private MarkerPanel(JEditorPane editor) { + this.editor = editor; + setLayout(new MarkerLayout()); + + Insets editorInsets = editor.getInsets(); + + if (editorInsets.top != 0 || editorInsets.bottom != 0) { + setBorder(BorderFactory.createEmptyBorder(editorInsets.top, 0, editorInsets.bottom, 0)); + } + + setFont(editor.getFont()); + + editor.addCaretListener(this); + editor.getDocument().addDocumentListener(this); + editor.addPropertyChangeListener(this); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + int currentLine; + + try { + currentLine = ActionUtils.getLineNumber(editor, editor.getCaretPosition()); + } catch (BadLocationException ex) { + return; // no valid caret -> nothing to draw + } + + FontMetrics fm = getFontMetrics(getFont()); + int lh = fm.getHeight(); + Insets insets = getInsets(); + + int y = currentLine * lh; + + g.setColor(currentLineColor); + g.fillRect(0, y, getWidth(), lh); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("document")) { + repaint(); + } + } + + @Override + public void caretUpdate(CaretEvent e) { + repaint(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + documentChanged(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + documentChanged(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + documentChanged(); + } + + private void documentChanged() { + SwingUtilities.invokeLater(this::repaint); + } + } + + private static class MarkerLayout implements LayoutManager2 { + private static final int GAP = 2; + private static final int HUGE_HEIGHT = 0x100000; // taken from LineNumbersRuler + + private final Map constraints = new HashMap<>(); + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + this.constraints.put(comp, (Constraint) constraints); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return preferredLayoutSize(target); + } + + @Override + public float getLayoutAlignmentX(Container target) { + return 0.5f; + } + + @Override + public float getLayoutAlignmentY(Container target) { + return 0.5f; + } + + @Override + public void invalidateLayout(Container target) { + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + constraints.remove(comp); + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + synchronized (parent.getTreeLock()) { + Insets insets = parent.getInsets(); + + if (constraints.isEmpty()) { + return new Dimension(insets.left + insets.right, HUGE_HEIGHT); + } + + Map leftCount = new HashMap<>(); + Map rightCount = new HashMap<>(); + + for (Constraint constraint : constraints.values()) { + switch (constraint.alignment) { + case LEFT -> leftCount.merge(constraint.line, 1, Integer::sum); + case RIGHT -> rightCount.merge(constraint.line, 1, Integer::sum); + } + } + + int maxLeft = leftCount.values().stream().mapToInt(Integer::intValue).max().orElse(0); + int maxRight = rightCount.values().stream().mapToInt(Integer::intValue).max().orElse(0); + + int lineHeight = parent.getFontMetrics(parent.getFont()).getHeight(); + return new Dimension( + GAP + insets.left + insets.right + (maxLeft + maxRight) * lineHeight, + HUGE_HEIGHT + ); + } + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return preferredLayoutSize(parent); + } + + @Override + public void layoutContainer(Container parent) { + synchronized (parent.getTreeLock()) { + Map leftCount = new HashMap<>(); + Map rightCount = new HashMap<>(); + + int lineHeight = parent.getFontMetrics(parent.getFont()).getHeight(); + + int numComponents = parent.getComponentCount(); + + for (int i = 0; i < numComponents; i++) { + Component comp = parent.getComponent(i); + Constraint constraint = constraints.get(comp); + + if (constraint == null) { + continue; + } + + int left = switch (constraint.alignment) { + case LEFT -> GAP + (leftCount.merge(constraint.line, 1, Integer::sum) - 1) * lineHeight + GAP / 2; + case RIGHT -> parent.getWidth() - rightCount.merge(constraint.line, 1, Integer::sum) * lineHeight + GAP / 2; + }; + comp.setBounds(left, constraint.line * lineHeight + GAP / 2, lineHeight - GAP, lineHeight - GAP); + } + } + } + + private record Constraint(int line, GuiService.GutterMarkerAlignment alignment) { + } + } +} 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 0000000..8e07df3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/EnigmaIconImpl.java @@ -0,0 +1,8 @@ +package cuchaz.enigma.gui.util; + +import com.formdev.flatlaf.extras.FlatSVGIcon; + +import cuchaz.enigma.api.EnigmaIcon; + +public record EnigmaIconImpl(FlatSVGIcon icon) implements EnigmaIcon { +} 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 0000000..e948f5a --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/IconLoadingServiceImpl.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.gui.util; + +import java.io.IOException; +import java.io.InputStream; + +import com.formdev.flatlaf.extras.FlatSVGIcon; + +import cuchaz.enigma.api.EnigmaIcon; +import cuchaz.enigma.utils.IconLoadingService; + +public class IconLoadingServiceImpl implements IconLoadingService { + @Override + public EnigmaIcon loadIcon(InputStream in) throws IOException { + return new EnigmaIconImpl(new FlatSVGIcon(in)); + } +} 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 0000000..649dfd0 --- /dev/null +++ b/enigma-swing/src/main/resources/META-INF/services/cuchaz.enigma.utils.IconLoadingService @@ -0,0 +1 @@ +cuchaz.enigma.gui.util.IconLoadingServiceImpl -- cgit v1.2.3