/*******************************************************************************
* 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 com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
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.*;
import javax.swing.text.Highlighter.HighlightPainter;
import cuchaz.enigma.Constants;
import cuchaz.enigma.Deobfuscator;
import cuchaz.enigma.analysis.SourceIndex;
import cuchaz.enigma.analysis.Token;
import cuchaz.enigma.convert.ClassMatches;
import cuchaz.enigma.convert.MemberMatches;
import cuchaz.enigma.gui.highlight.DeobfuscatedHighlightPainter;
import cuchaz.enigma.gui.highlight.ObfuscatedHighlightPainter;
import cuchaz.enigma.mapping.ClassEntry;
import cuchaz.enigma.mapping.Entry;
import de.sciss.syntaxpane.DefaultSyntaxKit;
public class MemberMatchingGui {
private 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 interface SaveListener {
void save(MemberMatches matches);
}
// controls
private JFrame frame;
private Map sourceTypeButtons;
private ClassSelector sourceClasses;
private CodeReader sourceReader;
private CodeReader destReader;
private JButton matchButton;
private JButton unmatchableButton;
private JLabel sourceLabel;
private JLabel destLabel;
private HighlightPainter unmatchedHighlightPainter;
private HighlightPainter matchedHighlightPainter;
private ClassMatches classMatches;
private MemberMatches memberMatches;
private Deobfuscator sourceDeobfuscator;
private Deobfuscator destDeobfuscator;
private SaveListener saveListener;
private SourceType sourceType;
private ClassEntry obfSourceClass;
private ClassEntry obfDestClass;
private T obfSourceEntry;
private T obfDestEntry;
public MemberMatchingGui(ClassMatches classMatches, MemberMatches fieldMatches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
this.classMatches = classMatches;
memberMatches = fieldMatches;
this.sourceDeobfuscator = sourceDeobfuscator;
this.destDeobfuscator = destDeobfuscator;
// init frame
frame = new JFrame(Constants.NAME + " - Member Matcher");
final Container pane = 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 = event -> setSourceType(SourceType.valueOf(event.getActionCommand()));
ButtonGroup sourceTypeButtons = new ButtonGroup();
this.sourceTypeButtons = Maps.newHashMap();
for (SourceType sourceType : SourceType.values()) {
JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
this.sourceTypeButtons.put(sourceType, button);
sourceTypePanel.add(button);
}
sourceClasses = new ClassSelector(null, ClassSelector.DEOBF_CLASS_COMPARATOR, false);
sourceClasses.setSelectionListener(this::setSourceClass);
JScrollPane sourceScroller = new JScrollPane(sourceClasses);
classesPanel.add(sourceScroller);
// init readers
DefaultSyntaxKit.initKit();
sourceReader = new CodeReader();
sourceReader.setSelectionListener(reference ->
{
if (reference != null) {
onSelectSource(reference.entry);
} else {
onSelectSource(null);
}
});
destReader = new CodeReader();
destReader.setSelectionListener(reference ->
{
if (reference != null) {
onSelectDest(reference.entry);
} else {
onSelectDest(null);
}
});
// add key bindings
KeyAdapter keyListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.VK_M)
matchButton.doClick();
}
};
sourceReader.addKeyListener(keyListener);
destReader.addKeyListener(keyListener);
// init all the splits
JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(sourceReader), new JScrollPane(
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);
matchButton = new JButton();
unmatchableButton = new JButton();
sourceLabel = new JLabel();
bottomPanel.add(sourceLabel);
bottomPanel.add(matchButton);
bottomPanel.add(unmatchableButton);
destLabel = new JLabel();
bottomPanel.add(destLabel);
// show the frame
pane.doLayout();
frame.setSize(1024, 576);
frame.setMinimumSize(new Dimension(640, 480));
frame.setVisible(true);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
unmatchedHighlightPainter = new ObfuscatedHighlightPainter();
matchedHighlightPainter = new DeobfuscatedHighlightPainter();
// init state
saveListener = null;
obfSourceClass = null;
obfDestClass = null;
obfSourceEntry = null;
obfDestEntry = null;
setSourceType(SourceType.getDefault());
updateButtons();
}
protected void setSourceType(SourceType val) {
sourceType = val;
updateSourceClasses();
}
public void setSaveListener(SaveListener val) {
saveListener = val;
}
private void updateSourceClasses() {
String selectedPackage = sourceClasses.getSelectedPackage();
List deobfClassEntries = Lists.newArrayList();
for (ClassEntry entry : sourceType.getObfSourceClasses(memberMatches)) {
deobfClassEntries.add(sourceDeobfuscator.deobfuscateEntry(entry));
}
sourceClasses.setClasses(deobfClassEntries);
if (selectedPackage != null) {
sourceClasses.expandPackage(selectedPackage);
}
for (SourceType sourceType : SourceType.values()) {
sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
sourceType.name(), sourceType.getObfSourceClasses(memberMatches).size()
));
}
}
protected void setSourceClass(ClassEntry sourceClass) {
obfSourceClass = sourceDeobfuscator.obfuscateEntry(sourceClass);
obfDestClass = classMatches.getUniqueMatches().get(obfSourceClass);
if (obfDestClass == null) {
throw new Error("No matching dest class for source class: " + obfSourceClass);
}
sourceReader.decompileClass(obfSourceClass, sourceDeobfuscator, false, this::updateSourceHighlights);
destReader.decompileClass(obfDestClass, destDeobfuscator, false, this::updateDestHighlights);
}
protected void updateSourceHighlights() {
highlightEntries(sourceReader, sourceDeobfuscator, memberMatches.matches().keySet(), memberMatches.getUnmatchedSourceEntries());
}
protected void updateDestHighlights() {
highlightEntries(destReader, destDeobfuscator, memberMatches.matches().values(), memberMatches.getUnmatchedDestEntries());
}
private void highlightEntries(CodeReader reader, Deobfuscator deobfuscator, Collection obfMatchedEntries, Collection obfUnmatchedEntries) {
reader.clearHighlights();
// matched fields
updateHighlighted(obfMatchedEntries, deobfuscator, reader, matchedHighlightPainter);
// unmatched fields
updateHighlighted(obfUnmatchedEntries, deobfuscator, reader, unmatchedHighlightPainter);
}
private void updateHighlighted(Collection entries, Deobfuscator deobfuscator, CodeReader reader, HighlightPainter painter)
{
SourceIndex index = reader.getSourceIndex();
for (T obfT : entries) {
T deobfT = deobfuscator.deobfuscateEntry(obfT);
Token token = index.getDeclarationToken(deobfT);
if (token != null) {
reader.setHighlightedToken(token, painter);
}
}
}
private boolean isSelectionMatched() {
return obfSourceEntry != null && obfDestEntry != null
&& memberMatches.isMatched(obfSourceEntry, 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 = sourceDeobfuscator.obfuscateEntry(sourceEntry);
if (memberMatches.hasSource(obfSourceEntry)) {
setSource(obfSourceEntry);
// look for a matched dest too
T obfDestEntry = 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 = destDeobfuscator.obfuscateEntry(destEntry);
if (memberMatches.hasDest(obfDestEntry)) {
setDest(obfDestEntry);
// look for a matched source too
T obfSourceEntry = memberMatches.matches().inverse().get(obfDestEntry);
if (obfSourceEntry != null) {
setSource(obfSourceEntry);
}
}
}
updateButtons();
}
private void setSource(T obfEntry) {
if (obfEntry == null) {
obfSourceEntry = null;
sourceLabel.setText("");
} else {
obfSourceEntry = obfEntry;
sourceLabel.setText(getEntryLabel(obfEntry, sourceDeobfuscator));
}
}
private void setDest(T obfEntry) {
if (obfEntry == null) {
obfDestEntry = null;
destLabel.setText("");
} else {
obfDestEntry = obfEntry;
destLabel.setText(getEntryLabel(obfEntry, 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(matchButton);
GuiTricks.deactivateButton(unmatchableButton);
if (obfSourceEntry != null && obfDestEntry != null) {
if (memberMatches.isMatched(obfSourceEntry, obfDestEntry))
GuiTricks.activateButton(matchButton, "Unmatch", event -> unmatch());
else if (!memberMatches.isMatchedSourceEntry(obfSourceEntry) && !memberMatches.isMatchedDestEntry(
obfDestEntry))
GuiTricks.activateButton(matchButton, "Match", event -> match());
} else if (obfSourceEntry != null)
GuiTricks.activateButton(unmatchableButton, "Set Unmatchable", event -> unmatchable());
}
protected void match() {
// update the field matches
memberMatches.makeMatch(obfSourceEntry, obfDestEntry, sourceDeobfuscator, destDeobfuscator);
save();
// update the ui
onSelectSource(null);
onSelectDest(null);
updateSourceHighlights();
updateDestHighlights();
updateSourceClasses();
}
protected void unmatch() {
// update the field matches
memberMatches.unmakeMatch(obfSourceEntry, obfDestEntry, sourceDeobfuscator, destDeobfuscator);
save();
// update the ui
onSelectSource(null);
onSelectDest(null);
updateSourceHighlights();
updateDestHighlights();
updateSourceClasses();
}
protected void unmatchable() {
// update the field matches
memberMatches.makeSourceUnmatchable(obfSourceEntry, sourceDeobfuscator);
save();
// update the ui
onSelectSource(null);
onSelectDest(null);
updateSourceHighlights();
updateDestHighlights();
updateSourceClasses();
}
private void save() {
if (saveListener != null) {
saveListener.save(memberMatches);
}
}
}