/******************************************************************************* * Copyright (c) 2015 Jeff Martin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public * License v3.0 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * *

Contributors: * Jeff Martin - initial API and implementation ******************************************************************************/ package cuchaz.enigma.gui; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Point; import java.awt.event.ActionEvent; 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.function.Consumer; import java.util.function.Function; import javax.swing.AbstractAction; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.tree.DefaultMutableTreeNode; import org.jetbrains.annotations.Nullable; import cuchaz.enigma.Enigma; import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.api.service.GuiService; import cuchaz.enigma.gui.config.Themes; import cuchaz.enigma.gui.config.UiConfig; import cuchaz.enigma.gui.dialog.JavadocDialog; import cuchaz.enigma.gui.dialog.SearchDialog; import cuchaz.enigma.gui.elements.CallsTree; import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; import cuchaz.enigma.gui.elements.EditorTabbedPane; import cuchaz.enigma.gui.elements.ImplementationsTree; import cuchaz.enigma.gui.elements.InheritanceTree; import cuchaz.enigma.gui.elements.MainWindow; import cuchaz.enigma.gui.elements.MenuBar; import cuchaz.enigma.gui.elements.ValidatableUi; import cuchaz.enigma.gui.panels.DeobfPanel; import cuchaz.enigma.gui.panels.EditorPanel; import cuchaz.enigma.gui.panels.IdentifierPanel; import cuchaz.enigma.gui.panels.ObfPanel; import cuchaz.enigma.gui.panels.StructurePanel; import cuchaz.enigma.gui.renderer.MessageListCellRenderer; import cuchaz.enigma.gui.util.ExtensionFileFilter; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.gui.util.LanguageUtil; import cuchaz.enigma.gui.util.ScaleUtil; import cuchaz.enigma.network.Message; import cuchaz.enigma.network.packet.MessageC2SPacket; import cuchaz.enigma.source.Token; import cuchaz.enigma.translation.mapping.EntryChange; import cuchaz.enigma.translation.mapping.EntryRemapper; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.utils.I18n; import cuchaz.enigma.utils.validation.ParameterizedMessage; import cuchaz.enigma.utils.validation.ValidationContext; public class Gui { private final MainWindow mainWindow = new MainWindow(Enigma.NAME); private final GuiController controller; private ConnectionState connectionState; private boolean isJarOpen; private final Set editableTypes; private boolean singleClassTree; private final MenuBar menuBar; private final ObfPanel obfPanel; private final DeobfPanel deobfPanel; private final IdentifierPanel infoPanel; private final StructurePanel structurePanel; private final InheritanceTree inheritanceTree; private final ImplementationsTree implementationsTree; private final CallsTree callsTree; private final EditorTabbedPane editorTabbedPane; private final JPanel classesPanel = new JPanel(new BorderLayout()); private final JSplitPane splitClasses; private final JTabbedPane tabs = new JTabbedPane(); private final CollapsibleTabbedPane logTabs = new CollapsibleTabbedPane(JTabbedPane.BOTTOM); private final JSplitPane logSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, tabs, logTabs); private final JPanel centerPanel = new JPanel(new BorderLayout()); private final JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, this.logSplit); private final JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.classesPanel, splitRight); private final DefaultListModel userModel = new DefaultListModel<>(); private final DefaultListModel messageModel = new DefaultListModel<>(); private final JList users = new JList<>(userModel); private final JList messages = new JList<>(messageModel); private final JPanel messagePanel = new JPanel(new BorderLayout()); private final JScrollPane messageScrollPane = new JScrollPane(this.messages); private final JTextField chatBox = new JTextField(); private final JLabel connectionStatusLabel = new JLabel(); public final JFileChooser jarFileChooser = new JFileChooser(); public final JFileChooser mappingsFileChooser = new JFileChooser(); public final JFileChooser exportSourceFileChooser = new JFileChooser(); public final JFileChooser exportJarFileChooser = new JFileChooser(); public SearchDialog searchDialog; public Gui(Enigma enigma, Set editableTypes) { this.editableTypes = editableTypes; this.controller = new GuiController(this, enigma); this.structurePanel = new StructurePanel(this); this.deobfPanel = new DeobfPanel(this); this.infoPanel = new IdentifierPanel(this); this.obfPanel = new ObfPanel(this); this.menuBar = new MenuBar(this); this.inheritanceTree = new InheritanceTree(this); this.implementationsTree = new ImplementationsTree(this); this.callsTree = new CallsTree(this); this.editorTabbedPane = new EditorTabbedPane(this); this.splitClasses = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, this.obfPanel, this.deobfPanel); this.setupUi(); LanguageUtil.addListener(this::retranslateUi); Themes.addListener((lookAndFeel, boxHighlightPainters) -> SwingUtilities.updateComponentTreeUI(this.getFrame())); for (GuiService guiService : enigma.getServices().get(GuiService.TYPE)) { guiService.onStart(controller); } this.mainWindow.setVisible(true); } private void setupUi() { this.jarFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); this.jarFileChooser.setMultiSelectionEnabled(true); this.exportSourceFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); this.exportSourceFileChooser.setAcceptAllFileFilterUsed(false); this.exportJarFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); this.splitClasses.setResizeWeight(0.3); this.classesPanel.setPreferredSize(ScaleUtil.getDimension(250, 0)); // layout controls Container workArea = this.mainWindow.workArea(); workArea.setLayout(new BorderLayout()); centerPanel.add(infoPanel.getUi(), BorderLayout.NORTH); centerPanel.add(this.editorTabbedPane.getUi(), BorderLayout.CENTER); tabs.setPreferredSize(ScaleUtil.getDimension(250, 0)); tabs.addTab(I18n.translate("info_panel.tree.structure"), structurePanel.getPanel()); tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritanceTree.getPanel()); tabs.addTab(I18n.translate("info_panel.tree.implementations"), implementationsTree.getPanel()); tabs.addTab(I18n.translate("info_panel.tree.calls"), callsTree.getPanel()); messages.setCellRenderer(new MessageListCellRenderer()); JPanel chatPanel = new JPanel(new BorderLayout()); 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(messageScrollPane, BorderLayout.CENTER); 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.setResizeWeight(0.5); logSplit.resetToPreferredSizes(); splitRight.setResizeWeight(1); // let the left side take all the slack splitRight.resetToPreferredSizes(); splitCenter.setResizeWeight(0); // let the right side take all the slack workArea.add(splitCenter, BorderLayout.CENTER); // restore state int[] layout = UiConfig.getLayout(); if (layout.length >= 4) { this.splitClasses.setDividerLocation(layout[0]); this.splitCenter.setDividerLocation(layout[1]); this.splitRight.setDividerLocation(layout[2]); this.logSplit.setDividerLocation(layout[3]); } this.mainWindow.statusBar().addPermanentComponent(this.connectionStatusLabel); // init state setConnectionState(ConnectionState.NOT_CONNECTED); onCloseJar(); JFrame frame = this.mainWindow.frame(); frame.addWindowListener(GuiUtil.onWindowClose(e -> this.close())); frame.setSize(UiConfig.getWindowSize("Main Window", ScaleUtil.getDimension(1024, 576))); frame.setExtendedState(UiConfig.isFullscreen("Main Window") ? JFrame.MAXIMIZED_BOTH : JFrame.NORMAL); frame.setMinimumSize(ScaleUtil.getDimension(640, 480)); frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); Point windowPos = UiConfig.getWindowPos("Main Window", null); if (windowPos != null) { frame.setLocation(windowPos); } else { frame.setLocationRelativeTo(null); } this.retranslateUi(); } public MainWindow getMainWindow() { return this.mainWindow; } public JFrame getFrame() { return this.mainWindow.frame(); } public GuiController getController() { return this.controller; } public void setSingleClassTree(boolean singleClassTree) { this.singleClassTree = singleClassTree; this.classesPanel.removeAll(); this.classesPanel.add(isSingleClassTree() ? deobfPanel : splitClasses); getController().refreshClasses(); retranslateUi(); } public boolean isSingleClassTree() { return singleClassTree; } public void onStartOpenJar() { this.classesPanel.removeAll(); redraw(); } public void onFinishOpenJar(String jarName) { // update gui this.mainWindow.setTitle(Enigma.NAME + " - " + jarName); this.classesPanel.removeAll(); this.classesPanel.add(isSingleClassTree() ? deobfPanel : splitClasses); this.editorTabbedPane.closeAllEditorTabs(); // update menu isJarOpen = true; updateUiState(); redraw(); } public void onCloseJar() { // update gui this.mainWindow.setTitle(Enigma.NAME); setObfClasses(null); setDeobfClasses(null); this.editorTabbedPane.closeAllEditorTabs(); this.classesPanel.removeAll(); // update menu isJarOpen = false; setMappingsFile(null); updateUiState(); redraw(); } public EditorPanel openClass(ClassEntry entry) { return this.editorTabbedPane.openClass(entry); } @Nullable public EditorPanel getActiveEditor() { return this.editorTabbedPane.getActiveEditor(); } public void closeEditor(EditorPanel editor) { this.editorTabbedPane.closeEditor(editor); } /** * Navigates to the reference without modifying history. If the class is not currently loaded, it will be loaded. * * @param reference the reference */ public void showReference(EntryReference, Entry> reference) { this.editorTabbedPane.openClass(reference.getLocationClassEntry().getOutermostClass()).showReference(reference); } 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.mappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); updateUiState(); } public void showTokens(EditorPanel editor, List tokens) { if (tokens.size() > 1) { this.controller.setTokenHandle(editor.getClassHandle().copy()); this.callsTree.showTokens(tokens); } else { this.callsTree.clearTokens(); } // show the first token editor.navigateToToken(tokens.get(0)); } public void showCursorReference(EntryReference, Entry> reference) { infoPanel.setReference(reference == null ? null : reference.entry); } @Nullable public EntryReference, Entry> getCursorReference() { EditorPanel activeEditor = this.editorTabbedPane.getActiveEditor(); return activeEditor == null ? null : activeEditor.getCursorReference(); } @Nullable public Entry getCursorDeclaration() { EditorPanel activeEditor = this.editorTabbedPane.getActiveEditor(); return activeEditor == null ? null : activeEditor.getCursorDeclaration(); } public void startDocChange(EditorPanel editor) { EntryReference, Entry> cursorReference = editor.getCursorReference(); if (cursorReference == null || !this.isEditable(EditableType.JAVADOC)) { return; } JavadocDialog.show(mainWindow.frame(), getController(), cursorReference); } public void startRename(EditorPanel editor, String text) { if (editor != this.editorTabbedPane.getActiveEditor()) { return; } infoPanel.startRenaming(text); } public void startRename(EditorPanel editor) { if (editor != this.editorTabbedPane.getActiveEditor()) { return; } infoPanel.startRenaming(); } public void showStructure(EditorPanel editor) { this.structurePanel.showStructure(editor); } public void showInheritance(EditorPanel editor) { EntryReference, Entry> cursorReference = editor.getCursorReference(); if (cursorReference == null) { return; } this.inheritanceTree.display(cursorReference.entry); tabs.setSelectedIndex(1); } public void showImplementations(EditorPanel editor) { EntryReference, Entry> cursorReference = editor.getCursorReference(); if (cursorReference == null) { return; } this.implementationsTree.display(cursorReference.entry); tabs.setSelectedIndex(2); } public void showCalls(EditorPanel editor, boolean recurse) { EntryReference, Entry> cursorReference = editor.getCursorReference(); if (cursorReference == null) { return; } this.callsTree.showCalls(cursorReference.entry, recurse); tabs.setSelectedIndex(3); } public void toggleMapping(EditorPanel editor) { EntryReference, Entry> cursorReference = editor.getCursorReference(); if (cursorReference == null) { return; } Entry obfEntry = cursorReference.getNameableEntry(); toggleMappingFromEntry(obfEntry); } public void toggleMappingFromEntry(Entry obfEntry) { if (this.controller.project.getMapper().getDeobfMapping(obfEntry).targetName() != null) { validateImmediateAction(vc -> this.controller.applyChange(vc, EntryChange.modify(obfEntry).clearDeobfName())); } else { validateImmediateAction(vc -> this.controller.applyChange(vc, EntryChange.modify(obfEntry).withDefaultDeobfName(this.getController().project))); } } public void showDiscardDiag(Function callback, String... options) { int response = JOptionPane.showOptionDialog(this.mainWindow.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 CompletableFuture saveMapping() { ExtensionFileFilter.setupFileChooser(this.mappingsFileChooser, this.controller.getLoadedMappingFormat()); if (this.mappingsFileChooser.getSelectedFile() != null || this.mappingsFileChooser.showSaveDialog(this.mainWindow.frame()) == JFileChooser.APPROVE_OPTION) { return this.controller.saveMappings(ExtensionFileFilter.getSavePath(this.mappingsFileChooser)); } return CompletableFuture.completedFuture(null); } 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().thenRun(this::exit); // do not join, as join waits on swing to clear events } else if (response == JOptionPane.NO_OPTION) { exit(); } return null; }, I18n.translate("prompt.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.cancel")); } } private void exit() { UiConfig.setWindowPos("Main Window", this.mainWindow.frame().getLocationOnScreen()); UiConfig.setWindowSize("Main Window", this.mainWindow.frame().getSize()); UiConfig.setFullscreen("Main Window", this.mainWindow.frame().getExtendedState() == JFrame.MAXIMIZED_BOTH); UiConfig.setLayout(this.splitClasses.getDividerLocation(), this.splitCenter.getDividerLocation(), this.splitRight.getDividerLocation(), this.logSplit.getDividerLocation()); UiConfig.save(); if (searchDialog != null) { searchDialog.dispose(); } this.mainWindow.frame().dispose(); System.exit(0); } public void redraw() { JFrame frame = this.mainWindow.frame(); frame.validate(); frame.repaint(); } public void onRenameFromClassTree(ValidationContext vc, Object prevData, Object data, DefaultMutableTreeNode node) { if (data instanceof String) { // package rename 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()); onRenameFromClassTree(vc, prevDataChild, dataChild, node); } node.setUserObject(data); // Ob package will never be modified, just reload deob view this.deobfPanel.deobfClasses.reload(); } else if (data instanceof ClassEntry) { // class rename // TODO optimize reverse class lookup, although it looks like it's // fast enough for now EntryRemapper mapper = this.controller.project.getMapper(); ClassEntry deobf = (ClassEntry) prevData; ClassEntry obf = mapper.getObfToDeobf().getAllEntries().filter(e -> e instanceof ClassEntry).map(e -> (ClassEntry) e).filter(e -> mapper.deobfuscate(e).equals(deobf)).findAny().orElse(deobf); this.controller.applyChange(vc, EntryChange.modify(obf).withDeobfName(((ClassEntry) data).getFullName())); } else { throw new IllegalStateException(String.format("unhandled rename object data: '%s'", data)); } } public void moveClassTree(Entry obfEntry, String newName) { String oldEntry = obfEntry.getContainingClass().getPackageName(); String newEntry = new ClassEntry(newName).getPackageName(); moveClassTree(obfEntry, oldEntry == null, newEntry == null); } // TODO: getExpansionState will *not* actually update itself based on name changes! public void moveClassTree(Entry obfEntry, boolean isOldOb, boolean isNewOb) { ClassEntry classEntry = obfEntry.getContainingClass(); List stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(); List stateObf = this.obfPanel.obfClasses.getExpansionState(); // Ob -> deob if (!isNewOb) { this.deobfPanel.deobfClasses.moveClassIn(classEntry); this.obfPanel.obfClasses.removeEntry(classEntry); this.deobfPanel.deobfClasses.reload(); this.obfPanel.obfClasses.reload(); } else if (!isOldOb) { // Deob -> ob this.obfPanel.obfClasses.moveClassIn(classEntry); this.deobfPanel.deobfClasses.removeEntry(classEntry); this.deobfPanel.deobfClasses.reload(); this.obfPanel.obfClasses.reload(); } else if (isOldOb) { // Local move this.obfPanel.obfClasses.moveClassIn(classEntry); this.obfPanel.obfClasses.reload(); } else { this.deobfPanel.deobfClasses.moveClassIn(classEntry); this.deobfPanel.deobfClasses.reload(); } this.deobfPanel.deobfClasses.restoreExpansionState(stateDeobf); this.obfPanel.obfClasses.restoreExpansionState(stateObf); } public ObfPanel getObfPanel() { return obfPanel; } public DeobfPanel getDeobfPanel() { return deobfPanel; } public SearchDialog getSearchDialog() { if (searchDialog == null) { searchDialog = new SearchDialog(this); } return searchDialog; } 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())); } this.mainWindow.statusBar().showMessage(message.translate(), 5000); } 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.updateUiState(); 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); } splitRight.setDividerLocation(splitRight.getDividerLocation()); } public void retranslateUi() { this.jarFileChooser.setDialogTitle(I18n.translate("menu.file.jar.open")); this.exportJarFileChooser.setDialogTitle(I18n.translate("menu.file.export.jar")); this.tabs.setTitleAt(0, I18n.translate("info_panel.tree.structure")); this.tabs.setTitleAt(1, I18n.translate("info_panel.tree.inheritance")); this.tabs.setTitleAt(2, I18n.translate("info_panel.tree.implementations")); this.tabs.setTitleAt(3, I18n.translate("info_panel.tree.calls")); this.logTabs.setTitleAt(0, I18n.translate("log_panel.users")); this.logTabs.setTitleAt(1, I18n.translate("log_panel.messages")); this.connectionStatusLabel.setText(I18n.translate(connectionState == ConnectionState.NOT_CONNECTED ? "status.disconnected" : "status.connected")); this.updateUiState(); this.menuBar.retranslateUi(); this.obfPanel.retranslateUi(); this.deobfPanel.retranslateUi(); this.infoPanel.retranslateUi(); this.structurePanel.retranslateUi(); this.editorTabbedPane.retranslateUi(); this.inheritanceTree.retranslateUi(); this.implementationsTree.retranslateUi(); this.structurePanel.retranslateUi(); this.callsTree.retranslateUi(); } public void setConnectionState(ConnectionState state) { connectionState = state; updateUiState(); } public boolean isJarOpen() { return isJarOpen; } public ConnectionState getConnectionState() { return this.connectionState; } public boolean validateImmediateAction(Consumer op) { ValidationContext vc = new ValidationContext(); op.accept(vc); if (!vc.canProceed()) { List messages = vc.getMessages(); String text = ValidatableUi.formatMessages(messages); JOptionPane.showMessageDialog(this.getFrame(), text, String.format("%d message(s)", messages.size()), JOptionPane.ERROR_MESSAGE); } return vc.canProceed(); } public boolean isEditable(EditableType t) { return this.editableTypes.contains(t); } }