From e3f452250e51b7271f3989c7dfd12e4422934942 Mon Sep 17 00:00:00 2001
From: Michael Smith
Date: Thu, 21 May 2015 23:30:00 +0100
Subject: Support Gradle alongside SSJB
This makes builds faster, simpler and better automated but still keeps
Cuchaz happy. :)
---
src/cuchaz/enigma/gui/AboutDialog.java | 86 ++
src/cuchaz/enigma/gui/BoxHighlightPainter.java | 64 ++
src/cuchaz/enigma/gui/BrowserCaret.java | 45 +
src/cuchaz/enigma/gui/ClassListCellRenderer.java | 36 +
src/cuchaz/enigma/gui/ClassMatchingGui.java | 589 ++++++++++
src/cuchaz/enigma/gui/ClassSelector.java | 293 +++++
src/cuchaz/enigma/gui/ClassSelectorClassNode.java | 50 +
.../enigma/gui/ClassSelectorPackageNode.java | 45 +
src/cuchaz/enigma/gui/CodeReader.java | 222 ++++
src/cuchaz/enigma/gui/CrashDialog.java | 101 ++
.../enigma/gui/DeobfuscatedHighlightPainter.java | 21 +
src/cuchaz/enigma/gui/Gui.java | 1122 ++++++++++++++++++++
src/cuchaz/enigma/gui/GuiController.java | 358 +++++++
src/cuchaz/enigma/gui/GuiTricks.java | 56 +
src/cuchaz/enigma/gui/MemberMatchingGui.java | 499 +++++++++
.../enigma/gui/ObfuscatedHighlightPainter.java | 21 +
src/cuchaz/enigma/gui/OtherHighlightPainter.java | 21 +
src/cuchaz/enigma/gui/ProgressDialog.java | 105 ++
src/cuchaz/enigma/gui/ReadableToken.java | 36 +
src/cuchaz/enigma/gui/RenameListener.java | 17 +
src/cuchaz/enigma/gui/ScoredClassEntry.java | 30 +
.../enigma/gui/SelectionHighlightPainter.java | 34 +
src/cuchaz/enigma/gui/TokenListCellRenderer.java | 38 +
23 files changed, 3889 insertions(+)
create mode 100644 src/cuchaz/enigma/gui/AboutDialog.java
create mode 100644 src/cuchaz/enigma/gui/BoxHighlightPainter.java
create mode 100644 src/cuchaz/enigma/gui/BrowserCaret.java
create mode 100644 src/cuchaz/enigma/gui/ClassListCellRenderer.java
create mode 100644 src/cuchaz/enigma/gui/ClassMatchingGui.java
create mode 100644 src/cuchaz/enigma/gui/ClassSelector.java
create mode 100644 src/cuchaz/enigma/gui/ClassSelectorClassNode.java
create mode 100644 src/cuchaz/enigma/gui/ClassSelectorPackageNode.java
create mode 100644 src/cuchaz/enigma/gui/CodeReader.java
create mode 100644 src/cuchaz/enigma/gui/CrashDialog.java
create mode 100644 src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java
create mode 100644 src/cuchaz/enigma/gui/Gui.java
create mode 100644 src/cuchaz/enigma/gui/GuiController.java
create mode 100644 src/cuchaz/enigma/gui/GuiTricks.java
create mode 100644 src/cuchaz/enigma/gui/MemberMatchingGui.java
create mode 100644 src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java
create mode 100644 src/cuchaz/enigma/gui/OtherHighlightPainter.java
create mode 100644 src/cuchaz/enigma/gui/ProgressDialog.java
create mode 100644 src/cuchaz/enigma/gui/ReadableToken.java
create mode 100644 src/cuchaz/enigma/gui/RenameListener.java
create mode 100644 src/cuchaz/enigma/gui/ScoredClassEntry.java
create mode 100644 src/cuchaz/enigma/gui/SelectionHighlightPainter.java
create mode 100644 src/cuchaz/enigma/gui/TokenListCellRenderer.java
(limited to 'src/cuchaz/enigma/gui')
diff --git a/src/cuchaz/enigma/gui/AboutDialog.java b/src/cuchaz/enigma/gui/AboutDialog.java
new file mode 100644
index 0000000..3eba1e5
--- /dev/null
+++ b/src/cuchaz/enigma/gui/AboutDialog.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Cursor;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.WindowConstants;
+
+import cuchaz.enigma.Constants;
+import cuchaz.enigma.Util;
+
+public class AboutDialog {
+
+ public static void show(JFrame parent) {
+ // init frame
+ final JFrame frame = new JFrame(Constants.Name + " - About");
+ final Container pane = frame.getContentPane();
+ pane.setLayout(new FlowLayout());
+
+ // load the content
+ try {
+ String html = Util.readResourceToString("/about.html");
+ html = String.format(html, Constants.Name, Constants.Version);
+ JLabel label = new JLabel(html);
+ label.setHorizontalAlignment(JLabel.CENTER);
+ pane.add(label);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+
+ // show the link
+ String html = "%s";
+ html = String.format(html, Constants.Url, Constants.Url);
+ JButton link = new JButton(html);
+ link.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ Util.openUrl(Constants.Url);
+ }
+ });
+ link.setBorderPainted(false);
+ link.setOpaque(false);
+ link.setBackground(Color.WHITE);
+ link.setCursor(new Cursor(Cursor.HAND_CURSOR));
+ link.setFocusable(false);
+ JPanel linkPanel = new JPanel();
+ linkPanel.add(link);
+ pane.add(linkPanel);
+
+ // show ok button
+ JButton okButton = new JButton("Ok");
+ pane.add(okButton);
+ okButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ frame.dispose();
+ }
+ });
+
+ // show the frame
+ pane.doLayout();
+ frame.setSize(400, 220);
+ frame.setResizable(false);
+ frame.setLocationRelativeTo(parent);
+ frame.setVisible(true);
+ frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/BoxHighlightPainter.java b/src/cuchaz/enigma/gui/BoxHighlightPainter.java
new file mode 100644
index 0000000..e5e0557
--- /dev/null
+++ b/src/cuchaz/enigma/gui/BoxHighlightPainter.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Rectangle;
+import java.awt.Shape;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Highlighter;
+import javax.swing.text.JTextComponent;
+
+public abstract class BoxHighlightPainter implements Highlighter.HighlightPainter {
+
+ private Color m_fillColor;
+ private Color m_borderColor;
+
+ protected BoxHighlightPainter(Color fillColor, Color borderColor) {
+ m_fillColor = fillColor;
+ m_borderColor = borderColor;
+ }
+
+ @Override
+ public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) {
+ Rectangle bounds = getBounds(text, start, end);
+
+ // fill the area
+ if (m_fillColor != null) {
+ g.setColor(m_fillColor);
+ g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
+ }
+
+ // draw a box around the area
+ g.setColor(m_borderColor);
+ g.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
+ }
+
+ protected static Rectangle getBounds(JTextComponent text, int start, int end) {
+ try {
+ // determine the bounds of the text
+ Rectangle bounds = text.modelToView(start).union(text.modelToView(end));
+
+ // adjust the box so it looks nice
+ bounds.x -= 2;
+ bounds.width += 2;
+ bounds.y += 1;
+ bounds.height -= 2;
+
+ return bounds;
+ } catch (BadLocationException ex) {
+ // don't care... just return something
+ return new Rectangle(0, 0, 0, 0);
+ }
+ }
+}
diff --git a/src/cuchaz/enigma/gui/BrowserCaret.java b/src/cuchaz/enigma/gui/BrowserCaret.java
new file mode 100644
index 0000000..6af4d24
--- /dev/null
+++ b/src/cuchaz/enigma/gui/BrowserCaret.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Graphics;
+import java.awt.Shape;
+
+import javax.swing.text.DefaultCaret;
+import javax.swing.text.Highlighter;
+import javax.swing.text.JTextComponent;
+
+public class BrowserCaret extends DefaultCaret {
+
+ private static final long serialVersionUID = 1158977422507969940L;
+
+ private static final Highlighter.HighlightPainter m_selectionPainter = new Highlighter.HighlightPainter() {
+ @Override
+ public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
+ // don't paint anything
+ }
+ };
+
+ @Override
+ public boolean isSelectionVisible() {
+ return false;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return true;
+ }
+
+ @Override
+ public Highlighter.HighlightPainter getSelectionPainter() {
+ return m_selectionPainter;
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ClassListCellRenderer.java b/src/cuchaz/enigma/gui/ClassListCellRenderer.java
new file mode 100644
index 0000000..cde3e4c
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ClassListCellRenderer.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Component;
+
+import javassist.bytecode.Descriptor;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+
+public class ClassListCellRenderer implements ListCellRenderer {
+
+ private DefaultListCellRenderer m_defaultRenderer;
+
+ public ClassListCellRenderer() {
+ m_defaultRenderer = new DefaultListCellRenderer();
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends String> list, String className, int index, boolean isSelected, boolean hasFocus) {
+ JLabel label = (JLabel)m_defaultRenderer.getListCellRendererComponent(list, className, index, isSelected, hasFocus);
+ label.setText(Descriptor.toJavaName(className));
+ return label;
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ClassMatchingGui.java b/src/cuchaz/enigma/gui/ClassMatchingGui.java
new file mode 100644
index 0000000..89b19c3
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ClassMatchingGui.java
@@ -0,0 +1,589 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.SwingConstants;
+import javax.swing.WindowConstants;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import cuchaz.enigma.Constants;
+import cuchaz.enigma.Deobfuscator;
+import cuchaz.enigma.convert.ClassIdentifier;
+import cuchaz.enigma.convert.ClassIdentity;
+import cuchaz.enigma.convert.ClassMatch;
+import cuchaz.enigma.convert.ClassMatches;
+import cuchaz.enigma.convert.ClassMatching;
+import cuchaz.enigma.convert.ClassNamer;
+import cuchaz.enigma.convert.MappingsConverter;
+import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.Mappings;
+import cuchaz.enigma.mapping.MappingsChecker;
+import de.sciss.syntaxpane.DefaultSyntaxKit;
+
+
+public class ClassMatchingGui {
+
+ private static enum SourceType {
+ Matched {
+
+ @Override
+ public Collection getSourceClasses(ClassMatches matches) {
+ return matches.getUniqueMatches().keySet();
+ }
+ },
+ Unmatched {
+
+ @Override
+ public Collection getSourceClasses(ClassMatches matches) {
+ return matches.getUnmatchedSourceClasses();
+ }
+ },
+ Ambiguous {
+
+ @Override
+ public Collection getSourceClasses(ClassMatches matches) {
+ return matches.getAmbiguouslyMatchedSourceClasses();
+ }
+ };
+
+ public JRadioButton newRadio(ActionListener listener, ButtonGroup group) {
+ JRadioButton button = new JRadioButton(name(), this == getDefault());
+ button.setActionCommand(name());
+ button.addActionListener(listener);
+ group.add(button);
+ return button;
+ }
+
+ public abstract Collection getSourceClasses(ClassMatches matches);
+
+ public static SourceType getDefault() {
+ return values()[0];
+ }
+ }
+
+ public static interface SaveListener {
+ public void save(ClassMatches matches);
+ }
+
+ // controls
+ private JFrame m_frame;
+ private ClassSelector m_sourceClasses;
+ private ClassSelector m_destClasses;
+ private CodeReader m_sourceReader;
+ private CodeReader m_destReader;
+ private JLabel m_sourceClassLabel;
+ private JLabel m_destClassLabel;
+ private JButton m_matchButton;
+ private Map m_sourceTypeButtons;
+ private JCheckBox m_advanceCheck;
+
+ private ClassMatches m_classMatches;
+ private Deobfuscator m_sourceDeobfuscator;
+ private Deobfuscator m_destDeobfuscator;
+ private ClassEntry m_sourceClass;
+ private ClassEntry m_destClass;
+ private SourceType m_sourceType;
+ private SaveListener m_saveListener;
+
+ public ClassMatchingGui(ClassMatches matches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
+
+ m_classMatches = matches;
+ m_sourceDeobfuscator = sourceDeobfuscator;
+ m_destDeobfuscator = destDeobfuscator;
+
+ // init frame
+ m_frame = new JFrame(Constants.Name + " - Class Matcher");
+ final Container pane = m_frame.getContentPane();
+ pane.setLayout(new BorderLayout());
+
+ // init source side
+ JPanel sourcePanel = new JPanel();
+ sourcePanel.setLayout(new BoxLayout(sourcePanel, BoxLayout.PAGE_AXIS));
+ sourcePanel.setPreferredSize(new Dimension(200, 0));
+ pane.add(sourcePanel, BorderLayout.WEST);
+ sourcePanel.add(new JLabel("Source Classes"));
+
+ // init source type radios
+ JPanel sourceTypePanel = new JPanel();
+ sourcePanel.add(sourceTypePanel);
+ sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS));
+ ActionListener sourceTypeListener = new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ setSourceType(SourceType.valueOf(event.getActionCommand()));
+ }
+ };
+ ButtonGroup sourceTypeButtons = new ButtonGroup();
+ m_sourceTypeButtons = Maps.newHashMap();
+ for (SourceType sourceType : SourceType.values()) {
+ JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
+ m_sourceTypeButtons.put(sourceType, button);
+ sourceTypePanel.add(button);
+ }
+
+ m_sourceClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator);
+ m_sourceClasses.setListener(new ClassSelectionListener() {
+ @Override
+ public void onSelectClass(ClassEntry classEntry) {
+ setSourceClass(classEntry);
+ }
+ });
+ JScrollPane sourceScroller = new JScrollPane(m_sourceClasses);
+ sourcePanel.add(sourceScroller);
+
+ // init dest side
+ JPanel destPanel = new JPanel();
+ destPanel.setLayout(new BoxLayout(destPanel, BoxLayout.PAGE_AXIS));
+ destPanel.setPreferredSize(new Dimension(200, 0));
+ pane.add(destPanel, BorderLayout.WEST);
+ destPanel.add(new JLabel("Destination Classes"));
+
+ m_destClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator);
+ m_destClasses.setListener(new ClassSelectionListener() {
+ @Override
+ public void onSelectClass(ClassEntry classEntry) {
+ setDestClass(classEntry);
+ }
+ });
+ JScrollPane destScroller = new JScrollPane(m_destClasses);
+ destPanel.add(destScroller);
+
+ JButton autoMatchButton = new JButton("AutoMatch");
+ autoMatchButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ autoMatch();
+ }
+ });
+ destPanel.add(autoMatchButton);
+
+ // init source panels
+ DefaultSyntaxKit.initKit();
+ m_sourceReader = new CodeReader();
+ m_destReader = new CodeReader();
+
+ // init all the splits
+ JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, sourcePanel, new JScrollPane(m_sourceReader));
+ splitLeft.setResizeWeight(0); // let the right side take all the slack
+ JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_destReader), destPanel);
+ splitRight.setResizeWeight(1); // let the left side take all the slack
+ JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, splitLeft, splitRight);
+ splitCenter.setResizeWeight(0.5); // resize 50:50
+ pane.add(splitCenter, BorderLayout.CENTER);
+ splitCenter.resetToPreferredSizes();
+
+ // init bottom panel
+ JPanel bottomPanel = new JPanel();
+ bottomPanel.setLayout(new FlowLayout());
+
+ m_sourceClassLabel = new JLabel();
+ m_sourceClassLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ m_destClassLabel = new JLabel();
+ m_destClassLabel.setHorizontalAlignment(SwingConstants.LEFT);
+
+ m_matchButton = new JButton();
+
+ m_advanceCheck = new JCheckBox("Advance to next likely match");
+ m_advanceCheck.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_advanceCheck.isSelected()) {
+ advance();
+ }
+ }
+ });
+
+ bottomPanel.add(m_sourceClassLabel);
+ bottomPanel.add(m_matchButton);
+ bottomPanel.add(m_destClassLabel);
+ bottomPanel.add(m_advanceCheck);
+ pane.add(bottomPanel, BorderLayout.SOUTH);
+
+ // show the frame
+ pane.doLayout();
+ m_frame.setSize(1024, 576);
+ m_frame.setMinimumSize(new Dimension(640, 480));
+ m_frame.setVisible(true);
+ m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+
+ // init state
+ updateDestMappings();
+ setSourceType(SourceType.getDefault());
+ updateMatchButton();
+ m_saveListener = null;
+ }
+
+ public void setSaveListener(SaveListener val) {
+ m_saveListener = val;
+ }
+
+ private void updateDestMappings() {
+
+ Mappings newMappings = MappingsConverter.newMappings(
+ m_classMatches,
+ m_sourceDeobfuscator.getMappings(),
+ m_sourceDeobfuscator,
+ m_destDeobfuscator
+ );
+
+ // look for dropped mappings
+ MappingsChecker checker = new MappingsChecker(m_destDeobfuscator.getJarIndex());
+ checker.dropBrokenMappings(newMappings);
+
+ // count them
+ int numDroppedFields = checker.getDroppedFieldMappings().size();
+ int numDroppedMethods = checker.getDroppedMethodMappings().size();
+ System.out.println(String.format(
+ "%d mappings from matched classes don't match the dest jar:\n\t%5d fields\n\t%5d methods",
+ numDroppedFields + numDroppedMethods,
+ numDroppedFields,
+ numDroppedMethods
+ ));
+
+ m_destDeobfuscator.setMappings(newMappings);
+ }
+
+ protected void setSourceType(SourceType val) {
+
+ // show the source classes
+ m_sourceType = val;
+ m_sourceClasses.setClasses(deobfuscateClasses(m_sourceType.getSourceClasses(m_classMatches), m_sourceDeobfuscator));
+
+ // update counts
+ for (SourceType sourceType : SourceType.values()) {
+ m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
+ sourceType.name(),
+ sourceType.getSourceClasses(m_classMatches).size()
+ ));
+ }
+ }
+
+ private Collection deobfuscateClasses(Collection in, Deobfuscator deobfuscator) {
+ List out = Lists.newArrayList();
+ for (ClassEntry entry : in) {
+
+ ClassEntry deobf = deobfuscator.deobfuscateEntry(entry);
+
+ // make sure we preserve any scores
+ if (entry instanceof ScoredClassEntry) {
+ deobf = new ScoredClassEntry(deobf, ((ScoredClassEntry)entry).getScore());
+ }
+
+ out.add(deobf);
+ }
+ return out;
+ }
+
+ protected void setSourceClass(ClassEntry classEntry) {
+
+ Runnable onGetDestClasses = null;
+ if (m_advanceCheck.isSelected()) {
+ onGetDestClasses = new Runnable() {
+ @Override
+ public void run() {
+ pickBestDestClass();
+ }
+ };
+ }
+
+ setSourceClass(classEntry, onGetDestClasses);
+ }
+
+ protected void setSourceClass(ClassEntry classEntry, final Runnable onGetDestClasses) {
+
+ // update the current source class
+ m_sourceClass = classEntry;
+ m_sourceClassLabel.setText(m_sourceClass != null ? m_sourceClass.getName() : "");
+
+ if (m_sourceClass != null) {
+
+ // show the dest class(es)
+ ClassMatch match = m_classMatches.getMatchBySource(m_sourceDeobfuscator.obfuscateEntry(m_sourceClass));
+ assert(match != null);
+ if (match.destClasses.isEmpty()) {
+
+ m_destClasses.setClasses(null);
+
+ // run in a separate thread to keep ui responsive
+ new Thread() {
+ @Override
+ public void run() {
+ m_destClasses.setClasses(deobfuscateClasses(getLikelyMatches(m_sourceClass), m_destDeobfuscator));
+ m_destClasses.expandAll();
+
+ if (onGetDestClasses != null) {
+ onGetDestClasses.run();
+ }
+ }
+ }.start();
+
+ } else {
+
+ m_destClasses.setClasses(deobfuscateClasses(match.destClasses, m_destDeobfuscator));
+ m_destClasses.expandAll();
+
+ if (onGetDestClasses != null) {
+ onGetDestClasses.run();
+ }
+ }
+ }
+
+ setDestClass(null);
+ m_sourceReader.decompileClass(m_sourceClass, m_sourceDeobfuscator, new Runnable() {
+ @Override
+ public void run() {
+ m_sourceReader.navigateToClassDeclaration(m_sourceClass);
+ }
+ });
+
+ updateMatchButton();
+ }
+
+ private Collection getLikelyMatches(ClassEntry sourceClass) {
+
+ ClassEntry obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass);
+
+ // set up identifiers
+ ClassNamer namer = new ClassNamer(m_classMatches.getUniqueMatches());
+ ClassIdentifier sourceIdentifier = new ClassIdentifier(
+ m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(),
+ namer.getSourceNamer(), true
+ );
+ ClassIdentifier destIdentifier = new ClassIdentifier(
+ m_destDeobfuscator.getJar(), m_destDeobfuscator.getJarIndex(),
+ namer.getDestNamer(), true
+ );
+
+ try {
+
+ // rank all the unmatched dest classes against the source class
+ ClassIdentity sourceIdentity = sourceIdentifier.identify(obfSourceClass);
+ List scoredDestClasses = Lists.newArrayList();
+ for (ClassEntry unmatchedDestClass : m_classMatches.getUnmatchedDestClasses()) {
+ ClassIdentity destIdentity = destIdentifier.identify(unmatchedDestClass);
+ float score = 100.0f*(sourceIdentity.getMatchScore(destIdentity) + destIdentity.getMatchScore(sourceIdentity))
+ /(sourceIdentity.getMaxMatchScore() + destIdentity.getMaxMatchScore());
+ scoredDestClasses.add(new ScoredClassEntry(unmatchedDestClass, score));
+ }
+ return scoredDestClasses;
+
+ } catch (ClassNotFoundException ex) {
+ throw new Error("Unable to find class " + ex.getMessage());
+ }
+ }
+
+ protected void setDestClass(ClassEntry classEntry) {
+
+ // update the current source class
+ m_destClass = classEntry;
+ m_destClassLabel.setText(m_destClass != null ? m_destClass.getName() : "");
+
+ m_destReader.decompileClass(m_destClass, m_destDeobfuscator, new Runnable() {
+ @Override
+ public void run() {
+ m_destReader.navigateToClassDeclaration(m_destClass);
+ }
+ });
+
+ updateMatchButton();
+ }
+
+ private void updateMatchButton() {
+
+ ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
+ ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass);
+
+ BiMap uniqueMatches = m_classMatches.getUniqueMatches();
+ boolean twoSelected = m_sourceClass != null && m_destClass != null;
+ boolean isMatched = uniqueMatches.containsKey(obfSource) && uniqueMatches.containsValue(obfDest);
+ boolean canMatch = !uniqueMatches.containsKey(obfSource) && ! uniqueMatches.containsValue(obfDest);
+
+ GuiTricks.deactivateButton(m_matchButton);
+ if (twoSelected) {
+ if (isMatched) {
+ GuiTricks.activateButton(m_matchButton, "Unmatch", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ onUnmatchClick();
+ }
+ });
+ } else if (canMatch) {
+ GuiTricks.activateButton(m_matchButton, "Match", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ onMatchClick();
+ }
+ });
+ }
+ }
+ }
+
+ private void onMatchClick() {
+ // precondition: source and dest classes are set correctly
+
+ ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
+ ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass);
+
+ // remove the classes from their match
+ m_classMatches.removeSource(obfSource);
+ m_classMatches.removeDest(obfDest);
+
+ // add them as matched classes
+ m_classMatches.add(new ClassMatch(obfSource, obfDest));
+
+ ClassEntry nextClass = null;
+ if (m_advanceCheck.isSelected()) {
+ nextClass = m_sourceClasses.getNextClass(m_sourceClass);
+ }
+
+ save();
+ updateMatches();
+
+ if (nextClass != null) {
+ advance(nextClass);
+ }
+ }
+
+ private void onUnmatchClick() {
+ // precondition: source and dest classes are set to a unique match
+
+ ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
+
+ // remove the source to break the match, then add the source back as unmatched
+ m_classMatches.removeSource(obfSource);
+ m_classMatches.add(new ClassMatch(obfSource, null));
+
+ save();
+ updateMatches();
+ }
+
+ private void updateMatches() {
+ updateDestMappings();
+ setDestClass(null);
+ m_destClasses.setClasses(null);
+ updateMatchButton();
+
+ // remember where we were in the source tree
+ String packageName = m_sourceClasses.getSelectedPackage();
+
+ setSourceType(m_sourceType);
+
+ m_sourceClasses.expandPackage(packageName);
+ }
+
+ private void save() {
+ if (m_saveListener != null) {
+ m_saveListener.save(m_classMatches);
+ }
+ }
+
+ private void autoMatch() {
+
+ System.out.println("Automatching...");
+
+ // compute a new matching
+ ClassMatching matching = MappingsConverter.computeMatching(
+ m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(),
+ m_destDeobfuscator.getJar(), m_destDeobfuscator.getJarIndex(),
+ m_classMatches.getUniqueMatches()
+ );
+ ClassMatches newMatches = new ClassMatches(matching.matches());
+ System.out.println(String.format("Automatch found %d new matches",
+ newMatches.getUniqueMatches().size() - m_classMatches.getUniqueMatches().size()
+ ));
+
+ // update the current matches
+ m_classMatches = newMatches;
+ save();
+ updateMatches();
+ }
+
+ private void advance() {
+ advance(null);
+ }
+
+ private void advance(ClassEntry sourceClass) {
+
+ // make sure we have a source class
+ if (sourceClass == null) {
+ sourceClass = m_sourceClasses.getSelectedClass();
+ if (sourceClass != null) {
+ sourceClass = m_sourceClasses.getNextClass(sourceClass);
+ } else {
+ sourceClass = m_sourceClasses.getFirstClass();
+ }
+ }
+
+ // set the source class
+ setSourceClass(sourceClass, new Runnable() {
+ @Override
+ public void run() {
+ pickBestDestClass();
+ }
+ });
+ m_sourceClasses.setSelectionClass(sourceClass);
+ }
+
+ private void pickBestDestClass() {
+
+ // then, pick the best dest class
+ ClassEntry firstClass = null;
+ ScoredClassEntry bestDestClass = null;
+ for (ClassSelectorPackageNode packageNode : m_destClasses.packageNodes()) {
+ for (ClassSelectorClassNode classNode : m_destClasses.classNodes(packageNode)) {
+ if (firstClass == null) {
+ firstClass = classNode.getClassEntry();
+ }
+ if (classNode.getClassEntry() instanceof ScoredClassEntry) {
+ ScoredClassEntry scoredClass = (ScoredClassEntry)classNode.getClassEntry();
+ if (bestDestClass == null || bestDestClass.getScore() < scoredClass.getScore()) {
+ bestDestClass = scoredClass;
+ }
+ }
+ }
+ }
+
+ // pick the entry to show
+ ClassEntry destClass = null;
+ if (bestDestClass != null) {
+ destClass = bestDestClass;
+ } else if (firstClass != null) {
+ destClass = firstClass;
+ }
+
+ setDestClass(destClass);
+ m_destClasses.setSelectionClass(destClass);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ClassSelector.java b/src/cuchaz/enigma/gui/ClassSelector.java
new file mode 100644
index 0000000..11333a9
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ClassSelector.java
@@ -0,0 +1,293 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import cuchaz.enigma.mapping.ClassEntry;
+
+public class ClassSelector extends JTree {
+
+ private static final long serialVersionUID = -7632046902384775977L;
+
+ public interface ClassSelectionListener {
+ void onSelectClass(ClassEntry classEntry);
+ }
+
+ public static Comparator ObfuscatedClassEntryComparator;
+ public static Comparator DeobfuscatedClassEntryComparator;
+
+ static {
+ ObfuscatedClassEntryComparator = new Comparator() {
+ @Override
+ public int compare(ClassEntry a, ClassEntry b) {
+ String aname = a.getName();
+ String bname = a.getName();
+ if (aname.length() != bname.length()) {
+ return aname.length() - bname.length();
+ }
+ return aname.compareTo(bname);
+ }
+ };
+
+ DeobfuscatedClassEntryComparator = new Comparator() {
+ @Override
+ public int compare(ClassEntry a, ClassEntry b) {
+ if (a instanceof ScoredClassEntry && b instanceof ScoredClassEntry) {
+ return Float.compare(
+ ((ScoredClassEntry)b).getScore(),
+ ((ScoredClassEntry)a).getScore()
+ );
+ }
+ return a.getName().compareTo(b.getName());
+ }
+ };
+ }
+
+ private ClassSelectionListener m_listener;
+ private Comparator m_comparator;
+
+ public ClassSelector(Comparator comparator) {
+ m_comparator = comparator;
+
+ // configure the tree control
+ setRootVisible(false);
+ setShowsRootHandles(false);
+ setModel(null);
+
+ // hook events
+ addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ if (m_listener != null && event.getClickCount() == 2) {
+ // get the selected node
+ TreePath path = getSelectionPath();
+ if (path != null && path.getLastPathComponent() instanceof ClassSelectorClassNode) {
+ ClassSelectorClassNode node = (ClassSelectorClassNode)path.getLastPathComponent();
+ m_listener.onSelectClass(node.getClassEntry());
+ }
+ }
+ }
+ });
+
+ // init defaults
+ m_listener = null;
+ }
+
+ public void setListener(ClassSelectionListener val) {
+ m_listener = val;
+ }
+
+ public void setClasses(Collection classEntries) {
+ if (classEntries == null) {
+ setModel(null);
+ return;
+ }
+
+ // build the package names
+ Map packages = Maps.newHashMap();
+ for (ClassEntry classEntry : classEntries) {
+ packages.put(classEntry.getPackageName(), null);
+ }
+
+ // sort the packages
+ List sortedPackageNames = Lists.newArrayList(packages.keySet());
+ Collections.sort(sortedPackageNames, new Comparator() {
+ @Override
+ public int compare(String a, String b) {
+ // I can never keep this rule straight when writing these damn things...
+ // a < b => -1, a == b => 0, a > b => +1
+
+ String[] aparts = a.split("/");
+ String[] bparts = b.split("/");
+ for (int i = 0; true; i++) {
+ if (i >= aparts.length) {
+ return -1;
+ } else if (i >= bparts.length) {
+ return 1;
+ }
+
+ int result = aparts[i].compareTo(bparts[i]);
+ if (result != 0) {
+ return result;
+ }
+ }
+ }
+ });
+
+ // create the root node and the package nodes
+ DefaultMutableTreeNode root = new DefaultMutableTreeNode();
+ for (String packageName : sortedPackageNames) {
+ ClassSelectorPackageNode node = new ClassSelectorPackageNode(packageName);
+ packages.put(packageName, node);
+ root.add(node);
+ }
+
+ // put the classes into packages
+ Multimap packagedClassEntries = ArrayListMultimap.create();
+ for (ClassEntry classEntry : classEntries) {
+ packagedClassEntries.put(classEntry.getPackageName(), classEntry);
+ }
+
+ // build the class nodes
+ for (String packageName : packagedClassEntries.keySet()) {
+ // sort the class entries
+ List classEntriesInPackage = Lists.newArrayList(packagedClassEntries.get(packageName));
+ Collections.sort(classEntriesInPackage, m_comparator);
+
+ // create the nodes in order
+ for (ClassEntry classEntry : classEntriesInPackage) {
+ ClassSelectorPackageNode node = packages.get(packageName);
+ node.add(new ClassSelectorClassNode(classEntry));
+ }
+ }
+
+ // finally, update the tree control
+ setModel(new DefaultTreeModel(root));
+ }
+
+ public ClassEntry getSelectedClass() {
+ if (!isSelectionEmpty()) {
+ Object selectedNode = getSelectionPath().getLastPathComponent();
+ if (selectedNode instanceof ClassSelectorClassNode) {
+ ClassSelectorClassNode classNode = (ClassSelectorClassNode)selectedNode;
+ return classNode.getClassEntry();
+ }
+ }
+ return null;
+ }
+
+ public String getSelectedPackage() {
+ if (!isSelectionEmpty()) {
+ Object selectedNode = getSelectionPath().getLastPathComponent();
+ if (selectedNode instanceof ClassSelectorPackageNode) {
+ ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode)selectedNode;
+ return packageNode.getPackageName();
+ } else if (selectedNode instanceof ClassSelectorClassNode) {
+ ClassSelectorClassNode classNode = (ClassSelectorClassNode)selectedNode;
+ return classNode.getClassEntry().getPackageName();
+ }
+ }
+ return null;
+ }
+
+ public Iterable packageNodes() {
+ List nodes = Lists.newArrayList();
+ DefaultMutableTreeNode root = (DefaultMutableTreeNode)getModel().getRoot();
+ Enumeration> children = root.children();
+ while (children.hasMoreElements()) {
+ ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode)children.nextElement();
+ nodes.add(packageNode);
+ }
+ return nodes;
+ }
+
+ public Iterable classNodes(ClassSelectorPackageNode packageNode) {
+ List nodes = Lists.newArrayList();
+ Enumeration> children = packageNode.children();
+ while (children.hasMoreElements()) {
+ ClassSelectorClassNode classNode = (ClassSelectorClassNode)children.nextElement();
+ nodes.add(classNode);
+ }
+ return nodes;
+ }
+
+ public void expandPackage(String packageName) {
+ if (packageName == null) {
+ return;
+ }
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ if (packageNode.getPackageName().equals(packageName)) {
+ expandPath(new TreePath(new Object[] {getModel().getRoot(), packageNode}));
+ return;
+ }
+ }
+ }
+
+ public void expandAll() {
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ expandPath(new TreePath(new Object[] {getModel().getRoot(), packageNode}));
+ }
+ }
+
+ public ClassEntry getFirstClass() {
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
+ return classNode.getClassEntry();
+ }
+ }
+ return null;
+ }
+
+ public ClassSelectorPackageNode getPackageNode(ClassEntry entry) {
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ if (packageNode.getPackageName().equals(entry.getPackageName())) {
+ return packageNode;
+ }
+ }
+ return null;
+ }
+
+ public ClassEntry getNextClass(ClassEntry entry) {
+ boolean foundIt = false;
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ if (!foundIt) {
+ // skip to the package with our target in it
+ if (packageNode.getPackageName().equals(entry.getPackageName())) {
+ for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
+ if (!foundIt) {
+ if (classNode.getClassEntry().equals(entry)) {
+ foundIt = true;
+ }
+ } else {
+ // return the next class
+ return classNode.getClassEntry();
+ }
+ }
+ }
+ } else {
+ // return the next class
+ for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
+ return classNode.getClassEntry();
+ }
+ }
+ }
+ return null;
+ }
+
+ public void setSelectionClass(ClassEntry classEntry) {
+ expandPackage(classEntry.getPackageName());
+ for (ClassSelectorPackageNode packageNode : packageNodes()) {
+ for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
+ if (classNode.getClassEntry().equals(classEntry)) {
+ setSelectionPath(new TreePath(new Object[] {getModel().getRoot(), packageNode, classNode}));
+ }
+ }
+ }
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ClassSelectorClassNode.java b/src/cuchaz/enigma/gui/ClassSelectorClassNode.java
new file mode 100644
index 0000000..1219e89
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ClassSelectorClassNode.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+import cuchaz.enigma.mapping.ClassEntry;
+
+public class ClassSelectorClassNode extends DefaultMutableTreeNode {
+
+ private static final long serialVersionUID = -8956754339813257380L;
+
+ private ClassEntry m_classEntry;
+
+ public ClassSelectorClassNode(ClassEntry classEntry) {
+ m_classEntry = classEntry;
+ }
+
+ public ClassEntry getClassEntry() {
+ return m_classEntry;
+ }
+
+ @Override
+ public String toString() {
+ if (m_classEntry instanceof ScoredClassEntry) {
+ return String.format("%d%% %s", (int)((ScoredClassEntry)m_classEntry).getScore(), m_classEntry.getSimpleName());
+ }
+ return m_classEntry.getSimpleName();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ClassSelectorClassNode) {
+ return equals((ClassSelectorClassNode)other);
+ }
+ return false;
+ }
+
+ public boolean equals(ClassSelectorClassNode other) {
+ return m_classEntry.equals(other.m_classEntry);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java b/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java
new file mode 100644
index 0000000..7259f54
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+public class ClassSelectorPackageNode extends DefaultMutableTreeNode {
+
+ private static final long serialVersionUID = -3730868701219548043L;
+
+ private String m_packageName;
+
+ public ClassSelectorPackageNode(String packageName) {
+ m_packageName = packageName;
+ }
+
+ public String getPackageName() {
+ return m_packageName;
+ }
+
+ @Override
+ public String toString() {
+ return m_packageName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ClassSelectorPackageNode) {
+ return equals((ClassSelectorPackageNode)other);
+ }
+ return false;
+ }
+
+ public boolean equals(ClassSelectorPackageNode other) {
+ return m_packageName.equals(other.m_packageName);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/CodeReader.java b/src/cuchaz/enigma/gui/CodeReader.java
new file mode 100644
index 0000000..5033a2c
--- /dev/null
+++ b/src/cuchaz/enigma/gui/CodeReader.java
@@ -0,0 +1,222 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JEditorPane;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Highlighter.HighlightPainter;
+
+import com.strobel.decompiler.languages.java.ast.CompilationUnit;
+
+import cuchaz.enigma.Deobfuscator;
+import cuchaz.enigma.analysis.EntryReference;
+import cuchaz.enigma.analysis.SourceIndex;
+import cuchaz.enigma.analysis.Token;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.Entry;
+import de.sciss.syntaxpane.DefaultSyntaxKit;
+
+
+public class CodeReader extends JEditorPane {
+
+ private static final long serialVersionUID = 3673180950485748810L;
+
+ private static final Object m_lock = new Object();
+
+ public static interface SelectionListener {
+ void onSelect(EntryReference reference);
+ }
+
+ private SelectionHighlightPainter m_selectionHighlightPainter;
+ private SourceIndex m_sourceIndex;
+ private SelectionListener m_selectionListener;
+
+ public CodeReader() {
+
+ setEditable(false);
+ setContentType("text/java");
+
+ // turn off token highlighting (it's wrong most of the time anyway...)
+ DefaultSyntaxKit kit = (DefaultSyntaxKit)getEditorKit();
+ kit.toggleComponent(this, "de.sciss.syntaxpane.components.TokenMarker");
+
+ // hook events
+ addCaretListener(new CaretListener() {
+ @Override
+ public void caretUpdate(CaretEvent event) {
+ if (m_selectionListener != null && m_sourceIndex != null) {
+ Token token = m_sourceIndex.getReferenceToken(event.getDot());
+ if (token != null) {
+ m_selectionListener.onSelect(m_sourceIndex.getDeobfReference(token));
+ } else {
+ m_selectionListener.onSelect(null);
+ }
+ }
+ }
+ });
+
+ m_selectionHighlightPainter = new SelectionHighlightPainter();
+ m_sourceIndex = null;
+ m_selectionListener = null;
+ }
+
+ public void setSelectionListener(SelectionListener val) {
+ m_selectionListener = val;
+ }
+
+ public void setCode(String code) {
+ // sadly, the java lexer is not thread safe, so we have to serialize all these calls
+ synchronized (m_lock) {
+ setText(code);
+ }
+ }
+
+ public SourceIndex getSourceIndex() {
+ return m_sourceIndex;
+ }
+
+ public void decompileClass(ClassEntry classEntry, Deobfuscator deobfuscator) {
+ decompileClass(classEntry, deobfuscator, null);
+ }
+
+ public void decompileClass(ClassEntry classEntry, Deobfuscator deobfuscator, Runnable callback) {
+ decompileClass(classEntry, deobfuscator, null, callback);
+ }
+
+ public void decompileClass(final ClassEntry classEntry, final Deobfuscator deobfuscator, final Boolean ignoreBadTokens, final Runnable callback) {
+
+ if (classEntry == null) {
+ setCode(null);
+ return;
+ }
+
+ setCode("(decompiling...)");
+
+ // run decompilation in a separate thread to keep ui responsive
+ new Thread() {
+ @Override
+ public void run() {
+
+ // decompile it
+ CompilationUnit sourceTree = deobfuscator.getSourceTree(classEntry.getOutermostClassName());
+ String source = deobfuscator.getSource(sourceTree);
+ setCode(source);
+ m_sourceIndex = deobfuscator.getSourceIndex(sourceTree, source, ignoreBadTokens);
+
+ if (callback != null) {
+ callback.run();
+ }
+ }
+ }.start();
+ }
+
+ public void navigateToClassDeclaration(ClassEntry classEntry) {
+
+ // navigate to the class declaration
+ Token token = m_sourceIndex.getDeclarationToken(classEntry);
+ if (token == null) {
+ // couldn't find the class declaration token, might be an anonymous class
+ // look for any declaration in that class instead
+ for (Entry entry : m_sourceIndex.declarations()) {
+ if (entry.getClassEntry().equals(classEntry)) {
+ token = m_sourceIndex.getDeclarationToken(entry);
+ break;
+ }
+ }
+ }
+
+ if (token != null) {
+ navigateToToken(token);
+ } else {
+ // couldn't find anything =(
+ System.out.println("Unable to find declaration in source for " + classEntry);
+ }
+ }
+
+ public void navigateToToken(final Token token) {
+ navigateToToken(this, token, m_selectionHighlightPainter);
+ }
+
+ // HACKHACK: someday we can update the main GUI to use this code reader
+ public static void navigateToToken(final JEditorPane editor, final Token token, final HighlightPainter highlightPainter) {
+
+ // set the caret position to the token
+ editor.setCaretPosition(token.start);
+ editor.grabFocus();
+
+ try {
+ // make sure the token is visible in the scroll window
+ Rectangle start = editor.modelToView(token.start);
+ Rectangle end = editor.modelToView(token.end);
+ final Rectangle show = start.union(end);
+ show.grow(start.width * 10, start.height * 6);
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ editor.scrollRectToVisible(show);
+ }
+ });
+ } catch (BadLocationException ex) {
+ throw new Error(ex);
+ }
+
+ // highlight the token momentarily
+ final Timer timer = new Timer(200, new ActionListener() {
+ private int m_counter = 0;
+ private Object m_highlight = null;
+
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_counter % 2 == 0) {
+ try {
+ m_highlight = editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter);
+ } catch (BadLocationException ex) {
+ // don't care
+ }
+ } else if (m_highlight != null) {
+ editor.getHighlighter().removeHighlight(m_highlight);
+ }
+
+ if (m_counter++ > 6) {
+ Timer timer = (Timer)event.getSource();
+ timer.stop();
+ }
+ }
+ });
+ timer.start();
+ }
+
+ public void setHighlightedTokens(Iterable tokens, HighlightPainter painter) {
+ for (Token token : tokens) {
+ setHighlightedToken(token, painter);
+ }
+ }
+
+ public void setHighlightedToken(Token token, HighlightPainter painter) {
+ try {
+ getHighlighter().addHighlight(token.start, token.end, painter);
+ } catch (BadLocationException ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ public void clearHighlights() {
+ getHighlighter().removeAllHighlights();
+ }
+}
diff --git a/src/cuchaz/enigma/gui/CrashDialog.java b/src/cuchaz/enigma/gui/CrashDialog.java
new file mode 100644
index 0000000..904273c
--- /dev/null
+++ b/src/cuchaz/enigma/gui/CrashDialog.java
@@ -0,0 +1,101 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.WindowConstants;
+
+import cuchaz.enigma.Constants;
+
+public class CrashDialog {
+
+ private static CrashDialog m_instance = null;
+
+ private JFrame m_frame;
+ private JTextArea m_text;
+
+ private CrashDialog(JFrame parent) {
+ // init frame
+ m_frame = new JFrame(Constants.Name + " - Crash Report");
+ final Container pane = m_frame.getContentPane();
+ pane.setLayout(new BorderLayout());
+
+ JLabel label = new JLabel(Constants.Name + " has crashed! =(");
+ label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ pane.add(label, BorderLayout.NORTH);
+
+ // report panel
+ m_text = new JTextArea();
+ m_text.setTabSize(2);
+ pane.add(new JScrollPane(m_text), BorderLayout.CENTER);
+
+ // buttons panel
+ JPanel buttonsPanel = new JPanel();
+ FlowLayout buttonsLayout = new FlowLayout();
+ buttonsLayout.setAlignment(FlowLayout.RIGHT);
+ buttonsPanel.setLayout(buttonsLayout);
+ buttonsPanel.add(GuiTricks.unboldLabel(new JLabel("If you choose exit, you will lose any unsaved work.")));
+ JButton ignoreButton = new JButton("Ignore");
+ ignoreButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ // close (hide) the dialog
+ m_frame.setVisible(false);
+ }
+ });
+ buttonsPanel.add(ignoreButton);
+ JButton exitButton = new JButton("Exit");
+ exitButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ // exit enigma
+ System.exit(1);
+ }
+ });
+ buttonsPanel.add(exitButton);
+ pane.add(buttonsPanel, BorderLayout.SOUTH);
+
+ // show the frame
+ m_frame.setSize(600, 400);
+ m_frame.setLocationRelativeTo(parent);
+ m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+ }
+
+ public static void init(JFrame parent) {
+ m_instance = new CrashDialog(parent);
+ }
+
+ public static void show(Throwable ex) {
+ // get the error report
+ StringWriter buf = new StringWriter();
+ ex.printStackTrace(new PrintWriter(buf));
+ String report = buf.toString();
+
+ // show it!
+ m_instance.m_text.setText(report);
+ m_instance.m_frame.doLayout();
+ m_instance.m_frame.setVisible(true);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java b/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java
new file mode 100644
index 0000000..57210a8
--- /dev/null
+++ b/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Color;
+
+public class DeobfuscatedHighlightPainter extends BoxHighlightPainter {
+
+ public DeobfuscatedHighlightPainter() {
+ // green ish
+ super(new Color(220, 255, 220), new Color(80, 160, 80));
+ }
+}
diff --git a/src/cuchaz/enigma/gui/Gui.java b/src/cuchaz/enigma/gui/Gui.java
new file mode 100644
index 0000000..f9192d3
--- /dev/null
+++ b/src/cuchaz/enigma/gui/Gui.java
@@ -0,0 +1,1122 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyAdapter;
+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.io.File;
+import java.io.IOException;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+import java.util.jar.JarFile;
+
+import javax.swing.BorderFactory;
+import javax.swing.JEditorPane;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.JTree;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.WindowConstants;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Highlighter;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+
+import com.google.common.collect.Lists;
+
+import cuchaz.enigma.Constants;
+import cuchaz.enigma.ExceptionIgnorer;
+import cuchaz.enigma.analysis.BehaviorReferenceTreeNode;
+import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
+import cuchaz.enigma.analysis.ClassInheritanceTreeNode;
+import cuchaz.enigma.analysis.EntryReference;
+import cuchaz.enigma.analysis.FieldReferenceTreeNode;
+import cuchaz.enigma.analysis.MethodImplementationsTreeNode;
+import cuchaz.enigma.analysis.MethodInheritanceTreeNode;
+import cuchaz.enigma.analysis.ReferenceTreeNode;
+import cuchaz.enigma.analysis.Token;
+import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener;
+import cuchaz.enigma.mapping.ArgumentEntry;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.ConstructorEntry;
+import cuchaz.enigma.mapping.Entry;
+import cuchaz.enigma.mapping.FieldEntry;
+import cuchaz.enigma.mapping.IllegalNameException;
+import cuchaz.enigma.mapping.MappingParseException;
+import cuchaz.enigma.mapping.MethodEntry;
+import cuchaz.enigma.mapping.Signature;
+import de.sciss.syntaxpane.DefaultSyntaxKit;
+
+public class Gui {
+
+ private GuiController m_controller;
+
+ // controls
+ private JFrame m_frame;
+ private ClassSelector m_obfClasses;
+ private ClassSelector m_deobfClasses;
+ private JEditorPane m_editor;
+ private JPanel m_classesPanel;
+ private JSplitPane m_splitClasses;
+ private JPanel m_infoPanel;
+ private ObfuscatedHighlightPainter m_obfuscatedHighlightPainter;
+ private DeobfuscatedHighlightPainter m_deobfuscatedHighlightPainter;
+ private OtherHighlightPainter m_otherHighlightPainter;
+ private SelectionHighlightPainter m_selectionHighlightPainter;
+ private JTree m_inheritanceTree;
+ private JTree m_implementationsTree;
+ private JTree m_callsTree;
+ private JList m_tokens;
+ private JTabbedPane m_tabs;
+
+ // dynamic menu items
+ private JMenuItem m_closeJarMenu;
+ private JMenuItem m_openMappingsMenu;
+ private JMenuItem m_saveMappingsMenu;
+ private JMenuItem m_saveMappingsAsMenu;
+ private JMenuItem m_closeMappingsMenu;
+ private JMenuItem m_renameMenu;
+ private JMenuItem m_showInheritanceMenu;
+ private JMenuItem m_openEntryMenu;
+ private JMenuItem m_openPreviousMenu;
+ private JMenuItem m_showCallsMenu;
+ private JMenuItem m_showImplementationsMenu;
+ private JMenuItem m_toggleMappingMenu;
+ private JMenuItem m_exportSourceMenu;
+ private JMenuItem m_exportJarMenu;
+
+ // state
+ private EntryReference m_reference;
+ private JFileChooser m_jarFileChooser;
+ private JFileChooser m_mappingsFileChooser;
+ private JFileChooser m_exportSourceFileChooser;
+ private JFileChooser m_exportJarFileChooser;
+
+ public Gui() {
+
+ // init frame
+ m_frame = new JFrame(Constants.Name);
+ final Container pane = m_frame.getContentPane();
+ pane.setLayout(new BorderLayout());
+
+ if (Boolean.parseBoolean(System.getProperty("enigma.catchExceptions", "true"))) {
+ // install a global exception handler to the event thread
+ CrashDialog.init(m_frame);
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable t) {
+ t.printStackTrace(System.err);
+ if (!ExceptionIgnorer.shouldIgnore(t)) {
+ CrashDialog.show(t);
+ }
+ }
+ });
+ }
+
+ m_controller = new GuiController(this);
+
+ // init file choosers
+ m_jarFileChooser = new JFileChooser();
+ m_mappingsFileChooser = new JFileChooser();
+ m_exportSourceFileChooser = new JFileChooser();
+ m_exportSourceFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ m_exportJarFileChooser = new JFileChooser();
+
+ // init obfuscated classes list
+ m_obfClasses = new ClassSelector(ClassSelector.ObfuscatedClassEntryComparator);
+ m_obfClasses.setListener(new ClassSelectionListener() {
+ @Override
+ public void onSelectClass(ClassEntry classEntry) {
+ navigateTo(classEntry);
+ }
+ });
+ JScrollPane obfScroller = new JScrollPane(m_obfClasses);
+ JPanel obfPanel = new JPanel();
+ obfPanel.setLayout(new BorderLayout());
+ obfPanel.add(new JLabel("Obfuscated Classes"), BorderLayout.NORTH);
+ obfPanel.add(obfScroller, BorderLayout.CENTER);
+
+ // init deobfuscated classes list
+ m_deobfClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator);
+ m_deobfClasses.setListener(new ClassSelectionListener() {
+ @Override
+ public void onSelectClass(ClassEntry classEntry) {
+ navigateTo(classEntry);
+ }
+ });
+ JScrollPane deobfScroller = new JScrollPane(m_deobfClasses);
+ JPanel deobfPanel = new JPanel();
+ deobfPanel.setLayout(new BorderLayout());
+ deobfPanel.add(new JLabel("De-obfuscated Classes"), BorderLayout.NORTH);
+ deobfPanel.add(deobfScroller, BorderLayout.CENTER);
+
+ // set up classes panel (don't add the splitter yet)
+ m_splitClasses = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, obfPanel, deobfPanel);
+ m_splitClasses.setResizeWeight(0.3);
+ m_classesPanel = new JPanel();
+ m_classesPanel.setLayout(new BorderLayout());
+ m_classesPanel.setPreferredSize(new Dimension(250, 0));
+
+ // init info panel
+ m_infoPanel = new JPanel();
+ m_infoPanel.setLayout(new GridLayout(4, 1, 0, 0));
+ m_infoPanel.setPreferredSize(new Dimension(0, 100));
+ m_infoPanel.setBorder(BorderFactory.createTitledBorder("Identifier Info"));
+ clearReference();
+
+ // init editor
+ DefaultSyntaxKit.initKit();
+ m_obfuscatedHighlightPainter = new ObfuscatedHighlightPainter();
+ m_deobfuscatedHighlightPainter = new DeobfuscatedHighlightPainter();
+ m_otherHighlightPainter = new OtherHighlightPainter();
+ m_selectionHighlightPainter = new SelectionHighlightPainter();
+ m_editor = new JEditorPane();
+ m_editor.setEditable(false);
+ m_editor.setCaret(new BrowserCaret());
+ JScrollPane sourceScroller = new JScrollPane(m_editor);
+ m_editor.setContentType("text/java");
+ m_editor.addCaretListener(new CaretListener() {
+ @Override
+ public void caretUpdate(CaretEvent event) {
+ onCaretMove(event.getDot());
+ }
+ });
+ m_editor.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.VK_R:
+ m_renameMenu.doClick();
+ break;
+
+ case KeyEvent.VK_I:
+ m_showInheritanceMenu.doClick();
+ break;
+
+ case KeyEvent.VK_M:
+ m_showImplementationsMenu.doClick();
+ break;
+
+ case KeyEvent.VK_N:
+ m_openEntryMenu.doClick();
+ break;
+
+ case KeyEvent.VK_P:
+ m_openPreviousMenu.doClick();
+ break;
+
+ case KeyEvent.VK_C:
+ m_showCallsMenu.doClick();
+ break;
+
+ case KeyEvent.VK_T:
+ m_toggleMappingMenu.doClick();
+ break;
+ }
+ }
+ });
+
+ // turn off token highlighting (it's wrong most of the time anyway...)
+ DefaultSyntaxKit kit = (DefaultSyntaxKit)m_editor.getEditorKit();
+ kit.toggleComponent(m_editor, "de.sciss.syntaxpane.components.TokenMarker");
+
+ // init editor popup menu
+ JPopupMenu popupMenu = new JPopupMenu();
+ m_editor.setComponentPopupMenu(popupMenu);
+ {
+ JMenuItem menu = new JMenuItem("Rename");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ startRename();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_renameMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Show Inheritance");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ showInheritance();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_showInheritanceMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Show Implementations");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ showImplementations();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_showImplementationsMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Show Calls");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ showCalls();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_showCallsMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Go to Declaration");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ navigateTo(m_reference.entry);
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_openEntryMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Go to previous");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ m_controller.openPreviousReference();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_openPreviousMenu = menu;
+ }
+ {
+ JMenuItem menu = new JMenuItem("Mark as deobfuscated");
+ menu.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ toggleMapping();
+ }
+ });
+ menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0));
+ menu.setEnabled(false);
+ popupMenu.add(menu);
+ m_toggleMappingMenu = menu;
+ }
+
+ // init inheritance panel
+ m_inheritanceTree = new JTree();
+ m_inheritanceTree.setModel(null);
+ m_inheritanceTree.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ if (event.getClickCount() == 2) {
+ // get the selected node
+ TreePath path = m_inheritanceTree.getSelectionPath();
+ if (path == null) {
+ return;
+ }
+
+ Object node = path.getLastPathComponent();
+ if (node instanceof ClassInheritanceTreeNode) {
+ ClassInheritanceTreeNode classNode = (ClassInheritanceTreeNode)node;
+ navigateTo(new ClassEntry(classNode.getObfClassName()));
+ } else if (node instanceof MethodInheritanceTreeNode) {
+ MethodInheritanceTreeNode methodNode = (MethodInheritanceTreeNode)node;
+ if (methodNode.isImplemented()) {
+ navigateTo(methodNode.getMethodEntry());
+ }
+ }
+ }
+ }
+ });
+ JPanel inheritancePanel = new JPanel();
+ inheritancePanel.setLayout(new BorderLayout());
+ inheritancePanel.add(new JScrollPane(m_inheritanceTree));
+
+ // init implementations panel
+ m_implementationsTree = new JTree();
+ m_implementationsTree.setModel(null);
+ m_implementationsTree.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ if (event.getClickCount() == 2) {
+ // get the selected node
+ TreePath path = m_implementationsTree.getSelectionPath();
+ if (path == null) {
+ return;
+ }
+
+ Object node = path.getLastPathComponent();
+ if (node instanceof ClassImplementationsTreeNode) {
+ ClassImplementationsTreeNode classNode = (ClassImplementationsTreeNode)node;
+ navigateTo(classNode.getClassEntry());
+ } else if (node instanceof MethodImplementationsTreeNode) {
+ MethodImplementationsTreeNode methodNode = (MethodImplementationsTreeNode)node;
+ navigateTo(methodNode.getMethodEntry());
+ }
+ }
+ }
+ });
+ JPanel implementationsPanel = new JPanel();
+ implementationsPanel.setLayout(new BorderLayout());
+ implementationsPanel.add(new JScrollPane(m_implementationsTree));
+
+ // init call panel
+ m_callsTree = new JTree();
+ m_callsTree.setModel(null);
+ m_callsTree.addMouseListener(new MouseAdapter() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ if (event.getClickCount() == 2) {
+ // get the selected node
+ TreePath path = m_callsTree.getSelectionPath();
+ if (path == null) {
+ return;
+ }
+
+ Object node = path.getLastPathComponent();
+ if (node instanceof ReferenceTreeNode) {
+ ReferenceTreeNode referenceNode = ((ReferenceTreeNode)node);
+ if (referenceNode.getReference() != null) {
+ navigateTo(referenceNode.getReference());
+ } else {
+ navigateTo(referenceNode.getEntry());
+ }
+ }
+ }
+ }
+ });
+ m_tokens = new JList();
+ m_tokens.setCellRenderer(new TokenListCellRenderer(m_controller));
+ m_tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ m_tokens.setLayoutOrientation(JList.VERTICAL);
+ m_tokens.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ if (event.getClickCount() == 2) {
+ Token selected = m_tokens.getSelectedValue();
+ if (selected != null) {
+ showToken(selected);
+ }
+ }
+ }
+ });
+ m_tokens.setPreferredSize(new Dimension(0, 200));
+ m_tokens.setMinimumSize(new Dimension(0, 200));
+ JSplitPane callPanel = new JSplitPane(
+ JSplitPane.VERTICAL_SPLIT,
+ true,
+ new JScrollPane(m_callsTree),
+ new JScrollPane(m_tokens)
+ );
+ callPanel.setResizeWeight(1); // let the top side take all the slack
+ callPanel.resetToPreferredSizes();
+
+ // layout controls
+ JPanel centerPanel = new JPanel();
+ centerPanel.setLayout(new BorderLayout());
+ centerPanel.add(m_infoPanel, BorderLayout.NORTH);
+ centerPanel.add(sourceScroller, BorderLayout.CENTER);
+ m_tabs = new JTabbedPane();
+ m_tabs.setPreferredSize(new Dimension(250, 0));
+ m_tabs.addTab("Inheritance", inheritancePanel);
+ m_tabs.addTab("Implementations", implementationsPanel);
+ m_tabs.addTab("Call Graph", callPanel);
+ JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, m_tabs);
+ splitRight.setResizeWeight(1); // let the left side take all the slack
+ splitRight.resetToPreferredSizes();
+ JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, m_classesPanel, splitRight);
+ splitCenter.setResizeWeight(0); // let the right side take all the slack
+ pane.add(splitCenter, BorderLayout.CENTER);
+
+ // init menus
+ JMenuBar menuBar = new JMenuBar();
+ m_frame.setJMenuBar(menuBar);
+ {
+ JMenu menu = new JMenu("File");
+ menuBar.add(menu);
+ {
+ JMenuItem item = new JMenuItem("Open Jar...");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_jarFileChooser.showOpenDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ // load the jar in a separate thread
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ m_controller.openJar(new JarFile(m_jarFileChooser.getSelectedFile()));
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ }.start();
+ }
+ }
+ });
+ }
+ {
+ JMenuItem item = new JMenuItem("Close Jar");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ m_controller.closeJar();
+ }
+ });
+ m_closeJarMenu = item;
+ }
+ menu.addSeparator();
+ {
+ JMenuItem item = new JMenuItem("Open Mappings...");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_mappingsFileChooser.showOpenDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ try {
+ m_controller.openMappings(m_mappingsFileChooser.getSelectedFile());
+ } catch (IOException ex) {
+ throw new Error(ex);
+ } catch (MappingParseException ex) {
+ JOptionPane.showMessageDialog(m_frame, ex.getMessage());
+ }
+ }
+ }
+ });
+ m_openMappingsMenu = item;
+ }
+ {
+ JMenuItem item = new JMenuItem("Save Mappings");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ try {
+ m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile());
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ });
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));
+ m_saveMappingsMenu = item;
+ }
+ {
+ JMenuItem item = new JMenuItem("Save Mappings As...");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_mappingsFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ try {
+ m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile());
+ m_saveMappingsMenu.setEnabled(true);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ }
+ });
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
+ m_saveMappingsAsMenu = item;
+ }
+ {
+ JMenuItem item = new JMenuItem("Close Mappings");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ m_controller.closeMappings();
+ }
+ });
+ m_closeMappingsMenu = item;
+ }
+ menu.addSeparator();
+ {
+ JMenuItem item = new JMenuItem("Export Source...");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_exportSourceFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ m_controller.exportSource(m_exportSourceFileChooser.getSelectedFile());
+ }
+ }
+ });
+ m_exportSourceMenu = item;
+ }
+ {
+ JMenuItem item = new JMenuItem("Export Jar...");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ if (m_exportJarFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ m_controller.exportJar(m_exportJarFileChooser.getSelectedFile());
+ }
+ }
+ });
+ m_exportJarMenu = item;
+ }
+ menu.addSeparator();
+ {
+ JMenuItem item = new JMenuItem("Exit");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ close();
+ }
+ });
+ }
+ }
+ {
+ JMenu menu = new JMenu("Help");
+ menuBar.add(menu);
+ {
+ JMenuItem item = new JMenuItem("About");
+ menu.add(item);
+ item.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ AboutDialog.show(m_frame);
+ }
+ });
+ }
+ }
+
+ // init state
+ onCloseJar();
+
+ m_frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent event) {
+ close();
+ }
+ });
+
+ // show the frame
+ pane.doLayout();
+ m_frame.setSize(1024, 576);
+ m_frame.setMinimumSize(new Dimension(640, 480));
+ m_frame.setVisible(true);
+ m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+ }
+
+ public JFrame getFrame() {
+ return m_frame;
+ }
+
+ public GuiController getController() {
+ return m_controller;
+ }
+
+ public void onStartOpenJar() {
+ m_classesPanel.removeAll();
+ JPanel panel = new JPanel();
+ panel.setLayout(new FlowLayout());
+ panel.add(new JLabel("Loading..."));
+ m_classesPanel.add(panel);
+ redraw();
+ }
+
+ public void onFinishOpenJar(String jarName) {
+ // update gui
+ m_frame.setTitle(Constants.Name + " - " + jarName);
+ m_classesPanel.removeAll();
+ m_classesPanel.add(m_splitClasses);
+ setSource(null);
+
+ // update menu
+ m_closeJarMenu.setEnabled(true);
+ m_openMappingsMenu.setEnabled(true);
+ m_saveMappingsMenu.setEnabled(false);
+ m_saveMappingsAsMenu.setEnabled(true);
+ m_closeMappingsMenu.setEnabled(true);
+ m_exportSourceMenu.setEnabled(true);
+ m_exportJarMenu.setEnabled(true);
+
+ redraw();
+ }
+
+ public void onCloseJar() {
+ // update gui
+ m_frame.setTitle(Constants.Name);
+ setObfClasses(null);
+ setDeobfClasses(null);
+ setSource(null);
+ m_classesPanel.removeAll();
+
+ // update menu
+ m_closeJarMenu.setEnabled(false);
+ m_openMappingsMenu.setEnabled(false);
+ m_saveMappingsMenu.setEnabled(false);
+ m_saveMappingsAsMenu.setEnabled(false);
+ m_closeMappingsMenu.setEnabled(false);
+ m_exportSourceMenu.setEnabled(false);
+ m_exportJarMenu.setEnabled(false);
+
+ redraw();
+ }
+
+ public void setObfClasses(Collection obfClasses) {
+ m_obfClasses.setClasses(obfClasses);
+ }
+
+ public void setDeobfClasses(Collection deobfClasses) {
+ m_deobfClasses.setClasses(deobfClasses);
+ }
+
+ public void setMappingsFile(File file) {
+ m_mappingsFileChooser.setSelectedFile(file);
+ m_saveMappingsMenu.setEnabled(file != null);
+ }
+
+ public void setSource(String source) {
+ m_editor.getHighlighter().removeAllHighlights();
+ m_editor.setText(source);
+ }
+
+ public void showToken(final Token token) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token cannot be null!");
+ }
+ CodeReader.navigateToToken(m_editor, token, m_selectionHighlightPainter);
+ redraw();
+ }
+
+ public void showTokens(Collection tokens) {
+ Vector sortedTokens = new Vector(tokens);
+ Collections.sort(sortedTokens);
+ if (sortedTokens.size() > 1) {
+ // sort the tokens and update the tokens panel
+ m_tokens.setListData(sortedTokens);
+ m_tokens.setSelectedIndex(0);
+ } else {
+ m_tokens.setListData(new Vector());
+ }
+
+ // show the first token
+ showToken(sortedTokens.get(0));
+ }
+
+ public void setHighlightedTokens(Iterable obfuscatedTokens, Iterable deobfuscatedTokens, Iterable otherTokens) {
+
+ // remove any old highlighters
+ m_editor.getHighlighter().removeAllHighlights();
+
+ // color things based on the index
+ if (obfuscatedTokens != null) {
+ setHighlightedTokens(obfuscatedTokens, m_obfuscatedHighlightPainter);
+ }
+ if (deobfuscatedTokens != null) {
+ setHighlightedTokens(deobfuscatedTokens, m_deobfuscatedHighlightPainter);
+ }
+ if (otherTokens != null) {
+ setHighlightedTokens(otherTokens, m_otherHighlightPainter);
+ }
+
+ redraw();
+ }
+
+ private void setHighlightedTokens(Iterable tokens, Highlighter.HighlightPainter painter) {
+ for (Token token : tokens) {
+ try {
+ m_editor.getHighlighter().addHighlight(token.start, token.end, painter);
+ } catch (BadLocationException ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+ }
+
+ private void clearReference() {
+ m_infoPanel.removeAll();
+ JLabel label = new JLabel("No identifier selected");
+ GuiTricks.unboldLabel(label);
+ label.setHorizontalAlignment(JLabel.CENTER);
+ m_infoPanel.add(label);
+
+ redraw();
+ }
+
+ private void showReference(EntryReference reference) {
+ if (reference == null) {
+ clearReference();
+ return;
+ }
+
+ m_reference = reference;
+
+ m_infoPanel.removeAll();
+ if (reference.entry instanceof ClassEntry) {
+ showClassEntry((ClassEntry)m_reference.entry);
+ } else if (m_reference.entry instanceof FieldEntry) {
+ showFieldEntry((FieldEntry)m_reference.entry);
+ } else if (m_reference.entry instanceof MethodEntry) {
+ showMethodEntry((MethodEntry)m_reference.entry);
+ } else if (m_reference.entry instanceof ConstructorEntry) {
+ showConstructorEntry((ConstructorEntry)m_reference.entry);
+ } else if (m_reference.entry instanceof ArgumentEntry) {
+ showArgumentEntry((ArgumentEntry)m_reference.entry);
+ } else {
+ throw new Error("Unknown entry type: " + m_reference.entry.getClass().getName());
+ }
+
+ redraw();
+ }
+
+ private void showClassEntry(ClassEntry entry) {
+ addNameValue(m_infoPanel, "Class", entry.getName());
+ }
+
+ private void showFieldEntry(FieldEntry entry) {
+ addNameValue(m_infoPanel, "Field", entry.getName());
+ addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName());
+ addNameValue(m_infoPanel, "Type", entry.getType().toString());
+ }
+
+ private void showMethodEntry(MethodEntry entry) {
+ addNameValue(m_infoPanel, "Method", entry.getName());
+ addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName());
+ addNameValue(m_infoPanel, "Signature", entry.getSignature().toString());
+ }
+
+ private void showConstructorEntry(ConstructorEntry entry) {
+ addNameValue(m_infoPanel, "Constructor", entry.getClassEntry().getName());
+ if (!entry.isStatic()) {
+ addNameValue(m_infoPanel, "Signature", entry.getSignature().toString());
+ }
+ }
+
+ private void showArgumentEntry(ArgumentEntry entry) {
+ addNameValue(m_infoPanel, "Argument", entry.getName());
+ addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName());
+ addNameValue(m_infoPanel, "Method", entry.getBehaviorEntry().getName());
+ addNameValue(m_infoPanel, "Index", Integer.toString(entry.getIndex()));
+ }
+
+ private void addNameValue(JPanel container, String name, String value) {
+ JPanel panel = new JPanel();
+ panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0));
+ container.add(panel);
+
+ JLabel label = new JLabel(name + ":", JLabel.RIGHT);
+ label.setPreferredSize(new Dimension(100, label.getPreferredSize().height));
+ panel.add(label);
+
+ panel.add(GuiTricks.unboldLabel(new JLabel(value, JLabel.LEFT)));
+ }
+
+ private void onCaretMove(int pos) {
+
+ Token token = m_controller.getToken(pos);
+ boolean isToken = token != null;
+
+ m_reference = m_controller.getDeobfReference(token);
+ boolean isClassEntry = isToken && m_reference.entry instanceof ClassEntry;
+ boolean isFieldEntry = isToken && m_reference.entry instanceof FieldEntry;
+ boolean isMethodEntry = isToken && m_reference.entry instanceof MethodEntry;
+ boolean isConstructorEntry = isToken && m_reference.entry instanceof ConstructorEntry;
+ boolean isInJar = isToken && m_controller.entryIsInJar(m_reference.entry);
+ boolean isRenameable = isToken && m_controller.referenceIsRenameable(m_reference);
+
+ if (isToken) {
+ showReference(m_reference);
+ } else {
+ clearReference();
+ }
+
+ m_renameMenu.setEnabled(isRenameable && isToken);
+ m_showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry);
+ m_showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry);
+ m_showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry);
+ m_openEntryMenu.setEnabled(isInJar && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry));
+ m_openPreviousMenu.setEnabled(m_controller.hasPreviousLocation());
+ m_toggleMappingMenu.setEnabled(isRenameable && isToken);
+
+ if (isToken && m_controller.entryHasDeobfuscatedName(m_reference.entry)) {
+ m_toggleMappingMenu.setText("Reset to obfuscated");
+ } else {
+ m_toggleMappingMenu.setText("Mark as deobfuscated");
+ }
+ }
+
+ private void navigateTo(Entry entry) {
+ if (!m_controller.entryIsInJar(entry)) {
+ // entry is not in the jar. Ignore it
+ return;
+ }
+ if (m_reference != null) {
+ m_controller.savePreviousReference(m_reference);
+ }
+ m_controller.openDeclaration(entry);
+ }
+
+ private void navigateTo(EntryReference reference) {
+ if (!m_controller.entryIsInJar(reference.getLocationClassEntry())) {
+ // reference is not in the jar. Ignore it
+ return;
+ }
+ if (m_reference != null) {
+ m_controller.savePreviousReference(m_reference);
+ }
+ m_controller.openReference(reference);
+ }
+
+ private void startRename() {
+
+ // init the text box
+ final JTextField text = new JTextField();
+ text.setText(m_reference.getNamableName());
+ text.setPreferredSize(new Dimension(360, text.getPreferredSize().height));
+ text.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.VK_ENTER:
+ finishRename(text, true);
+ break;
+
+ case KeyEvent.VK_ESCAPE:
+ finishRename(text, false);
+ break;
+ }
+ }
+ });
+
+ // find the label with the name and replace it with the text box
+ JPanel panel = (JPanel)m_infoPanel.getComponent(0);
+ panel.remove(panel.getComponentCount() - 1);
+ panel.add(text);
+ text.grabFocus();
+ text.selectAll();
+
+ redraw();
+ }
+
+ private void finishRename(JTextField text, boolean saveName) {
+ String newName = text.getText();
+ if (saveName && newName != null && newName.length() > 0) {
+ try {
+ m_controller.rename(m_reference, newName);
+ } catch (IllegalNameException ex) {
+ text.setBorder(BorderFactory.createLineBorder(Color.red, 1));
+ text.setToolTipText(ex.getReason());
+ GuiTricks.showToolTipNow(text);
+ }
+ return;
+ }
+
+ // abort the rename
+ JPanel panel = (JPanel)m_infoPanel.getComponent(0);
+ panel.remove(panel.getComponentCount() - 1);
+ panel.add(GuiTricks.unboldLabel(new JLabel(m_reference.getNamableName(), JLabel.LEFT)));
+
+ m_editor.grabFocus();
+
+ redraw();
+ }
+
+ private void showInheritance() {
+
+ if (m_reference == null) {
+ return;
+ }
+
+ m_inheritanceTree.setModel(null);
+
+ if (m_reference.entry instanceof ClassEntry) {
+ // get the class inheritance
+ ClassInheritanceTreeNode classNode = m_controller.getClassInheritance((ClassEntry)m_reference.entry);
+
+ // show the tree at the root
+ TreePath path = getPathToRoot(classNode);
+ m_inheritanceTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0)));
+ m_inheritanceTree.expandPath(path);
+ m_inheritanceTree.setSelectionRow(m_inheritanceTree.getRowForPath(path));
+ } else if (m_reference.entry instanceof MethodEntry) {
+ // get the method inheritance
+ MethodInheritanceTreeNode classNode = m_controller.getMethodInheritance((MethodEntry)m_reference.entry);
+
+ // show the tree at the root
+ TreePath path = getPathToRoot(classNode);
+ m_inheritanceTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0)));
+ m_inheritanceTree.expandPath(path);
+ m_inheritanceTree.setSelectionRow(m_inheritanceTree.getRowForPath(path));
+ }
+
+ m_tabs.setSelectedIndex(0);
+ redraw();
+ }
+
+ private void showImplementations() {
+
+ if (m_reference == null) {
+ return;
+ }
+
+ m_implementationsTree.setModel(null);
+
+ if (m_reference.entry instanceof ClassEntry) {
+ // get the class implementations
+ ClassImplementationsTreeNode node = m_controller.getClassImplementations((ClassEntry)m_reference.entry);
+ if (node != null) {
+ // show the tree at the root
+ TreePath path = getPathToRoot(node);
+ m_implementationsTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0)));
+ m_implementationsTree.expandPath(path);
+ m_implementationsTree.setSelectionRow(m_implementationsTree.getRowForPath(path));
+ }
+ } else if (m_reference.entry instanceof MethodEntry) {
+ // get the method implementations
+ MethodImplementationsTreeNode node = m_controller.getMethodImplementations((MethodEntry)m_reference.entry);
+ if (node != null) {
+ // show the tree at the root
+ TreePath path = getPathToRoot(node);
+ m_implementationsTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0)));
+ m_implementationsTree.expandPath(path);
+ m_implementationsTree.setSelectionRow(m_implementationsTree.getRowForPath(path));
+ }
+ }
+
+ m_tabs.setSelectedIndex(1);
+ redraw();
+ }
+
+ private void showCalls() {
+
+ if (m_reference == null) {
+ return;
+ }
+
+ if (m_reference.entry instanceof ClassEntry) {
+ // look for calls to the default constructor
+ // TODO: get a list of all the constructors and find calls to all of them
+ BehaviorReferenceTreeNode node = m_controller.getMethodReferences(new ConstructorEntry((ClassEntry)m_reference.entry, new Signature("()V")));
+ m_callsTree.setModel(new DefaultTreeModel(node));
+ } else if (m_reference.entry instanceof FieldEntry) {
+ FieldReferenceTreeNode node = m_controller.getFieldReferences((FieldEntry)m_reference.entry);
+ m_callsTree.setModel(new DefaultTreeModel(node));
+ } else if (m_reference.entry instanceof MethodEntry) {
+ BehaviorReferenceTreeNode node = m_controller.getMethodReferences((MethodEntry)m_reference.entry);
+ m_callsTree.setModel(new DefaultTreeModel(node));
+ } else if (m_reference.entry instanceof ConstructorEntry) {
+ BehaviorReferenceTreeNode node = m_controller.getMethodReferences((ConstructorEntry)m_reference.entry);
+ m_callsTree.setModel(new DefaultTreeModel(node));
+ }
+
+ m_tabs.setSelectedIndex(2);
+ redraw();
+ }
+
+ private void toggleMapping() {
+ if (m_controller.entryHasDeobfuscatedName(m_reference.entry)) {
+ m_controller.removeMapping(m_reference);
+ } else {
+ m_controller.markAsDeobfuscated(m_reference);
+ }
+ }
+
+ private TreePath getPathToRoot(TreeNode node) {
+ List nodes = Lists.newArrayList();
+ TreeNode n = node;
+ do {
+ nodes.add(n);
+ n = n.getParent();
+ } while (n != null);
+ Collections.reverse(nodes);
+ return new TreePath(nodes.toArray());
+ }
+
+ private void close() {
+ if (!m_controller.isDirty()) {
+ // everything is saved, we can exit safely
+ m_frame.dispose();
+ } else {
+ // ask to save before closing
+ String[] options = { "Save and exit", "Discard changes", "Cancel" };
+ int response = JOptionPane.showOptionDialog(m_frame, "Your mappings have not been saved yet. Do you want to save?", "Save your changes?", JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.QUESTION_MESSAGE, null, options, options[2]);
+ switch (response) {
+ case JOptionPane.YES_OPTION: // save and exit
+ if (m_mappingsFileChooser.getSelectedFile() != null || m_mappingsFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) {
+ try {
+ m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile());
+ m_frame.dispose();
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ break;
+
+ case JOptionPane.NO_OPTION:
+ // don't save, exit
+ m_frame.dispose();
+ break;
+
+ // cancel means do nothing
+ }
+ }
+ }
+
+ private void redraw() {
+ m_frame.validate();
+ m_frame.repaint();
+ }
+}
diff --git a/src/cuchaz/enigma/gui/GuiController.java b/src/cuchaz/enigma/gui/GuiController.java
new file mode 100644
index 0000000..6690622
--- /dev/null
+++ b/src/cuchaz/enigma/gui/GuiController.java
@@ -0,0 +1,358 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.List;
+import java.util.jar.JarFile;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Queues;
+import com.strobel.decompiler.languages.java.ast.CompilationUnit;
+
+import cuchaz.enigma.Deobfuscator;
+import cuchaz.enigma.Deobfuscator.ProgressListener;
+import cuchaz.enigma.analysis.BehaviorReferenceTreeNode;
+import cuchaz.enigma.analysis.ClassImplementationsTreeNode;
+import cuchaz.enigma.analysis.ClassInheritanceTreeNode;
+import cuchaz.enigma.analysis.EntryReference;
+import cuchaz.enigma.analysis.FieldReferenceTreeNode;
+import cuchaz.enigma.analysis.MethodImplementationsTreeNode;
+import cuchaz.enigma.analysis.MethodInheritanceTreeNode;
+import cuchaz.enigma.analysis.SourceIndex;
+import cuchaz.enigma.analysis.Token;
+import cuchaz.enigma.gui.ProgressDialog.ProgressRunnable;
+import cuchaz.enigma.mapping.BehaviorEntry;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.Entry;
+import cuchaz.enigma.mapping.FieldEntry;
+import cuchaz.enigma.mapping.MappingParseException;
+import cuchaz.enigma.mapping.MappingsReader;
+import cuchaz.enigma.mapping.MappingsWriter;
+import cuchaz.enigma.mapping.MethodEntry;
+import cuchaz.enigma.mapping.TranslationDirection;
+
+public class GuiController {
+
+ private Deobfuscator m_deobfuscator;
+ private Gui m_gui;
+ private SourceIndex m_index;
+ private ClassEntry m_currentObfClass;
+ private boolean m_isDirty;
+ private Deque> m_referenceStack;
+
+ public GuiController(Gui gui) {
+ m_gui = gui;
+ m_deobfuscator = null;
+ m_index = null;
+ m_currentObfClass = null;
+ m_isDirty = false;
+ m_referenceStack = Queues.newArrayDeque();
+ }
+
+ public boolean isDirty() {
+ return m_isDirty;
+ }
+
+ public void openJar(final JarFile jar) throws IOException {
+ m_gui.onStartOpenJar();
+ m_deobfuscator = new Deobfuscator(jar);
+ m_gui.onFinishOpenJar(m_deobfuscator.getJarName());
+ refreshClasses();
+ }
+
+ public void closeJar() {
+ m_deobfuscator = null;
+ m_gui.onCloseJar();
+ }
+
+ public void openMappings(File file) throws IOException, MappingParseException {
+ FileReader in = new FileReader(file);
+ m_deobfuscator.setMappings(new MappingsReader().read(in));
+ in.close();
+ m_isDirty = false;
+ m_gui.setMappingsFile(file);
+ refreshClasses();
+ refreshCurrentClass();
+ }
+
+ public void saveMappings(File file) throws IOException {
+ FileWriter out = new FileWriter(file);
+ new MappingsWriter().write(out, m_deobfuscator.getMappings());
+ out.close();
+ m_isDirty = false;
+ }
+
+ public void closeMappings() {
+ m_deobfuscator.setMappings(null);
+ m_gui.setMappingsFile(null);
+ refreshClasses();
+ refreshCurrentClass();
+ }
+
+ public void exportSource(final File dirOut) {
+ ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable() {
+ @Override
+ public void run(ProgressListener progress) throws Exception {
+ m_deobfuscator.writeSources(dirOut, progress);
+ }
+ });
+ }
+
+ public void exportJar(final File fileOut) {
+ ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable() {
+ @Override
+ public void run(ProgressListener progress) {
+ m_deobfuscator.writeJar(fileOut, progress);
+ }
+ });
+ }
+
+ public Token getToken(int pos) {
+ if (m_index == null) {
+ return null;
+ }
+ return m_index.getReferenceToken(pos);
+ }
+
+ public EntryReference getDeobfReference(Token token) {
+ if (m_index == null) {
+ return null;
+ }
+ return m_index.getDeobfReference(token);
+ }
+
+ public ReadableToken getReadableToken(Token token) {
+ if (m_index == null) {
+ return null;
+ }
+ return new ReadableToken(
+ m_index.getLineNumber(token.start),
+ m_index.getColumnNumber(token.start),
+ m_index.getColumnNumber(token.end)
+ );
+ }
+
+ public boolean entryHasDeobfuscatedName(Entry deobfEntry) {
+ return m_deobfuscator.hasDeobfuscatedName(m_deobfuscator.obfuscateEntry(deobfEntry));
+ }
+
+ public boolean entryIsInJar(Entry deobfEntry) {
+ return m_deobfuscator.isObfuscatedIdentifier(m_deobfuscator.obfuscateEntry(deobfEntry));
+ }
+
+ public boolean referenceIsRenameable(EntryReference deobfReference) {
+ return m_deobfuscator.isRenameable(m_deobfuscator.obfuscateReference(deobfReference));
+ }
+
+ public ClassInheritanceTreeNode getClassInheritance(ClassEntry deobfClassEntry) {
+ ClassEntry obfClassEntry = m_deobfuscator.obfuscateEntry(deobfClassEntry);
+ ClassInheritanceTreeNode rootNode = m_deobfuscator.getJarIndex().getClassInheritance(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfClassEntry
+ );
+ return ClassInheritanceTreeNode.findNode(rootNode, obfClassEntry);
+ }
+
+ public ClassImplementationsTreeNode getClassImplementations(ClassEntry deobfClassEntry) {
+ ClassEntry obfClassEntry = m_deobfuscator.obfuscateEntry(deobfClassEntry);
+ return m_deobfuscator.getJarIndex().getClassImplementations(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfClassEntry
+ );
+ }
+
+ public MethodInheritanceTreeNode getMethodInheritance(MethodEntry deobfMethodEntry) {
+ MethodEntry obfMethodEntry = m_deobfuscator.obfuscateEntry(deobfMethodEntry);
+ MethodInheritanceTreeNode rootNode = m_deobfuscator.getJarIndex().getMethodInheritance(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfMethodEntry
+ );
+ return MethodInheritanceTreeNode.findNode(rootNode, obfMethodEntry);
+ }
+
+ public MethodImplementationsTreeNode getMethodImplementations(MethodEntry deobfMethodEntry) {
+ MethodEntry obfMethodEntry = m_deobfuscator.obfuscateEntry(deobfMethodEntry);
+ List rootNodes = m_deobfuscator.getJarIndex().getMethodImplementations(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfMethodEntry
+ );
+ if (rootNodes.isEmpty()) {
+ return null;
+ }
+ if (rootNodes.size() > 1) {
+ System.err.println("WARNING: Method " + deobfMethodEntry + " implements multiple interfaces. Only showing first one.");
+ }
+ return MethodImplementationsTreeNode.findNode(rootNodes.get(0), obfMethodEntry);
+ }
+
+ public FieldReferenceTreeNode getFieldReferences(FieldEntry deobfFieldEntry) {
+ FieldEntry obfFieldEntry = m_deobfuscator.obfuscateEntry(deobfFieldEntry);
+ FieldReferenceTreeNode rootNode = new FieldReferenceTreeNode(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfFieldEntry
+ );
+ rootNode.load(m_deobfuscator.getJarIndex(), true);
+ return rootNode;
+ }
+
+ public BehaviorReferenceTreeNode getMethodReferences(BehaviorEntry deobfBehaviorEntry) {
+ BehaviorEntry obfBehaviorEntry = m_deobfuscator.obfuscateEntry(deobfBehaviorEntry);
+ BehaviorReferenceTreeNode rootNode = new BehaviorReferenceTreeNode(
+ m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating),
+ obfBehaviorEntry
+ );
+ rootNode.load(m_deobfuscator.getJarIndex(), true);
+ return rootNode;
+ }
+
+ public void rename(EntryReference deobfReference, String newName) {
+ EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference);
+ m_deobfuscator.rename(obfReference.getNameableEntry(), newName);
+ m_isDirty = true;
+ refreshClasses();
+ refreshCurrentClass(obfReference);
+ }
+
+ public void removeMapping(EntryReference deobfReference) {
+ EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference);
+ m_deobfuscator.removeMapping(obfReference.getNameableEntry());
+ m_isDirty = true;
+ refreshClasses();
+ refreshCurrentClass(obfReference);
+ }
+
+ public void markAsDeobfuscated(EntryReference deobfReference) {
+ EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference);
+ m_deobfuscator.markAsDeobfuscated(obfReference.getNameableEntry());
+ m_isDirty = true;
+ refreshClasses();
+ refreshCurrentClass(obfReference);
+ }
+
+ public void openDeclaration(Entry deobfEntry) {
+ if (deobfEntry == null) {
+ throw new IllegalArgumentException("Entry cannot be null!");
+ }
+ openReference(new EntryReference(deobfEntry, deobfEntry.getName()));
+ }
+
+ public void openReference(EntryReference deobfReference) {
+ if (deobfReference == null) {
+ throw new IllegalArgumentException("Reference cannot be null!");
+ }
+
+ // get the reference target class
+ EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference);
+ ClassEntry obfClassEntry = obfReference.getLocationClassEntry().getOutermostClassEntry();
+ if (!m_deobfuscator.isObfuscatedIdentifier(obfClassEntry)) {
+ throw new IllegalArgumentException("Obfuscated class " + obfClassEntry + " was not found in the jar!");
+ }
+ if (m_currentObfClass == null || !m_currentObfClass.equals(obfClassEntry)) {
+ // deobfuscate the class, then navigate to the reference
+ m_currentObfClass = obfClassEntry;
+ deobfuscate(m_currentObfClass, obfReference);
+ } else {
+ showReference(obfReference);
+ }
+ }
+
+ private void showReference(EntryReference obfReference) {
+ EntryReference deobfReference = m_deobfuscator.deobfuscateReference(obfReference);
+ Collection tokens = m_index.getReferenceTokens(deobfReference);
+ if (tokens.isEmpty()) {
+ // DEBUG
+ System.err.println(String.format("WARNING: no tokens found for %s in %s", deobfReference, m_currentObfClass));
+ } else {
+ m_gui.showTokens(tokens);
+ }
+ }
+
+ public void savePreviousReference(EntryReference deobfReference) {
+ m_referenceStack.push(m_deobfuscator.obfuscateReference(deobfReference));
+ }
+
+ public void openPreviousReference() {
+ if (hasPreviousLocation()) {
+ openReference(m_deobfuscator.deobfuscateReference(m_referenceStack.pop()));
+ }
+ }
+
+ public boolean hasPreviousLocation() {
+ return !m_referenceStack.isEmpty();
+ }
+
+ private void refreshClasses() {
+ List obfClasses = Lists.newArrayList();
+ List deobfClasses = Lists.newArrayList();
+ m_deobfuscator.getSeparatedClasses(obfClasses, deobfClasses);
+ m_gui.setObfClasses(obfClasses);
+ m_gui.setDeobfClasses(deobfClasses);
+ }
+
+ private void refreshCurrentClass() {
+ refreshCurrentClass(null);
+ }
+
+ private void refreshCurrentClass(EntryReference obfReference) {
+ if (m_currentObfClass != null) {
+ deobfuscate(m_currentObfClass, obfReference);
+ }
+ }
+
+ private void deobfuscate(final ClassEntry classEntry, final EntryReference obfReference) {
+
+ m_gui.setSource("(deobfuscating...)");
+
+ // run the deobfuscator in a separate thread so we don't block the GUI event queue
+ new Thread() {
+ @Override
+ public void run() {
+ // decompile,deobfuscate the bytecode
+ CompilationUnit sourceTree = m_deobfuscator.getSourceTree(classEntry.getClassName());
+ if (sourceTree == null) {
+ // decompilation of this class is not supported
+ m_gui.setSource("Unable to find class: " + classEntry);
+ return;
+ }
+ String source = m_deobfuscator.getSource(sourceTree);
+ m_index = m_deobfuscator.getSourceIndex(sourceTree, source);
+ m_gui.setSource(m_index.getSource());
+ if (obfReference != null) {
+ showReference(obfReference);
+ }
+
+ // set the highlighted tokens
+ List obfuscatedTokens = Lists.newArrayList();
+ List deobfuscatedTokens = Lists.newArrayList();
+ List otherTokens = Lists.newArrayList();
+ for (Token token : m_index.referenceTokens()) {
+ EntryReference reference = m_index.getDeobfReference(token);
+ if (referenceIsRenameable(reference)) {
+ if (entryHasDeobfuscatedName(reference.getNameableEntry())) {
+ deobfuscatedTokens.add(token);
+ } else {
+ obfuscatedTokens.add(token);
+ }
+ } else {
+ otherTokens.add(token);
+ }
+ }
+ m_gui.setHighlightedTokens(obfuscatedTokens, deobfuscatedTokens, otherTokens);
+ }
+ }.start();
+ }
+}
diff --git a/src/cuchaz/enigma/gui/GuiTricks.java b/src/cuchaz/enigma/gui/GuiTricks.java
new file mode 100644
index 0000000..5dc3ffb
--- /dev/null
+++ b/src/cuchaz/enigma/gui/GuiTricks.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Font;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.ToolTipManager;
+
+public class GuiTricks {
+
+ public static JLabel unboldLabel(JLabel label) {
+ Font font = label.getFont();
+ label.setFont(font.deriveFont(font.getStyle() & ~Font.BOLD));
+ return label;
+ }
+
+ public static void showToolTipNow(JComponent component) {
+ // HACKHACK: trick the tooltip manager into showing the tooltip right now
+ ToolTipManager manager = ToolTipManager.sharedInstance();
+ int oldDelay = manager.getInitialDelay();
+ manager.setInitialDelay(0);
+ manager.mouseMoved(new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 0, 0, 0, false));
+ manager.setInitialDelay(oldDelay);
+ }
+
+ public static void deactivateButton(JButton button) {
+ button.setEnabled(false);
+ button.setText("");
+ for (ActionListener listener : Arrays.asList(button.getActionListeners())) {
+ button.removeActionListener(listener);
+ }
+ }
+
+ public static void activateButton(JButton button, String text, ActionListener newListener) {
+ button.setText(text);
+ button.setEnabled(true);
+ for (ActionListener listener : Arrays.asList(button.getActionListeners())) {
+ button.removeActionListener(listener);
+ }
+ button.addActionListener(newListener);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/MemberMatchingGui.java b/src/cuchaz/enigma/gui/MemberMatchingGui.java
new file mode 100644
index 0000000..150eaad
--- /dev/null
+++ b/src/cuchaz/enigma/gui/MemberMatchingGui.java
@@ -0,0 +1,499 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.WindowConstants;
+import javax.swing.text.Highlighter.HighlightPainter;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import cuchaz.enigma.Constants;
+import cuchaz.enigma.Deobfuscator;
+import cuchaz.enigma.analysis.EntryReference;
+import cuchaz.enigma.analysis.SourceIndex;
+import cuchaz.enigma.analysis.Token;
+import cuchaz.enigma.convert.ClassMatches;
+import cuchaz.enigma.convert.MemberMatches;
+import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.Entry;
+import de.sciss.syntaxpane.DefaultSyntaxKit;
+
+
+public class MemberMatchingGui {
+
+ private static enum SourceType {
+ Matched {
+
+ @Override
+ public Collection getObfSourceClasses(MemberMatches matches) {
+ return matches.getSourceClassesWithoutUnmatchedEntries();
+ }
+ },
+ Unmatched {
+
+ @Override
+ public Collection getObfSourceClasses(MemberMatches matches) {
+ return matches.getSourceClassesWithUnmatchedEntries();
+ }
+ };
+
+ public JRadioButton newRadio(ActionListener listener, ButtonGroup group) {
+ JRadioButton button = new JRadioButton(name(), this == getDefault());
+ button.setActionCommand(name());
+ button.addActionListener(listener);
+ group.add(button);
+ return button;
+ }
+
+ public abstract Collection getObfSourceClasses(MemberMatches matches);
+
+ public static SourceType getDefault() {
+ return values()[0];
+ }
+ }
+
+ public static interface SaveListener {
+ public void save(MemberMatches matches);
+ }
+
+ // controls
+ private JFrame m_frame;
+ private Map m_sourceTypeButtons;
+ private ClassSelector m_sourceClasses;
+ private CodeReader m_sourceReader;
+ private CodeReader m_destReader;
+ private JButton m_matchButton;
+ private JButton m_unmatchableButton;
+ private JLabel m_sourceLabel;
+ private JLabel m_destLabel;
+ private HighlightPainter m_unmatchedHighlightPainter;
+ private HighlightPainter m_matchedHighlightPainter;
+
+ private ClassMatches m_classMatches;
+ private MemberMatches m_memberMatches;
+ private Deobfuscator m_sourceDeobfuscator;
+ private Deobfuscator m_destDeobfuscator;
+ private SaveListener m_saveListener;
+ private SourceType m_sourceType;
+ private ClassEntry m_obfSourceClass;
+ private ClassEntry m_obfDestClass;
+ private T m_obfSourceEntry;
+ private T m_obfDestEntry;
+
+ public MemberMatchingGui(ClassMatches classMatches, MemberMatches fieldMatches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
+
+ m_classMatches = classMatches;
+ m_memberMatches = fieldMatches;
+ m_sourceDeobfuscator = sourceDeobfuscator;
+ m_destDeobfuscator = destDeobfuscator;
+
+ // init frame
+ m_frame = new JFrame(Constants.Name + " - Member Matcher");
+ final Container pane = m_frame.getContentPane();
+ pane.setLayout(new BorderLayout());
+
+ // init classes side
+ JPanel classesPanel = new JPanel();
+ classesPanel.setLayout(new BoxLayout(classesPanel, BoxLayout.PAGE_AXIS));
+ classesPanel.setPreferredSize(new Dimension(200, 0));
+ pane.add(classesPanel, BorderLayout.WEST);
+ classesPanel.add(new JLabel("Classes"));
+
+ // init source type radios
+ JPanel sourceTypePanel = new JPanel();
+ classesPanel.add(sourceTypePanel);
+ sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS));
+ ActionListener sourceTypeListener = new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ setSourceType(SourceType.valueOf(event.getActionCommand()));
+ }
+ };
+ ButtonGroup sourceTypeButtons = new ButtonGroup();
+ m_sourceTypeButtons = Maps.newHashMap();
+ for (SourceType sourceType : SourceType.values()) {
+ JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
+ m_sourceTypeButtons.put(sourceType, button);
+ sourceTypePanel.add(button);
+ }
+
+ m_sourceClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator);
+ m_sourceClasses.setListener(new ClassSelectionListener() {
+ @Override
+ public void onSelectClass(ClassEntry classEntry) {
+ setSourceClass(classEntry);
+ }
+ });
+ JScrollPane sourceScroller = new JScrollPane(m_sourceClasses);
+ classesPanel.add(sourceScroller);
+
+ // init readers
+ DefaultSyntaxKit.initKit();
+ m_sourceReader = new CodeReader();
+ m_sourceReader.setSelectionListener(new CodeReader.SelectionListener() {
+ @Override
+ public void onSelect(EntryReference reference) {
+ if (reference != null) {
+ onSelectSource(reference.entry);
+ } else {
+ onSelectSource(null);
+ }
+ }
+ });
+ m_destReader = new CodeReader();
+ m_destReader.setSelectionListener(new CodeReader.SelectionListener() {
+ @Override
+ public void onSelect(EntryReference reference) {
+ if (reference != null) {
+ onSelectDest(reference.entry);
+ } else {
+ onSelectDest(null);
+ }
+ }
+ });
+
+ // add key bindings
+ KeyAdapter keyListener = new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.VK_M:
+ m_matchButton.doClick();
+ break;
+ }
+ }
+ };
+ m_sourceReader.addKeyListener(keyListener);
+ m_destReader.addKeyListener(keyListener);
+
+ // init all the splits
+ JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_sourceReader), new JScrollPane(m_destReader));
+ splitRight.setResizeWeight(0.5); // resize 50:50
+ JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, classesPanel, splitRight);
+ splitLeft.setResizeWeight(0); // let the right side take all the slack
+ pane.add(splitLeft, BorderLayout.CENTER);
+ splitLeft.resetToPreferredSizes();
+
+ // init bottom panel
+ JPanel bottomPanel = new JPanel();
+ bottomPanel.setLayout(new FlowLayout());
+ pane.add(bottomPanel, BorderLayout.SOUTH);
+
+ m_matchButton = new JButton();
+ m_unmatchableButton = new JButton();
+
+ m_sourceLabel = new JLabel();
+ bottomPanel.add(m_sourceLabel);
+ bottomPanel.add(m_matchButton);
+ bottomPanel.add(m_unmatchableButton);
+ m_destLabel = new JLabel();
+ bottomPanel.add(m_destLabel);
+
+ // show the frame
+ pane.doLayout();
+ m_frame.setSize(1024, 576);
+ m_frame.setMinimumSize(new Dimension(640, 480));
+ m_frame.setVisible(true);
+ m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+
+ m_unmatchedHighlightPainter = new ObfuscatedHighlightPainter();
+ m_matchedHighlightPainter = new DeobfuscatedHighlightPainter();
+
+ // init state
+ m_saveListener = null;
+ m_obfSourceClass = null;
+ m_obfDestClass = null;
+ m_obfSourceEntry = null;
+ m_obfDestEntry = null;
+ setSourceType(SourceType.getDefault());
+ updateButtons();
+ }
+
+ protected void setSourceType(SourceType val) {
+ m_sourceType = val;
+ updateSourceClasses();
+ }
+
+ public void setSaveListener(SaveListener val) {
+ m_saveListener = val;
+ }
+
+ private void updateSourceClasses() {
+
+ String selectedPackage = m_sourceClasses.getSelectedPackage();
+
+ List deobfClassEntries = Lists.newArrayList();
+ for (ClassEntry entry : m_sourceType.getObfSourceClasses(m_memberMatches)) {
+ deobfClassEntries.add(m_sourceDeobfuscator.deobfuscateEntry(entry));
+ }
+ m_sourceClasses.setClasses(deobfClassEntries);
+
+ if (selectedPackage != null) {
+ m_sourceClasses.expandPackage(selectedPackage);
+ }
+
+ for (SourceType sourceType : SourceType.values()) {
+ m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
+ sourceType.name(), sourceType.getObfSourceClasses(m_memberMatches).size()
+ ));
+ }
+ }
+
+ protected void setSourceClass(ClassEntry sourceClass) {
+
+ m_obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass);
+ m_obfDestClass = m_classMatches.getUniqueMatches().get(m_obfSourceClass);
+ if (m_obfDestClass == null) {
+ throw new Error("No matching dest class for source class: " + m_obfSourceClass);
+ }
+
+ m_sourceReader.decompileClass(m_obfSourceClass, m_sourceDeobfuscator, false, new Runnable() {
+ @Override
+ public void run() {
+ updateSourceHighlights();
+ }
+ });
+ m_destReader.decompileClass(m_obfDestClass, m_destDeobfuscator, false, new Runnable() {
+ @Override
+ public void run() {
+ updateDestHighlights();
+ }
+ });
+ }
+
+ protected void updateSourceHighlights() {
+ highlightEntries(m_sourceReader, m_sourceDeobfuscator, m_memberMatches.matches().keySet(), m_memberMatches.getUnmatchedSourceEntries());
+ }
+
+ protected void updateDestHighlights() {
+ highlightEntries(m_destReader, m_destDeobfuscator, m_memberMatches.matches().values(), m_memberMatches.getUnmatchedDestEntries());
+ }
+
+ private void highlightEntries(CodeReader reader, Deobfuscator deobfuscator, Collection obfMatchedEntries, Collection obfUnmatchedEntries) {
+ reader.clearHighlights();
+ SourceIndex index = reader.getSourceIndex();
+
+ // matched fields
+ for (T obfT : obfMatchedEntries) {
+ T deobfT = deobfuscator.deobfuscateEntry(obfT);
+ Token token = index.getDeclarationToken(deobfT);
+ if (token != null) {
+ reader.setHighlightedToken(token, m_matchedHighlightPainter);
+ }
+ }
+
+ // unmatched fields
+ for (T obfT : obfUnmatchedEntries) {
+ T deobfT = deobfuscator.deobfuscateEntry(obfT);
+ Token token = index.getDeclarationToken(deobfT);
+ if (token != null) {
+ reader.setHighlightedToken(token, m_unmatchedHighlightPainter);
+ }
+ }
+ }
+
+ private boolean isSelectionMatched() {
+ return m_obfSourceEntry != null && m_obfDestEntry != null
+ && m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry);
+ }
+
+ protected void onSelectSource(Entry source) {
+
+ // start with no selection
+ if (isSelectionMatched()) {
+ setDest(null);
+ }
+ setSource(null);
+
+ // then look for a valid source selection
+ if (source != null) {
+
+ // this looks really scary, but it's actually ok
+ // Deobfuscator.obfuscateEntry can handle all implementations of Entry
+ // and MemberMatches.hasSource() will only pass entries that actually match T
+ @SuppressWarnings("unchecked")
+ T sourceEntry = (T)source;
+
+ T obfSourceEntry = m_sourceDeobfuscator.obfuscateEntry(sourceEntry);
+ if (m_memberMatches.hasSource(obfSourceEntry)) {
+ setSource(obfSourceEntry);
+
+ // look for a matched dest too
+ T obfDestEntry = m_memberMatches.matches().get(obfSourceEntry);
+ if (obfDestEntry != null) {
+ setDest(obfDestEntry);
+ }
+ }
+ }
+
+ updateButtons();
+ }
+
+ protected void onSelectDest(Entry dest) {
+
+ // start with no selection
+ if (isSelectionMatched()) {
+ setSource(null);
+ }
+ setDest(null);
+
+ // then look for a valid dest selection
+ if (dest != null) {
+
+ // this looks really scary, but it's actually ok
+ // Deobfuscator.obfuscateEntry can handle all implementations of Entry
+ // and MemberMatches.hasSource() will only pass entries that actually match T
+ @SuppressWarnings("unchecked")
+ T destEntry = (T)dest;
+
+ T obfDestEntry = m_destDeobfuscator.obfuscateEntry(destEntry);
+ if (m_memberMatches.hasDest(obfDestEntry)) {
+ setDest(obfDestEntry);
+
+ // look for a matched source too
+ T obfSourceEntry = m_memberMatches.matches().inverse().get(obfDestEntry);
+ if (obfSourceEntry != null) {
+ setSource(obfSourceEntry);
+ }
+ }
+ }
+
+ updateButtons();
+ }
+
+ private void setSource(T obfEntry) {
+ if (obfEntry == null) {
+ m_obfSourceEntry = obfEntry;
+ m_sourceLabel.setText("");
+ } else {
+ m_obfSourceEntry = obfEntry;
+ m_sourceLabel.setText(getEntryLabel(obfEntry, m_sourceDeobfuscator));
+ }
+ }
+
+ private void setDest(T obfEntry) {
+ if (obfEntry == null) {
+ m_obfDestEntry = obfEntry;
+ m_destLabel.setText("");
+ } else {
+ m_obfDestEntry = obfEntry;
+ m_destLabel.setText(getEntryLabel(obfEntry, m_destDeobfuscator));
+ }
+ }
+
+ private String getEntryLabel(T obfEntry, Deobfuscator deobfuscator) {
+ // show obfuscated and deobfuscated names, but no types/signatures
+ T deobfEntry = deobfuscator.deobfuscateEntry(obfEntry);
+ return String.format("%s (%s)", deobfEntry.getName(), obfEntry.getName());
+ }
+
+ private void updateButtons() {
+
+ GuiTricks.deactivateButton(m_matchButton);
+ GuiTricks.deactivateButton(m_unmatchableButton);
+
+ if (m_obfSourceEntry != null && m_obfDestEntry != null) {
+ if (m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry)) {
+ GuiTricks.activateButton(m_matchButton, "Unmatch", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ unmatch();
+ }
+ });
+ } else if (!m_memberMatches.isMatchedSourceEntry(m_obfSourceEntry) && !m_memberMatches.isMatchedDestEntry(m_obfDestEntry)) {
+ GuiTricks.activateButton(m_matchButton, "Match", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ match();
+ }
+ });
+ }
+ } else if (m_obfSourceEntry != null) {
+ GuiTricks.activateButton(m_unmatchableButton, "Set Unmatchable", new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ unmatchable();
+ }
+ });
+ }
+ }
+
+ protected void match() {
+
+ // update the field matches
+ m_memberMatches.makeMatch(m_obfSourceEntry, m_obfDestEntry);
+ save();
+
+ // update the ui
+ onSelectSource(null);
+ onSelectDest(null);
+ updateSourceHighlights();
+ updateDestHighlights();
+ updateSourceClasses();
+ }
+
+ protected void unmatch() {
+
+ // update the field matches
+ m_memberMatches.unmakeMatch(m_obfSourceEntry, m_obfDestEntry);
+ save();
+
+ // update the ui
+ onSelectSource(null);
+ onSelectDest(null);
+ updateSourceHighlights();
+ updateDestHighlights();
+ updateSourceClasses();
+ }
+
+ protected void unmatchable() {
+
+ // update the field matches
+ m_memberMatches.makeSourceUnmatchable(m_obfSourceEntry);
+ save();
+
+ // update the ui
+ onSelectSource(null);
+ onSelectDest(null);
+ updateSourceHighlights();
+ updateDestHighlights();
+ updateSourceClasses();
+ }
+
+ private void save() {
+ if (m_saveListener != null) {
+ m_saveListener.save(m_memberMatches);
+ }
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java b/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java
new file mode 100644
index 0000000..4c3714a
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Color;
+
+public class ObfuscatedHighlightPainter extends BoxHighlightPainter {
+
+ public ObfuscatedHighlightPainter() {
+ // red ish
+ super(new Color(255, 220, 220), new Color(160, 80, 80));
+ }
+}
diff --git a/src/cuchaz/enigma/gui/OtherHighlightPainter.java b/src/cuchaz/enigma/gui/OtherHighlightPainter.java
new file mode 100644
index 0000000..8d3fbe8
--- /dev/null
+++ b/src/cuchaz/enigma/gui/OtherHighlightPainter.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Color;
+
+public class OtherHighlightPainter extends BoxHighlightPainter {
+
+ public OtherHighlightPainter() {
+ // grey
+ super(null, new Color(180, 180, 180));
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ProgressDialog.java b/src/cuchaz/enigma/gui/ProgressDialog.java
new file mode 100644
index 0000000..1c20f10
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ProgressDialog.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.WindowConstants;
+
+import cuchaz.enigma.Constants;
+import cuchaz.enigma.Deobfuscator.ProgressListener;
+
+public class ProgressDialog implements ProgressListener, AutoCloseable {
+
+ private JFrame m_frame;
+ private JLabel m_title;
+ private JLabel m_text;
+ private JProgressBar m_progress;
+
+ public ProgressDialog(JFrame parent) {
+
+ // init frame
+ m_frame = new JFrame(Constants.Name + " - Operation in progress");
+ final Container pane = m_frame.getContentPane();
+ FlowLayout layout = new FlowLayout();
+ layout.setAlignment(FlowLayout.LEFT);
+ pane.setLayout(layout);
+
+ m_title = new JLabel();
+ pane.add(m_title);
+
+ // set up the progress bar
+ JPanel panel = new JPanel();
+ pane.add(panel);
+ panel.setLayout(new BorderLayout());
+ m_text = GuiTricks.unboldLabel(new JLabel());
+ m_progress = new JProgressBar();
+ m_text.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
+ panel.add(m_text, BorderLayout.NORTH);
+ panel.add(m_progress, BorderLayout.CENTER);
+ panel.setPreferredSize(new Dimension(360, 50));
+
+ // show the frame
+ pane.doLayout();
+ m_frame.setSize(400, 120);
+ m_frame.setResizable(false);
+ m_frame.setLocationRelativeTo(parent);
+ m_frame.setVisible(true);
+ m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+ }
+
+ public void close() {
+ m_frame.dispose();
+ }
+
+ @Override
+ public void init(int totalWork, String title) {
+ m_title.setText(title);
+ m_progress.setMinimum(0);
+ m_progress.setMaximum(totalWork);
+ m_progress.setValue(0);
+ }
+
+ @Override
+ public void onProgress(int numDone, String message) {
+ m_text.setText(message);
+ m_progress.setValue(numDone);
+
+ // update the frame
+ m_frame.validate();
+ m_frame.repaint();
+ }
+
+ public static interface ProgressRunnable {
+ void run(ProgressListener listener) throws Exception;
+ }
+
+ public static void runInThread(final JFrame parent, final ProgressRunnable runnable) {
+ new Thread() {
+ @Override
+ public void run() {
+ try (ProgressDialog progress = new ProgressDialog(parent)) {
+ runnable.run(progress);
+ } catch (Exception ex) {
+ throw new Error(ex);
+ }
+ }
+ }.start();
+ }
+}
diff --git a/src/cuchaz/enigma/gui/ReadableToken.java b/src/cuchaz/enigma/gui/ReadableToken.java
new file mode 100644
index 0000000..0741af3
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ReadableToken.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+public class ReadableToken {
+
+ public int line;
+ public int startColumn;
+ public int endColumn;
+
+ public ReadableToken(int line, int startColumn, int endColumn) {
+ this.line = line;
+ this.startColumn = startColumn;
+ this.endColumn = endColumn;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("line ");
+ buf.append(line);
+ buf.append(" columns ");
+ buf.append(startColumn);
+ buf.append("-");
+ buf.append(endColumn);
+ return buf.toString();
+ }
+}
diff --git a/src/cuchaz/enigma/gui/RenameListener.java b/src/cuchaz/enigma/gui/RenameListener.java
new file mode 100644
index 0000000..8b515bb
--- /dev/null
+++ b/src/cuchaz/enigma/gui/RenameListener.java
@@ -0,0 +1,17 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import cuchaz.enigma.mapping.Entry;
+
+public interface RenameListener {
+ void rename(Entry obfEntry, String newName);
+}
diff --git a/src/cuchaz/enigma/gui/ScoredClassEntry.java b/src/cuchaz/enigma/gui/ScoredClassEntry.java
new file mode 100644
index 0000000..6070452
--- /dev/null
+++ b/src/cuchaz/enigma/gui/ScoredClassEntry.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import cuchaz.enigma.mapping.ClassEntry;
+
+
+public class ScoredClassEntry extends ClassEntry {
+
+ private static final long serialVersionUID = -8798725308554217105L;
+
+ private float m_score;
+
+ public ScoredClassEntry(ClassEntry other, float score) {
+ super(other);
+ m_score = score;
+ }
+
+ public float getScore() {
+ return m_score;
+ }
+}
diff --git a/src/cuchaz/enigma/gui/SelectionHighlightPainter.java b/src/cuchaz/enigma/gui/SelectionHighlightPainter.java
new file mode 100644
index 0000000..4165da4
--- /dev/null
+++ b/src/cuchaz/enigma/gui/SelectionHighlightPainter.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Shape;
+
+import javax.swing.text.Highlighter;
+import javax.swing.text.JTextComponent;
+
+public class SelectionHighlightPainter implements Highlighter.HighlightPainter {
+
+ @Override
+ public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) {
+ // draw a thick border
+ Graphics2D g2d = (Graphics2D)g;
+ Rectangle bounds = BoxHighlightPainter.getBounds(text, start, end);
+ g2d.setColor(Color.black);
+ g2d.setStroke(new BasicStroke(2.0f));
+ g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
+ }
+}
diff --git a/src/cuchaz/enigma/gui/TokenListCellRenderer.java b/src/cuchaz/enigma/gui/TokenListCellRenderer.java
new file mode 100644
index 0000000..e4f7c87
--- /dev/null
+++ b/src/cuchaz/enigma/gui/TokenListCellRenderer.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Jeff Martin.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser General Public
+ * License v3.0 which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma.gui;
+
+import java.awt.Component;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+
+import cuchaz.enigma.analysis.Token;
+
+public class TokenListCellRenderer implements ListCellRenderer {
+
+ private GuiController m_controller;
+ private DefaultListCellRenderer m_defaultRenderer;
+
+ public TokenListCellRenderer(GuiController controller) {
+ m_controller = controller;
+ m_defaultRenderer = new DefaultListCellRenderer();
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends Token> list, Token token, int index, boolean isSelected, boolean hasFocus) {
+ JLabel label = (JLabel)m_defaultRenderer.getListCellRendererComponent(list, token, index, isSelected, hasFocus);
+ label.setText(m_controller.getReadableToken(token).toString());
+ return label;
+ }
+}
--
cgit v1.2.3