From 854f4d49407e45d67dd5754afd21a7e59970ca5b Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sun, 3 May 2020 21:06:38 +0100 Subject: Multiplayer support (#221) * First pass on multiplayer * Apply review suggestions * Dedicated Enigma server * Don't jump to references when other users do stuff * Better UI + translations * french translation * Apply review suggestions * Document the protocol * Fix most issues with scrolling. * Apply review suggestions * Fix zip hash issues + add a bit more logging * Optimize zip hash * Fix a couple of login bugs * Add message log and user list * Make Message an abstract class * Make status bar work, add chat box * Hide message log/users list when not connected * Fix status bar not resetting entirely * Run stop server task on server thread to prevent multithreading race conditions * Add c2s message to packet id list * Fix message scroll bar not scrolling to the end * Formatting * User list size -> ushort * Combine contains and remove check * Check removal before sending packet * Add password to login packet * Fix the GUI closing the rename text field when someone else renames something * Update fr_fr.json * oups * Make connection/server create dialogs not useless if it fails once * Refactor UI state updating * Fix imports * Fix Collab menu * Fix NPE when rename not allowed * Make the log file a configurable option * Don't use modified UTF * Update fr_fr.json * Bump version to 0.15.4 * Apparently I can't spell neither words nor semantic versions Co-authored-by: Yanis48 Co-authored-by: 2xsaiko --- .../java/cuchaz/enigma/gui/ConnectionState.java | 7 + .../cuchaz/enigma/gui/DecompiledClassSource.java | 26 +++ src/main/java/cuchaz/enigma/gui/Gui.java | 193 +++++++++++++++++---- src/main/java/cuchaz/enigma/gui/GuiController.java | 150 ++++++++++++++-- .../cuchaz/enigma/gui/MessageListCellRenderer.java | 24 +++ .../enigma/gui/dialog/ConnectToServerDialog.java | 82 +++++++++ .../enigma/gui/dialog/CreateServerDialog.java | 65 +++++++ .../enigma/gui/elements/CollapsibleTabbedPane.java | 40 +++++ .../java/cuchaz/enigma/gui/elements/MenuBar.java | 79 +++++++-- 9 files changed, 613 insertions(+), 53 deletions(-) create mode 100644 src/main/java/cuchaz/enigma/gui/ConnectionState.java create mode 100644 src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java create mode 100644 src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java create mode 100644 src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java create mode 100644 src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java (limited to 'src/main/java/cuchaz/enigma/gui') diff --git a/src/main/java/cuchaz/enigma/gui/ConnectionState.java b/src/main/java/cuchaz/enigma/gui/ConnectionState.java new file mode 100644 index 0000000..db6590d --- /dev/null +++ b/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/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java index f7097f0..08df3e7 100644 --- a/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java +++ b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java @@ -126,6 +126,32 @@ public class DecompiledClassSource { 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/src/main/java/cuchaz/enigma/gui/Gui.java b/src/main/java/cuchaz/enigma/gui/Gui.java index 3412cd5..3adabae 100644 --- a/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/src/main/java/cuchaz/enigma/gui/Gui.java @@ -34,6 +34,7 @@ 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; @@ -46,10 +47,12 @@ 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; @@ -63,8 +66,11 @@ public class Gui { 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; @@ -76,6 +82,7 @@ public class Gui { private JFrame frame; public Config.LookAndFeel editorFeel; public PanelEditor editor; + public JScrollPane sourceScroller; private JPanel classesPanel; private JSplitPane splitClasses; private PanelIdentifier infoPanel; @@ -87,6 +94,20 @@ public class Gui { 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; @@ -150,7 +171,7 @@ public class Gui { // init editor selectionHighlightPainter = new SelectionHighlightPainter(); this.editor = new PanelEditor(this); - JScrollPane sourceScroller = new JScrollPane(this.editor); + 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(); @@ -283,7 +304,34 @@ public class Gui { 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); - JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, tabs); + 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); @@ -294,7 +342,17 @@ public class Gui { 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() { @@ -334,18 +392,14 @@ public class Gui { setEditorText(null); // update menu - this.menuBar.closeJarMenu.setEnabled(true); - this.menuBar.openMappingsMenus.forEach(item -> item.setEnabled(true)); - this.menuBar.saveMappingsMenu.setEnabled(false); - this.menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(true)); - this.menuBar.closeMappingsMenu.setEnabled(true); - this.menuBar.exportSourceMenu.setEnabled(true); - this.menuBar.exportJarMenu.setEnabled(true); + isJarOpen = true; + updateUiState(); redraw(); } public void onCloseJar() { + // update gui this.frame.setTitle(Constants.NAME); setObfClasses(null); @@ -354,14 +408,10 @@ public class Gui { this.classesPanel.removeAll(); // update menu - this.menuBar.closeJarMenu.setEnabled(false); - this.menuBar.openMappingsMenus.forEach(item -> item.setEnabled(false)); - this.menuBar.saveMappingsMenu.setEnabled(false); - this.menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(false)); - this.menuBar.closeMappingsMenu.setEnabled(false); - this.menuBar.exportSourceMenu.setEnabled(false); - this.menuBar.exportJarMenu.setEnabled(false); + isJarOpen = false; + setMappingsFile(null); + updateUiState(); redraw(); } @@ -375,7 +425,7 @@ public class Gui { public void setMappingsFile(Path path) { this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); - this.menuBar.saveMappingsMenu.setEnabled(path != null); + updateUiState(); } public void setEditorText(String source) { @@ -561,10 +611,12 @@ public class Gui { boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor(); boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference); - if (isToken) { - showCursorReference(cursorReference); - } else { - infoPanel.clearReference(); + if (!isRenaming()) { + if (isToken) { + showCursorReference(cursorReference); + } else { + infoPanel.clearReference(); + } } this.popupMenu.renameMenu.setEnabled(isRenamable); @@ -586,6 +638,11 @@ public class Gui { } public void startDocChange() { + EntryReference, Entry> curReference = cursorReference; + if (isRenaming()) { + finishRename(false); + } + renamingReference = curReference; // init the text box javadocTextArea = new JTextArea(10, 40); @@ -603,7 +660,8 @@ public class Gui { String newName = javadocTextArea.getText(); if (saveName) { try { - this.controller.changeDocs(cursorReference, newName); + 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()); @@ -665,14 +723,19 @@ public class Gui { 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(cursorReference, newName, true); + 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()); @@ -681,18 +744,20 @@ public class Gui { return; } - // abort the rename - JPanel panel = (JPanel) infoPanel.getComponent(0); - panel.remove(panel.getComponentCount() - 1); - panel.add(Utils.unboldLabel(new JLabel(cursorReference.getNameableName(), JLabel.LEFT))); - renameTextField = null; + // abort the rename + showCursorReference(cursorReference); + this.editor.grabFocus(); redraw(); } + private boolean isRenaming() { + return renameTextField != null; + } + public void showInheritance() { if (cursorReference == null) { @@ -783,8 +848,10 @@ public class Gui { 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())); } } @@ -850,6 +917,7 @@ public class Gui { 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); @@ -857,8 +925,10 @@ public class Gui { this.deobfPanel.deobfClasses.reload(); } // class rename - else if (data instanceof ClassEntry) + 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) { @@ -920,4 +990,69 @@ public class Gui { 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/src/main/java/cuchaz/enigma/gui/GuiController.java b/src/main/java/cuchaz/enigma/gui/GuiController.java index 742d6b8..cccc9e8 100644 --- a/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -24,24 +24,33 @@ import cuchaz.enigma.gui.dialog.ProgressDialog; import cuchaz.enigma.gui.stats.StatsGenerator; import cuchaz.enigma.gui.stats.StatsMember; import cuchaz.enigma.gui.util.History; +import cuchaz.enigma.network.EnigmaClient; +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.IntegratedEnigmaServer; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.network.packet.LoginC2SPacket; +import cuchaz.enigma.network.packet.Packet; import cuchaz.enigma.source.*; import cuchaz.enigma.throwables.MappingParseException; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.mapping.*; import cuchaz.enigma.translation.mapping.serde.MappingFormat; 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.Message; import cuchaz.enigma.utils.ReadableToken; import cuchaz.enigma.utils.Utils; import org.objectweb.asm.tree.ClassNode; import javax.annotation.Nullable; import javax.swing.JOptionPane; -import java.awt.Desktop; +import javax.swing.SwingUtilities; +import java.awt.*; import java.awt.event.ItemEvent; import java.io.*; import java.nio.file.Path; @@ -76,6 +85,9 @@ public class GuiController { 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() @@ -143,6 +155,14 @@ public class GuiController { }); } + public void openMappings(EntryTree mappings) { + if (project == null) return; + + project.setMappings(mappings); + refreshClasses(); + refreshCurrentClass(); + } + public CompletableFuture saveMappings(Path path) { return saveMappings(path, loadedMappingFormat); } @@ -388,11 +408,39 @@ public class GuiController { private void refreshCurrentClass(EntryReference, Entry> reference, RefreshMode mode) { if (currentSource != null) { - loadClass(currentSource.getEntry(), () -> { - if (reference != null) { - showReference(reference); + 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 = Utils.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 = Utils.safeModelToView(gui.editor, anchorModelPos); } - }, mode); + 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 = Utils.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); + } } } @@ -528,43 +576,59 @@ public class GuiController { } public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree) { + rename(reference, newName, refreshClassTree, true); + } + + 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(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void removeMapping(EntryReference, Entry> reference) { + removeMapping(reference, true); + } + + 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(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void changeDocs(EntryReference, Entry> reference, String updatedDocs) { - changeDoc(reference.entry, updatedDocs); + changeDocs(reference, updatedDocs, true); + } - refreshCurrentClass(reference, RefreshMode.JAVADOCS); + public void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference) { + changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs); + + refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS); } - public void changeDoc(Entry obfEntry, String newDoc) { + private void changeDoc(Entry obfEntry, String newDoc) { EntryRemapper mapper = project.getMapper(); if (mapper.getDeobfMapping(obfEntry) == null) { - markAsDeobfuscated(obfEntry,false); // NPE + markAsDeobfuscated(obfEntry, false); // NPE } mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); } - public void markAsDeobfuscated(Entry obfEntry, boolean renaming) { + 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); + } + + 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())); @@ -572,7 +636,7 @@ public class GuiController { if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) this.gui.moveClassTree(reference, true, false); - refreshCurrentClass(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void openStats(Set includedMembers) { @@ -602,4 +666,64 @@ public class GuiController { decompiler = createDecompiler(); 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); + } + + 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); + }); + } + + public void sendPacket(Packet packet) { + if (client != null) { + client.sendPacket(packet); + } + } + + public void addMessage(Message message) { + gui.addMessage(message); + } + + public void updateUserList(List users) { + gui.setUserList(users); + } + } diff --git a/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java b/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java new file mode 100644 index 0000000..c9e38cb --- /dev/null +++ b/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.utils.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/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java new file mode 100644 index 0000000..c5f505c --- /dev/null +++ b/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/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java new file mode 100644 index 0000000..eea1dff --- /dev/null +++ b/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/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java b/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java new file mode 100644 index 0000000..fb497b1 --- /dev/null +++ b/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/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java index 8098178..f8e4f7e 100644 --- a/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java +++ b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java @@ -1,5 +1,18 @@ package cuchaz.enigma.gui.elements; +import cuchaz.enigma.config.Config; +import cuchaz.enigma.config.Themes; +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.dialog.AboutDialog; +import cuchaz.enigma.gui.dialog.ConnectToServerDialog; +import cuchaz.enigma.gui.dialog.CreateServerDialog; +import cuchaz.enigma.gui.dialog.SearchDialog; +import cuchaz.enigma.gui.stats.StatsMember; +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.Container; import java.awt.Desktop; import java.awt.FlowLayout; @@ -13,21 +26,11 @@ 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.*; -import cuchaz.enigma.config.Config; -import cuchaz.enigma.config.Themes; -import cuchaz.enigma.gui.Gui; -import cuchaz.enigma.gui.dialog.AboutDialog; -import cuchaz.enigma.gui.dialog.SearchDialog; -import cuchaz.enigma.gui.stats.StatsMember; -import cuchaz.enigma.gui.util.ScaleUtil; -import cuchaz.enigma.translation.mapping.serde.MappingFormat; -import cuchaz.enigma.utils.I18n; -import cuchaz.enigma.utils.Pair; import javax.swing.*; @@ -49,6 +52,8 @@ public class MenuBar extends JMenuBar { 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) { @@ -342,6 +347,58 @@ public class MenuBar extends JMenuBar { } } + /* + * 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 */ -- cgit v1.2.3