From 0f47403d0220757fed189b76e2071e25b1025cb8 Mon Sep 17 00:00:00 2001 From: Runemoro Date: Wed, 3 Jun 2020 13:39:42 -0400 Subject: Split GUI code to separate module (#242) * Split into modules * Post merge compile fixes Co-authored-by: modmuss50 --- enigma-swing/build.gradle | 23 + .../main/java/cuchaz/enigma/gui/BrowserCaret.java | 28 + .../main/java/cuchaz/enigma/gui/ClassSelector.java | 532 ++++++++++ .../main/java/cuchaz/enigma/gui/CodeReader.java | 73 ++ .../java/cuchaz/enigma/gui/ConnectionState.java | 7 + .../cuchaz/enigma/gui/DecompiledClassSource.java | 160 +++ .../cuchaz/enigma/gui/EnigmaQuickFindDialog.java | 90 ++ .../java/cuchaz/enigma/gui/EnigmaSyntaxKit.java | 44 + .../java/cuchaz/enigma/gui/ExceptionIgnorer.java | 35 + .../src/main/java/cuchaz/enigma/gui/Gui.java | 1058 ++++++++++++++++++++ .../main/java/cuchaz/enigma/gui/GuiController.java | 719 +++++++++++++ .../src/main/java/cuchaz/enigma/gui/Main.java | 118 +++ .../cuchaz/enigma/gui/MessageListCellRenderer.java | 24 + .../cuchaz/enigma/gui/MethodTreeCellRenderer.java | 42 + .../java/cuchaz/enigma/gui/QuickFindAction.java | 45 + .../main/java/cuchaz/enigma/gui/ReadableToken.java | 30 + .../main/java/cuchaz/enigma/gui/RefreshMode.java | 7 + .../cuchaz/enigma/gui/TokenListCellRenderer.java | 35 + .../main/java/cuchaz/enigma/gui/config/Config.java | 261 +++++ .../main/java/cuchaz/enigma/gui/config/Themes.java | 45 + .../java/cuchaz/enigma/gui/dialog/AboutDialog.java | 70 ++ .../cuchaz/enigma/gui/dialog/ChangeDialog.java | 50 + .../enigma/gui/dialog/ConnectToServerDialog.java | 82 ++ .../java/cuchaz/enigma/gui/dialog/CrashDialog.java | 105 ++ .../enigma/gui/dialog/CreateServerDialog.java | 65 ++ .../cuchaz/enigma/gui/dialog/JavadocDialog.java | 159 +++ .../cuchaz/enigma/gui/dialog/ProgressDialog.java | 109 ++ .../cuchaz/enigma/gui/dialog/SearchDialog.java | 261 +++++ .../java/cuchaz/enigma/gui/dialog/StatsDialog.java | 82 ++ .../enigma/gui/elements/CollapsibleTabbedPane.java | 40 + .../java/cuchaz/enigma/gui/elements/MenuBar.java | 386 +++++++ .../cuchaz/enigma/gui/elements/PopupMenuBar.java | 125 +++ .../enigma/gui/filechooser/FileChooserAny.java | 10 + .../enigma/gui/filechooser/FileChooserFile.java | 8 + .../enigma/gui/filechooser/FileChooserFolder.java | 11 + .../enigma/gui/highlight/BoxHighlightPainter.java | 69 ++ .../gui/highlight/SelectionHighlightPainter.java | 31 + .../enigma/gui/highlight/TokenHighlightType.java | 7 + .../enigma/gui/node/ClassSelectorClassNode.java | 72 ++ .../enigma/gui/node/ClassSelectorPackageNode.java | 58 ++ .../java/cuchaz/enigma/gui/panels/PanelDeobf.java | 26 + .../java/cuchaz/enigma/gui/panels/PanelEditor.java | 171 ++++ .../cuchaz/enigma/gui/panels/PanelIdentifier.java | 32 + .../java/cuchaz/enigma/gui/panels/PanelObf.java | 37 + .../java/cuchaz/enigma/gui/search/SearchEntry.java | 17 + .../java/cuchaz/enigma/gui/search/SearchUtil.java | 268 +++++ .../cuchaz/enigma/gui/stats/StatsGenerator.java | 197 ++++ .../java/cuchaz/enigma/gui/stats/StatsMember.java | 8 + .../enigma/gui/util/AbstractListCellRenderer.java | 77 ++ .../main/java/cuchaz/enigma/gui/util/GuiUtil.java | 56 ++ .../main/java/cuchaz/enigma/gui/util/History.java | 49 + .../enigma/gui/util/ScaleChangeListener.java | 8 + .../java/cuchaz/enigma/gui/util/ScaleUtil.java | 110 ++ enigma-swing/src/main/resources/about.html | 6 + enigma-swing/src/main/resources/stats.html | 34 + 55 files changed, 6272 insertions(+) create mode 100644 enigma-swing/build.gradle create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java create mode 100644 enigma-swing/src/main/resources/about.html create mode 100644 enigma-swing/src/main/resources/stats.html (limited to 'enigma-swing') diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle new file mode 100644 index 00000000..a1bcafc5 --- /dev/null +++ b/enigma-swing/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '5.2.0' +} + +dependencies { + implementation project(':enigma') + implementation project(':enigma-server') + + implementation 'net.sf.jopt-simple:jopt-simple:6.0-alpha-3' + implementation 'com.bulenkov:darcula:1.0.0-bobbylight' + implementation 'de.sciss:syntaxpane:1.2.0' + implementation 'com.github.lukeu:swing-dpi:0.6' +} + +jar.manifest.attributes 'Main-Class': 'cuchaz.enigma.gui.Main' + +publishing { + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + } + } +} \ No newline at end of file diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java new file mode 100644 index 00000000..af105dbd --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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.text.DefaultCaret; + +public class BrowserCaret extends DefaultCaret { + + @Override + public boolean isSelectionVisible() { + return true; + } + + @Override + public boolean isVisible() { + return true; + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java new file mode 100644 index 00000000..3d0e04c9 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java @@ -0,0 +1,532 @@ +/******************************************************************************* + * 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.*; + +import javax.annotation.Nullable; +import javax.swing.JOptionPane; +import javax.swing.JTree; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.tree.*; + +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.gui.node.ClassSelectorClassNode; +import cuchaz.enigma.gui.node.ClassSelectorPackageNode; +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +public class ClassSelector extends JTree { + + public static final Comparator DEOBF_CLASS_COMPARATOR = Comparator.comparing(ClassEntry::getFullName); + + private final GuiController controller; + + private DefaultMutableTreeNode rootNodes; + private ClassSelectionListener selectionListener; + private RenameSelectionListener renameSelectionListener; + private Comparator comparator; + + private final Map displayedObfToDeobf = new HashMap<>(); + + public ClassSelector(Gui gui, Comparator comparator, boolean isRenamable) { + this.comparator = comparator; + this.controller = gui.getController(); + + // configure the tree control + setEditable(true); + setRootVisible(false); + setShowsRootHandles(false); + setModel(null); + + // hook events + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (selectionListener != null && event.getClickCount() == 2) { + // get the selected node + TreePath path = getSelectionPath(); + if (path != null && path.getLastPathComponent() instanceof ClassSelectorClassNode) { + ClassSelectorClassNode node = (ClassSelectorClassNode) path.getLastPathComponent(); + selectionListener.onSelectClass(node.getObfEntry()); + } + } + } + }); + + final JTree tree = this; + + final DefaultTreeCellEditor editor = new DefaultTreeCellEditor(tree, + (DefaultTreeCellRenderer) tree.getCellRenderer()) { + @Override + public boolean isCellEditable(EventObject event) { + return isRenamable && !(event instanceof MouseEvent) && super.isCellEditable(event); + } + }; + this.setCellEditor(editor); + editor.addCellEditorListener(new CellEditorListener() { + @Override + public void editingStopped(ChangeEvent e) { + String data = editor.getCellEditorValue().toString(); + TreePath path = getSelectionPath(); + + Object realPath = path.getLastPathComponent(); + if (realPath != null && realPath instanceof DefaultMutableTreeNode && data != null) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) realPath; + TreeNode parentNode = node.getParent(); + if (parentNode == null) + return; + boolean allowEdit = true; + for (int i = 0; i < parentNode.getChildCount(); i++) { + TreeNode childNode = parentNode.getChildAt(i); + if (childNode != null && childNode.toString().equals(data) && childNode != node) { + allowEdit = false; + break; + } + } + if (allowEdit && renameSelectionListener != null) { + Object prevData = node.getUserObject(); + Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data; + try { + renameSelectionListener.onSelectionRename(node.getUserObject(), objectData, node); + node.setUserObject(objectData); // Make sure that it's modified + } catch (IllegalNameException ex) { + JOptionPane.showOptionDialog(gui.getFrame(), ex.getMessage(), "Enigma - Error", JOptionPane.OK_OPTION, + JOptionPane.ERROR_MESSAGE, null, new String[]{"Ok"}, "OK"); + editor.cancelCellEditing(); + } + } else + editor.cancelCellEditing(); + } + + } + + @Override + public void editingCanceled(ChangeEvent e) { + // NOP + } + }); + // init defaults + this.selectionListener = null; + this.renameSelectionListener = null; + } + + public boolean isDuplicate(Object[] nodes, String data) { + int count = 0; + + for (Object node : nodes) { + if (node.toString().equals(data)) { + count++; + if (count == 2) + return true; + } + } + return false; + } + + public void setSelectionListener(ClassSelectionListener val) { + this.selectionListener = val; + } + + public void setRenameSelectionListener(RenameSelectionListener renameSelectionListener) { + this.renameSelectionListener = renameSelectionListener; + } + + public void setClasses(Collection classEntries) { + displayedObfToDeobf.clear(); + + List state = getExpansionState(this); + if (classEntries == null) { + setModel(null); + return; + } + + Translator translator = controller.project.getMapper().getDeobfuscator(); + + // build the package names + Map packages = Maps.newHashMap(); + for (ClassEntry obfClass : classEntries) { + ClassEntry deobfClass = translator.translate(obfClass); + packages.put(deobfClass.getPackageName(), null); + } + + // sort the packages + List sortedPackageNames = Lists.newArrayList(packages.keySet()); + sortedPackageNames.sort((a, b) -> + { + // I can never keep this rule straight when writing these damn things... + // a < b => -1, a == b => 0, a > b => +1 + + if (b == null || a == null) { + return 0; + } + + 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 rootNodes node and the package nodes + rootNodes = new DefaultMutableTreeNode(); + for (String packageName : sortedPackageNames) { + ClassSelectorPackageNode node = new ClassSelectorPackageNode(packageName); + packages.put(packageName, node); + rootNodes.add(node); + } + + // put the classes into packages + Multimap packagedClassEntries = ArrayListMultimap.create(); + for (ClassEntry obfClass : classEntries) { + ClassEntry deobfClass = translator.translate(obfClass); + packagedClassEntries.put(deobfClass.getPackageName(), obfClass); + } + + // build the class nodes + for (String packageName : packagedClassEntries.keySet()) { + // sort the class entries + List classEntriesInPackage = Lists.newArrayList(packagedClassEntries.get(packageName)); + classEntriesInPackage.sort((o1, o2) -> comparator.compare(translator.translate(o1), translator.translate(o2))); + + // create the nodes in order + for (ClassEntry obfClass : classEntriesInPackage) { + ClassEntry deobfClass = translator.translate(obfClass); + ClassSelectorPackageNode node = packages.get(packageName); + ClassSelectorClassNode classNode = new ClassSelectorClassNode(obfClass, deobfClass); + displayedObfToDeobf.put(obfClass, deobfClass); + node.add(classNode); + } + } + + // finally, update the tree control + setModel(new DefaultTreeModel(rootNodes)); + + restoreExpansionState(this, state); + } + + 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 boolean isDescendant(TreePath path1, TreePath path2) { + int count1 = path1.getPathCount(); + int count2 = path2.getPathCount(); + if (count1 <= count2) { + return false; + } + while (count1 != count2) { + path1 = path1.getParentPath(); + count1--; + } + return path1.equals(path2); + } + + public enum State { + EXPANDED, + SELECTED + } + + public static class StateEntry { + public final State state; + public final TreePath path; + + public StateEntry(State state, TreePath path) { + this.state = state; + this.path = path; + } + } + + public List getExpansionState(JTree tree) { + List state = new ArrayList<>(); + int rowCount = tree.getRowCount(); + for (int i = 0; i < rowCount; i++) { + TreePath path = tree.getPathForRow(i); + if (tree.isPathSelected(path)) { + state.add(new StateEntry(State.SELECTED, path)); + } + if (tree.isExpanded(path)) { + state.add(new StateEntry(State.EXPANDED, path)); + } + } + return state; + } + + public void restoreExpansionState(JTree tree, List expansionState) { + tree.clearSelection(); + + for (StateEntry entry : expansionState) { + switch (entry.state) { + case SELECTED: + tree.addSelectionPath(entry.path); + break; + case EXPANDED: + tree.expandPath(entry.path); + break; + } + } + } + + public List 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 List 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() { + ClassSelectorPackageNode packageNode = packageNodes().get(0); + if (packageNode != null) { + ClassSelectorClassNode classNode = classNodes(packageNode).get(0); + if (classNode != null) { + return classNode.getClassEntry(); + } + } + return null; + } + + public ClassSelectorPackageNode getPackageNode(ClassEntry entry) { + String packageName = entry.getPackageName(); + if (packageName == null) { + packageName = "(none)"; + } + for (ClassSelectorPackageNode packageNode : packageNodes()) { + if (packageNode.getPackageName().equals(packageName)) { + return packageNode; + } + } + return null; + } + + @Nullable + public ClassEntry getDisplayedDeobf(ClassEntry obfEntry) { + return displayedObfToDeobf.get(obfEntry); + } + + public ClassSelectorPackageNode getPackageNode(ClassSelector selector, ClassEntry entry) { + ClassSelectorPackageNode packageNode = getPackageNode(entry); + + if (selector != null && packageNode == null && selector.getPackageNode(entry) != null) + return selector.getPackageNode(entry); + return packageNode; + } + + 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 + ClassSelectorClassNode classNode = classNodes(packageNode).get(0); + if (classNode != null) { + 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)) { + TreePath path = new TreePath(new Object[]{getModel().getRoot(), packageNode, classNode}); + setSelectionPath(path); + scrollPathToVisible(path); + } + } + } + } + + public void removeNode(ClassSelectorPackageNode packageNode, ClassEntry entry) { + DefaultTreeModel model = (DefaultTreeModel) getModel(); + + if (packageNode == null) + return; + + for (int i = 0; i < packageNode.getChildCount(); i++) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) packageNode.getChildAt(i); + if (childNode.getUserObject() instanceof ClassEntry && childNode.getUserObject().equals(entry)) { + model.removeNodeFromParent(childNode); + if (childNode instanceof ClassSelectorClassNode) { + displayedObfToDeobf.remove(((ClassSelectorClassNode) childNode).getObfEntry()); + } + break; + } + } + } + + public void removeNodeIfEmpty(ClassSelectorPackageNode packageNode) { + if (packageNode != null && packageNode.getChildCount() == 0) + ((DefaultTreeModel) getModel()).removeNodeFromParent(packageNode); + } + + public void moveClassIn(ClassEntry classEntry) { + removeEntry(classEntry); + insertNode(classEntry); + } + + public void moveClassOut(ClassEntry classEntry) { + removeEntry(classEntry); + } + + private void removeEntry(ClassEntry classEntry) { + ClassEntry previousDeobf = displayedObfToDeobf.get(classEntry); + if (previousDeobf != null) { + ClassSelectorPackageNode packageNode = getPackageNode(previousDeobf); + removeNode(packageNode, previousDeobf); + removeNodeIfEmpty(packageNode); + } + } + + public ClassSelectorPackageNode getOrCreatePackage(ClassEntry entry) { + DefaultTreeModel model = (DefaultTreeModel) getModel(); + ClassSelectorPackageNode newPackageNode = getPackageNode(entry); + if (newPackageNode == null) { + newPackageNode = new ClassSelectorPackageNode(entry.getPackageName()); + model.insertNodeInto(newPackageNode, (MutableTreeNode) model.getRoot(), getPlacementIndex(newPackageNode)); + } + return newPackageNode; + } + + public void insertNode(ClassEntry obfEntry) { + ClassEntry deobfEntry = controller.project.getMapper().deobfuscate(obfEntry); + ClassSelectorPackageNode packageNode = getOrCreatePackage(deobfEntry); + + DefaultTreeModel model = (DefaultTreeModel) getModel(); + ClassSelectorClassNode classNode = new ClassSelectorClassNode(obfEntry, deobfEntry); + model.insertNodeInto(classNode, packageNode, getPlacementIndex(packageNode, classNode)); + + displayedObfToDeobf.put(obfEntry, deobfEntry); + } + + public void reload() { + DefaultTreeModel model = (DefaultTreeModel) getModel(); + model.reload(rootNodes); + } + + private int getPlacementIndex(ClassSelectorPackageNode newPackageNode, ClassSelectorClassNode classNode) { + List classNodes = classNodes(newPackageNode); + classNodes.add(classNode); + classNodes.sort((a, b) -> comparator.compare(a.getClassEntry(), b.getClassEntry())); + for (int i = 0; i < classNodes.size(); i++) + if (classNodes.get(i) == classNode) + return i; + + return 0; + } + + private int getPlacementIndex(ClassSelectorPackageNode newPackageNode) { + List packageNodes = packageNodes(); + if (!packageNodes.contains(newPackageNode)) { + packageNodes.add(newPackageNode); + packageNodes.sort(Comparator.comparing(ClassSelectorPackageNode::toString)); + } + + for (int i = 0; i < packageNodes.size(); i++) + if (packageNodes.get(i) == newPackageNode) + return i; + + return 0; + } + + public interface ClassSelectionListener { + void onSelectClass(ClassEntry classEntry); + } + + public interface RenameSelectionListener { + void onSelectionRename(Object prevData, Object data, DefaultMutableTreeNode node); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java new file mode 100644 index 00000000..356656b9 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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.source.Token; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Highlighter.HighlightPainter; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class CodeReader extends JEditorPane { + private static final long serialVersionUID = 3673180950485748810L; + + // 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 + Document document = editor.getDocument(); + int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); + + editor.setCaretPosition(clampedPosition); + 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(() -> editor.scrollRectToVisible(show)); + } catch (BadLocationException ex) { + throw new Error(ex); + } + + // highlight the token momentarily + final Timer timer = new Timer(200, new ActionListener() { + private int counter = 0; + private Object highlight = null; + + @Override + public void actionPerformed(ActionEvent event) { + if (counter % 2 == 0) { + try { + highlight = editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); + } catch (BadLocationException ex) { + // don't care + } + } else if (highlight != null) { + editor.getHighlighter().removeHighlight(highlight); + } + + if (counter++ > 6) { + Timer timer = (Timer) event.getSource(); + timer.stop(); + } + } + }); + timer.start(); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java new file mode 100644 index 00000000..db6590de --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.gui; + +public enum ConnectionState { + NOT_CONNECTED, + HOSTING, + CONNECTED, +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java new file mode 100644 index 00000000..aca5d724 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java @@ -0,0 +1,160 @@ +package cuchaz.enigma.gui; + +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.EnigmaServices; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.gui.highlight.TokenHighlightType; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.SourceRemapper; +import cuchaz.enigma.translation.LocalNameGenerator; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry; + +import javax.annotation.Nullable; +import java.util.*; + +public class DecompiledClassSource { + private final ClassEntry classEntry; + + private final SourceIndex obfuscatedIndex; + private SourceIndex remappedIndex; + + private final Map> highlightedTokens = new EnumMap<>(TokenHighlightType.class); + + public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) { + this.classEntry = classEntry; + this.obfuscatedIndex = index; + this.remappedIndex = index; + } + + public static DecompiledClassSource text(ClassEntry classEntry, String text) { + return new DecompiledClassSource(classEntry, new SourceIndex(text)); + } + + public void remapSource(EnigmaProject project, Translator translator) { + highlightedTokens.clear(); + + SourceRemapper remapper = new SourceRemapper(obfuscatedIndex.getSource(), obfuscatedIndex.referenceTokens()); + + SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> remapToken(project, token, movedToken, translator)); + remappedIndex = obfuscatedIndex.remapTo(remapResult); + } + + private String remapToken(EnigmaProject project, Token token, Token movedToken, Translator translator) { + EntryReference, Entry> reference = obfuscatedIndex.getReference(token); + + Entry entry = reference.getNameableEntry(); + Entry translatedEntry = translator.translate(entry); + + if (project.isRenamable(reference)) { + if (isDeobfuscated(entry, translatedEntry)) { + highlightToken(movedToken, TokenHighlightType.DEOBFUSCATED); + return translatedEntry.getSourceRemapName(); + } else { + Optional proposedName = proposeName(project, entry); + if (proposedName.isPresent()) { + highlightToken(movedToken, TokenHighlightType.PROPOSED); + return proposedName.get(); + } + + highlightToken(movedToken, TokenHighlightType.OBFUSCATED); + } + } + + String defaultName = generateDefaultName(translatedEntry); + if (defaultName != null) { + return defaultName; + } + + return null; + } + + private Optional proposeName(EnigmaProject project, Entry entry) { + EnigmaServices services = project.getEnigma().getServices(); + + return services.get(NameProposalService.TYPE).stream().flatMap(nameProposalService -> { + EntryRemapper mapper = project.getMapper(); + Collection> resolved = mapper.getObfResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); + + return resolved.stream() + .map(e -> nameProposalService.proposeName(e, mapper)) + .filter(Optional::isPresent) + .map(Optional::get); + }).findFirst(); + } + + @Nullable + private String generateDefaultName(Entry entry) { + if (entry instanceof LocalVariableDefEntry) { + LocalVariableDefEntry localVariable = (LocalVariableDefEntry) entry; + + int index = localVariable.getIndex(); + if (localVariable.isArgument()) { + List arguments = localVariable.getParent().getDesc().getArgumentDescs(); + return LocalNameGenerator.generateArgumentName(index, localVariable.getDesc(), arguments); + } else { + return LocalNameGenerator.generateLocalVariableName(index, localVariable.getDesc()); + } + } + + return null; + } + + private boolean isDeobfuscated(Entry entry, Entry translatedEntry) { + return !entry.getName().equals(translatedEntry.getName()); + } + + public ClassEntry getEntry() { + return classEntry; + } + + public SourceIndex getIndex() { + return remappedIndex; + } + + public Map> getHighlightedTokens() { + return highlightedTokens; + } + + private void highlightToken(Token token, TokenHighlightType highlightType) { + highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token); + } + + public int getObfuscatedOffset(int deobfOffset) { + return getOffset(remappedIndex, obfuscatedIndex, deobfOffset); + } + + public int getDeobfuscatedOffset(int obfOffset) { + return getOffset(obfuscatedIndex, remappedIndex, obfOffset); + } + + private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fromOffset) { + int relativeOffset = 0; + + Iterator fromTokenItr = fromIndex.referenceTokens().iterator(); + Iterator toTokenItr = toIndex.referenceTokens().iterator(); + while (fromTokenItr.hasNext() && toTokenItr.hasNext()) { + Token fromToken = fromTokenItr.next(); + Token toToken = toTokenItr.next(); + if (fromToken.end > fromOffset) { + break; + } + + relativeOffset = toToken.end - fromToken.end; + } + + return fromOffset + relativeOffset; + } + + @Override + public String toString() { + return remappedIndex.getSource(); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java new file mode 100644 index 00000000..c912be3a --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java @@ -0,0 +1,90 @@ +package cuchaz.enigma.gui; + +import de.sciss.syntaxpane.actions.DocumentSearchData; +import de.sciss.syntaxpane.actions.gui.QuickFindDialog; + +import javax.swing.*; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class EnigmaQuickFindDialog extends QuickFindDialog { + public EnigmaQuickFindDialog(JTextComponent target) { + super(target, DocumentSearchData.getFromEditor(target)); + + JToolBar toolBar = getToolBar(); + JTextField textField = getTextField(toolBar); + + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + super.keyPressed(e); + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + JToolBar toolBar = getToolBar(); + boolean next = !e.isShiftDown(); + JButton button = next ? getNextButton(toolBar) : getPrevButton(toolBar); + button.doClick(); + } + } + }); + } + + @Override + public void showFor(JTextComponent target) { + String selectedText = target.getSelectedText(); + + try { + super.showFor(target); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + Container view = target.getParent(); + Point loc = new Point(0, view.getHeight() - getSize().height); + setLocationRelativeTo(view); + SwingUtilities.convertPointToScreen(loc, view); + setLocation(loc); + + JToolBar toolBar = getToolBar(); + JTextField textField = getTextField(toolBar); + + if (selectedText != null) { + textField.setText(selectedText); + } + + textField.selectAll(); + } + + private JToolBar getToolBar() { + return components(getContentPane(), JToolBar.class).findFirst().orElse(null); + } + + private JTextField getTextField(JToolBar toolBar) { + return components(toolBar, JTextField.class).findFirst().orElse(null); + } + + private JButton getNextButton(JToolBar toolBar) { + Stream buttons = components(toolBar, JButton.class); + return buttons.skip(1).findFirst().orElse(null); + } + + private JButton getPrevButton(JToolBar toolBar) { + Stream buttons = components(toolBar, JButton.class); + return buttons.findFirst().orElse(null); + } + + private static Stream components(Container container) { + return IntStream.range(0, container.getComponentCount()) + .mapToObj(container::getComponent); + } + + private static Stream components(Container container, Class type) { + return components(container) + .filter(type::isInstance) + .map(type::cast); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java new file mode 100644 index 00000000..2f08a269 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.gui; + +import cuchaz.enigma.gui.config.Config; +import de.sciss.syntaxpane.components.LineNumbersRuler; +import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit; +import de.sciss.syntaxpane.util.Configuration; + +public class EnigmaSyntaxKit extends JavaSyntaxKit { + private static Configuration configuration = null; + + @Override + public Configuration getConfig() { + if(configuration == null){ + initConfig(super.getConfig(JavaSyntaxKit.class)); + } + return configuration; + } + + public void initConfig(Configuration baseConfig){ + configuration = baseConfig; + //See de.sciss.syntaxpane.TokenType + configuration.put("Style.KEYWORD", Config.getInstance().highlightColor + ", 0"); + configuration.put("Style.KEYWORD2", Config.getInstance().highlightColor + ", 3"); + configuration.put("Style.STRING", Config.getInstance().stringColor + ", 0"); + configuration.put("Style.STRING2", Config.getInstance().stringColor + ", 1"); + configuration.put("Style.NUMBER", Config.getInstance().numberColor + ", 1"); + configuration.put("Style.OPERATOR", Config.getInstance().operatorColor + ", 0"); + configuration.put("Style.DELIMITER", Config.getInstance().delimiterColor + ", 1"); + configuration.put("Style.TYPE", Config.getInstance().typeColor + ", 2"); + configuration.put("Style.TYPE2", Config.getInstance().typeColor + ", 1"); + configuration.put("Style.IDENTIFIER", Config.getInstance().identifierColor + ", 0"); + configuration.put("Style.DEFAULT", Config.getInstance().defaultTextColor + ", 0"); + configuration.put(LineNumbersRuler.PROPERTY_BACKGROUND, Config.getInstance().lineNumbersBackground + ""); + configuration.put(LineNumbersRuler.PROPERTY_FOREGROUND, Config.getInstance().lineNumbersForeground + ""); + configuration.put(LineNumbersRuler.PROPERTY_CURRENT_BACK, Config.getInstance().lineNumbersSelected + ""); + configuration.put("RightMarginColumn", "999"); //No need to have a right margin, if someone wants it add a config + + configuration.put("Action.quick-find", "cuchaz.enigma.gui.QuickFindAction, menu F"); + } + + public static void invalidate(){ + configuration = null; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java new file mode 100644 index 00000000..6246192c --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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 ExceptionIgnorer { + + public static boolean shouldIgnore(Throwable t) { + + // is this that pesky concurrent access bug in the highlight painter system? + // (ancient ui code is ancient) + if (t instanceof ArrayIndexOutOfBoundsException) { + StackTraceElement[] stackTrace = t.getStackTrace(); + if (stackTrace.length > 1) { + + // does this stack frame match javax.swing.text.DefaultHighlighter.paint*() ? + StackTraceElement frame = stackTrace[1]; + if (frame.getClassName().equals("javax.swing.text.DefaultHighlighter") && frame.getMethodName().startsWith("paint")) { + return true; + } + } + } + + return false; + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java new file mode 100644 index 00000000..2ed1010f --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java @@ -0,0 +1,1058 @@ +/******************************************************************************* + * 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.*; +import java.awt.event.*; +import java.nio.file.Path; +import java.util.List; +import java.util.*; +import java.util.function.Function; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Highlighter; +import javax.swing.tree.*; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import cuchaz.enigma.Enigma; +import cuchaz.enigma.EnigmaProfile; +import cuchaz.enigma.analysis.*; +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.gui.config.Themes; +import cuchaz.enigma.gui.dialog.CrashDialog; +import cuchaz.enigma.gui.dialog.JavadocDialog; +import cuchaz.enigma.gui.dialog.SearchDialog; +import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; +import cuchaz.enigma.gui.elements.MenuBar; +import cuchaz.enigma.gui.elements.PopupMenuBar; +import cuchaz.enigma.gui.filechooser.FileChooserAny; +import cuchaz.enigma.gui.filechooser.FileChooserFolder; +import cuchaz.enigma.gui.highlight.BoxHighlightPainter; +import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; +import cuchaz.enigma.gui.highlight.TokenHighlightType; +import cuchaz.enigma.gui.panels.PanelDeobf; +import cuchaz.enigma.gui.panels.PanelEditor; +import cuchaz.enigma.gui.panels.PanelIdentifier; +import cuchaz.enigma.gui.panels.PanelObf; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.gui.util.History; +import cuchaz.enigma.network.packet.*; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.mapping.IllegalNameException; +import cuchaz.enigma.translation.mapping.*; +import cuchaz.enigma.translation.representation.entry.*; +import cuchaz.enigma.network.Message; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; +import de.sciss.syntaxpane.DefaultSyntaxKit; + +public class Gui { + + public final PopupMenuBar popupMenu; + private final PanelObf obfPanel; + private final PanelDeobf deobfPanel; + + private final MenuBar menuBar; + // state + public History, Entry>> referenceHistory; + public EntryReference, Entry> renamingReference; + public EntryReference, Entry> cursorReference; + private boolean shouldNavigateOnClick; + private ConnectionState connectionState; + private boolean isJarOpen; + + public FileDialog jarFileChooser; + public FileDialog tinyMappingsFileChooser; + public SearchDialog searchDialog; + public JFileChooser enigmaMappingsFileChooser; + public JFileChooser exportSourceFileChooser; + public FileDialog exportJarFileChooser; + private GuiController controller; + private JFrame frame; + public Config.LookAndFeel editorFeel; + public PanelEditor editor; + public JScrollPane sourceScroller; + private JPanel classesPanel; + private JSplitPane splitClasses; + private PanelIdentifier infoPanel; + public Map boxHighlightPainters; + private SelectionHighlightPainter selectionHighlightPainter; + private JTree inheritanceTree; + private JTree implementationsTree; + private JTree callsTree; + private JList tokens; + private JTabbedPane tabs; + + private JSplitPane splitRight; + private JSplitPane logSplit; + private CollapsibleTabbedPane logTabs; + private JList users; + private DefaultListModel userModel; + private JScrollPane messageScrollPane; + private JList messages; + private DefaultListModel messageModel; + private JTextField chatBox; + + private JPanel statusBar; + private JLabel connectionStatusLabel; + private JLabel statusLabel; + + public JTextField renameTextField; + public JTextArea javadocTextArea; + + public void setEditorTheme(Config.LookAndFeel feel) { + if (editor != null && (editorFeel == null || editorFeel != feel)) { + editor.updateUI(); + editor.setBackground(new Color(Config.getInstance().editorBackground)); + if (editorFeel != null) { + getController().refreshCurrentClass(); + } + + editorFeel = feel; + } + } + + public Gui(EnigmaProfile profile) { + Config.getInstance().lookAndFeel.setGlobalLAF(); + + // init frame + this.frame = new JFrame(Enigma.NAME); + final Container pane = this.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(this.frame); + Thread.setDefaultUncaughtExceptionHandler((thread, t) -> { + t.printStackTrace(System.err); + if (!ExceptionIgnorer.shouldIgnore(t)) { + CrashDialog.show(t); + } + }); + } + + this.controller = new GuiController(this, profile); + + Themes.updateTheme(this); + + // init file choosers + this.jarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.jar.open"), FileDialog.LOAD); + + this.tinyMappingsFileChooser = new FileDialog(getFrame(), "Open tiny Mappings", FileDialog.LOAD); + this.enigmaMappingsFileChooser = new FileChooserAny(); + this.exportSourceFileChooser = new FileChooserFolder(); + this.exportJarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.export.jar"), FileDialog.SAVE); + + this.obfPanel = new PanelObf(this); + this.deobfPanel = new PanelDeobf(this); + + // set up classes panel (don't add the splitter yet) + splitClasses = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, this.obfPanel, this.deobfPanel); + splitClasses.setResizeWeight(0.3); + this.classesPanel = new JPanel(); + this.classesPanel.setLayout(new BorderLayout()); + this.classesPanel.setPreferredSize(ScaleUtil.getDimension(250, 0)); + + // init info panel + infoPanel = new PanelIdentifier(this); + infoPanel.clearReference(); + + // init editor + selectionHighlightPainter = new SelectionHighlightPainter(); + this.editor = new PanelEditor(this); + this.sourceScroller = new JScrollPane(this.editor); + this.editor.setContentType("text/enigma-sources"); + this.editor.setBackground(new Color(Config.getInstance().editorBackground)); + DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit(); + kit.toggleComponent(this.editor, "de.sciss.syntaxpane.components.TokenMarker"); + + // init editor popup menu + this.popupMenu = new PopupMenuBar(this); + this.editor.setComponentPopupMenu(this.popupMenu); + + // init inheritance panel + inheritanceTree = new JTree(); + inheritanceTree.setModel(null); + inheritanceTree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() >= 2) { + // get the selected node + TreePath path = inheritanceTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ClassInheritanceTreeNode) { + ClassInheritanceTreeNode classNode = (ClassInheritanceTreeNode) node; + controller.navigateTo(new ClassEntry(classNode.getObfClassName())); + } else if (node instanceof MethodInheritanceTreeNode) { + MethodInheritanceTreeNode methodNode = (MethodInheritanceTreeNode) node; + if (methodNode.isImplemented()) { + controller.navigateTo(methodNode.getMethodEntry()); + } + } + } + } + }); + TreeCellRenderer cellRenderer = inheritanceTree.getCellRenderer(); + inheritanceTree.setCellRenderer(new MethodTreeCellRenderer(cellRenderer)); + + JPanel inheritancePanel = new JPanel(); + inheritancePanel.setLayout(new BorderLayout()); + inheritancePanel.add(new JScrollPane(inheritanceTree)); + + // init implementations panel + implementationsTree = new JTree(); + implementationsTree.setModel(null); + implementationsTree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() >= 2) { + // get the selected node + TreePath path = implementationsTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ClassImplementationsTreeNode) { + ClassImplementationsTreeNode classNode = (ClassImplementationsTreeNode) node; + controller.navigateTo(classNode.getClassEntry()); + } else if (node instanceof MethodImplementationsTreeNode) { + MethodImplementationsTreeNode methodNode = (MethodImplementationsTreeNode) node; + controller.navigateTo(methodNode.getMethodEntry()); + } + } + } + }); + JPanel implementationsPanel = new JPanel(); + implementationsPanel.setLayout(new BorderLayout()); + implementationsPanel.add(new JScrollPane(implementationsTree)); + + // init call panel + callsTree = new JTree(); + callsTree.setModel(null); + callsTree.addMouseListener(new MouseAdapter() { + @SuppressWarnings("unchecked") + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() >= 2) { + // get the selected node + TreePath path = callsTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ReferenceTreeNode) { + ReferenceTreeNode, Entry> referenceNode = ((ReferenceTreeNode, Entry>) node); + if (referenceNode.getReference() != null) { + controller.navigateTo(referenceNode.getReference()); + } else { + controller.navigateTo(referenceNode.getEntry()); + } + } + } + } + }); + tokens = new JList<>(); + tokens.setCellRenderer(new TokenListCellRenderer(this.controller)); + tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + tokens.setLayoutOrientation(JList.VERTICAL); + tokens.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() == 2) { + Token selected = tokens.getSelectedValue(); + if (selected != null) { + showToken(selected); + } + } + } + }); + tokens.setPreferredSize(ScaleUtil.getDimension(0, 200)); + tokens.setMinimumSize(ScaleUtil.getDimension(0, 200)); + JSplitPane callPanel = new JSplitPane( + JSplitPane.VERTICAL_SPLIT, + true, + new JScrollPane(callsTree), + new JScrollPane(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(infoPanel, BorderLayout.NORTH); + centerPanel.add(sourceScroller, BorderLayout.CENTER); + tabs = new JTabbedPane(); + tabs.setPreferredSize(ScaleUtil.getDimension(250, 0)); + tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel); + tabs.addTab(I18n.translate("info_panel.tree.implementations"), implementationsPanel); + tabs.addTab(I18n.translate("info_panel.tree.calls"), callPanel); + logTabs = new CollapsibleTabbedPane(JTabbedPane.BOTTOM); + userModel = new DefaultListModel<>(); + users = new JList<>(userModel); + messageModel = new DefaultListModel<>(); + messages = new JList<>(messageModel); + messages.setCellRenderer(new MessageListCellRenderer()); + JPanel messagePanel = new JPanel(new BorderLayout()); + messageScrollPane = new JScrollPane(this.messages); + messagePanel.add(messageScrollPane, BorderLayout.CENTER); + JPanel chatPanel = new JPanel(new BorderLayout()); + chatBox = new JTextField(); + AbstractAction sendListener = new AbstractAction("Send") { + @Override + public void actionPerformed(ActionEvent e) { + sendMessage(); + } + }; + chatBox.addActionListener(sendListener); + JButton chatSendButton = new JButton(sendListener); + chatPanel.add(chatBox, BorderLayout.CENTER); + chatPanel.add(chatSendButton, BorderLayout.EAST); + messagePanel.add(chatPanel, BorderLayout.SOUTH); + logTabs.addTab(I18n.translate("log_panel.users"), new JScrollPane(this.users)); + logTabs.addTab(I18n.translate("log_panel.messages"), messagePanel); + logSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, tabs, logTabs); + logSplit.setResizeWeight(0.5); + logSplit.resetToPreferredSizes(); + splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, this.logSplit); + splitRight.setResizeWeight(1); // let the left side take all the slack + splitRight.resetToPreferredSizes(); + JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.classesPanel, splitRight); + splitCenter.setResizeWeight(0); // let the right side take all the slack + pane.add(splitCenter, BorderLayout.CENTER); + + // init menus + this.menuBar = new MenuBar(this); + this.frame.setJMenuBar(this.menuBar); + + // init status bar + statusBar = new JPanel(new BorderLayout()); + statusBar.setBorder(BorderFactory.createLoweredBevelBorder()); + connectionStatusLabel = new JLabel(); + statusLabel = new JLabel(); + statusBar.add(statusLabel, BorderLayout.CENTER); + statusBar.add(connectionStatusLabel, BorderLayout.EAST); + pane.add(statusBar, BorderLayout.SOUTH); + + // init state + setConnectionState(ConnectionState.NOT_CONNECTED); + onCloseJar(); + + this.frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + close(); + } + }); + + // show the frame + pane.doLayout(); + this.frame.setSize(ScaleUtil.getDimension(1024, 576)); + this.frame.setMinimumSize(ScaleUtil.getDimension(640, 480)); + this.frame.setVisible(true); + this.frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + this.frame.setLocationRelativeTo(null); + } + + public JFrame getFrame() { + return this.frame; + } + + public GuiController getController() { + return this.controller; + } + + public void onStartOpenJar() { + this.classesPanel.removeAll(); + redraw(); + } + + public void onFinishOpenJar(String jarName) { + // update gui + this.frame.setTitle(Enigma.NAME + " - " + jarName); + this.classesPanel.removeAll(); + this.classesPanel.add(splitClasses); + setEditorText(null); + + // update menu + isJarOpen = true; + + updateUiState(); + redraw(); + } + + public void onCloseJar() { + + // update gui + this.frame.setTitle(Enigma.NAME); + setObfClasses(null); + setDeobfClasses(null); + setEditorText(null); + this.classesPanel.removeAll(); + + // update menu + isJarOpen = false; + setMappingsFile(null); + + updateUiState(); + redraw(); + } + + public void setObfClasses(Collection obfClasses) { + this.obfPanel.obfClasses.setClasses(obfClasses); + } + + public void setDeobfClasses(Collection deobfClasses) { + this.deobfPanel.deobfClasses.setClasses(deobfClasses); + } + + public void setMappingsFile(Path path) { + this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); + updateUiState(); + } + + public void setEditorText(String source) { + this.editor.getHighlighter().removeAllHighlights(); + this.editor.setText(source); + } + + public void setSource(DecompiledClassSource source) { + editor.setText(source.toString()); + setHighlightedTokens(source.getHighlightedTokens()); + } + + public void showToken(final Token token) { + if (token == null) { + throw new IllegalArgumentException("Token cannot be null!"); + } + CodeReader.navigateToToken(this.editor, token, 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 + this.tokens.setListData(sortedTokens); + this.tokens.setSelectedIndex(0); + } else { + this.tokens.setListData(new Vector<>()); + } + + // show the first token + showToken(sortedTokens.get(0)); + } + + public void setHighlightedTokens(Map> tokens) { + // remove any old highlighters + this.editor.getHighlighter().removeAllHighlights(); + + if (boxHighlightPainters != null) { + for (TokenHighlightType type : tokens.keySet()) { + BoxHighlightPainter painter = boxHighlightPainters.get(type); + if (painter != null) { + setHighlightedTokens(tokens.get(type), painter); + } + } + } + + redraw(); + } + + private void setHighlightedTokens(Iterable tokens, Highlighter.HighlightPainter painter) { + for (Token token : tokens) { + try { + this.editor.getHighlighter().addHighlight(token.start, token.end, painter); + } catch (BadLocationException ex) { + throw new IllegalArgumentException(ex); + } + } + } + + private void showCursorReference(EntryReference, Entry> reference) { + if (reference == null) { + infoPanel.clearReference(); + return; + } + + this.cursorReference = reference; + + EntryReference, Entry> translatedReference = controller.project.getMapper().deobfuscate(reference); + + infoPanel.removeAll(); + if (translatedReference.entry instanceof ClassEntry) { + showClassEntry((ClassEntry) translatedReference.entry); + } else if (translatedReference.entry instanceof FieldEntry) { + showFieldEntry((FieldEntry) translatedReference.entry); + } else if (translatedReference.entry instanceof MethodEntry) { + showMethodEntry((MethodEntry) translatedReference.entry); + } else if (translatedReference.entry instanceof LocalVariableEntry) { + showLocalVariableEntry((LocalVariableEntry) translatedReference.entry); + } else { + throw new Error("Unknown entry desc: " + translatedReference.entry.getClass().getName()); + } + + redraw(); + } + + private void showLocalVariableEntry(LocalVariableEntry entry) { + addNameValue(infoPanel, I18n.translate("info_panel.identifier.variable"), entry.getName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getContainingClass().getFullName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.method"), entry.getParent().getName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.index"), Integer.toString(entry.getIndex())); + } + + private void showClassEntry(ClassEntry entry) { + addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getFullName()); + addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry); + } + + private void showFieldEntry(FieldEntry entry) { + addNameValue(infoPanel, I18n.translate("info_panel.identifier.field"), entry.getName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getParent().getFullName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.type_descriptor"), entry.getDesc().toString()); + addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry); + } + + private void showMethodEntry(MethodEntry entry) { + if (entry.isConstructor()) { + addNameValue(infoPanel, I18n.translate("info_panel.identifier.constructor"), entry.getParent().getFullName()); + } else { + addNameValue(infoPanel, I18n.translate("info_panel.identifier.method"), entry.getName()); + addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getParent().getFullName()); + } + addNameValue(infoPanel, I18n.translate("info_panel.identifier.method_descriptor"), entry.getDesc().toString()); + addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry); + } + + private void addNameValue(JPanel container, String name, String value) { + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0)); + + JLabel label = new JLabel(name + ":", JLabel.RIGHT); + label.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height))); + panel.add(label); + + panel.add(GuiUtil.unboldLabel(new JLabel(value, JLabel.LEFT))); + + container.add(panel); + } + + private JComboBox addModifierComboBox(JPanel container, String name, Entry entry) { + if (!getController().project.isRenamable(entry)) + return null; + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0)); + JLabel label = new JLabel(name + ":", JLabel.RIGHT); + label.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height))); + panel.add(label); + JComboBox combo = new JComboBox<>(AccessModifier.values()); + ((JLabel) combo.getRenderer()).setHorizontalAlignment(JLabel.LEFT); + combo.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height))); + + EntryMapping mapping = controller.project.getMapper().getDeobfMapping(entry); + if (mapping != null) { + combo.setSelectedIndex(mapping.getAccessModifier().ordinal()); + } else { + combo.setSelectedIndex(AccessModifier.UNCHANGED.ordinal()); + } + + combo.addItemListener(controller::modifierChange); + + panel.add(combo); + + container.add(panel); + + return combo; + } + + public void onCaretMove(int pos, boolean fromClick) { + if (controller.project == null) + return; + EntryRemapper mapper = controller.project.getMapper(); + Token token = this.controller.getToken(pos); + boolean isToken = token != null; + + cursorReference = this.controller.getReference(token); + Entry referenceEntry = cursorReference != null ? cursorReference.entry : null; + + if (referenceEntry != null && shouldNavigateOnClick && fromClick) { + shouldNavigateOnClick = false; + Entry navigationEntry = referenceEntry; + if (cursorReference.context == null) { + EntryResolver resolver = mapper.getObfResolver(); + navigationEntry = resolver.resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT); + } + controller.navigateTo(navigationEntry); + return; + } + + boolean isClassEntry = isToken && referenceEntry instanceof ClassEntry; + boolean isFieldEntry = isToken && referenceEntry instanceof FieldEntry; + boolean isMethodEntry = isToken && referenceEntry instanceof MethodEntry && !((MethodEntry) referenceEntry).isConstructor(); + boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor(); + boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference); + + if (!isRenaming()) { + if (isToken) { + showCursorReference(cursorReference); + } else { + infoPanel.clearReference(); + } + } + + this.popupMenu.renameMenu.setEnabled(isRenamable); + this.popupMenu.editJavadocMenu.setEnabled(isRenamable); + this.popupMenu.showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry); + this.popupMenu.showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry); + this.popupMenu.showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry); + this.popupMenu.showCallsSpecificMenu.setEnabled(isMethodEntry); + this.popupMenu.openEntryMenu.setEnabled(isRenamable && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry)); + this.popupMenu.openPreviousMenu.setEnabled(this.controller.hasPreviousReference()); + this.popupMenu.openNextMenu.setEnabled(this.controller.hasNextReference()); + this.popupMenu.toggleMappingMenu.setEnabled(isRenamable); + + if (isToken && !Objects.equals(referenceEntry, mapper.deobfuscate(referenceEntry))) { + this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.reset_obfuscated")); + } else { + this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.mark_deobfuscated")); + } + } + + public void startDocChange() { + EntryReference, Entry> curReference = cursorReference; + if (isRenaming()) { + finishRename(false); + } + renamingReference = curReference; + + // init the text box + javadocTextArea = new JTextArea(10, 40); + + EntryReference, Entry> translatedReference = controller.project.getMapper().deobfuscate(cursorReference); + javadocTextArea.setText(Strings.nullToEmpty(translatedReference.entry.getJavadocs())); + + JavadocDialog.init(frame, javadocTextArea, this::finishDocChange); + javadocTextArea.grabFocus(); + + redraw(); + } + + private void finishDocChange(JFrame ui, boolean saveName) { + String newName = javadocTextArea.getText(); + if (saveName) { + try { + this.controller.changeDocs(renamingReference, newName); + this.controller.sendPacket(new ChangeDocsC2SPacket(renamingReference.getNameableEntry(), newName)); + } catch (IllegalNameException ex) { + javadocTextArea.setBorder(BorderFactory.createLineBorder(Color.red, 1)); + javadocTextArea.setToolTipText(ex.getReason()); + GuiUtil.showToolTipNow(javadocTextArea); + return; + } + + ui.setVisible(false); + showCursorReference(cursorReference); + return; + } + + // abort the jd change + javadocTextArea = null; + ui.setVisible(false); + showCursorReference(cursorReference); + + this.editor.grabFocus(); + + redraw(); + } + + public void startRename() { + + // init the text box + renameTextField = new JTextField(); + + EntryReference, Entry> translatedReference = controller.project.getMapper().deobfuscate(cursorReference); + renameTextField.setText(translatedReference.getNameableName()); + + renameTextField.setPreferredSize(ScaleUtil.getDimension(360, ScaleUtil.invert(renameTextField.getPreferredSize().height))); + renameTextField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.VK_ENTER: + finishRename(true); + break; + + case KeyEvent.VK_ESCAPE: + finishRename(false); + break; + default: + break; + } + } + }); + + // find the label with the name and replace it with the text box + JPanel panel = (JPanel) infoPanel.getComponent(0); + panel.remove(panel.getComponentCount() - 1); + panel.add(renameTextField); + renameTextField.grabFocus(); + + int offset = renameTextField.getText().lastIndexOf('/') + 1; + // If it's a class and isn't in the default package, assume that it's deobfuscated. + if (translatedReference.getNameableEntry() instanceof ClassEntry && renameTextField.getText().contains("/") && offset != 0) + renameTextField.select(offset, renameTextField.getText().length()); + else + renameTextField.selectAll(); + + renamingReference = cursorReference; + + redraw(); + } + + private void finishRename(boolean saveName) { + String newName = renameTextField.getText(); + + if (saveName && newName != null && !newName.isEmpty()) { + try { + this.controller.rename(renamingReference, newName, true); + this.controller.sendPacket(new RenameC2SPacket(renamingReference.getNameableEntry(), newName, true)); + renameTextField = null; + } catch (IllegalNameException ex) { + renameTextField.setBorder(BorderFactory.createLineBorder(Color.red, 1)); + renameTextField.setToolTipText(ex.getReason()); + GuiUtil.showToolTipNow(renameTextField); + } + return; + } + + renameTextField = null; + + // abort the rename + showCursorReference(cursorReference); + + this.editor.grabFocus(); + + redraw(); + } + + private boolean isRenaming() { + return renameTextField != null; + } + + public void showInheritance() { + + if (cursorReference == null) { + return; + } + + inheritanceTree.setModel(null); + + if (cursorReference.entry instanceof ClassEntry) { + // get the class inheritance + ClassInheritanceTreeNode classNode = this.controller.getClassInheritance((ClassEntry) cursorReference.entry); + + // show the tree at the root + TreePath path = getPathToRoot(classNode); + inheritanceTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0))); + inheritanceTree.expandPath(path); + inheritanceTree.setSelectionRow(inheritanceTree.getRowForPath(path)); + } else if (cursorReference.entry instanceof MethodEntry) { + // get the method inheritance + MethodInheritanceTreeNode classNode = this.controller.getMethodInheritance((MethodEntry) cursorReference.entry); + + // show the tree at the root + TreePath path = getPathToRoot(classNode); + inheritanceTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0))); + inheritanceTree.expandPath(path); + inheritanceTree.setSelectionRow(inheritanceTree.getRowForPath(path)); + } + + tabs.setSelectedIndex(0); + + redraw(); + } + + public void showImplementations() { + + if (cursorReference == null) { + return; + } + + implementationsTree.setModel(null); + + DefaultMutableTreeNode node = null; + + // get the class implementations + if (cursorReference.entry instanceof ClassEntry) + node = this.controller.getClassImplementations((ClassEntry) cursorReference.entry); + else // get the method implementations + if (cursorReference.entry instanceof MethodEntry) + node = this.controller.getMethodImplementations((MethodEntry) cursorReference.entry); + + if (node != null) { + // show the tree at the root + TreePath path = getPathToRoot(node); + implementationsTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0))); + implementationsTree.expandPath(path); + implementationsTree.setSelectionRow(implementationsTree.getRowForPath(path)); + } + + tabs.setSelectedIndex(1); + + redraw(); + } + + public void showCalls(boolean recurse) { + if (cursorReference == null) { + return; + } + + if (cursorReference.entry instanceof ClassEntry) { + ClassReferenceTreeNode node = this.controller.getClassReferences((ClassEntry) cursorReference.entry); + callsTree.setModel(new DefaultTreeModel(node)); + } else if (cursorReference.entry instanceof FieldEntry) { + FieldReferenceTreeNode node = this.controller.getFieldReferences((FieldEntry) cursorReference.entry); + callsTree.setModel(new DefaultTreeModel(node)); + } else if (cursorReference.entry instanceof MethodEntry) { + MethodReferenceTreeNode node = this.controller.getMethodReferences((MethodEntry) cursorReference.entry, recurse); + callsTree.setModel(new DefaultTreeModel(node)); + } + + tabs.setSelectedIndex(2); + + redraw(); + } + + public void toggleMapping() { + Entry obfEntry = cursorReference.entry; + Entry deobfEntry = controller.project.getMapper().deobfuscate(obfEntry); + + if (!Objects.equals(obfEntry, deobfEntry)) { + this.controller.removeMapping(cursorReference); + this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry())); + } else { + this.controller.markAsDeobfuscated(cursorReference); + this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry())); + } + } + + 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()); + } + + public void showDiscardDiag(Function callback, String... options) { + int response = JOptionPane.showOptionDialog(this.frame, I18n.translate("prompt.close.summary"), I18n.translate("prompt.close.title"), JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, null, options, options[2]); + callback.apply(response); + } + + public void saveMapping() { + if (this.enigmaMappingsFileChooser.getSelectedFile() != null || this.enigmaMappingsFileChooser.showSaveDialog(this.frame) == JFileChooser.APPROVE_OPTION) + this.controller.saveMappings(this.enigmaMappingsFileChooser.getSelectedFile().toPath()); + } + + public void close() { + if (!this.controller.isDirty()) { + // everything is saved, we can exit safely + exit(); + } else { + // ask to save before closing + showDiscardDiag((response) -> { + if (response == JOptionPane.YES_OPTION) { + this.saveMapping(); + exit(); + } else if (response == JOptionPane.NO_OPTION) { + exit(); + } + + return null; + }, I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.close.cancel")); + } + } + + private void exit() { + if (searchDialog != null) { + searchDialog.dispose(); + } + this.frame.dispose(); + System.exit(0); + } + + public void redraw() { + this.frame.validate(); + this.frame.repaint(); + } + + public void onPanelRename(Object prevData, Object data, DefaultMutableTreeNode node) throws IllegalNameException { + // package rename + if (data instanceof String) { + for (int i = 0; i < node.getChildCount(); i++) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt(i); + ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject(); + ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName()); + this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false); + this.controller.sendPacket(new RenameC2SPacket(prevDataChild, dataChild.getFullName(), false)); + childNode.setUserObject(dataChild); + } + node.setUserObject(data); + // Ob package will never be modified, just reload deob view + this.deobfPanel.deobfClasses.reload(); + } + // class rename + else if (data instanceof ClassEntry) { + this.controller.rename(new EntryReference<>((ClassEntry) prevData, ((ClassEntry) prevData).getFullName()), ((ClassEntry) data).getFullName(), false); + this.controller.sendPacket(new RenameC2SPacket((ClassEntry) prevData, ((ClassEntry) data).getFullName(), false)); + } + } + + public void moveClassTree(EntryReference, Entry> obfReference, String newName) { + String oldEntry = obfReference.entry.getContainingClass().getPackageName(); + String newEntry = new ClassEntry(newName).getPackageName(); + moveClassTree(obfReference, oldEntry == null, newEntry == null); + } + + // TODO: getExpansionState will *not* actually update itself based on name changes! + public void moveClassTree(EntryReference, Entry> obfReference, boolean isOldOb, boolean isNewOb) { + ClassEntry classEntry = obfReference.entry.getContainingClass(); + + List stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(this.deobfPanel.deobfClasses); + List stateObf = this.obfPanel.obfClasses.getExpansionState(this.obfPanel.obfClasses); + + // Ob -> deob + if (!isNewOb) { + this.deobfPanel.deobfClasses.moveClassIn(classEntry); + this.obfPanel.obfClasses.moveClassOut(classEntry); + this.deobfPanel.deobfClasses.reload(); + this.obfPanel.obfClasses.reload(); + } + // Deob -> ob + else if (!isOldOb) { + this.obfPanel.obfClasses.moveClassIn(classEntry); + this.deobfPanel.deobfClasses.moveClassOut(classEntry); + this.deobfPanel.deobfClasses.reload(); + this.obfPanel.obfClasses.reload(); + } + // Local move + else if (isOldOb) { + this.obfPanel.obfClasses.moveClassIn(classEntry); + this.obfPanel.obfClasses.reload(); + } else { + this.deobfPanel.deobfClasses.moveClassIn(classEntry); + this.deobfPanel.deobfClasses.reload(); + } + + this.deobfPanel.deobfClasses.restoreExpansionState(this.deobfPanel.deobfClasses, stateDeobf); + this.obfPanel.obfClasses.restoreExpansionState(this.obfPanel.obfClasses, stateObf); + } + + public PanelObf getObfPanel() { + return obfPanel; + } + + public PanelDeobf getDeobfPanel() { + return deobfPanel; + } + + public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) { + this.shouldNavigateOnClick = shouldNavigateOnClick; + } + + public SearchDialog getSearchDialog() { + if (searchDialog == null) { + searchDialog = new SearchDialog(this); + } + return searchDialog; + } + + + public MenuBar getMenuBar() { + return menuBar; + } + + public void addMessage(Message message) { + JScrollBar verticalScrollBar = messageScrollPane.getVerticalScrollBar(); + boolean isAtBottom = verticalScrollBar.getValue() >= verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent(); + messageModel.addElement(message); + if (isAtBottom) { + SwingUtilities.invokeLater(() -> verticalScrollBar.setValue(verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent())); + } + statusLabel.setText(message.translate()); + } + + public void setUserList(List users) { + userModel.clear(); + users.forEach(userModel::addElement); + connectionStatusLabel.setText(String.format(I18n.translate("status.connected_user_count"), users.size())); + } + + private void sendMessage() { + String text = chatBox.getText().trim(); + if (!text.isEmpty()) { + getController().sendPacket(new MessageC2SPacket(text)); + } + chatBox.setText(""); + } + + /** + * Updates the state of the UI elements (button text, enabled state, ...) to reflect the current program state. + * This is a central place to update the UI state to prevent multiple code paths from changing the same state, + * causing inconsistencies. + */ + public void updateUiState() { + menuBar.connectToServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.HOSTING); + menuBar.connectToServerMenu.setText(I18n.translate(connectionState != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect")); + menuBar.startServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.CONNECTED); + menuBar.startServerMenu.setText(I18n.translate(connectionState != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop")); + + menuBar.closeJarMenu.setEnabled(isJarOpen); + menuBar.openMappingsMenus.forEach(item -> item.setEnabled(isJarOpen)); + menuBar.saveMappingsMenu.setEnabled(isJarOpen && enigmaMappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED); + menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(isJarOpen)); + menuBar.closeMappingsMenu.setEnabled(isJarOpen); + menuBar.exportSourceMenu.setEnabled(isJarOpen); + menuBar.exportJarMenu.setEnabled(isJarOpen); + + connectionStatusLabel.setText(I18n.translate(connectionState == ConnectionState.NOT_CONNECTED ? "status.disconnected" : "status.connected")); + + if (connectionState == ConnectionState.NOT_CONNECTED) { + logSplit.setLeftComponent(null); + splitRight.setRightComponent(tabs); + } else { + splitRight.setRightComponent(logSplit); + logSplit.setLeftComponent(tabs); + } + } + + public void setConnectionState(ConnectionState state) { + connectionState = state; + statusLabel.setText(I18n.translate("status.ready")); + updateUiState(); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java new file mode 100644 index 00000000..94979e77 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -0,0 +1,719 @@ +/******************************************************************************* + * 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.util.concurrent.ThreadFactoryBuilder; +import cuchaz.enigma.Enigma; +import cuchaz.enigma.EnigmaProfile; +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.analysis.*; +import cuchaz.enigma.api.service.ObfuscationTestService; +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.gui.dialog.ProgressDialog; +import cuchaz.enigma.gui.stats.StatsGenerator; +import cuchaz.enigma.gui.stats.StatsMember; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.gui.util.History; +import cuchaz.enigma.network.*; +import cuchaz.enigma.network.packet.LoginC2SPacket; +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.source.*; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.*; +import cuchaz.enigma.translation.mapping.serde.MappingFormat; +import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.Utils; + +import javax.annotation.Nullable; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.io.*; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GuiController implements ClientPacketHandler { + private static final ExecutorService DECOMPILER_SERVICE = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("decompiler-thread") + .build() + ); + + private final Gui gui; + public final Enigma enigma; + + public EnigmaProject project; + private DecompilerService decompilerService; + private Decompiler decompiler; + private IndexTreeBuilder indexTreeBuilder; + + private Path loadedMappingPath; + private MappingFormat loadedMappingFormat; + + private DecompiledClassSource currentSource; + private Source uncommentedSource; + + private EnigmaClient client; + private EnigmaServer server; + + public GuiController(Gui gui, EnigmaProfile profile) { + this.gui = gui; + this.enigma = Enigma.builder() + .setProfile(profile) + .build(); + + decompilerService = Config.getInstance().decompiler.service; + } + + public boolean isDirty() { + return project != null && project.getMapper().isDirty(); + } + + public CompletableFuture openJar(final Path jarPath) { + this.gui.onStartOpenJar(); + + return ProgressDialog.runOffThread(gui.getFrame(), progress -> { + project = enigma.openJar(jarPath, progress); + indexTreeBuilder = new IndexTreeBuilder(project.getJarIndex()); + decompiler = project.createDecompiler(decompilerService); + gui.onFinishOpenJar(jarPath.getFileName().toString()); + refreshClasses(); + }); + } + + public void closeJar() { + this.project = null; + this.gui.onCloseJar(); + } + + public CompletableFuture openMappings(MappingFormat format, Path path) { + if (project == null) return CompletableFuture.completedFuture(null); + + gui.setMappingsFile(path); + + return ProgressDialog.runOffThread(gui.getFrame(), progress -> { + try { + MappingSaveParameters saveParameters = enigma.getProfile().getMappingSaveParameters(); + + EntryTree mappings = format.read(path, progress, saveParameters); + project.setMappings(mappings); + + loadedMappingFormat = format; + loadedMappingPath = path; + + refreshClasses(); + refreshCurrentClass(); + } catch (MappingParseException e) { + JOptionPane.showMessageDialog(gui.getFrame(), e.getMessage()); + } + }); + } + + @Override + public void openMappings(EntryTree mappings) { + if (project == null) return; + + project.setMappings(mappings); + refreshClasses(); + refreshCurrentClass(); + } + + public CompletableFuture saveMappings(Path path) { + return saveMappings(path, loadedMappingFormat); + } + + public CompletableFuture saveMappings(Path path, MappingFormat format) { + if (project == null) return CompletableFuture.completedFuture(null); + + return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> { + EntryRemapper mapper = project.getMapper(); + MappingSaveParameters saveParameters = enigma.getProfile().getMappingSaveParameters(); + + MappingDelta delta = mapper.takeMappingDelta(); + boolean saveAll = !path.equals(loadedMappingPath); + + loadedMappingFormat = format; + loadedMappingPath = path; + + if (saveAll) { + format.write(mapper.getObfToDeobf(), path, progress, saveParameters); + } else { + format.write(mapper.getObfToDeobf(), delta, path, progress, saveParameters); + } + }); + } + + public void closeMappings() { + if (project == null) return; + + project.setMappings(null); + + this.gui.setMappingsFile(null); + refreshClasses(); + refreshCurrentClass(); + } + + public CompletableFuture dropMappings() { + if (project == null) return CompletableFuture.completedFuture(null); + + return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> project.dropMappings(progress)); + } + + public CompletableFuture exportSource(final Path path) { + if (project == null) return CompletableFuture.completedFuture(null); + + return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> { + EnigmaProject.JarExport jar = project.exportRemappedJar(progress); + EnigmaProject.SourceExport source = jar.decompile(progress, decompilerService); + + source.write(path, progress); + }); + } + + public CompletableFuture exportJar(final Path path) { + if (project == null) return CompletableFuture.completedFuture(null); + + return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> { + EnigmaProject.JarExport jar = project.exportRemappedJar(progress); + jar.write(path, progress); + }); + } + + public Token getToken(int pos) { + if (this.currentSource == null) { + return null; + } + return this.currentSource.getIndex().getReferenceToken(pos); + } + + @Nullable + public EntryReference, Entry> getReference(Token token) { + if (this.currentSource == null) { + return null; + } + return this.currentSource.getIndex().getReference(token); + } + + public ReadableToken getReadableToken(Token token) { + if (this.currentSource == null) { + return null; + } + + SourceIndex index = this.currentSource.getIndex(); + return new ReadableToken( + index.getLineNumber(token.start), + index.getColumnNumber(token.start), + index.getColumnNumber(token.end) + ); + } + + /** + * Navigates to the declaration with respect to navigation history + * + * @param entry the entry whose declaration will be navigated to + */ + public void openDeclaration(Entry entry) { + if (entry == null) { + throw new IllegalArgumentException("Entry cannot be null!"); + } + openReference(new EntryReference<>(entry, entry.getName())); + } + + /** + * Navigates to the reference with respect to navigation history + * + * @param reference the reference + */ + public void openReference(EntryReference, Entry> reference) { + if (reference == null) { + throw new IllegalArgumentException("Reference cannot be null!"); + } + if (this.gui.referenceHistory == null) { + this.gui.referenceHistory = new History<>(reference); + } else { + if (!reference.equals(this.gui.referenceHistory.getCurrent())) { + this.gui.referenceHistory.push(reference); + } + } + setReference(reference); + } + + /** + * Navigates to the reference without modifying history. If the class is not currently loaded, it will be loaded. + * + * @param reference the reference + */ + private void setReference(EntryReference, Entry> reference) { + // get the reference target class + ClassEntry classEntry = reference.getLocationClassEntry(); + if (!project.isRenamable(classEntry)) { + throw new IllegalArgumentException("Obfuscated class " + classEntry + " was not found in the jar!"); + } + + if (this.currentSource == null || !this.currentSource.getEntry().equals(classEntry)) { + // deobfuscate the class, then navigate to the reference + loadClass(classEntry, () -> showReference(reference)); + } else { + showReference(reference); + } + } + + /** + * Navigates to the reference without modifying history. Assumes the class is loaded. + * + * @param reference + */ + private void showReference(EntryReference, Entry> reference) { + Collection tokens = getTokensForReference(reference); + if (tokens.isEmpty()) { + // DEBUG + System.err.println(String.format("WARNING: no tokens found for %s in %s", reference, this.currentSource.getEntry())); + } else { + this.gui.showTokens(tokens); + } + } + + public Collection getTokensForReference(EntryReference, Entry> reference) { + EntryRemapper mapper = this.project.getMapper(); + + SourceIndex index = this.currentSource.getIndex(); + return mapper.getObfResolver().resolveReference(reference, ResolutionStrategy.RESOLVE_CLOSEST) + .stream() + .flatMap(r -> index.getReferenceTokens(r).stream()) + .collect(Collectors.toList()); + } + + public void openPreviousReference() { + if (hasPreviousReference()) { + setReference(gui.referenceHistory.goBack()); + } + } + + public boolean hasPreviousReference() { + return gui.referenceHistory != null && gui.referenceHistory.canGoBack(); + } + + public void openNextReference() { + if (hasNextReference()) { + setReference(gui.referenceHistory.goForward()); + } + } + + public boolean hasNextReference() { + return gui.referenceHistory != null && gui.referenceHistory.canGoForward(); + } + + public void navigateTo(Entry entry) { + if (!project.isRenamable(entry)) { + // entry is not in the jar. Ignore it + return; + } + openDeclaration(entry); + } + + public void navigateTo(EntryReference, Entry> reference) { + if (!project.isRenamable(reference.getLocationClassEntry())) { + return; + } + openReference(reference); + } + + private void refreshClasses() { + List obfClasses = Lists.newArrayList(); + List deobfClasses = Lists.newArrayList(); + this.addSeparatedClasses(obfClasses, deobfClasses); + this.gui.setObfClasses(obfClasses); + this.gui.setDeobfClasses(deobfClasses); + } + + public void addSeparatedClasses(List obfClasses, List deobfClasses) { + EntryRemapper mapper = project.getMapper(); + + Collection classes = project.getJarIndex().getEntryIndex().getClasses(); + Stream visibleClasses = classes.stream() + .filter(entry -> !entry.isInnerClass()); + + visibleClasses.forEach(entry -> { + ClassEntry deobfEntry = mapper.deobfuscate(entry); + + List obfService = enigma.getServices().get(ObfuscationTestService.TYPE); + boolean obfuscated = deobfEntry.equals(entry); + + if (obfuscated && !obfService.isEmpty()) { + if (obfService.stream().anyMatch(service -> service.testDeobfuscated(entry))) { + obfuscated = false; + } + } + + if (obfuscated) { + obfClasses.add(entry); + } else { + deobfClasses.add(entry); + } + }); + } + + public void refreshCurrentClass() { + refreshCurrentClass(null); + } + + private void refreshCurrentClass(EntryReference, Entry> reference) { + refreshCurrentClass(reference, RefreshMode.MINIMAL); + } + + private void refreshCurrentClass(EntryReference, Entry> reference, RefreshMode mode) { + if (currentSource != null) { + if (reference == null) { + int obfSelectionStart = currentSource.getObfuscatedOffset(gui.editor.getSelectionStart()); + int obfSelectionEnd = currentSource.getObfuscatedOffset(gui.editor.getSelectionEnd()); + + Rectangle viewportBounds = gui.sourceScroller.getViewport().getViewRect(); + // Here we pick an "anchor position", which we want to stay in the same vertical location on the screen after the new text has been set + int anchorModelPos = gui.editor.getSelectionStart(); + Rectangle anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos); + if (anchorViewPos.y < viewportBounds.y || anchorViewPos.y >= viewportBounds.y + viewportBounds.height) { + anchorModelPos = gui.editor.viewToModel(new Point(0, viewportBounds.y)); + anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos); + } + int obfAnchorPos = currentSource.getObfuscatedOffset(anchorModelPos); + Rectangle anchorViewPos_f = anchorViewPos; + int scrollX = gui.sourceScroller.getHorizontalScrollBar().getValue(); + + loadClass(currentSource.getEntry(), () -> SwingUtilities.invokeLater(() -> { + int newAnchorModelPos = currentSource.getDeobfuscatedOffset(obfAnchorPos); + Rectangle newAnchorViewPos = GuiUtil.safeModelToView(gui.editor, newAnchorModelPos); + int newScrollY = newAnchorViewPos.y - (anchorViewPos_f.y - viewportBounds.y); + + gui.editor.select(currentSource.getDeobfuscatedOffset(obfSelectionStart), currentSource.getDeobfuscatedOffset(obfSelectionEnd)); + // Changing the selection scrolls to the caret position inside a SwingUtilities.invokeLater call, so + // we need to wrap our change to the scroll position inside another invokeLater so it happens after + // the caret's own scrolling. + SwingUtilities.invokeLater(() -> { + gui.sourceScroller.getHorizontalScrollBar().setValue(Math.min(scrollX, gui.sourceScroller.getHorizontalScrollBar().getMaximum())); + gui.sourceScroller.getVerticalScrollBar().setValue(Math.min(newScrollY, gui.sourceScroller.getVerticalScrollBar().getMaximum())); + }); + }), mode); + } else { + loadClass(currentSource.getEntry(), () -> showReference(reference), mode); + } + } + } + + private void loadClass(ClassEntry classEntry, Runnable callback) { + loadClass(classEntry, callback, RefreshMode.MINIMAL); + } + + private void loadClass(ClassEntry classEntry, Runnable callback, RefreshMode mode) { + ClassEntry targetClass = classEntry.getOutermostClass(); + + boolean requiresDecompile = mode == RefreshMode.FULL || currentSource == null || !currentSource.getEntry().equals(targetClass); + if (requiresDecompile) { + currentSource = null; // Or the GUI may try to find a nonexistent token + gui.setEditorText(I18n.translate("info_panel.editor.class.decompiling")); + } + + DECOMPILER_SERVICE.submit(() -> { + try { + if (requiresDecompile || mode == RefreshMode.JAVADOCS) { + currentSource = decompileSource(targetClass, mode == RefreshMode.JAVADOCS); + } + + remapSource(project.getMapper().getDeobfuscator()); + callback.run(); + } catch (Throwable t) { + System.err.println("An exception was thrown while decompiling class " + classEntry.getFullName()); + t.printStackTrace(System.err); + } + }); + } + + private DecompiledClassSource decompileSource(ClassEntry targetClass, boolean onlyRefreshJavadocs) { + try { + if (!onlyRefreshJavadocs || currentSource == null || !currentSource.getEntry().equals(targetClass)) { + uncommentedSource = decompiler.getSource(targetClass.getFullName()); + } + + Source source = uncommentedSource.addJavadocs(project.getMapper()); + + if (source == null) { + gui.setEditorText(I18n.translate("info_panel.editor.class.not_found") + " " + targetClass); + return DecompiledClassSource.text(targetClass, "Unable to find class"); + } + + SourceIndex index = source.index(); + index.resolveReferences(project.getMapper().getObfResolver()); + + return new DecompiledClassSource(targetClass, index); + } catch (Throwable t) { + StringWriter traceWriter = new StringWriter(); + t.printStackTrace(new PrintWriter(traceWriter)); + + return DecompiledClassSource.text(targetClass, traceWriter.toString()); + } + } + + private void remapSource(Translator translator) { + if (currentSource == null) { + return; + } + + currentSource.remapSource(project, translator); + + gui.setEditorTheme(Config.getInstance().lookAndFeel); + gui.setSource(currentSource); + } + + public void modifierChange(ItemEvent event) { + if (event.getStateChange() == ItemEvent.SELECTED) { + EntryRemapper mapper = project.getMapper(); + Entry entry = gui.cursorReference.entry; + AccessModifier modifier = (AccessModifier) event.getItem(); + + EntryMapping mapping = mapper.getDeobfMapping(entry); + if (mapping != null) { + mapper.mapFromObf(entry, new EntryMapping(mapping.getTargetName(), modifier)); + } else { + mapper.mapFromObf(entry, new EntryMapping(entry.getName(), modifier)); + } + + refreshCurrentClass(); + } + } + + public ClassInheritanceTreeNode getClassInheritance(ClassEntry entry) { + Translator translator = project.getMapper().getDeobfuscator(); + ClassInheritanceTreeNode rootNode = indexTreeBuilder.buildClassInheritance(translator, entry); + return ClassInheritanceTreeNode.findNode(rootNode, entry); + } + + public ClassImplementationsTreeNode getClassImplementations(ClassEntry entry) { + Translator translator = project.getMapper().getDeobfuscator(); + return this.indexTreeBuilder.buildClassImplementations(translator, entry); + } + + public MethodInheritanceTreeNode getMethodInheritance(MethodEntry entry) { + Translator translator = project.getMapper().getDeobfuscator(); + MethodInheritanceTreeNode rootNode = indexTreeBuilder.buildMethodInheritance(translator, entry); + return MethodInheritanceTreeNode.findNode(rootNode, entry); + } + + public MethodImplementationsTreeNode getMethodImplementations(MethodEntry entry) { + Translator translator = project.getMapper().getDeobfuscator(); + List rootNodes = indexTreeBuilder.buildMethodImplementations(translator, entry); + if (rootNodes.isEmpty()) { + return null; + } + if (rootNodes.size() > 1) { + System.err.println("WARNING: Method " + entry + " implements multiple interfaces. Only showing first one."); + } + return MethodImplementationsTreeNode.findNode(rootNodes.get(0), entry); + } + + public ClassReferenceTreeNode getClassReferences(ClassEntry entry) { + Translator deobfuscator = project.getMapper().getDeobfuscator(); + ClassReferenceTreeNode rootNode = new ClassReferenceTreeNode(deobfuscator, entry); + rootNode.load(project.getJarIndex(), true); + return rootNode; + } + + public FieldReferenceTreeNode getFieldReferences(FieldEntry entry) { + Translator translator = project.getMapper().getDeobfuscator(); + FieldReferenceTreeNode rootNode = new FieldReferenceTreeNode(translator, entry); + rootNode.load(project.getJarIndex(), true); + return rootNode; + } + + public MethodReferenceTreeNode getMethodReferences(MethodEntry entry, boolean recursive) { + Translator translator = project.getMapper().getDeobfuscator(); + MethodReferenceTreeNode rootNode = new MethodReferenceTreeNode(translator, entry); + rootNode.load(project.getJarIndex(), true, recursive); + return rootNode; + } + + public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree) { + rename(reference, newName, refreshClassTree, true); + } + + @Override + public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean jumpToReference) { + Entry entry = reference.getNameableEntry(); + project.getMapper().mapFromObf(entry, new EntryMapping(newName)); + + if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) + this.gui.moveClassTree(reference, newName); + + refreshCurrentClass(jumpToReference ? reference : null); + } + + public void removeMapping(EntryReference, Entry> reference) { + removeMapping(reference, true); + } + + @Override + public void removeMapping(EntryReference, Entry> reference, boolean jumpToReference) { + project.getMapper().removeByObf(reference.getNameableEntry()); + + if (reference.entry instanceof ClassEntry) + this.gui.moveClassTree(reference, false, true); + refreshCurrentClass(jumpToReference ? reference : null); + } + + public void changeDocs(EntryReference, Entry> reference, String updatedDocs) { + changeDocs(reference, updatedDocs, true); + } + + @Override + public void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference) { + changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs); + + refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS); + } + + private void changeDoc(Entry obfEntry, String newDoc) { + EntryRemapper mapper = project.getMapper(); + if (mapper.getDeobfMapping(obfEntry) == null) { + markAsDeobfuscated(obfEntry, false); // NPE + } + mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); + } + + private void markAsDeobfuscated(Entry obfEntry, boolean renaming) { + EntryRemapper mapper = project.getMapper(); + mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming); + } + + public void markAsDeobfuscated(EntryReference, Entry> reference) { + markAsDeobfuscated(reference, true); + } + + @Override + public void markAsDeobfuscated(EntryReference, Entry> reference, boolean jumpToReference) { + EntryRemapper mapper = project.getMapper(); + Entry entry = reference.getNameableEntry(); + mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName())); + + if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) + this.gui.moveClassTree(reference, true, false); + + refreshCurrentClass(jumpToReference ? reference : null); + } + + public void openStats(Set includedMembers) { + ProgressDialog.runOffThread(gui.getFrame(), progress -> { + String data = new StatsGenerator(project).generate(progress, includedMembers); + + try { + File statsFile = File.createTempFile("stats", ".html"); + + try (FileWriter w = new FileWriter(statsFile)) { + w.write( + Utils.readResourceToString("/stats.html") + .replace("/*data*/", data) + ); + } + + Desktop.getDesktop().open(statsFile); + } catch (IOException e) { + throw new Error(e); + } + }); + } + + public void setDecompiler(DecompilerService service) { + uncommentedSource = null; + decompilerService = service; + decompiler = project.createDecompiler(decompilerService); + refreshCurrentClass(null, RefreshMode.FULL); + } + + public EnigmaClient getClient() { + return client; + } + + public EnigmaServer getServer() { + return server; + } + + public void createClient(String username, String ip, int port, char[] password) throws IOException { + client = new EnigmaClient(this, ip, port); + client.connect(); + client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, username)); + gui.setConnectionState(ConnectionState.CONNECTED); + } + + public void createServer(int port, char[] password) throws IOException { + server = new IntegratedEnigmaServer(project.getJarChecksum(), password, EntryRemapper.mapped(project.getJarIndex(), new HashEntryTree<>(project.getMapper().getObfToDeobf())), port); + server.start(); + client = new EnigmaClient(this, "127.0.0.1", port); + client.connect(); + client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, EnigmaServer.OWNER_USERNAME)); + gui.setConnectionState(ConnectionState.HOSTING); + } + + @Override + public synchronized void disconnectIfConnected(String reason) { + if (client == null && server == null) { + return; + } + + if (client != null) { + client.disconnect(); + } + if (server != null) { + server.stop(); + } + client = null; + server = null; + SwingUtilities.invokeLater(() -> { + if (reason != null) { + JOptionPane.showMessageDialog(gui.getFrame(), I18n.translate(reason), I18n.translate("disconnect.disconnected"), JOptionPane.INFORMATION_MESSAGE); + } + gui.setConnectionState(ConnectionState.NOT_CONNECTED); + }); + } + + @Override + public void sendPacket(Packet packet) { + if (client != null) { + client.sendPacket(packet); + } + } + + @Override + public void addMessage(Message message) { + gui.addMessage(message); + } + + @Override + public void updateUserList(List users) { + gui.setUserList(users); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java new file mode 100644 index 00000000..1f3aa2cb --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * 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.EnigmaProfile; +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.translation.mapping.serde.MappingFormat; + +import cuchaz.enigma.utils.I18n; +import joptsimple.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.google.common.io.MoreFiles; + +public class Main { + + public static void main(String[] args) throws IOException { + OptionParser parser = new OptionParser(); + + OptionSpec jar = parser.accepts("jar", "Jar file to open at startup") + .withRequiredArg() + .withValuesConvertedBy(PathConverter.INSTANCE); + + OptionSpec mappings = parser.accepts("mappings", "Mappings file to open at startup") + .withRequiredArg() + .withValuesConvertedBy(PathConverter.INSTANCE); + + OptionSpec profile = parser.accepts("profile", "Profile json to apply at startup") + .withRequiredArg() + .withValuesConvertedBy(PathConverter.INSTANCE); + + parser.accepts("help", "Displays help information"); + + try { + OptionSet options = parser.parse(args); + + if (options.has("help")) { + parser.printHelpOn(System.out); + return; + } + + EnigmaProfile parsedProfile = EnigmaProfile.read(options.valueOf(profile)); + + I18n.setLanguage(Config.getInstance().language); + Gui gui = new Gui(parsedProfile); + GuiController controller = gui.getController(); + + if (options.has(jar)) { + Path jarPath = options.valueOf(jar); + controller.openJar(jarPath) + .whenComplete((v, t) -> { + if (options.has(mappings)) { + Path mappingsPath = options.valueOf(mappings); + if (Files.isDirectory(mappingsPath)) { + controller.openMappings(MappingFormat.ENIGMA_DIRECTORY, mappingsPath); + } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsPath))) { + controller.openMappings(MappingFormat.ENIGMA_ZIP, mappingsPath); + } else { + controller.openMappings(MappingFormat.ENIGMA_FILE, mappingsPath); + } + } + }); + } + } catch (OptionException e) { + System.out.println("Invalid arguments: " + e.getMessage()); + System.out.println(); + parser.printHelpOn(System.out); + } + } + + public static class PathConverter implements ValueConverter { + public static final ValueConverter INSTANCE = new PathConverter(); + + PathConverter() { + } + + @Override + public Path convert(String path) { + // expand ~ to the home dir + if (path.startsWith("~")) { + // get the home dir + Path dirHome = Paths.get(System.getProperty("user.home")); + + // is the path just ~/ or is it ~user/ ? + if (path.startsWith("~/")) { + return dirHome.resolve(path.substring(2)); + } else { + return dirHome.getParent().resolve(path.substring(1)); + } + } + + return Paths.get(path); + } + + @Override + public Class valueType() { + return Path.class; + } + + @Override + public String valuePattern() { + return "path"; + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java new file mode 100644 index 00000000..1d603409 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java @@ -0,0 +1,24 @@ +package cuchaz.enigma.gui; + +import java.awt.Component; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; + +import cuchaz.enigma.network.Message; + +// For now, just render the translated text. +// TODO: Icons or something later? +public class MessageListCellRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + Message message = (Message) value; + if (message != null) { + setText(message.translate()); + } + return this; + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java new file mode 100644 index 00000000..1eead6eb --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * 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.analysis.MethodInheritanceTreeNode; +import cuchaz.enigma.gui.config.Config; + +import javax.swing.*; +import javax.swing.tree.TreeCellRenderer; +import java.awt.*; + +class MethodTreeCellRenderer implements TreeCellRenderer { + + private final TreeCellRenderer parent; + + MethodTreeCellRenderer(TreeCellRenderer parent) { + this.parent = parent; + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + Component ret = parent.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + Config config = Config.getInstance(); + if (!(value instanceof MethodInheritanceTreeNode) || ((MethodInheritanceTreeNode) value).isImplemented()) { + ret.setForeground(new Color(config.defaultTextColor)); + ret.setFont(ret.getFont().deriveFont(Font.PLAIN)); + } else { + ret.setForeground(new Color(config.numberColor)); + ret.setFont(ret.getFont().deriveFont(Font.ITALIC)); + } + return ret; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java new file mode 100644 index 00000000..b7fa2eba --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java @@ -0,0 +1,45 @@ +package cuchaz.enigma.gui; + +import de.sciss.syntaxpane.SyntaxDocument; +import de.sciss.syntaxpane.actions.DefaultSyntaxAction; + +import javax.swing.text.JTextComponent; +import java.awt.event.ActionEvent; + +public final class QuickFindAction extends DefaultSyntaxAction { + public QuickFindAction() { + super("quick-find"); + } + + @Override + public void actionPerformed(JTextComponent target, SyntaxDocument document, int dot, ActionEvent event) { + Data data = Data.get(target); + data.showFindDialog(target); + } + + private static class Data { + private static final String KEY = "enigma-find-data"; + private EnigmaQuickFindDialog findDialog; + + private Data() { + } + + public static Data get(JTextComponent target) { + Object o = target.getDocument().getProperty(KEY); + if (o instanceof Data) { + return (Data) o; + } + + Data data = new Data(); + target.getDocument().putProperty(KEY, data); + return data; + } + + public void showFindDialog(JTextComponent target) { + if (findDialog == null) { + findDialog = new EnigmaQuickFindDialog(target); + } + findDialog.showFor(target); + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java new file mode 100644 index 00000000..3e4b30cd --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.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; + +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() { + return "line " + line + " columns " + startColumn + "-" + endColumn; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java new file mode 100644 index 00000000..87cb83b2 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.gui; + +public enum RefreshMode { + MINIMAL, + JAVADOCS, + FULL +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java new file mode 100644 index 00000000..10c418c2 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.source.Token; + +import javax.swing.*; +import java.awt.*; + +public class TokenListCellRenderer implements ListCellRenderer { + + private GuiController controller; + private DefaultListCellRenderer defaultRenderer; + + public TokenListCellRenderer(GuiController controller) { + this.controller = controller; + this.defaultRenderer = new DefaultListCellRenderer(); + } + + @Override + public Component getListCellRendererComponent(JList list, Token token, int index, boolean isSelected, boolean hasFocus) { + JLabel label = (JLabel) this.defaultRenderer.getListCellRendererComponent(list, token, index, isSelected, hasFocus); + label.setText(this.controller.getReadableToken(token).toString()); + return label; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java new file mode 100644 index 00000000..373dcf04 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java @@ -0,0 +1,261 @@ +package cuchaz.enigma.gui.config; + +import com.bulenkov.darcula.DarculaLaf; +import com.google.common.io.Files; +import com.google.gson.*; +import cuchaz.enigma.source.DecompilerService; +import cuchaz.enigma.source.Decompilers; + +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import javax.swing.plaf.metal.MetalLookAndFeel; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; + +public class Config { + public static class AlphaColorEntry { + public Integer rgb; + public float alpha = 1.0f; + + public AlphaColorEntry(Integer rgb, float alpha) { + this.rgb = rgb; + this.alpha = alpha; + } + + public Color get() { + if (rgb == null) { + return new Color(0, 0, 0, 0); + } + + Color baseColor = new Color(rgb); + return new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), (int)(255 * alpha)); + } + } + + public enum LookAndFeel { + DEFAULT("Default"), + DARCULA("Darcula"), + SYSTEM("System"), + NONE("None (JVM default)"); + + // the "JVM default" look and feel, get it at the beginning and store it so we can set it later + private static javax.swing.LookAndFeel NONE_LAF = UIManager.getLookAndFeel(); + private final String name; + + LookAndFeel(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setGlobalLAF() { + try { + switch (this) { + case NONE: + UIManager.setLookAndFeel(NONE_LAF); + break; + case DEFAULT: + UIManager.setLookAndFeel(new MetalLookAndFeel()); + break; + case DARCULA: + UIManager.setLookAndFeel(new DarculaLaf()); + break; + case SYSTEM: + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + } catch (Exception e){ + throw new Error("Failed to set global look and feel", e); + } + } + + public static boolean isDarkLaf() { + // a bit of a hack because swing doesn't give any API for that, and we need colors that aren't defined in look and feel + JPanel panel = new JPanel(); + panel.setSize(new Dimension(10, 10)); + panel.doLayout(); + + BufferedImage image = new BufferedImage(panel.getSize().width, panel.getSize().height, BufferedImage.TYPE_INT_RGB); + panel.printAll(image.getGraphics()); + + Color c = new Color(image.getRGB(0, 0)); + + // convert the color we got to grayscale + int b = (int) (0.3 * c.getRed() + 0.59 * c.getGreen() + 0.11 * c.getBlue()); + return b < 85; + } + + public void apply(Config config) { + boolean isDark = this == LookAndFeel.DARCULA || isDarkLaf(); + if (!isDark) {//Defaults found here: https://github.com/Sciss/SyntaxPane/blob/122da367ff7a5d31627a70c62a48a9f0f4f85a0a/src/main/resources/de/sciss/syntaxpane/defaultsyntaxkit/config.properties#L139 + config.lineNumbersForeground = 0x333300; + config.lineNumbersBackground = 0xEEEEFF; + config.lineNumbersSelected = 0xCCCCEE; + config.obfuscatedColor = new AlphaColorEntry(0xFFDCDC, 1.0f); + config.obfuscatedColorOutline = new AlphaColorEntry(0xA05050, 1.0f); + config.proposedColor = new AlphaColorEntry(0x000000, 0.075f); + config.proposedColorOutline = new AlphaColorEntry(0x000000, 0.15f); + config.deobfuscatedColor = new AlphaColorEntry(0xDCFFDC, 1.0f); + config.deobfuscatedColorOutline = new AlphaColorEntry(0x50A050, 1.0f); + config.editorBackground = 0xFFFFFF; + config.highlightColor = 0x3333EE; + config.caretColor = 0x000000; + config.selectionHighlightColor = 0x000000; + config.stringColor = 0xCC6600; + config.numberColor = 0x999933; + config.operatorColor = 0x000000; + config.delimiterColor = 0x000000; + config.typeColor = 0x000000; + config.identifierColor = 0x000000; + config.defaultTextColor = 0x000000; + } else {//Based off colors found here: https://github.com/dracula/dracula-theme/ + config.lineNumbersForeground = 0xA4A4A3; + config.lineNumbersBackground = 0x313335; + config.lineNumbersSelected = 0x606366; + config.obfuscatedColor = new AlphaColorEntry(0xFF5555, 0.3f); + config.obfuscatedColorOutline = new AlphaColorEntry(0xFF5555, 0.5f); + config.deobfuscatedColor = new AlphaColorEntry(0x50FA7B, 0.3f); + config.deobfuscatedColorOutline = new AlphaColorEntry(0x50FA7B, 0.5f); + config.proposedColor = new AlphaColorEntry(0x606366, 0.3f); + config.proposedColorOutline = new AlphaColorEntry(0x606366, 0.5f); + config.editorBackground = 0x282A36; + config.highlightColor = 0xFF79C6; + config.caretColor = 0xF8F8F2; + config.selectionHighlightColor = 0xF8F8F2; + config.stringColor = 0xF1FA8C; + config.numberColor = 0xBD93F9; + config.operatorColor = 0xF8F8F2; + config.delimiterColor = 0xF8F8F2; + config.typeColor = 0xF8F8F2; + config.identifierColor = 0xF8F8F2; + config.defaultTextColor = 0xF8F8F2; + } + } + } + + public enum Decompiler { + PROCYON("Procyon", Decompilers.PROCYON), + CFR("CFR", Decompilers.CFR); + + public final DecompilerService service; + public final String name; + + Decompiler(String name, DecompilerService service) { + this.name = name; + this.service = service; + } + } + + private static final File DIR_HOME = new File(System.getProperty("user.home")); + private static final File ENIGMA_DIR = new File(DIR_HOME, ".enigma"); + private static final File CONFIG_FILE = new File(ENIGMA_DIR, "config.json"); + private static final Config INSTANCE = new Config(); + + private final transient Gson gson; // transient to exclude it from being exposed + + public AlphaColorEntry obfuscatedColor; + public AlphaColorEntry obfuscatedColorOutline; + public AlphaColorEntry proposedColor; + public AlphaColorEntry proposedColorOutline; + public AlphaColorEntry deobfuscatedColor; + public AlphaColorEntry deobfuscatedColorOutline; + + public Integer editorBackground; + public Integer highlightColor; + public Integer caretColor; + public Integer selectionHighlightColor; + + public Integer stringColor; + public Integer numberColor; + public Integer operatorColor; + public Integer delimiterColor; + public Integer typeColor; + public Integer identifierColor; + public Integer defaultTextColor; + + public Integer lineNumbersBackground; + public Integer lineNumbersSelected; + public Integer lineNumbersForeground; + + public String language = I18n.DEFAULT_LANGUAGE; + + public LookAndFeel lookAndFeel = LookAndFeel.DEFAULT; + + public float scaleFactor = 1.0f; + + public Decompiler decompiler = Decompiler.PROCYON; + + private Config() { + gson = new GsonBuilder() + .registerTypeAdapter(Integer.class, new IntSerializer()) + .registerTypeAdapter(Integer.class, new IntDeserializer()) + .registerTypeAdapter(Config.class, (InstanceCreator) type -> this) + .setPrettyPrinting() + .create(); + try { + this.loadConfig(); + } catch (IOException ignored) { + try { + this.reset(); + } catch (IOException ignored1) { + } + } + } + + public void loadConfig() throws IOException { + if (!ENIGMA_DIR.exists()) ENIGMA_DIR.mkdirs(); + File configFile = new File(ENIGMA_DIR, "config.json"); + boolean loaded = false; + + if (configFile.exists()) { + try { + gson.fromJson(Files.asCharSource(configFile, Charset.defaultCharset()).read(), Config.class); + loaded = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!loaded) { + this.reset(); + Files.touch(configFile); + } + saveConfig(); + } + + public void saveConfig() throws IOException { + Files.asCharSink(CONFIG_FILE, Charset.defaultCharset()).write(gson.toJson(this)); + } + + public void reset() throws IOException { + this.lookAndFeel = LookAndFeel.DEFAULT; + this.lookAndFeel.apply(this); + this.decompiler = Decompiler.PROCYON; + this.language = I18n.DEFAULT_LANGUAGE; + this.saveConfig(); + } + + private static class IntSerializer implements JsonSerializer { + @Override + public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("#" + Integer.toHexString(src).toUpperCase()); + } + } + + private static class IntDeserializer implements JsonDeserializer { + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return (int) Long.parseLong(json.getAsString().replace("#", ""), 16); + } + } + + public static Config getInstance() { + return INSTANCE; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java new file mode 100644 index 00000000..035b2381 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java @@ -0,0 +1,45 @@ +package cuchaz.enigma.gui.config; + +import java.io.IOException; + +import javax.swing.SwingUtilities; + +import com.google.common.collect.ImmutableMap; +import cuchaz.enigma.gui.EnigmaSyntaxKit; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.highlight.BoxHighlightPainter; +import cuchaz.enigma.gui.highlight.TokenHighlightType; +import cuchaz.enigma.gui.util.ScaleUtil; +import de.sciss.syntaxpane.DefaultSyntaxKit; + +public class Themes { + + public static void setLookAndFeel(Gui gui, Config.LookAndFeel lookAndFeel) { + Config.getInstance().lookAndFeel = lookAndFeel; + updateTheme(gui); + } + + public static void updateTheme(Gui gui) { + Config config = Config.getInstance(); + config.lookAndFeel.setGlobalLAF(); + config.lookAndFeel.apply(config); + try { + config.saveConfig(); + } catch (IOException e) { + e.printStackTrace(); + } + EnigmaSyntaxKit.invalidate(); + DefaultSyntaxKit.initKit(); + DefaultSyntaxKit.registerContentType("text/enigma-sources", EnigmaSyntaxKit.class.getName()); + gui.boxHighlightPainters = ImmutableMap.of( + TokenHighlightType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline), + TokenHighlightType.PROPOSED, BoxHighlightPainter.create(config.proposedColor, config.proposedColorOutline), + TokenHighlightType.DEOBFUSCATED, BoxHighlightPainter.create(config.deobfuscatedColor, config.deobfuscatedColorOutline) + ); + gui.setEditorTheme(config.lookAndFeel); + SwingUtilities.updateComponentTreeUI(gui.getFrame()); + ScaleUtil.applyScaling(); + } + + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java new file mode 100644 index 00000000..fff755d3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.dialog; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.Utils; + +import javax.swing.*; +import java.awt.*; +import java.io.IOException; + +public class AboutDialog { + + public static void show(JFrame parent) { + // init frame + final JFrame frame = new JFrame(String.format(I18n.translate("menu.help.about.title"), Enigma.NAME)); + final Container pane = frame.getContentPane(); + pane.setLayout(new FlowLayout()); + + // load the content + try { + String html = Utils.readResourceToString("/about.html"); + html = String.format(html, Enigma.NAME, Enigma.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, Enigma.URL, Enigma.URL); + JButton link = new JButton(html); + link.addActionListener(event -> GuiUtil.openUrl(Enigma.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(I18n.translate("menu.help.about.ok")); + pane.add(okButton); + okButton.addActionListener(arg0 -> frame.dispose()); + + // show the frame + pane.doLayout(); + frame.setSize(ScaleUtil.getDimension(400, 220)); + frame.setResizable(false); + frame.setLocationRelativeTo(parent); + frame.setVisible(true); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java new file mode 100644 index 00000000..64219ab8 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java @@ -0,0 +1,50 @@ +package cuchaz.enigma.gui.dialog; + +import java.awt.BorderLayout; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.utils.I18n; + +public class ChangeDialog { + + public static void show(Gui gui) { + // init frame + JFrame frame = new JFrame(I18n.translate("menu.view.change.title")); + JPanel textPanel = new JPanel(); + JPanel buttonPanel = new JPanel(); + frame.setLayout(new BorderLayout()); + frame.add(BorderLayout.NORTH, textPanel); + frame.add(BorderLayout.SOUTH, buttonPanel); + + // show text + JLabel text = new JLabel((I18n.translate("menu.view.change.summary"))); + text.setHorizontalAlignment(JLabel.CENTER); + textPanel.add(text); + + // show ok button + JButton okButton = new JButton(I18n.translate("menu.view.change.ok")); + buttonPanel.add(okButton); + okButton.addActionListener(event -> frame.dispose()); + okButton.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + frame.dispose(); + } + } + }); + + // show the frame + frame.pack(); + frame.setVisible(true); + frame.setResizable(false); + frame.setLocationRelativeTo(gui.getFrame()); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java new file mode 100644 index 00000000..c5f505cf --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java @@ -0,0 +1,82 @@ +package cuchaz.enigma.gui.dialog; + +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import java.awt.Frame; + +public class ConnectToServerDialog { + + public static Result show(Frame parentComponent) { + JTextField usernameField = new JTextField(System.getProperty("user.name"), 20); + JPanel usernameRow = new JPanel(); + usernameRow.add(new JLabel(I18n.translate("prompt.connect.username"))); + usernameRow.add(usernameField); + JTextField ipField = new JTextField(20); + JPanel ipRow = new JPanel(); + ipRow.add(new JLabel(I18n.translate("prompt.connect.ip"))); + ipRow.add(ipField); + JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10); + JPanel portRow = new JPanel(); + portRow.add(new JLabel(I18n.translate("prompt.port"))); + portRow.add(portField); + JPasswordField passwordField = new JPasswordField(20); + JPanel passwordRow = new JPanel(); + passwordRow.add(new JLabel(I18n.translate("prompt.password"))); + passwordRow.add(passwordField); + + int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{usernameRow, ipRow, portRow, passwordRow}, I18n.translate("prompt.connect.title"), JOptionPane.OK_CANCEL_OPTION); + if (response != JOptionPane.OK_OPTION) { + return null; + } + + String username = usernameField.getText(); + String ip = ipField.getText(); + int port; + try { + port = Integer.parseInt(portField.getText()); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE); + return null; + } + if (port < 0 || port >= 65536) { + JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE); + return null; + } + char[] password = passwordField.getPassword(); + + return new Result(username, ip, port, password); + } + + public static class Result { + private final String username; + private final String ip; + private final int port; + private final char[] password; + + public Result(String username, String ip, int port, char[] password) { + this.username = username; + this.ip = ip; + this.port = port; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public char[] getPassword() { + return password; + } + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java new file mode 100644 index 00000000..c2a93fa5 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.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.dialog; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.util.ScaleUtil; + +import javax.swing.*; +import java.awt.*; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.FileWriter; +import java.io.File; +import java.io.IOException; + +public class CrashDialog { + + private static CrashDialog instance = null; + + private JFrame frame; + private JTextArea text; + + private CrashDialog(JFrame parent) { + // init frame + frame = new JFrame(String.format(I18n.translate("crash.title"), Enigma.NAME)); + final Container pane = frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + JLabel label = new JLabel(String.format(I18n.translate("crash.summary"), Enigma.NAME)); + label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + pane.add(label, BorderLayout.NORTH); + + // report panel + text = new JTextArea(); + text.setTabSize(2); + pane.add(new JScrollPane(text), BorderLayout.CENTER); + + // buttons panel + JPanel buttonsPanel = new JPanel(); + buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.LINE_AXIS)); + JButton exportButton = new JButton(I18n.translate("crash.export")); + exportButton.addActionListener(event -> { + JFileChooser chooser = new JFileChooser(); + chooser.setSelectedFile(new File("enigma_crash.log")); + if (chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { + try { + File file = chooser.getSelectedFile(); + FileWriter writer = new FileWriter(file); + writer.write(instance.text.getText()); + writer.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + }); + buttonsPanel.add(exportButton); + buttonsPanel.add(Box.createHorizontalGlue()); + buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("crash.exit.warning")))); + JButton ignoreButton = new JButton(I18n.translate("crash.ignore")); + ignoreButton.addActionListener(event -> { + // close (hide) the dialog + frame.setVisible(false); + }); + buttonsPanel.add(ignoreButton); + JButton exitButton = new JButton(I18n.translate("crash.exit")); + exitButton.addActionListener(event -> { + // exit enigma + System.exit(1); + }); + buttonsPanel.add(exitButton); + pane.add(buttonsPanel, BorderLayout.SOUTH); + + // show the frame + frame.setSize(ScaleUtil.getDimension(600, 400)); + frame.setLocationRelativeTo(parent); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } + + public static void init(JFrame parent) { + 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! + instance.text.setText(report); + instance.frame.doLayout(); + instance.frame.setVisible(true); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java new file mode 100644 index 00000000..eea1dff1 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java @@ -0,0 +1,65 @@ +package cuchaz.enigma.gui.dialog; + +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import java.awt.*; + +public class CreateServerDialog { + + public static Result show(Frame parentComponent) { + JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10); + JPanel portRow = new JPanel(); + portRow.add(new JLabel(I18n.translate("prompt.port"))); + portRow.add(portField); + JPasswordField passwordField = new JPasswordField(20); + JPanel passwordRow = new JPanel(); + passwordRow.add(new JLabel(I18n.translate("prompt.password"))); + passwordRow.add(passwordField); + + int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{portRow, passwordRow}, I18n.translate("prompt.create_server.title"), JOptionPane.OK_CANCEL_OPTION); + if (response != JOptionPane.OK_OPTION) { + return null; + } + + int port; + try { + port = Integer.parseInt(portField.getText()); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE); + return null; + } + if (port < 0 || port >= 65536) { + JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE); + return null; + } + + char[] password = passwordField.getPassword(); + if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) { + JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.password.too_long"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE); + return null; + } + + return new Result(port, password); + } + + public static class Result { + private final int port; + private final char[] password; + + public Result(int port, char[] password) { + this.port = port; + this.password = password; + } + + public int getPort() { + return port; + } + + public char[] getPassword() { + return password; + } + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java new file mode 100644 index 00000000..d81460ab --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.dialog; + +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.util.ScaleUtil; + +import javax.swing.*; +import javax.swing.text.html.HTML; + +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +public class JavadocDialog { + + private static JavadocDialog instance = null; + + private JFrame frame; + + private JavadocDialog(JFrame parent, JTextArea text, Callback callback) { + // init frame + frame = new JFrame(I18n.translate("javadocs.edit")); + final Container pane = frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + // editor panel + text.setTabSize(2); + pane.add(new JScrollPane(text), BorderLayout.CENTER); + text.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.VK_ENTER: + if (event.isControlDown()) + callback.closeUi(frame, true); + break; + case KeyEvent.VK_ESCAPE: + callback.closeUi(frame, false); + break; + default: + break; + } + } + }); + + // buttons panel + JPanel buttonsPanel = new JPanel(); + FlowLayout buttonsLayout = new FlowLayout(); + buttonsLayout.setAlignment(FlowLayout.RIGHT); + buttonsPanel.setLayout(buttonsLayout); + buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("javadocs.instruction")))); + JButton cancelButton = new JButton(I18n.translate("javadocs.cancel")); + cancelButton.addActionListener(event -> { + // close (hide) the dialog + callback.closeUi(frame, false); + }); + buttonsPanel.add(cancelButton); + JButton saveButton = new JButton(I18n.translate("javadocs.save")); + saveButton.addActionListener(event -> { + // exit enigma + callback.closeUi(frame, true); + }); + buttonsPanel.add(saveButton); + pane.add(buttonsPanel, BorderLayout.SOUTH); + + // tags panel + JMenuBar tagsMenu = new JMenuBar(); + + // add javadoc tags + for (JavadocTag tag : JavadocTag.values()) { + JButton tagButton = new JButton(tag.getText()); + tagButton.addActionListener(action -> { + boolean textSelected = text.getSelectedText() != null; + String tagText = tag.isInline() ? "{" + tag.getText() + " }" : tag.getText() + " "; + + if (textSelected) { + if (tag.isInline()) { + tagText = "{" + tag.getText() + " " + text.getSelectedText() + "}"; + } else { + tagText = tag.getText() + " " + text.getSelectedText(); + } + text.replaceSelection(tagText); + } else { + text.insert(tagText, text.getCaretPosition()); + } + + if (tag.isInline()) { + text.setCaretPosition(text.getCaretPosition() - 1); + } + text.grabFocus(); + }); + tagsMenu.add(tagButton); + } + + // add html tags + JComboBox htmlList = new JComboBox(); + htmlList.setPreferredSize(new Dimension()); + for (HTML.Tag htmlTag : HTML.getAllTags()) { + htmlList.addItem(htmlTag.toString()); + } + htmlList.addActionListener(action -> { + String tagText = "<" + htmlList.getSelectedItem().toString() + ">"; + text.insert(tagText, text.getCaretPosition()); + text.grabFocus(); + }); + tagsMenu.add(htmlList); + + pane.add(tagsMenu, BorderLayout.NORTH); + + // show the frame + frame.setSize(ScaleUtil.getDimension(600, 400)); + frame.setLocationRelativeTo(parent); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } + + public static void init(JFrame parent, JTextArea area, Callback callback) { + instance = new JavadocDialog(parent, area, callback); + instance.frame.doLayout(); + instance.frame.setVisible(true); + } + + public interface Callback { + void closeUi(JFrame frame, boolean save); + } + + private enum JavadocTag { + CODE(true), + LINK(true), + LINKPLAIN(true), + RETURN(false), + SEE(false), + THROWS(false); + + private boolean inline; + + private JavadocTag(boolean inline) { + this.inline = inline; + } + + public String getText() { + return "@" + this.name().toLowerCase(); + } + + public boolean isInline() { + return this.inline; + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java new file mode 100644 index 00000000..fa40af75 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * 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.dialog; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.util.ScaleUtil; + +import javax.swing.*; +import java.awt.*; +import java.util.concurrent.CompletableFuture; + +public class ProgressDialog implements ProgressListener, AutoCloseable { + + private JFrame frame; + private JLabel labelTitle; + private JLabel labelText; + private JProgressBar progress; + + public ProgressDialog(JFrame parent) { + + // init frame + this.frame = new JFrame(String.format(I18n.translate("progress.operation"), Enigma.NAME)); + final Container pane = this.frame.getContentPane(); + FlowLayout layout = new FlowLayout(); + layout.setAlignment(FlowLayout.LEFT); + pane.setLayout(layout); + + this.labelTitle = new JLabel(); + pane.add(this.labelTitle); + + // set up the progress bar + JPanel panel = new JPanel(); + pane.add(panel); + panel.setLayout(new BorderLayout()); + this.labelText = GuiUtil.unboldLabel(new JLabel()); + this.progress = new JProgressBar(); + this.labelText.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + panel.add(this.labelText, BorderLayout.NORTH); + panel.add(this.progress, BorderLayout.CENTER); + panel.setPreferredSize(ScaleUtil.getDimension(360, 50)); + + // show the frame + pane.doLayout(); + this.frame.setSize(ScaleUtil.getDimension(400, 120)); + this.frame.setResizable(false); + this.frame.setLocationRelativeTo(parent); + this.frame.setVisible(true); + this.frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + } + + public static CompletableFuture runOffThread(final JFrame parent, final ProgressRunnable runnable) { + CompletableFuture future = new CompletableFuture<>(); + new Thread(() -> + { + try (ProgressDialog progress = new ProgressDialog(parent)) { + runnable.run(progress); + future.complete(null); + } catch (Exception ex) { + future.completeExceptionally(ex); + throw new Error(ex); + } + }).start(); + return future; + } + + @Override + public void close() { + this.frame.dispose(); + } + + @Override + public void init(int totalWork, String title) { + this.labelTitle.setText(title); + this.progress.setMinimum(0); + this.progress.setMaximum(totalWork); + this.progress.setValue(0); + } + + @Override + public void step(int numDone, String message) { + this.labelText.setText(message); + if (numDone != -1) { + this.progress.setValue(numDone); + this.progress.setIndeterminate(false); + } else { + this.progress.setIndeterminate(true); + } + + // update the frame + this.frame.validate(); + this.frame.repaint(); + } + + public interface ProgressRunnable { + void run(ProgressListener listener) throws Exception; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java new file mode 100644 index 00000000..2d396c36 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * 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.dialog; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.util.AbstractListCellRenderer; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.search.SearchEntry; +import cuchaz.enigma.gui.search.SearchUtil; + +public class SearchDialog { + + private final JTextField searchField; + private DefaultListModel classListModel; + private final JList classList; + private final JDialog dialog; + + private final Gui parent; + private final SearchUtil su; + private SearchUtil.SearchControl currentSearch; + + public SearchDialog(Gui parent) { + this.parent = parent; + + su = new SearchUtil<>(); + + dialog = new JDialog(parent.getFrame(), I18n.translate("menu.view.search"), true); + JPanel contentPane = new JPanel(); + contentPane.setBorder(ScaleUtil.createEmptyBorder(4, 4, 4, 4)); + contentPane.setLayout(new BorderLayout(ScaleUtil.scale(4), ScaleUtil.scale(4))); + + searchField = new JTextField(); + searchField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void insertUpdate(DocumentEvent e) { + updateList(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateList(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateList(); + } + + }); + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_DOWN) { + int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1; + classList.setSelectedIndex(next); + classList.ensureIndexIsVisible(next); + } else if (e.getKeyCode() == KeyEvent.VK_UP) { + int prev = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1; + classList.setSelectedIndex(prev); + classList.ensureIndexIsVisible(prev); + } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + close(); + } + } + }); + searchField.addActionListener(e -> openSelected()); + contentPane.add(searchField, BorderLayout.NORTH); + + classListModel = new DefaultListModel<>(); + classList = new JList<>(); + classList.setModel(classListModel); + classList.setCellRenderer(new ListCellRendererImpl()); + classList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + classList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent mouseEvent) { + if (mouseEvent.getClickCount() >= 2) { + int idx = classList.locationToIndex(mouseEvent.getPoint()); + SearchEntryImpl entry = classList.getModel().getElementAt(idx); + openEntry(entry); + } + } + }); + contentPane.add(new JScrollPane(classList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER); + + JPanel buttonBar = new JPanel(); + buttonBar.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton open = new JButton(I18n.translate("prompt.open")); + open.addActionListener(event -> openSelected()); + buttonBar.add(open); + JButton cancel = new JButton(I18n.translate("prompt.cancel")); + cancel.addActionListener(event -> close()); + buttonBar.add(cancel); + contentPane.add(buttonBar, BorderLayout.SOUTH); + + // apparently the class list doesn't update by itself when the list + // state changes and the dialog is hidden + dialog.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + classList.updateUI(); + } + }); + + dialog.setContentPane(contentPane); + dialog.setSize(ScaleUtil.getDimension(400, 500)); + dialog.setLocationRelativeTo(parent.getFrame()); + } + + public void show() { + su.clear(); + parent.getController().project.getJarIndex().getEntryIndex().getClasses().parallelStream() + .filter(e -> !e.isInnerClass()) + .map(e -> SearchEntryImpl.from(e, parent.getController())) + .map(SearchUtil.Entry::from) + .sequential() + .forEach(su::add); + + updateList(); + + searchField.requestFocus(); + searchField.selectAll(); + + dialog.setVisible(true); + } + + private void openSelected() { + SearchEntryImpl selectedValue = classList.getSelectedValue(); + if (selectedValue != null) { + openEntry(selectedValue); + } + } + + private void openEntry(SearchEntryImpl e) { + close(); + su.hit(e); + parent.getController().navigateTo(e.obf); + if (e.deobf != null) { + parent.getDeobfPanel().deobfClasses.setSelectionClass(e.deobf); + } else { + parent.getObfPanel().obfClasses.setSelectionClass(e.obf); + } + } + + private void close() { + dialog.setVisible(false); + } + + // Updates the list of class names + private void updateList() { + if (currentSearch != null) currentSearch.stop(); + + DefaultListModel classListModel = new DefaultListModel<>(); + this.classListModel = classListModel; + classList.setModel(classListModel); + + currentSearch = su.asyncSearch(searchField.getText(), (idx, e) -> SwingUtilities.invokeLater(() -> classListModel.insertElementAt(e, idx))); + } + + public void dispose() { + dialog.dispose(); + } + + private static final class SearchEntryImpl implements SearchEntry { + + public final ClassEntry obf; + public final ClassEntry deobf; + + private SearchEntryImpl(ClassEntry obf, ClassEntry deobf) { + this.obf = obf; + this.deobf = deobf; + } + + @Override + public List getSearchableNames() { + if (deobf != null) { + return Arrays.asList(obf.getSimpleName(), deobf.getSimpleName()); + } else { + return Collections.singletonList(obf.getSimpleName()); + } + } + + @Override + public String getIdentifier() { + return obf.getFullName(); + } + + @Override + public String toString() { + return String.format("SearchEntryImpl { obf: %s, deobf: %s }", obf, deobf); + } + + public static SearchEntryImpl from(ClassEntry e, GuiController controller) { + ClassEntry deobf = controller.project.getMapper().deobfuscate(e); + if (deobf.equals(e)) deobf = null; + return new SearchEntryImpl(e, deobf); + } + + } + + private static final class ListCellRendererImpl extends AbstractListCellRenderer { + + private final JLabel mainName; + private final JLabel secondaryName; + + public ListCellRendererImpl() { + this.setLayout(new BorderLayout()); + + mainName = new JLabel(); + this.add(mainName, BorderLayout.WEST); + + secondaryName = new JLabel(); + secondaryName.setFont(secondaryName.getFont().deriveFont(Font.ITALIC)); + secondaryName.setForeground(Color.GRAY); + this.add(secondaryName, BorderLayout.EAST); + } + + @Override + public void updateUiForEntry(JList list, SearchEntryImpl value, int index, boolean isSelected, boolean cellHasFocus) { + if (value.deobf == null) { + mainName.setText(value.obf.getSimpleName()); + mainName.setToolTipText(value.obf.getFullName()); + secondaryName.setText(""); + secondaryName.setToolTipText(""); + } else { + mainName.setText(value.deobf.getSimpleName()); + mainName.setToolTipText(value.deobf.getFullName()); + secondaryName.setText(value.obf.getSimpleName()); + secondaryName.setToolTipText(value.obf.getFullName()); + } + } + + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java new file mode 100644 index 00000000..868eba79 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java @@ -0,0 +1,82 @@ +package cuchaz.enigma.gui.dialog; + +import java.awt.BorderLayout; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JPanel; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.stats.StatsMember; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.I18n; + +public class StatsDialog { + + public static void show(Gui gui) { + // init frame + JFrame frame = new JFrame(I18n.translate("menu.file.stats.title")); + JPanel checkboxesPanel = new JPanel(); + JPanel buttonPanel = new JPanel(); + frame.setLayout(new BorderLayout()); + frame.add(BorderLayout.NORTH, checkboxesPanel); + frame.add(BorderLayout.SOUTH, buttonPanel); + + // show checkboxes + Map checkboxes = Arrays + .stream(StatsMember.values()) + .collect(Collectors.toMap(m -> m, m -> { + JCheckBox checkbox = new JCheckBox(I18n.translate("type." + m.name().toLowerCase(Locale.ROOT))); + checkboxesPanel.add(checkbox); + return checkbox; + })); + + // show generate button + JButton button = new JButton(I18n.translate("menu.file.stats.generate")); + buttonPanel.add(button); + button.setEnabled(false); + button.addActionListener(action -> { + frame.dispose(); + generateStats(gui, checkboxes); + }); + + // add action listener to each checkbox + checkboxes.entrySet().forEach(checkbox -> { + checkbox.getValue().addActionListener(action -> { + if (!button.isEnabled()) { + button.setEnabled(true); + } else if (checkboxes.entrySet().stream().allMatch(entry -> !entry.getValue().isSelected())) { + button.setEnabled(false); + } + }); + }); + + // show the frame + frame.pack(); + frame.setVisible(true); + frame.setSize(ScaleUtil.getDimension(500, 120)); + frame.setResizable(false); + frame.setLocationRelativeTo(gui.getFrame()); + } + + private static void generateStats(Gui gui, Map checkboxes) { + // get members from selected checkboxes + Set includedMembers = checkboxes + .entrySet() + .stream() + .filter(entry -> entry.getValue().isSelected()) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + // checks if a projet is open + if (gui.getController().project != null) { + gui.getController().openStats(includedMembers); + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java new file mode 100644 index 00000000..fb497b11 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.event.MouseEvent; + +import javax.swing.JTabbedPane; + +public class CollapsibleTabbedPane extends JTabbedPane { + + public CollapsibleTabbedPane() { + } + + public CollapsibleTabbedPane(int tabPlacement) { + super(tabPlacement); + } + + public CollapsibleTabbedPane(int tabPlacement, int tabLayoutPolicy) { + super(tabPlacement, tabLayoutPolicy); + } + + @Override + protected void processMouseEvent(MouseEvent e) { + int id = e.getID(); + if (id == MouseEvent.MOUSE_PRESSED) { + if (!isEnabled()) return; + int tabIndex = getUI().tabForCoordinate(this, e.getX(), e.getY()); + if (tabIndex >= 0 && isEnabledAt(tabIndex)) { + if (tabIndex == getSelectedIndex()) { + if (isFocusOwner() && isRequestFocusEnabled()) { + requestFocus(); + } else { + setSelectedIndex(-1); + } + return; + } + } + } + super.processMouseEvent(e); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java new file mode 100644 index 00000000..24f42ff0 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java @@ -0,0 +1,386 @@ +package cuchaz.enigma.gui.elements; + +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.gui.config.Themes; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.dialog.AboutDialog; +import cuchaz.enigma.gui.dialog.ChangeDialog; +import cuchaz.enigma.gui.dialog.ConnectToServerDialog; +import cuchaz.enigma.gui.dialog.CreateServerDialog; +import cuchaz.enigma.gui.dialog.StatsDialog; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.translation.mapping.serde.MappingFormat; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.Pair; + +import java.awt.Desktop; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.swing.*; + +public class MenuBar extends JMenuBar { + + public final JMenuItem closeJarMenu; + public final List openMappingsMenus; + public final JMenuItem saveMappingsMenu; + public final List saveMappingsMenus; + public final JMenuItem closeMappingsMenu; + public final JMenuItem dropMappingsMenu; + public final JMenuItem exportSourceMenu; + public final JMenuItem exportJarMenu; + public final JMenuItem connectToServerMenu; + public final JMenuItem startServerMenu; + private final Gui gui; + + public MenuBar(Gui gui) { + this.gui = gui; + + /* + * File menu + */ + { + JMenu menu = new JMenu(I18n.translate("menu.file")); + this.add(menu); + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.jar.open")); + menu.add(item); + item.addActionListener(event -> { + this.gui.jarFileChooser.setVisible(true); + String file = this.gui.jarFileChooser.getFile(); + // checks if the file name is not empty + if (file != null) { + Path path = Paths.get(this.gui.jarFileChooser.getDirectory()).resolve(file); + // checks if the file name corresponds to an existing file + if (Files.exists(path)) { + gui.getController().openJar(path); + } + } + }); + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.jar.close")); + menu.add(item); + item.addActionListener(event -> this.gui.getController().closeJar()); + this.closeJarMenu = item; + } + menu.addSeparator(); + JMenu openMenu = new JMenu(I18n.translate("menu.file.mappings.open")); + menu.add(openMenu); + { + openMappingsMenus = new ArrayList<>(); + for (MappingFormat format : MappingFormat.values()) { + if (format.getReader() != null) { + JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT))); + openMenu.add(item); + item.addActionListener(event -> { + if (this.gui.enigmaMappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.gui.enigmaMappingsFileChooser.getSelectedFile(); + this.gui.getController().openMappings(format, selectedFile.toPath()); + } + }); + openMappingsMenus.add(item); + } + } + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.save")); + menu.add(item); + item.addActionListener(event -> { + this.gui.getController().saveMappings(this.gui.enigmaMappingsFileChooser.getSelectedFile().toPath()); + }); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); + this.saveMappingsMenu = item; + } + JMenu saveMenu = new JMenu(I18n.translate("menu.file.mappings.save_as")); + menu.add(saveMenu); + { + saveMappingsMenus = new ArrayList<>(); + for (MappingFormat format : MappingFormat.values()) { + if (format.getWriter() != null) { + JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT))); + saveMenu.add(item); + item.addActionListener(event -> { + // TODO: Use a specific file chooser for it + if (this.gui.enigmaMappingsFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + this.gui.getController().saveMappings(this.gui.enigmaMappingsFileChooser.getSelectedFile().toPath(), format); + this.saveMappingsMenu.setEnabled(true); + } + }); + saveMappingsMenus.add(item); + } + } + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.close")); + menu.add(item); + item.addActionListener(event -> { + if (this.gui.getController().isDirty()) { + this.gui.showDiscardDiag((response -> { + if (response == JOptionPane.YES_OPTION) { + gui.saveMapping(); + this.gui.getController().closeMappings(); + } else if (response == JOptionPane.NO_OPTION) + this.gui.getController().closeMappings(); + return null; + }), I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.close.cancel")); + } else + this.gui.getController().closeMappings(); + + }); + this.closeMappingsMenu = item; + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.drop")); + menu.add(item); + item.addActionListener(event -> this.gui.getController().dropMappings()); + this.dropMappingsMenu = item; + } + menu.addSeparator(); + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.export.source")); + menu.add(item); + item.addActionListener(event -> { + if (this.gui.exportSourceFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + this.gui.getController().exportSource(this.gui.exportSourceFileChooser.getSelectedFile().toPath()); + } + }); + this.exportSourceMenu = item; + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.export.jar")); + menu.add(item); + item.addActionListener(event -> { + this.gui.exportJarFileChooser.setVisible(true); + if (this.gui.exportJarFileChooser.getFile() != null) { + Path path = Paths.get(this.gui.exportJarFileChooser.getDirectory(), this.gui.exportJarFileChooser.getFile()); + this.gui.getController().exportJar(path); + } + }); + this.exportJarMenu = item; + } + menu.addSeparator(); + { + JMenuItem stats = new JMenuItem(I18n.translate("menu.file.stats")); + menu.add(stats); + stats.addActionListener(event -> StatsDialog.show(this.gui)); + } + menu.addSeparator(); + { + JMenuItem item = new JMenuItem(I18n.translate("menu.file.exit")); + menu.add(item); + item.addActionListener(event -> this.gui.close()); + } + } + + /* + * Decompiler menu + */ + { + JMenu menu = new JMenu(I18n.translate("menu.decompiler")); + this.add(menu); + + ButtonGroup decompilerGroup = new ButtonGroup(); + + for (Config.Decompiler decompiler : Config.Decompiler.values()) { + JRadioButtonMenuItem decompilerButton = new JRadioButtonMenuItem(decompiler.name); + decompilerGroup.add(decompilerButton); + if (decompiler.equals(Config.getInstance().decompiler)) { + decompilerButton.setSelected(true); + } + menu.add(decompilerButton); + decompilerButton.addActionListener(event -> { + gui.getController().setDecompiler(decompiler.service); + + try { + Config.getInstance().decompiler = decompiler; + Config.getInstance().saveConfig(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + + /* + * View menu + */ + { + JMenu menu = new JMenu(I18n.translate("menu.view")); + this.add(menu); + { + JMenu themes = new JMenu(I18n.translate("menu.view.themes")); + menu.add(themes); + ButtonGroup themeGroup = new ButtonGroup(); + for (Config.LookAndFeel lookAndFeel : Config.LookAndFeel.values()) { + JRadioButtonMenuItem themeButton = new JRadioButtonMenuItem(I18n.translate("menu.view.themes." + lookAndFeel.name().toLowerCase(Locale.ROOT))); + themeGroup.add(themeButton); + if (lookAndFeel.equals(Config.getInstance().lookAndFeel)) { + themeButton.setSelected(true); + } + themes.add(themeButton); + themeButton.addActionListener(event -> Themes.setLookAndFeel(gui, lookAndFeel)); + } + } + { + JMenu languages = new JMenu(I18n.translate("menu.view.languages")); + menu.add(languages); + ButtonGroup languageGroup = new ButtonGroup(); + for (String lang : I18n.getAvailableLanguages()) { + JRadioButtonMenuItem languageButton = new JRadioButtonMenuItem(I18n.getLanguageName(lang)); + languageGroup.add(languageButton); + if (lang.equals(Config.getInstance().language)) { + languageButton.setSelected(true); + } + languages.add(languageButton); + languageButton.addActionListener(event -> { + I18n.setLanguage(lang); + ChangeDialog.show(this.gui); + }); + } + } + { + JMenu scale = new JMenu(I18n.translate("menu.view.scale")); + { + ButtonGroup scaleGroup = new ButtonGroup(); + Map map = IntStream.of(100, 125, 150, 175, 200) + .mapToObj(scaleFactor -> { + float realScaleFactor = scaleFactor / 100f; + JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(String.format("%d%%", scaleFactor)); + menuItem.addActionListener(event -> ScaleUtil.setScaleFactor(realScaleFactor)); + menuItem.addActionListener(event -> ChangeDialog.show(this.gui)); + scaleGroup.add(menuItem); + scale.add(menuItem); + return new Pair<>(realScaleFactor, menuItem); + }) + .collect(Collectors.toMap(x -> x.a, x -> x.b)); + + JMenuItem customScale = new JMenuItem(I18n.translate("menu.view.scale.custom")); + customScale.addActionListener(event -> { + String answer = (String) JOptionPane.showInputDialog(gui.getFrame(), I18n.translate("menu.view.scale.custom.title"), I18n.translate("menu.view.scale.custom.title"), + JOptionPane.QUESTION_MESSAGE, null, null, Float.toString(ScaleUtil.getScaleFactor() * 100)); + if (answer == null) return; + float newScale = 1.0f; + try { + newScale = Float.parseFloat(answer) / 100f; + } catch (NumberFormatException ignored) { + } + ScaleUtil.setScaleFactor(newScale); + ChangeDialog.show(this.gui); + }); + scale.add(customScale); + ScaleUtil.addListener((newScale, _oldScale) -> { + JRadioButtonMenuItem mi = map.get(newScale); + if (mi != null) { + mi.setSelected(true); + } else { + scaleGroup.clearSelection(); + } + }); + JRadioButtonMenuItem mi = map.get(ScaleUtil.getScaleFactor()); + if (mi != null) { + mi.setSelected(true); + } + } + menu.add(scale); + } + menu.addSeparator(); + { + JMenuItem search = new JMenuItem(I18n.translate("menu.view.search")); + search.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.SHIFT_MASK)); + menu.add(search); + search.addActionListener(event -> { + if (this.gui.getController().project != null) { + this.gui.getSearchDialog().show(); + } + }); + } + } + + /* + * Collab menu + */ + { + JMenu menu = new JMenu(I18n.translate("menu.collab")); + this.add(menu); + { + JMenuItem item = new JMenuItem(I18n.translate("menu.collab.connect")); + menu.add(item); + item.addActionListener(event -> { + if (this.gui.getController().getClient() != null) { + this.gui.getController().disconnectIfConnected(null); + return; + } + ConnectToServerDialog.Result result = ConnectToServerDialog.show(this.gui.getFrame()); + if (result == null) { + return; + } + this.gui.getController().disconnectIfConnected(null); + try { + this.gui.getController().createClient(result.getUsername(), result.getIp(), result.getPort(), result.getPassword()); + } catch (IOException e) { + JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.connect.error"), JOptionPane.ERROR_MESSAGE); + this.gui.getController().disconnectIfConnected(null); + } + Arrays.fill(result.getPassword(), (char)0); + }); + this.connectToServerMenu = item; + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.collab.server.start")); + menu.add(item); + item.addActionListener(event -> { + if (this.gui.getController().getServer() != null) { + this.gui.getController().disconnectIfConnected(null); + return; + } + CreateServerDialog.Result result = CreateServerDialog.show(this.gui.getFrame()); + if (result == null) { + return; + } + this.gui.getController().disconnectIfConnected(null); + try { + this.gui.getController().createServer(result.getPort(), result.getPassword()); + } catch (IOException e) { + JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.server.start.error"), JOptionPane.ERROR_MESSAGE); + this.gui.getController().disconnectIfConnected(null); + } + }); + this.startServerMenu = item; + } + } + + /* + * Help menu + */ + { + JMenu menu = new JMenu(I18n.translate("menu.help")); + this.add(menu); + { + JMenuItem item = new JMenuItem(I18n.translate("menu.help.about")); + menu.add(item); + item.addActionListener(event -> AboutDialog.show(this.gui.getFrame())); + } + { + JMenuItem item = new JMenuItem(I18n.translate("menu.help.github")); + menu.add(item); + item.addActionListener(event -> { + try { + Desktop.getDesktop().browse(new URL("https://github.com/FabricMC/Enigma").toURI()); + } catch (URISyntaxException | IOException ignored) { + } + }); + } + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java new file mode 100644 index 00000000..b92041c3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java @@ -0,0 +1,125 @@ +package cuchaz.enigma.gui.elements; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +public class PopupMenuBar extends JPopupMenu { + + public final JMenuItem renameMenu; + public final JMenuItem editJavadocMenu; + public final JMenuItem showInheritanceMenu; + public final JMenuItem showImplementationsMenu; + public final JMenuItem showCallsMenu; + public final JMenuItem showCallsSpecificMenu; + public final JMenuItem openEntryMenu; + public final JMenuItem openPreviousMenu; + public final JMenuItem openNextMenu; + public final JMenuItem toggleMappingMenu; + + public PopupMenuBar(Gui gui) { + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.rename")); + menu.addActionListener(event -> gui.startRename()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.renameMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.javadoc")); + menu.addActionListener(event -> gui.startDocChange()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.editJavadocMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.inheritance")); + menu.addActionListener(event -> gui.showInheritance()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.showInheritanceMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.implementations")); + menu.addActionListener(event -> gui.showImplementations()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.showImplementationsMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls")); + menu.addActionListener(event -> gui.showCalls(true)); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.showCallsMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls.specific")); + menu.addActionListener(event -> gui.showCalls(false)); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.showCallsSpecificMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.declaration")); + menu.addActionListener(event -> gui.getController().navigateTo(gui.cursorReference.entry)); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.openEntryMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.back")); + menu.addActionListener(event -> gui.getController().openPreviousReference()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.openPreviousMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.forward")); + menu.addActionListener(event -> gui.getController().openNextReference()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.openNextMenu = menu; + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.mark_deobfuscated")); + menu.addActionListener(event -> gui.toggleMapping()); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); + menu.setEnabled(false); + this.add(menu); + this.toggleMappingMenu = menu; + } + { + this.add(new JSeparator()); + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.in")); + menu.addActionListener(event -> gui.editor.offsetEditorZoom(2)); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK)); + this.add(menu); + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.out")); + menu.addActionListener(event -> gui.editor.offsetEditorZoom(-2)); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); + this.add(menu); + } + { + JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.reset")); + menu.addActionListener(event -> gui.editor.resetEditorZoom()); + this.add(menu); + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java new file mode 100644 index 00000000..f5f66287 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java @@ -0,0 +1,10 @@ +package cuchaz.enigma.gui.filechooser; + +import javax.swing.*; + +public class FileChooserAny extends JFileChooser { + public FileChooserAny() { + this.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + this.setAcceptAllFileFilterUsed(false); + } +} \ No newline at end of file diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java new file mode 100644 index 00000000..cea11a68 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java @@ -0,0 +1,8 @@ +package cuchaz.enigma.gui.filechooser; + +import javax.swing.*; + +public class FileChooserFile extends JFileChooser { + public FileChooserFile() { + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java new file mode 100644 index 00000000..c16e0afc --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java @@ -0,0 +1,11 @@ +package cuchaz.enigma.gui.filechooser; + +import javax.swing.*; + +public class FileChooserFolder extends JFileChooser { + + public FileChooserFolder() { + this.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + this.setAcceptAllFileFilterUsed(false); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java new file mode 100644 index 00000000..3ae4380f --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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.highlight; + +import cuchaz.enigma.gui.config.Config; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Highlighter; +import javax.swing.text.JTextComponent; +import java.awt.*; + +public class BoxHighlightPainter implements Highlighter.HighlightPainter { + private Color fillColor; + private Color borderColor; + + protected BoxHighlightPainter(Color fillColor, Color borderColor) { + this.fillColor = fillColor; + this.borderColor = borderColor; + } + + public static BoxHighlightPainter create(Config.AlphaColorEntry entry, Config.AlphaColorEntry entryOutline) { + return new BoxHighlightPainter(entry != null ? entry.get() : null, entryOutline != null ? entryOutline.get() : null); + } + + public static Rectangle getBounds(JTextComponent text, int start, int end) { + try { + // determine the bounds of the text + Rectangle startRect = text.modelToView(start); + Rectangle endRect = text.modelToView(end); + Rectangle bounds = startRect.union(endRect); + + // 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); + } + } + + @Override + public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) { + Rectangle bounds = getBounds(text, start, end); + + // fill the area + if (this.fillColor != null) { + g.setColor(this.fillColor); + g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } + + // draw a box around the area + g.setColor(this.borderColor); + g.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java new file mode 100644 index 00000000..2e4e462a --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.highlight; + +import cuchaz.enigma.gui.config.Config; + +import javax.swing.text.Highlighter; +import javax.swing.text.JTextComponent; +import java.awt.*; + +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(new Color(Config.getInstance().selectionHighlightColor)); + g2d.setStroke(new BasicStroke(2.0f)); + g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java new file mode 100644 index 00000000..ae23f324 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.gui.highlight; + +public enum TokenHighlightType { + OBFUSCATED, + DEOBFUSCATED, + PROPOSED +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java new file mode 100644 index 00000000..922f8f24 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.node; + +import cuchaz.enigma.translation.representation.entry.ClassEntry; + +import javax.swing.tree.DefaultMutableTreeNode; + +public class ClassSelectorClassNode extends DefaultMutableTreeNode { + + private final ClassEntry obfEntry; + private ClassEntry classEntry; + + public ClassSelectorClassNode(ClassEntry obfEntry, ClassEntry classEntry) { + this.obfEntry = obfEntry; + this.classEntry = classEntry; + this.setUserObject(classEntry); + } + + public ClassEntry getObfEntry() { + return obfEntry; + } + + public ClassEntry getClassEntry() { + return this.classEntry; + } + + @Override + public String toString() { + return this.classEntry.getSimpleName(); + } + + @Override + public boolean equals(Object other) { + return other instanceof ClassSelectorClassNode && equals((ClassSelectorClassNode) other); + } + + @Override + public int hashCode() { + return 17 + (classEntry != null ? classEntry.hashCode() : 0); + } + + @Override + public Object getUserObject() { + return classEntry; + } + + @Override + public void setUserObject(Object userObject) { + String packageName = ""; + if (classEntry.getPackageName() != null) + packageName = classEntry.getPackageName() + "/"; + if (userObject instanceof String) + this.classEntry = new ClassEntry(packageName + userObject); + else if (userObject instanceof ClassEntry) + this.classEntry = (ClassEntry) userObject; + super.setUserObject(classEntry); + } + + public boolean equals(ClassSelectorClassNode other) { + return this.classEntry.equals(other.classEntry); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java new file mode 100644 index 00000000..caa985c9 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * 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.node; + +import javax.swing.tree.DefaultMutableTreeNode; + +public class ClassSelectorPackageNode extends DefaultMutableTreeNode { + + private String packageName; + + public ClassSelectorPackageNode(String packageName) { + this.packageName = packageName != null ? packageName : "(none)"; + } + + public String getPackageName() { + return packageName; + } + + @Override + public Object getUserObject() { + return packageName; + } + + @Override + public void setUserObject(Object userObject) { + if (userObject instanceof String) + this.packageName = (String) userObject; + super.setUserObject(userObject); + } + + @Override + public String toString() { + return !packageName.equals("(none)") ? this.packageName : "(none)"; + } + + @Override + public boolean equals(Object other) { + return other instanceof ClassSelectorPackageNode && equals((ClassSelectorPackageNode) other); + } + + @Override + public int hashCode() { + return packageName.hashCode(); + } + + public boolean equals(ClassSelectorPackageNode other) { + return other != null && this.packageName.equals(other.packageName); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java new file mode 100644 index 00000000..c24226b3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java @@ -0,0 +1,26 @@ +package cuchaz.enigma.gui.panels; + +import cuchaz.enigma.gui.ClassSelector; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import java.awt.*; + +public class PanelDeobf extends JPanel { + + public final ClassSelector deobfClasses; + private final Gui gui; + + public PanelDeobf(Gui gui) { + this.gui = gui; + + this.deobfClasses = new ClassSelector(gui, ClassSelector.DEOBF_CLASS_COMPARATOR, true); + this.deobfClasses.setSelectionListener(gui.getController()::navigateTo); + this.deobfClasses.setRenameSelectionListener(gui::onPanelRename); + + this.setLayout(new BorderLayout()); + this.add(new JLabel(I18n.translate("info_panel.classes.deobfuscated")), BorderLayout.NORTH); + this.add(new JScrollPane(this.deobfClasses), BorderLayout.CENTER); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java new file mode 100644 index 00000000..346d6655 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java @@ -0,0 +1,171 @@ +package cuchaz.enigma.gui.panels; + +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.gui.BrowserCaret; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.gui.util.ScaleUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class PanelEditor extends JEditorPane { + private boolean mouseIsPressed = false; + public int fontSize = 12; + + public PanelEditor(Gui gui) { + this.setEditable(false); + this.setSelectionColor(new Color(31, 46, 90)); + this.setCaret(new BrowserCaret()); + this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); + this.addCaretListener(event -> gui.onCaretMove(event.getDot(), mouseIsPressed)); + final PanelEditor self = this; + this.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent mouseEvent) { + mouseIsPressed = true; + } + + @Override + public void mouseReleased(MouseEvent e) { + switch (e.getButton()) { + case MouseEvent.BUTTON3: // Right click + self.setCaretPosition(self.viewToModel(e.getPoint())); + break; + + case 4: // Back navigation + gui.getController().openPreviousReference(); + break; + + case 5: // Forward navigation + gui.getController().openNextReference(); + break; + } + mouseIsPressed = false; + } + }); + this.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.isControlDown()) { + gui.setShouldNavigateOnClick(false); + switch (event.getKeyCode()) { + case KeyEvent.VK_I: + gui.popupMenu.showInheritanceMenu.doClick(); + break; + + case KeyEvent.VK_M: + gui.popupMenu.showImplementationsMenu.doClick(); + break; + + case KeyEvent.VK_N: + gui.popupMenu.openEntryMenu.doClick(); + break; + + case KeyEvent.VK_P: + gui.popupMenu.openPreviousMenu.doClick(); + break; + + case KeyEvent.VK_E: + gui.popupMenu.openNextMenu.doClick(); + break; + + case KeyEvent.VK_C: + if (event.isShiftDown()) { + gui.popupMenu.showCallsSpecificMenu.doClick(); + } else { + gui.popupMenu.showCallsMenu.doClick(); + } + break; + + case KeyEvent.VK_O: + gui.popupMenu.toggleMappingMenu.doClick(); + break; + + case KeyEvent.VK_R: + gui.popupMenu.renameMenu.doClick(); + break; + + case KeyEvent.VK_D: + gui.popupMenu.editJavadocMenu.doClick(); + break; + + case KeyEvent.VK_F5: + gui.getController().refreshCurrentClass(); + break; + + case KeyEvent.VK_F: + // prevent navigating on click when quick find activated + break; + + case KeyEvent.VK_ADD: + case KeyEvent.VK_EQUALS: + case KeyEvent.VK_PLUS: + self.offsetEditorZoom(2); + break; + case KeyEvent.VK_SUBTRACT: + case KeyEvent.VK_MINUS: + self.offsetEditorZoom(-2); + break; + + default: + gui.setShouldNavigateOnClick(true); // CTRL + break; + } + } + } + + @Override + public void keyTyped(KeyEvent event) { + if (!gui.popupMenu.renameMenu.isEnabled()) return; + + if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) { + EnigmaProject project = gui.getController().project; + EntryReference, Entry> reference = project.getMapper().deobfuscate(gui.cursorReference); + Entry entry = reference.getNameableEntry(); + + String name = String.valueOf(event.getKeyChar()); + if (entry instanceof ClassEntry && ((ClassEntry) entry).getParent() == null) { + String packageName = ((ClassEntry) entry).getPackageName(); + if (packageName != null) { + name = packageName + "/" + name; + } + } + + gui.popupMenu.renameMenu.doClick(); + gui.renameTextField.setText(name); + } + } + + @Override + public void keyReleased(KeyEvent event) { + gui.setShouldNavigateOnClick(event.isControlDown()); + } + }); + } + + public void offsetEditorZoom(int zoomAmount) { + int newResult = this.fontSize + zoomAmount; + if (newResult > 8 && newResult < 72) { + this.fontSize = newResult; + this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); + } + } + + public void resetEditorZoom() { + this.fontSize = 12; + this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); + } + + @Override + public Color getCaretColor() { + return new Color(Config.getInstance().caretColor); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java new file mode 100644 index 00000000..8c19efb5 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java @@ -0,0 +1,32 @@ +package cuchaz.enigma.gui.panels; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.gui.util.ScaleUtil; + +import javax.swing.*; +import java.awt.*; + +public class PanelIdentifier extends JPanel { + + private final Gui gui; + + public PanelIdentifier(Gui gui) { + this.gui = gui; + + this.setLayout(new GridLayout(4, 1, 0, 0)); + this.setPreferredSize(ScaleUtil.getDimension(0, 100)); + this.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier"))); + } + + public void clearReference() { + this.removeAll(); + JLabel label = new JLabel(I18n.translate("info_panel.identifier.none")); + GuiUtil.unboldLabel(label); + label.setHorizontalAlignment(JLabel.CENTER); + this.add(label); + + gui.redraw(); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java new file mode 100644 index 00000000..dd7f9f97 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java @@ -0,0 +1,37 @@ +package cuchaz.enigma.gui.panels; + +import cuchaz.enigma.gui.ClassSelector; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.I18n; + +import javax.swing.*; +import java.awt.*; +import java.util.Comparator; + +public class PanelObf extends JPanel { + + public final ClassSelector obfClasses; + private final Gui gui; + + public PanelObf(Gui gui) { + this.gui = gui; + + Comparator obfClassComparator = (a, b) -> { + String aname = a.getFullName(); + String bname = b.getFullName(); + if (aname.length() != bname.length()) { + return aname.length() - bname.length(); + } + return aname.compareTo(bname); + }; + + this.obfClasses = new ClassSelector(gui, obfClassComparator, false); + this.obfClasses.setSelectionListener(gui.getController()::navigateTo); + this.obfClasses.setRenameSelectionListener(gui::onPanelRename); + + this.setLayout(new BorderLayout()); + this.add(new JLabel(I18n.translate("info_panel.classes.obfuscated")), BorderLayout.NORTH); + this.add(new JScrollPane(this.obfClasses), BorderLayout.CENTER); + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java new file mode 100644 index 00000000..91727c38 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java @@ -0,0 +1,17 @@ +package cuchaz.enigma.gui.search; + +import java.util.List; + +public interface SearchEntry { + + List getSearchableNames(); + + /** + * Returns a type that uniquely identifies this search entry across possible changes. + * This is used for tracking the amount of times this entry has been selected. + * + * @return a unique identifier for this search entry + */ + String getIdentifier(); + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java new file mode 100644 index 00000000..a3b35faa --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java @@ -0,0 +1,268 @@ +package cuchaz.enigma.gui.search; + +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import cuchaz.enigma.utils.Pair; + +public class SearchUtil { + + private final Map> entries = new HashMap<>(); + private final Map hitCount = new HashMap<>(); + private final Executor searchExecutor = Executors.newWorkStealingPool(); + + public void add(T entry) { + Entry e = Entry.from(entry); + entries.put(entry, e); + } + + public void add(Entry entry) { + entries.put(entry.searchEntry, entry); + } + + public void addAll(Collection entries) { + this.entries.putAll(entries.parallelStream().collect(Collectors.toMap(e -> e, Entry::from))); + } + + public void remove(T entry) { + entries.remove(entry); + } + + public void clear() { + entries.clear(); + } + + public void clearHits() { + hitCount.clear(); + } + + public Stream search(String term) { + return entries.values().parallelStream() + .map(e -> new Pair<>(e, e.getScore(term, hitCount.getOrDefault(e.searchEntry.getIdentifier(), 0)))) + .filter(e -> e.b > 0) + .sorted(Comparator.comparingDouble(o -> -o.b)) + .map(e -> e.a.searchEntry) + .sequential(); + } + + public SearchControl asyncSearch(String term, SearchResultConsumer consumer) { + Map hitCount = new HashMap<>(this.hitCount); + Map> entries = new HashMap<>(this.entries); + float[] scores = new float[entries.size()]; + Lock scoresLock = new ReentrantLock(); + AtomicInteger size = new AtomicInteger(); + AtomicBoolean control = new AtomicBoolean(false); + AtomicInteger elapsed = new AtomicInteger(); + for (Entry value : entries.values()) { + searchExecutor.execute(() -> { + try { + if (control.get()) return; + float score = value.getScore(term, hitCount.getOrDefault(value.searchEntry.getIdentifier(), 0)); + if (score <= 0) return; + score = -score; // sort descending + try { + scoresLock.lock(); + if (control.get()) return; + int dataSize = size.getAndIncrement(); + int index = Arrays.binarySearch(scores, 0, dataSize, score); + if (index < 0) { + index = ~index; + } + System.arraycopy(scores, index, scores, index + 1, dataSize - index); + scores[index] = score; + consumer.add(index, value.searchEntry); + } finally { + scoresLock.unlock(); + } + } finally { + elapsed.incrementAndGet(); + } + }); + } + + return new SearchControl() { + @Override + public void stop() { + control.set(true); + } + + @Override + public boolean isFinished() { + return entries.size() == elapsed.get(); + } + + @Override + public float getProgress() { + return (float) elapsed.get() / entries.size(); + } + }; + } + + public void hit(T entry) { + if (entries.containsKey(entry)) { + hitCount.compute(entry.getIdentifier(), (_id, i) -> i == null ? 1 : i + 1); + } + } + + public static final class Entry { + + public final T searchEntry; + private final String[][] components; + + private Entry(T searchEntry, String[][] components) { + this.searchEntry = searchEntry; + this.components = components; + } + + public float getScore(String term, int hits) { + String ucTerm = term.toUpperCase(Locale.ROOT); + float maxScore = (float) Arrays.stream(components) + .mapToDouble(name -> getScoreFor(ucTerm, name)) + .max().orElse(0.0); + return maxScore * (hits + 1); + } + + /** + * Computes the score for the given name against the given search term. + * + * @param term the search term (expected to be upper-case) + * @param name the entry name, split at word boundaries (see {@link Entry#wordwiseSplit(String)}) + * @return the computed score for the entry + */ + private static float getScoreFor(String term, String[] name) { + int totalLength = Arrays.stream(name).mapToInt(String::length).sum(); + float scorePerChar = 1f / totalLength; + + // This map contains a snapshot of all the states the search has + // been in. The keys are the remaining characters of the search + // term, the values are the maximum scores for that remaining + // search term part. + Map snapshots = new HashMap<>(); + snapshots.put(term, 0f); + + // For each component, start at each existing snapshot, searching + // for the next longest match, and calculate the new score for each + // match length until the maximum. Then the new scores are put back + // into the snapshot map. + for (int componentIndex = 0; componentIndex < name.length; componentIndex++) { + String component = name[componentIndex]; + float posMultiplier = (name.length - componentIndex) * 0.3f; + Map newSnapshots = new HashMap<>(); + for (Map.Entry snapshot : snapshots.entrySet()) { + String remaining = snapshot.getKey(); + float score = snapshot.getValue(); + component = component.toUpperCase(Locale.ROOT); + int l = compareEqualLength(remaining, component); + for (int i = 1; i <= l; i++) { + float baseScore = scorePerChar * i; + float chainBonus = (i - 1) * 0.5f; + merge(newSnapshots, Collections.singletonMap(remaining.substring(i), score + baseScore * posMultiplier + chainBonus), Math::max); + } + } + merge(snapshots, newSnapshots, Math::max); + } + + // Only return the score for when the search term was completely + // consumed. + return snapshots.getOrDefault("", 0f); + } + + private static void merge(Map self, Map source, BiFunction combiner) { + source.forEach((k, v) -> self.compute(k, (_k, v1) -> v1 == null ? v : v == null ? v1 : combiner.apply(v, v1))); + } + + public static Entry from(T e) { + String[][] components = e.getSearchableNames().parallelStream() + .map(Entry::wordwiseSplit) + .toArray(String[][]::new); + return new Entry<>(e, components); + } + + private static int compareEqualLength(String s1, String s2) { + int len = 0; + while (len < s1.length() && len < s2.length() && s1.charAt(len) == s2.charAt(len)) { + len += 1; + } + return len; + } + + /** + * Splits the given input into components, trying to detect word parts. + *

+ * Example of how words get split (using | as seperator): + *

MinecraftClientGame -> Minecraft|Client|Game

+ *

HTTPInputStream -> HTTP|Input|Stream

+ *

class_932 -> class|_|932

+ *

X11FontManager -> X|11|Font|Manager

+ *

openHTTPConnection -> open|HTTP|Connection

+ *

open_http_connection -> open|_|http|_|connection

+ * + * @param input the input to split + * @return the resulting components + */ + private static String[] wordwiseSplit(String input) { + List list = new ArrayList<>(); + while (!input.isEmpty()) { + int take; + if (Character.isLetter(input.charAt(0))) { + if (input.length() == 1) { + take = 1; + } else { + boolean nextSegmentIsUppercase = Character.isUpperCase(input.charAt(0)) && Character.isUpperCase(input.charAt(1)); + if (nextSegmentIsUppercase) { + int nextLowercase = 1; + while (Character.isUpperCase(input.charAt(nextLowercase))) { + nextLowercase += 1; + if (nextLowercase == input.length()) { + nextLowercase += 1; + break; + } + } + take = nextLowercase - 1; + } else { + int nextUppercase = 1; + while (nextUppercase < input.length() && Character.isLowerCase(input.charAt(nextUppercase))) { + nextUppercase += 1; + } + take = nextUppercase; + } + } + } else if (Character.isDigit(input.charAt(0))) { + int nextNonNum = 1; + while (nextNonNum < input.length() && Character.isLetter(input.charAt(nextNonNum)) && !Character.isLowerCase(input.charAt(nextNonNum))) { + nextNonNum += 1; + } + take = nextNonNum; + } else { + take = 1; + } + list.add(input.substring(0, take)); + input = input.substring(take); + } + return list.toArray(new String[0]); + } + + } + + @FunctionalInterface + public interface SearchResultConsumer { + void add(int index, T entry); + } + + public interface SearchControl { + void stop(); + + boolean isFinished(); + + float getProgress(); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java new file mode 100644 index 00000000..d7f7ec0a --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java @@ -0,0 +1,197 @@ +package cuchaz.enigma.gui.stats; + +import com.google.gson.GsonBuilder; +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.ProgressListener; +import cuchaz.enigma.analysis.index.EntryIndex; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.api.service.ObfuscationTestService; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.*; +import cuchaz.enigma.utils.I18n; + +import java.util.*; + +public class StatsGenerator { + private final EntryIndex entryIndex; + private final EntryRemapper mapper; + private final EntryResolver entryResolver; + private final List obfuscationTestServices; + private final List nameProposalServices; + + public StatsGenerator(EnigmaProject project) { + entryIndex = project.getJarIndex().getEntryIndex(); + mapper = project.getMapper(); + entryResolver = project.getJarIndex().getEntryResolver(); + obfuscationTestServices = project.getEnigma().getServices().get(ObfuscationTestService.TYPE); + nameProposalServices = project.getEnigma().getServices().get(NameProposalService.TYPE); + } + + public String generate(ProgressListener progress, Set includedMembers) { + includedMembers = EnumSet.copyOf(includedMembers); + int totalWork = 0; + + if (includedMembers.contains(StatsMember.METHODS) || includedMembers.contains(StatsMember.PARAMETERS)) { + totalWork += entryIndex.getMethods().size(); + } + + if (includedMembers.contains(StatsMember.FIELDS)) { + totalWork += entryIndex.getFields().size(); + } + + if (includedMembers.contains(StatsMember.CLASSES)) { + totalWork += entryIndex.getClasses().size(); + } + + progress.init(totalWork, "progress.stats"); + + Map counts = new HashMap<>(); + + int numDone = 0; + if (includedMembers.contains(StatsMember.METHODS) || includedMembers.contains(StatsMember.PARAMETERS)) { + for (MethodEntry method : entryIndex.getMethods()) { + progress.step(numDone++, I18n.translate("type.methods")); + MethodEntry root = entryResolver + .resolveEntry(method, ResolutionStrategy.RESOLVE_ROOT) + .stream() + .findFirst() + .orElseThrow(AssertionError::new); + + if (root == method && !((MethodDefEntry) method).getAccess().isSynthetic()) { + if (includedMembers.contains(StatsMember.METHODS)) { + update(counts, method); + } + + if (includedMembers.contains(StatsMember.PARAMETERS)) { + int index = ((MethodDefEntry) method).getAccess().isStatic() ? 0 : 1; + for (TypeDescriptor argument : method.getDesc().getArgumentDescs()) { + update(counts, new LocalVariableEntry(method, index, "", true,null)); + index += argument.getSize(); + } + } + } + } + } + + if (includedMembers.contains(StatsMember.FIELDS)) { + for (FieldEntry field : entryIndex.getFields()) { + progress.step(numDone++, I18n.translate("type.fields")); + update(counts, field); + } + } + + if (includedMembers.contains(StatsMember.CLASSES)) { + for (ClassEntry clazz : entryIndex.getClasses()) { + progress.step(numDone++, I18n.translate("type.classes")); + update(counts, clazz); + } + } + + progress.step(-1, I18n.translate("progress.stats.data")); + + Tree tree = new Tree<>(); + + for (Map.Entry entry : counts.entrySet()) { + if (entry.getKey().startsWith("com.mojang")) continue; // just a few unmapped names, no point in having a subsection + tree.getNode(entry.getKey()).value = entry.getValue(); + } + + tree.collapse(tree.root); + return new GsonBuilder().setPrettyPrinting().create().toJson(tree.root); + } + + private void update(Map counts, Entry entry) { + if (isObfuscated(entry)) { + String parent = mapper.deobfuscate(entry.getAncestry().get(0)).getName().replace('/', '.'); + counts.put(parent, counts.getOrDefault(parent, 0) + 1); + } + } + + private boolean isObfuscated(Entry entry) { + String name = entry.getName(); + + if (!obfuscationTestServices.isEmpty()) { + for (ObfuscationTestService service : obfuscationTestServices) { + if (service.testDeobfuscated(entry)) { + return false; + } + } + } + + if (!nameProposalServices.isEmpty()) { + for (NameProposalService service : nameProposalServices) { + if (service.proposeName(entry, mapper).isPresent()) { + return false; + } + } + } + + String mappedName = mapper.deobfuscate(entry).getName(); + if (mappedName != null && !mappedName.isEmpty() && !mappedName.equals(name)) { + return false; + } + + return true; + } + + private static class Tree { + public final Node root; + private final Map> nodes = new HashMap<>(); + + public static class Node { + public String name; + public T value; + public List> children = new ArrayList<>(); + private final transient Map> namedChildren = new HashMap<>(); + + public Node(String name, T value) { + this.name = name; + this.value = value; + } + } + + public Tree() { + root = new Node<>("", null); + } + + public Node getNode(String name) { + Node node = nodes.get(name); + + if (node == null) { + node = root; + + for (String part : name.split("\\.")) { + Node child = node.namedChildren.get(part); + + if (child == null) { + child = new Node<>(part, null); + node.namedChildren.put(part, child); + node.children.add(child); + } + + node = child; + } + + nodes.put(name, node); + } + + return node; + } + + public void collapse(Node node) { + while (node.children.size() == 1) { + Node child = node.children.get(0); + node.name = node.name.isEmpty() ? child.name : node.name + "." + child.name; + node.children = child.children; + node.value = child.value; + } + + for (Node child : node.children) { + collapse(child); + } + } + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java new file mode 100644 index 00000000..70b4f40d --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java @@ -0,0 +1,8 @@ +package cuchaz.enigma.gui.stats; + +public enum StatsMember { + METHODS, + FIELDS, + PARAMETERS, + CLASSES +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java new file mode 100644 index 00000000..612e3e92 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java @@ -0,0 +1,77 @@ +package cuchaz.enigma.gui.util; + +import java.awt.Component; +import java.awt.event.MouseEvent; + +import javax.swing.*; +import javax.swing.border.Border; + +public abstract class AbstractListCellRenderer extends JPanel implements ListCellRenderer { + + private static final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1); + + private Border noFocusBorder; + + public AbstractListCellRenderer() { + setBorder(getNoFocusBorder()); + } + + protected Border getNoFocusBorder() { + if (noFocusBorder == null) { + Border border = UIManager.getLookAndFeel().getDefaults().getBorder("List.List.cellNoFocusBorder"); + noFocusBorder = border != null ? border : NO_FOCUS_BORDER; + } + return noFocusBorder; + } + + protected Border getBorder(boolean isSelected, boolean cellHasFocus) { + Border b = null; + if (cellHasFocus) { + UIDefaults defaults = UIManager.getLookAndFeel().getDefaults(); + if (isSelected) { + b = defaults.getBorder("List.focusSelectedCellHighlightBorder"); + } + if (b == null) { + b = defaults.getBorder("List.focusCellHighlightBorder"); + } + } else { + b = getNoFocusBorder(); + } + return b; + } + + public abstract void updateUiForEntry(JList list, E value, int index, boolean isSelected, boolean cellHasFocus); + + @Override + public Component getListCellRendererComponent(JList list, E value, int index, boolean isSelected, boolean cellHasFocus) { + updateUiForEntry(list, value, index, isSelected, cellHasFocus); + + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + setEnabled(list.isEnabled()); + setFont(list.getFont()); + + setBorder(getBorder(isSelected, cellHasFocus)); + + // This isn't the width of the cell, but it's close enough for where it's needed (getComponentAt in getToolTipText) + setSize(list.getWidth(), getPreferredSize().height); + + return this; + } + + @Override + public String getToolTipText(MouseEvent event) { + Component c = getComponentAt(event.getPoint()); + if (c instanceof JComponent) { + return ((JComponent) c).getToolTipText(); + } + return getToolTipText(); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java new file mode 100644 index 00000000..70172fe7 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java @@ -0,0 +1,56 @@ +package cuchaz.enigma.gui.util; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.StringJoiner; + +public class GuiUtil { + public static void openUrl(String url) { + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + try { + desktop.browse(new URI(url)); + } catch (IOException ex) { + throw new Error(ex); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException(ex); + } + } + } + + 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 Rectangle safeModelToView(JTextComponent component, int modelPos) { + if (modelPos < 0) { + modelPos = 0; + } else if (modelPos >= component.getText().length()) { + modelPos = component.getText().length(); + } + try { + return component.modelToView(modelPos); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java new file mode 100644 index 00000000..b1286998 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java @@ -0,0 +1,49 @@ +package cuchaz.enigma.gui.util; + +import com.google.common.collect.Queues; + +import java.util.Deque; + +public class History { + private final Deque previous = Queues.newArrayDeque(); + private final Deque next = Queues.newArrayDeque(); + private T current; + + public History(T initial) { + current = initial; + } + + public T getCurrent() { + return current; + } + + public void push(T value) { + previous.addLast(current); + current = value; + next.clear(); + } + + public void replace(T value) { + current = value; + } + + public boolean canGoBack() { + return !previous.isEmpty(); + } + + public T goBack() { + next.addFirst(current); + current = previous.removeLast(); + return current; + } + + public boolean canGoForward() { + return !next.isEmpty(); + } + + public T goForward() { + previous.addLast(current); + current = next.removeFirst(); + return current; + } +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java new file mode 100644 index 00000000..d045c6d5 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java @@ -0,0 +1,8 @@ +package cuchaz.enigma.gui.util; + +@FunctionalInterface +public interface ScaleChangeListener { + + void onScaleChanged(float scale, float oldScale); + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java new file mode 100644 index 00000000..e7ee5657 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java @@ -0,0 +1,110 @@ +package cuchaz.enigma.gui.util; + +import java.awt.Dimension; +import java.awt.Font; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.UIManager; +import javax.swing.border.Border; + +import com.github.swingdpi.UiDefaultsScaler; +import com.github.swingdpi.plaf.BasicTweaker; +import com.github.swingdpi.plaf.MetalTweaker; +import com.github.swingdpi.plaf.NimbusTweaker; +import com.github.swingdpi.plaf.WindowsTweaker; +import cuchaz.enigma.gui.config.Config; +import de.sciss.syntaxpane.DefaultSyntaxKit; + +public class ScaleUtil { + + private static List listeners = new ArrayList<>(); + + public static float getScaleFactor() { + return Config.getInstance().scaleFactor; + } + + public static void setScaleFactor(float scaleFactor) { + float oldScale = getScaleFactor(); + float clamped = Math.min(Math.max(0.25f, scaleFactor), 10.0f); + Config.getInstance().scaleFactor = clamped; + try { + Config.getInstance().saveConfig(); + } catch (IOException e) { + e.printStackTrace(); + } + listeners.forEach(l -> l.onScaleChanged(clamped, oldScale)); + } + + public static void addListener(ScaleChangeListener listener) { + listeners.add(listener); + } + + public static void removeListener(ScaleChangeListener listener) { + listeners.remove(listener); + } + + public static Dimension getDimension(int width, int height) { + return new Dimension(scale(width), scale(height)); + } + + public static Font getFont(String fontName, int plain, int fontSize) { + return scaleFont(new Font(fontName, plain, fontSize)); + } + + public static Font scaleFont(Font font) { + return createTweakerForCurrentLook(getScaleFactor()).modifyFont("", font); + } + + public static float scale(float f) { + return f * getScaleFactor(); + } + + public static float invert(float f) { + return f / getScaleFactor(); + } + + public static int scale(int i) { + return (int) (i * getScaleFactor()); + } + + public static Border createEmptyBorder(int top, int left, int bottom, int right) { + return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right)); + } + + public static int invert(int i) { + return (int) (i / getScaleFactor()); + } + + public static void applyScaling() { + float scale = getScaleFactor(); + UiDefaultsScaler.updateAndApplyGlobalScaling((int) (100 * scale), true); + try { + Field defaultFontField = DefaultSyntaxKit.class.getDeclaredField("DEFAULT_FONT"); + defaultFontField.setAccessible(true); + Font font = (Font) defaultFontField.get(null); + font = font.deriveFont(12 * scale); + defaultFontField.set(null, font); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + private static BasicTweaker createTweakerForCurrentLook(float dpiScaling) { + String testString = UIManager.getLookAndFeel().getName().toLowerCase(); + if (testString.contains("windows")) { + return new WindowsTweaker(dpiScaling, testString.contains("classic")); + } + if (testString.contains("metal")) { + return new MetalTweaker(dpiScaling); + } + if (testString.contains("nimbus")) { + return new NimbusTweaker(dpiScaling); + } + return new BasicTweaker(dpiScaling); + } + +} diff --git a/enigma-swing/src/main/resources/about.html b/enigma-swing/src/main/resources/about.html new file mode 100644 index 00000000..b75c1bf0 --- /dev/null +++ b/enigma-swing/src/main/resources/about.html @@ -0,0 +1,6 @@ + +

%s

+

A tool for debofuscation of Java code

+

+

Version: %s

+ \ No newline at end of file diff --git a/enigma-swing/src/main/resources/stats.html b/enigma-swing/src/main/resources/stats.html new file mode 100644 index 00000000..fcff7c0f --- /dev/null +++ b/enigma-swing/src/main/resources/stats.html @@ -0,0 +1,34 @@ + + + + + + Stats + + + + + +
+ + + + + + + -- cgit v1.2.3