/******************************************************************************* * 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.Constants; import cuchaz.enigma.EnigmaProfile; import cuchaz.enigma.ExceptionIgnorer; import cuchaz.enigma.analysis.*; import cuchaz.enigma.config.Config; import cuchaz.enigma.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.History; import cuchaz.enigma.network.packet.*; import cuchaz.enigma.throwables.IllegalNameException; import cuchaz.enigma.translation.mapping.*; import cuchaz.enigma.translation.representation.entry.*; import cuchaz.enigma.utils.I18n; import cuchaz.enigma.utils.Message; import cuchaz.enigma.gui.util.ScaleUtil; import cuchaz.enigma.utils.Utils; 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(Constants.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(Constants.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(Constants.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(Utils.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()); Utils.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()); Utils.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(); } }