From 5a286d58e740f1aa5944488c602f5abc1318f6ca Mon Sep 17 00:00:00 2001 From: 2xsaiko Date: Wed, 3 Jun 2020 20:16:10 +0200 Subject: Editor tabs (#238) * Split into modules * Add validation utils from patch-1 branch * Tabs, iteration 1 * Delete RefreshMode * Load initial code asynchronously * Formatting * Don't do anything when close() gets called multiple times * Add scroll pane to editor * Fix getActiveEditor() * Rename components to more descriptive editorScrollPanes * Move ClassHandle and related types out of gui package * Fix tab title bar and other files not updating when changing mappings * Fix compilation errors * Start adding renaming functionality to new panel * Scale validation error marker * Make most user input validation use ValidationContext * Fix line numbers not displaying * Move CodeReader.navigateToToken into PanelEditor * Add close button on tabs * Remove TODO, it's fast enough * Remove JS script action for 2 seconds faster startup * Add comment on why the action is removed * ClassHandle/ClassHandleProvider documentation * Fix language file formatting * Bulk tab closing operations * Fix crash when renaming class and not connected to server * Fix caret jumping to the end of the file when opening * Increase identifier panel size * Make popup menu text translatable * Fix formatting * Fix compilation issues * CovertTextField -> ConvertingTextField * Retain formatting using spaces * Add de_de.json * Better decompilation error handling * Fix some caret related NPEs * Localization * Close editor on classhandle delete & fix onInvalidate not running on the Swing thread * Fix crash when trying to close a tab from onDeleted class handle listener Co-authored-by: Runemoro --- .../main/java/cuchaz/enigma/gui/ClassSelector.java | 25 +- .../main/java/cuchaz/enigma/gui/CodeReader.java | 73 --- .../cuchaz/enigma/gui/DecompiledClassSource.java | 160 ------ .../java/cuchaz/enigma/gui/EnigmaSyntaxKit.java | 16 +- .../src/main/java/cuchaz/enigma/gui/Gui.java | 601 +++++++-------------- .../main/java/cuchaz/enigma/gui/GuiController.java | 354 ++++-------- .../main/java/cuchaz/enigma/gui/RefreshMode.java | 7 - .../cuchaz/enigma/gui/TokenListCellRenderer.java | 1 + .../main/java/cuchaz/enigma/gui/config/Themes.java | 41 +- .../cuchaz/enigma/gui/dialog/JavadocDialog.java | 127 +++-- .../enigma/gui/elements/ConvertingTextField.java | 170 ++++++ .../enigma/gui/elements/EditorTabPopupMenu.java | 58 ++ .../enigma/gui/elements/JMultiLineToolTip.java | 132 +++++ .../java/cuchaz/enigma/gui/elements/MenuBar.java | 9 +- .../cuchaz/enigma/gui/elements/PopupMenuBar.java | 25 +- .../gui/elements/ValidatablePasswordField.java | 96 ++++ .../enigma/gui/elements/ValidatableTextArea.java | 100 ++++ .../enigma/gui/elements/ValidatableTextField.java | 96 ++++ .../cuchaz/enigma/gui/elements/ValidatableUi.java | 107 ++++ .../gui/events/ConvertingTextFieldListener.java | 17 + .../enigma/gui/events/EditorActionListener.java | 20 + .../enigma/gui/events/ThemeChangeListener.java | 13 + .../gui/highlight/SelectionHighlightPainter.java | 3 + .../enigma/gui/highlight/TokenHighlightType.java | 7 - .../enigma/gui/panels/ClosableTabTitlePane.java | 133 +++++ .../java/cuchaz/enigma/gui/panels/PanelEditor.java | 571 ++++++++++++++++++-- .../cuchaz/enigma/gui/panels/PanelIdentifier.java | 253 ++++++++- .../main/java/cuchaz/enigma/gui/util/GuiUtil.java | 13 - .../java/cuchaz/enigma/gui/util/ScaleUtil.java | 10 +- 29 files changed, 2195 insertions(+), 1043 deletions(-) delete mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java delete mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java delete mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/JMultiLineToolTip.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatablePasswordField.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextArea.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextField.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableUi.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/events/ConvertingTextFieldListener.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/events/EditorActionListener.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/events/ThemeChangeListener.java delete mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java create mode 100644 enigma-swing/src/main/java/cuchaz/enigma/gui/panels/ClosableTabTitlePane.java (limited to 'enigma-swing/src/main/java/cuchaz') diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java index 3d0e04c..488d04e 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java @@ -16,7 +16,6 @@ import java.awt.event.MouseEvent; import java.util.*; import javax.annotation.Nullable; -import javax.swing.JOptionPane; import javax.swing.JTree; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; @@ -28,9 +27,9 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import cuchaz.enigma.gui.node.ClassSelectorClassNode; import cuchaz.enigma.gui.node.ClassSelectorPackageNode; -import cuchaz.enigma.translation.mapping.IllegalNameException; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.validation.ValidationContext; public class ClassSelector extends JTree { @@ -103,18 +102,18 @@ public class ClassSelector extends JTree { if (allowEdit && renameSelectionListener != null) { Object prevData = node.getUserObject(); Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data; - try { - renameSelectionListener.onSelectionRename(node.getUserObject(), objectData, node); - node.setUserObject(objectData); // Make sure that it's modified - } catch (IllegalNameException ex) { - JOptionPane.showOptionDialog(gui.getFrame(), ex.getMessage(), "Enigma - Error", JOptionPane.OK_OPTION, - JOptionPane.ERROR_MESSAGE, null, new String[]{"Ok"}, "OK"); - editor.cancelCellEditing(); - } - } else + gui.validateImmediateAction(vc -> { + renameSelectionListener.onSelectionRename(vc, node.getUserObject(), objectData, node); + if (vc.canProceed()) { + node.setUserObject(objectData); // Make sure that it's modified + } else { + editor.cancelCellEditing(); + } + }); + } else { editor.cancelCellEditing(); + } } - } @Override @@ -527,6 +526,6 @@ public class ClassSelector extends JTree { } public interface RenameSelectionListener { - void onSelectionRename(Object prevData, Object data, DefaultMutableTreeNode node); + void onSelectionRename(ValidationContext vc, Object prevData, Object data, DefaultMutableTreeNode node); } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java deleted file mode 100644 index 356656b..0000000 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java +++ /dev/null @@ -1,73 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2015 Jeff Martin. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Lesser General Public - * License v3.0 which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/lgpl.html - *

- * Contributors: - * Jeff Martin - initial API and implementation - ******************************************************************************/ - -package cuchaz.enigma.gui; - -import cuchaz.enigma.source.Token; - -import javax.swing.*; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.Highlighter.HighlightPainter; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -public class CodeReader extends JEditorPane { - private static final long serialVersionUID = 3673180950485748810L; - - // HACKHACK: someday we can update the main GUI to use this code reader - public static void navigateToToken(final JEditorPane editor, final Token token, final HighlightPainter highlightPainter) { - - // set the caret position to the token - Document document = editor.getDocument(); - int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); - - editor.setCaretPosition(clampedPosition); - editor.grabFocus(); - - try { - // make sure the token is visible in the scroll window - Rectangle start = editor.modelToView(token.start); - Rectangle end = editor.modelToView(token.end); - final Rectangle show = start.union(end); - show.grow(start.width * 10, start.height * 6); - SwingUtilities.invokeLater(() -> editor.scrollRectToVisible(show)); - } catch (BadLocationException ex) { - throw new Error(ex); - } - - // highlight the token momentarily - final Timer timer = new Timer(200, new ActionListener() { - private int counter = 0; - private Object highlight = null; - - @Override - public void actionPerformed(ActionEvent event) { - if (counter % 2 == 0) { - try { - highlight = editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); - } catch (BadLocationException ex) { - // don't care - } - } else if (highlight != null) { - editor.getHighlighter().removeHighlight(highlight); - } - - if (counter++ > 6) { - Timer timer = (Timer) event.getSource(); - timer.stop(); - } - } - }); - timer.start(); - } -} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java deleted file mode 100644 index aca5d72..0000000 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java +++ /dev/null @@ -1,160 +0,0 @@ -package cuchaz.enigma.gui; - -import cuchaz.enigma.EnigmaProject; -import cuchaz.enigma.EnigmaServices; -import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.source.Token; -import cuchaz.enigma.api.service.NameProposalService; -import cuchaz.enigma.gui.highlight.TokenHighlightType; -import cuchaz.enigma.source.SourceIndex; -import cuchaz.enigma.source.SourceRemapper; -import cuchaz.enigma.translation.LocalNameGenerator; -import cuchaz.enigma.translation.Translator; -import cuchaz.enigma.translation.mapping.EntryRemapper; -import cuchaz.enigma.translation.mapping.ResolutionStrategy; -import cuchaz.enigma.translation.representation.TypeDescriptor; -import cuchaz.enigma.translation.representation.entry.ClassEntry; -import cuchaz.enigma.translation.representation.entry.Entry; -import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry; - -import javax.annotation.Nullable; -import java.util.*; - -public class DecompiledClassSource { - private final ClassEntry classEntry; - - private final SourceIndex obfuscatedIndex; - private SourceIndex remappedIndex; - - private final Map> highlightedTokens = new EnumMap<>(TokenHighlightType.class); - - public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) { - this.classEntry = classEntry; - this.obfuscatedIndex = index; - this.remappedIndex = index; - } - - public static DecompiledClassSource text(ClassEntry classEntry, String text) { - return new DecompiledClassSource(classEntry, new SourceIndex(text)); - } - - public void remapSource(EnigmaProject project, Translator translator) { - highlightedTokens.clear(); - - SourceRemapper remapper = new SourceRemapper(obfuscatedIndex.getSource(), obfuscatedIndex.referenceTokens()); - - SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> remapToken(project, token, movedToken, translator)); - remappedIndex = obfuscatedIndex.remapTo(remapResult); - } - - private String remapToken(EnigmaProject project, Token token, Token movedToken, Translator translator) { - EntryReference, Entry> reference = obfuscatedIndex.getReference(token); - - Entry entry = reference.getNameableEntry(); - Entry translatedEntry = translator.translate(entry); - - if (project.isRenamable(reference)) { - if (isDeobfuscated(entry, translatedEntry)) { - highlightToken(movedToken, TokenHighlightType.DEOBFUSCATED); - return translatedEntry.getSourceRemapName(); - } else { - Optional proposedName = proposeName(project, entry); - if (proposedName.isPresent()) { - highlightToken(movedToken, TokenHighlightType.PROPOSED); - return proposedName.get(); - } - - highlightToken(movedToken, TokenHighlightType.OBFUSCATED); - } - } - - String defaultName = generateDefaultName(translatedEntry); - if (defaultName != null) { - return defaultName; - } - - return null; - } - - private Optional proposeName(EnigmaProject project, Entry entry) { - EnigmaServices services = project.getEnigma().getServices(); - - return services.get(NameProposalService.TYPE).stream().flatMap(nameProposalService -> { - EntryRemapper mapper = project.getMapper(); - Collection> resolved = mapper.getObfResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); - - return resolved.stream() - .map(e -> nameProposalService.proposeName(e, mapper)) - .filter(Optional::isPresent) - .map(Optional::get); - }).findFirst(); - } - - @Nullable - private String generateDefaultName(Entry entry) { - if (entry instanceof LocalVariableDefEntry) { - LocalVariableDefEntry localVariable = (LocalVariableDefEntry) entry; - - int index = localVariable.getIndex(); - if (localVariable.isArgument()) { - List arguments = localVariable.getParent().getDesc().getArgumentDescs(); - return LocalNameGenerator.generateArgumentName(index, localVariable.getDesc(), arguments); - } else { - return LocalNameGenerator.generateLocalVariableName(index, localVariable.getDesc()); - } - } - - return null; - } - - private boolean isDeobfuscated(Entry entry, Entry translatedEntry) { - return !entry.getName().equals(translatedEntry.getName()); - } - - public ClassEntry getEntry() { - return classEntry; - } - - public SourceIndex getIndex() { - return remappedIndex; - } - - public Map> getHighlightedTokens() { - return highlightedTokens; - } - - private void highlightToken(Token token, TokenHighlightType highlightType) { - highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token); - } - - public int getObfuscatedOffset(int deobfOffset) { - return getOffset(remappedIndex, obfuscatedIndex, deobfOffset); - } - - public int getDeobfuscatedOffset(int obfOffset) { - return getOffset(obfuscatedIndex, remappedIndex, obfOffset); - } - - private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fromOffset) { - int relativeOffset = 0; - - Iterator fromTokenItr = fromIndex.referenceTokens().iterator(); - Iterator toTokenItr = toIndex.referenceTokens().iterator(); - while (fromTokenItr.hasNext() && toTokenItr.hasNext()) { - Token fromToken = fromTokenItr.next(); - Token toToken = toTokenItr.next(); - if (fromToken.end > fromOffset) { - break; - } - - relativeOffset = toToken.end - fromToken.end; - } - - return fromOffset + relativeOffset; - } - - @Override - public String toString() { - return remappedIndex.getSource(); - } -} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java index 2f08a26..d4a71f5 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java @@ -1,22 +1,24 @@ package cuchaz.enigma.gui; import cuchaz.enigma.gui.config.Config; +import de.sciss.syntaxpane.DefaultSyntaxKit; import de.sciss.syntaxpane.components.LineNumbersRuler; import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit; import de.sciss.syntaxpane.util.Configuration; public class EnigmaSyntaxKit extends JavaSyntaxKit { + private static Configuration configuration = null; @Override public Configuration getConfig() { - if(configuration == null){ - initConfig(super.getConfig(JavaSyntaxKit.class)); + if (configuration == null) { + initConfig(DefaultSyntaxKit.getConfig(JavaSyntaxKit.class)); } return configuration; } - public void initConfig(Configuration baseConfig){ + public void initConfig(Configuration baseConfig) { configuration = baseConfig; //See de.sciss.syntaxpane.TokenType configuration.put("Style.KEYWORD", Config.getInstance().highlightColor + ", 0"); @@ -36,9 +38,15 @@ public class EnigmaSyntaxKit extends JavaSyntaxKit { configuration.put("RightMarginColumn", "999"); //No need to have a right margin, if someone wants it add a config configuration.put("Action.quick-find", "cuchaz.enigma.gui.QuickFindAction, menu F"); + + // This is an action written in javascript that is useless for enigma's + // use case, and removing it causes the editor to load way faster the + // first time + configuration.remove("Action.insert-date"); } - public static void invalidate(){ + public static void invalidate() { configuration = null; } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java index c67fc5b..0a5d3f7 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java @@ -11,64 +11,64 @@ package cuchaz.enigma.gui; -import java.awt.*; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FileDialog; import java.awt.event.*; import java.nio.file.Path; -import java.util.List; import java.util.*; +import java.util.function.Consumer; import java.util.function.Function; +import javax.annotation.Nullable; 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.HashBiMap; import com.google.common.collect.Lists; import cuchaz.enigma.Enigma; import cuchaz.enigma.EnigmaProfile; import cuchaz.enigma.analysis.*; +import cuchaz.enigma.classhandle.ClassHandle; import cuchaz.enigma.gui.config.Config; import cuchaz.enigma.gui.config.Themes; import cuchaz.enigma.gui.dialog.CrashDialog; import cuchaz.enigma.gui.dialog.JavadocDialog; import cuchaz.enigma.gui.dialog.SearchDialog; import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; +import cuchaz.enigma.gui.elements.EditorTabPopupMenu; import cuchaz.enigma.gui.elements.MenuBar; -import cuchaz.enigma.gui.elements.PopupMenuBar; +import cuchaz.enigma.gui.elements.ValidatableUi; +import cuchaz.enigma.gui.events.EditorActionListener; import cuchaz.enigma.gui.filechooser.FileChooserAny; import cuchaz.enigma.gui.filechooser.FileChooserFolder; -import cuchaz.enigma.gui.highlight.BoxHighlightPainter; -import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; -import cuchaz.enigma.gui.highlight.TokenHighlightType; -import cuchaz.enigma.gui.panels.PanelDeobf; -import cuchaz.enigma.gui.panels.PanelEditor; -import cuchaz.enigma.gui.panels.PanelIdentifier; -import cuchaz.enigma.gui.panels.PanelObf; -import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.gui.panels.*; import cuchaz.enigma.gui.util.History; -import cuchaz.enigma.network.packet.*; -import cuchaz.enigma.source.Token; -import cuchaz.enigma.translation.mapping.IllegalNameException; -import cuchaz.enigma.translation.mapping.*; -import cuchaz.enigma.translation.representation.entry.*; -import cuchaz.enigma.network.Message; import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.network.Message; +import cuchaz.enigma.network.packet.MarkDeobfuscatedC2SPacket; +import cuchaz.enigma.network.packet.MessageC2SPacket; +import cuchaz.enigma.network.packet.RemoveMappingC2SPacket; +import cuchaz.enigma.network.packet.RenameC2SPacket; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.mapping.EntryRemapper; +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 de.sciss.syntaxpane.DefaultSyntaxKit; +import cuchaz.enigma.utils.validation.ParameterizedMessage; +import cuchaz.enigma.utils.validation.ValidationContext; 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; @@ -80,14 +80,9 @@ public class Gui { 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; @@ -108,20 +103,9 @@ public class Gui { 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; - } - } + private final EditorTabPopupMenu editorTabPopupMenu; + private final JTabbedPane openFiles; + private final HashBiMap editors = HashBiMap.create(); public Gui(EnigmaProfile profile) { Config.getInstance().lookAndFeel.setGlobalLAF(); @@ -144,7 +128,9 @@ public class Gui { this.controller = new GuiController(this, profile); - Themes.updateTheme(this); + Themes.addListener((lookAndFeel, boxHighlightPainters) -> SwingUtilities.updateComponentTreeUI(getFrame())); + + Themes.updateTheme(); // init file choosers this.jarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.jar.open"), FileDialog.LOAD); @@ -166,20 +152,6 @@ public class Gui { // 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(); @@ -269,7 +241,7 @@ public class Gui { } }); tokens = new JList<>(); - tokens.setCellRenderer(new TokenListCellRenderer(this.controller)); + tokens.setCellRenderer(new TokenListCellRenderer(controller)); tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); tokens.setLayoutOrientation(JList.VERTICAL); tokens.addMouseListener(new MouseAdapter() { @@ -278,7 +250,7 @@ public class Gui { if (event.getClickCount() == 2) { Token selected = tokens.getSelectedValue(); if (selected != null) { - showToken(selected); + openClass(controller.getTokenHandle().getRef()).navigateToToken(selected); } } } @@ -294,11 +266,25 @@ public class Gui { callPanel.setResizeWeight(1); // let the top side take all the slack callPanel.resetToPreferredSizes(); + editorTabPopupMenu = new EditorTabPopupMenu(this); + openFiles = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT); + openFiles.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + int i = openFiles.getUI().tabForCoordinate(openFiles, e.getX(), e.getY()); + if (i != -1) { + editorTabPopupMenu.show(openFiles, e.getX(), e.getY(), PanelEditor.byUi(openFiles.getComponentAt(i))); + } + } + } + }); + // layout controls JPanel centerPanel = new JPanel(); centerPanel.setLayout(new BorderLayout()); - centerPanel.add(infoPanel, BorderLayout.NORTH); - centerPanel.add(sourceScroller, BorderLayout.CENTER); + centerPanel.add(infoPanel.getUi(), BorderLayout.NORTH); + centerPanel.add(openFiles, BorderLayout.CENTER); tabs = new JTabbedPane(); tabs.setPreferredSize(ScaleUtil.getDimension(250, 0)); tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel); @@ -389,7 +375,7 @@ public class Gui { this.frame.setTitle(Enigma.NAME + " - " + jarName); this.classesPanel.removeAll(); this.classesPanel.add(splitClasses); - setEditorText(null); + closeAllEditorTabs(); // update menu isJarOpen = true; @@ -404,7 +390,7 @@ public class Gui { this.frame.setTitle(Enigma.NAME); setObfClasses(null); setDeobfClasses(null); - setEditorText(null); + closeAllEditorTabs(); this.classesPanel.removeAll(); // update menu @@ -415,6 +401,54 @@ public class Gui { redraw(); } + public PanelEditor openClass(ClassEntry entry) { + PanelEditor panelEditor = editors.computeIfAbsent(entry, e -> { + ClassHandle ch = controller.getClassHandleProvider().openClass(entry); + if (ch == null) return null; + PanelEditor ed = new PanelEditor(this); + ed.setup(); + ed.setClassHandle(ch); + openFiles.addTab(ed.getFileName(), ed.getUi()); + + ClosableTabTitlePane titlePane = new ClosableTabTitlePane(ed.getFileName(), () -> closeEditor(ed)); + openFiles.setTabComponentAt(openFiles.indexOfComponent(ed.getUi()), titlePane.getUi()); + titlePane.setTabbedPane(openFiles); + + ed.addListener(new EditorActionListener() { + @Override + public void onCursorReferenceChanged(PanelEditor editor, EntryReference, Entry> ref) { + updateSelectedReference(editor, ref); + } + + @Override + public void onClassHandleChanged(PanelEditor editor, ClassEntry old, ClassHandle ch) { + editors.remove(old); + editors.put(ch.getRef(), editor); + } + + @Override + public void onTitleChanged(PanelEditor editor, String title) { + titlePane.setText(editor.getFileName()); + } + }); + + ed.getEditor().addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_4 && (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + closeEditor(ed); + } + } + }); + + return ed; + }); + if (panelEditor != null) { + openFiles.setSelectedComponent(editors.get(entry).getUi()); + } + return panelEditor; + } + public void setObfClasses(Collection obfClasses) { this.obfPanel.obfClasses.setClasses(obfClasses); } @@ -428,29 +462,49 @@ public class Gui { updateUiState(); } - public void setEditorText(String source) { - this.editor.getHighlighter().removeAllHighlights(); - this.editor.setText(source); + public void closeEditor(PanelEditor ed) { + openFiles.remove(ed.getUi()); + editors.inverse().remove(ed); + ed.destroy(); } - public void setSource(DecompiledClassSource source) { - editor.setText(source.toString()); - setHighlightedTokens(source.getHighlightedTokens()); + public void closeAllEditorTabs() { + for (Iterator iter = editors.values().iterator(); iter.hasNext(); ) { + PanelEditor e = iter.next(); + openFiles.remove(e.getUi()); + e.destroy(); + iter.remove(); + } } - public void showToken(final Token token) { - if (token == null) { - throw new IllegalArgumentException("Token cannot be null!"); + public void closeTabsLeftOf(PanelEditor ed) { + int index = openFiles.indexOfComponent(ed.getUi()); + for (int i = index - 1; i >= 0; i--) { + closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i))); + } + } + + public void closeTabsRightOf(PanelEditor ed) { + int index = openFiles.indexOfComponent(ed.getUi()); + for (int i = openFiles.getTabCount() - 1; i > index; i--) { + closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i))); + } + } + + public void closeTabsExcept(PanelEditor ed) { + int index = openFiles.indexOfComponent(ed.getUi()); + for (int i = openFiles.getTabCount() - 1; i >= 0; i--) { + if (i == index) continue; + closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i))); } - CodeReader.navigateToToken(this.editor, token, selectionHighlightPainter); - redraw(); } - public void showTokens(Collection tokens) { + public void showTokens(PanelEditor editor, Collection tokens) { Vector sortedTokens = new Vector<>(tokens); Collections.sort(sortedTokens); if (sortedTokens.size() > 1) { // sort the tokens and update the tokens panel + this.controller.setTokenHandle(editor.getClassHandle().copy()); this.tokens.setListData(sortedTokens); this.tokens.setSelectedIndex(0); } else { @@ -458,311 +512,51 @@ public class Gui { } // show the first token - showToken(sortedTokens.get(0)); + editor.navigateToToken(sortedTokens.get(0)); } - public void setHighlightedTokens(Map> tokens) { - // remove any old highlighters - this.editor.getHighlighter().removeAllHighlights(); + private void updateSelectedReference(PanelEditor editor, EntryReference, Entry> ref) { + if (editor != getActiveEditor()) return; - 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); - } - } + showCursorReference(ref); } 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); + infoPanel.setReference(reference == null ? null : reference.entry); } - private void addNameValue(JPanel container, String name, String value) { - JPanel panel = new JPanel(); - panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0)); - - JLabel label = new JLabel(name + ":", JLabel.RIGHT); - label.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height))); - panel.add(label); - - panel.add(GuiUtil.unboldLabel(new JLabel(value, JLabel.LEFT))); - - container.add(panel); + @Nullable + public PanelEditor getActiveEditor() { + return PanelEditor.byUi(openFiles.getSelectedComponent()); } - 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; + @Nullable + public EntryReference, Entry> getCursorReference() { + PanelEditor activeEditor = getActiveEditor(); + return activeEditor == null ? null : activeEditor.getCursorReference(); } - public void onCaretMove(int pos, boolean fromClick) { - if (controller.project == null) - return; - EntryRemapper mapper = controller.project.getMapper(); - Token token = this.controller.getToken(pos); - boolean isToken = token != null; - - cursorReference = this.controller.getReference(token); - Entry referenceEntry = cursorReference != null ? cursorReference.entry : null; - - if (referenceEntry != null && shouldNavigateOnClick && fromClick) { - shouldNavigateOnClick = false; - Entry navigationEntry = referenceEntry; - if (cursorReference.context == null) { - EntryResolver resolver = mapper.getObfResolver(); - navigationEntry = resolver.resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT); - } - controller.navigateTo(navigationEntry); - return; - } - - boolean isClassEntry = isToken && referenceEntry instanceof ClassEntry; - boolean isFieldEntry = isToken && referenceEntry instanceof FieldEntry; - boolean isMethodEntry = isToken && referenceEntry instanceof MethodEntry && !((MethodEntry) referenceEntry).isConstructor(); - boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor(); - boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference); - - if (!isRenaming()) { - if (isToken) { - showCursorReference(cursorReference); - } else { - infoPanel.clearReference(); - } - } - - this.popupMenu.renameMenu.setEnabled(isRenamable); - this.popupMenu.editJavadocMenu.setEnabled(isRenamable); - this.popupMenu.showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry); - this.popupMenu.showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry); - this.popupMenu.showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry); - this.popupMenu.showCallsSpecificMenu.setEnabled(isMethodEntry); - this.popupMenu.openEntryMenu.setEnabled(isRenamable && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry)); - this.popupMenu.openPreviousMenu.setEnabled(this.controller.hasPreviousReference()); - this.popupMenu.openNextMenu.setEnabled(this.controller.hasNextReference()); - this.popupMenu.toggleMappingMenu.setEnabled(isRenamable); - - if (isToken && !Objects.equals(referenceEntry, mapper.deobfuscate(referenceEntry))) { - this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.reset_obfuscated")); - } else { - this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.mark_deobfuscated")); - } - } - - public void startDocChange() { - EntryReference, Entry> curReference = cursorReference; - if (isRenaming()) { - finishRename(false); - } - renamingReference = curReference; - - // init the text box - javadocTextArea = new JTextArea(10, 40); - - EntryReference, Entry> translatedReference = controller.project.getMapper().deobfuscate(cursorReference); - javadocTextArea.setText(Strings.nullToEmpty(translatedReference.entry.getJavadocs())); - - JavadocDialog.init(frame, javadocTextArea, this::finishDocChange); - javadocTextArea.grabFocus(); - - redraw(); - } - - private void finishDocChange(JFrame ui, boolean saveName) { - String newName = javadocTextArea.getText(); - if (saveName) { - try { - this.controller.changeDocs(renamingReference, newName); - this.controller.sendPacket(new ChangeDocsC2SPacket(renamingReference.getNameableEntry(), newName)); - } catch (IllegalNameException ex) { - javadocTextArea.setBorder(BorderFactory.createLineBorder(Color.red, 1)); - javadocTextArea.setToolTipText(ex.getReason()); - GuiUtil.showToolTipNow(javadocTextArea); - return; - } - - ui.setVisible(false); - showCursorReference(cursorReference); - return; - } - - // abort the jd change - javadocTextArea = null; - ui.setVisible(false); - showCursorReference(cursorReference); - - this.editor.grabFocus(); - - redraw(); + public void startDocChange(PanelEditor editor) { + EntryReference, Entry> cursorReference = editor.getCursorReference(); + if (cursorReference == null) return; + JavadocDialog.show(frame, getController(), cursorReference); } - public void startRename() { + public void startRename(PanelEditor editor, String text) { + if (editor != getActiveEditor()) return; - // 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(); + infoPanel.startRenaming(text); } - private void finishRename(boolean saveName) { - String newName = renameTextField.getText(); - - if (saveName && newName != null && !newName.isEmpty()) { - try { - this.controller.rename(renamingReference, newName, true); - this.controller.sendPacket(new RenameC2SPacket(renamingReference.getNameableEntry(), newName, true)); - renameTextField = null; - } catch (IllegalNameException ex) { - renameTextField.setBorder(BorderFactory.createLineBorder(Color.red, 1)); - renameTextField.setToolTipText(ex.getReason()); - GuiUtil.showToolTipNow(renameTextField); - } - return; - } - - renameTextField = null; + public void startRename(PanelEditor editor) { + if (editor != getActiveEditor()) return; - // abort the rename - showCursorReference(cursorReference); - - this.editor.grabFocus(); - - redraw(); + infoPanel.startRenaming(); } - private boolean isRenaming() { - return renameTextField != null; - } - - public void showInheritance() { - - if (cursorReference == null) { - return; - } + public void showInheritance(PanelEditor editor) { + EntryReference, Entry> cursorReference = editor.getCursorReference(); + if (cursorReference == null) return; inheritanceTree.setModel(null); @@ -791,11 +585,9 @@ public class Gui { redraw(); } - public void showImplementations() { - - if (cursorReference == null) { - return; - } + public void showImplementations(PanelEditor editor) { + EntryReference, Entry> cursorReference = editor.getCursorReference(); + if (cursorReference == null) return; implementationsTree.setModel(null); @@ -821,10 +613,9 @@ public class Gui { redraw(); } - public void showCalls(boolean recurse) { - if (cursorReference == null) { - return; - } + public void showCalls(PanelEditor editor, boolean recurse) { + EntryReference, Entry> cursorReference = editor.getCursorReference(); + if (cursorReference == null) return; if (cursorReference.entry instanceof ClassEntry) { ClassReferenceTreeNode node = this.controller.getClassReferences((ClassEntry) cursorReference.entry); @@ -842,15 +633,18 @@ public class Gui { redraw(); } - public void toggleMapping() { + public void toggleMapping(PanelEditor editor) { + EntryReference, Entry> cursorReference = editor.getCursorReference(); + if (cursorReference == null) return; + Entry obfEntry = cursorReference.entry; Entry deobfEntry = controller.project.getMapper().deobfuscate(obfEntry); if (!Objects.equals(obfEntry, deobfEntry)) { - this.controller.removeMapping(cursorReference); + if (!validateImmediateAction(vc -> this.controller.removeMapping(vc, cursorReference))) return; this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry())); } else { - this.controller.markAsDeobfuscated(cursorReference); + if (!validateImmediateAction(vc -> this.controller.markAsDeobfuscated(vc, cursorReference))) return; this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry())); } } @@ -909,37 +703,51 @@ public class Gui { this.frame.repaint(); } - public void onPanelRename(Object prevData, Object data, DefaultMutableTreeNode node) throws IllegalNameException { - // package rename + public void onPanelRename(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()); - this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false); + this.controller.rename(vc, new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false); + if (!vc.canProceed()) return; 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)); + } else if (data instanceof ClassEntry) { + // class rename + + // assume this is deobf since the obf tree doesn't allow renaming in + // the first place + // 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().get(); + + this.controller.rename(vc, new EntryReference<>(obf, obf.getFullName()), ((ClassEntry) data).getFullName(), false); + if (!vc.canProceed()) return; + this.controller.sendPacket(new RenameC2SPacket(obf, ((ClassEntry) data).getFullName(), false)); } } - public void moveClassTree(EntryReference, Entry> obfReference, String newName) { - String oldEntry = obfReference.entry.getContainingClass().getPackageName(); + public void moveClassTree(Entry obfEntry, String newName) { + String oldEntry = obfEntry.getContainingClass().getPackageName(); String newEntry = new ClassEntry(newName).getPackageName(); - moveClassTree(obfReference, oldEntry == null, newEntry == null); + moveClassTree(obfEntry, 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(); + public void moveClassTree(Entry obfEntry, boolean isOldOb, boolean isNewOb) { + ClassEntry classEntry = obfEntry.getContainingClass(); List stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(this.deobfPanel.deobfClasses); List stateObf = this.obfPanel.obfClasses.getExpansionState(this.obfPanel.obfClasses); @@ -979,10 +787,6 @@ public class Gui { return deobfPanel; } - public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) { - this.shouldNavigateOnClick = shouldNavigateOnClick; - } - public SearchDialog getSearchDialog() { if (searchDialog == null) { searchDialog = new SearchDialog(this); @@ -1052,4 +856,19 @@ public class Gui { return this.connectionState; } + public PanelIdentifier getInfoPanel() { + return infoPanel; + } + + 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(); + } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java index 94979e7..10f36b8 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -11,27 +11,46 @@ package cuchaz.enigma.gui; +import java.awt.Desktop; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + import com.google.common.collect.Lists; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import cuchaz.enigma.Enigma; import cuchaz.enigma.EnigmaProfile; import cuchaz.enigma.EnigmaProject; import cuchaz.enigma.analysis.*; import cuchaz.enigma.api.service.ObfuscationTestService; +import cuchaz.enigma.classhandle.ClassHandle; +import cuchaz.enigma.classhandle.ClassHandleProvider; import cuchaz.enigma.gui.config.Config; import cuchaz.enigma.gui.dialog.ProgressDialog; import cuchaz.enigma.gui.stats.StatsGenerator; import cuchaz.enigma.gui.stats.StatsMember; -import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.gui.util.History; import cuchaz.enigma.network.*; import cuchaz.enigma.network.packet.LoginC2SPacket; import cuchaz.enigma.network.packet.Packet; -import cuchaz.enigma.source.*; -import cuchaz.enigma.translation.mapping.serde.MappingParseException; +import cuchaz.enigma.source.DecompiledClassSource; +import cuchaz.enigma.source.DecompilerService; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.Token; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.mapping.*; import cuchaz.enigma.translation.mapping.serde.MappingFormat; +import cuchaz.enigma.translation.mapping.serde.MappingParseException; import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; import cuchaz.enigma.translation.mapping.tree.EntryTree; import cuchaz.enigma.translation.mapping.tree.HashEntryTree; @@ -41,44 +60,21 @@ import cuchaz.enigma.translation.representation.entry.FieldEntry; import cuchaz.enigma.translation.representation.entry.MethodEntry; import cuchaz.enigma.utils.I18n; import cuchaz.enigma.utils.Utils; - -import javax.annotation.Nullable; -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import java.awt.*; -import java.awt.event.ItemEvent; -import java.io.*; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import cuchaz.enigma.utils.validation.ValidationContext; public class GuiController implements ClientPacketHandler { - private static final ExecutorService DECOMPILER_SERVICE = Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("decompiler-thread") - .build() - ); - private final Gui gui; public final Enigma enigma; public EnigmaProject project; - private DecompilerService decompilerService; - private Decompiler decompiler; private IndexTreeBuilder indexTreeBuilder; private Path loadedMappingPath; private MappingFormat loadedMappingFormat; - private DecompiledClassSource currentSource; - private Source uncommentedSource; + private ClassHandleProvider chp; + + private ClassHandle tokenHandle; private EnigmaClient client; private EnigmaServer server; @@ -88,8 +84,6 @@ public class GuiController implements ClientPacketHandler { this.enigma = Enigma.builder() .setProfile(profile) .build(); - - decompilerService = Config.getInstance().decompiler.service; } public boolean isDirty() { @@ -102,13 +96,15 @@ public class GuiController implements ClientPacketHandler { return ProgressDialog.runOffThread(gui.getFrame(), progress -> { project = enigma.openJar(jarPath, progress); indexTreeBuilder = new IndexTreeBuilder(project.getJarIndex()); - decompiler = project.createDecompiler(decompilerService); + chp = new ClassHandleProvider(project, Config.getInstance().decompiler.service); gui.onFinishOpenJar(jarPath.getFileName().toString()); refreshClasses(); }); } public void closeJar() { + this.chp.destroy(); + this.chp = null; this.project = null; this.gui.onCloseJar(); } @@ -129,7 +125,7 @@ public class GuiController implements ClientPacketHandler { loadedMappingPath = path; refreshClasses(); - refreshCurrentClass(); + chp.invalidateMapped(); } catch (MappingParseException e) { JOptionPane.showMessageDialog(gui.getFrame(), e.getMessage()); } @@ -142,7 +138,7 @@ public class GuiController implements ClientPacketHandler { project.setMappings(mappings); refreshClasses(); - refreshCurrentClass(); + chp.invalidateMapped(); } public CompletableFuture saveMappings(Path path) { @@ -177,7 +173,7 @@ public class GuiController implements ClientPacketHandler { this.gui.setMappingsFile(null); refreshClasses(); - refreshCurrentClass(); + chp.invalidateMapped(); } public CompletableFuture dropMappings() { @@ -191,7 +187,7 @@ public class GuiController implements ClientPacketHandler { return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> { EnigmaProject.JarExport jar = project.exportRemappedJar(progress); - EnigmaProject.SourceExport source = jar.decompile(progress, decompilerService); + EnigmaProject.SourceExport source = jar.decompile(progress, chp.getDecompilerService()); source.write(path, progress); }); @@ -206,32 +202,34 @@ public class GuiController implements ClientPacketHandler { }); } - public Token getToken(int pos) { - if (this.currentSource == null) { - return null; + public void setTokenHandle(ClassHandle handle) { + if (tokenHandle != null) { + tokenHandle.close(); } - return this.currentSource.getIndex().getReferenceToken(pos); + + tokenHandle = handle; } - @Nullable - public EntryReference, Entry> getReference(Token token) { - if (this.currentSource == null) { - return null; - } - return this.currentSource.getIndex().getReference(token); + public ClassHandle getTokenHandle() { + return tokenHandle; } public ReadableToken getReadableToken(Token token) { - if (this.currentSource == null) { + if (tokenHandle == null) { return null; } - SourceIndex index = this.currentSource.getIndex(); - return new ReadableToken( - index.getLineNumber(token.start), - index.getColumnNumber(token.start), - index.getColumnNumber(token.end) - ); + try { + return tokenHandle.getSource().get() + .map(DecompiledClassSource::getIndex) + .map(index -> new ReadableToken( + index.getLineNumber(token.start), + index.getColumnNumber(token.start), + index.getColumnNumber(token.end))) + .unwrapOr(null); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } } /** @@ -271,39 +269,13 @@ public class GuiController implements ClientPacketHandler { * @param reference the reference */ private void setReference(EntryReference, Entry> reference) { - // get the reference target class - ClassEntry classEntry = reference.getLocationClassEntry(); - if (!project.isRenamable(classEntry)) { - throw new IllegalArgumentException("Obfuscated class " + classEntry + " was not found in the jar!"); - } - - if (this.currentSource == null || !this.currentSource.getEntry().equals(classEntry)) { - // deobfuscate the class, then navigate to the reference - loadClass(classEntry, () -> showReference(reference)); - } else { - showReference(reference); - } - } - - /** - * Navigates to the reference without modifying history. Assumes the class is loaded. - * - * @param reference - */ - private void showReference(EntryReference, Entry> reference) { - Collection tokens = getTokensForReference(reference); - if (tokens.isEmpty()) { - // DEBUG - System.err.println(String.format("WARNING: no tokens found for %s in %s", reference, this.currentSource.getEntry())); - } else { - this.gui.showTokens(tokens); - } + gui.openClass(reference.getLocationClassEntry()).showReference(reference); } - public Collection getTokensForReference(EntryReference, Entry> reference) { + public Collection getTokensForReference(DecompiledClassSource source, EntryReference, Entry> reference) { EntryRemapper mapper = this.project.getMapper(); - SourceIndex index = this.currentSource.getIndex(); + SourceIndex index = source.getIndex(); return mapper.getObfResolver().resolveReference(reference, ResolutionStrategy.RESOLVE_CLOSEST) .stream() .flatMap(r -> index.getReferenceTokens(r).stream()) @@ -380,131 +352,17 @@ public class GuiController implements ClientPacketHandler { }); } - public void refreshCurrentClass() { - refreshCurrentClass(null); - } - - private void refreshCurrentClass(EntryReference, Entry> reference) { - refreshCurrentClass(reference, RefreshMode.MINIMAL); - } - - private void refreshCurrentClass(EntryReference, Entry> reference, RefreshMode mode) { - if (currentSource != null) { - if (reference == null) { - int obfSelectionStart = currentSource.getObfuscatedOffset(gui.editor.getSelectionStart()); - int obfSelectionEnd = currentSource.getObfuscatedOffset(gui.editor.getSelectionEnd()); - - Rectangle viewportBounds = gui.sourceScroller.getViewport().getViewRect(); - // Here we pick an "anchor position", which we want to stay in the same vertical location on the screen after the new text has been set - int anchorModelPos = gui.editor.getSelectionStart(); - Rectangle anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos); - if (anchorViewPos.y < viewportBounds.y || anchorViewPos.y >= viewportBounds.y + viewportBounds.height) { - anchorModelPos = gui.editor.viewToModel(new Point(0, viewportBounds.y)); - anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos); - } - int obfAnchorPos = currentSource.getObfuscatedOffset(anchorModelPos); - Rectangle anchorViewPos_f = anchorViewPos; - int scrollX = gui.sourceScroller.getHorizontalScrollBar().getValue(); - - loadClass(currentSource.getEntry(), () -> SwingUtilities.invokeLater(() -> { - int newAnchorModelPos = currentSource.getDeobfuscatedOffset(obfAnchorPos); - Rectangle newAnchorViewPos = GuiUtil.safeModelToView(gui.editor, newAnchorModelPos); - int newScrollY = newAnchorViewPos.y - (anchorViewPos_f.y - viewportBounds.y); - - gui.editor.select(currentSource.getDeobfuscatedOffset(obfSelectionStart), currentSource.getDeobfuscatedOffset(obfSelectionEnd)); - // Changing the selection scrolls to the caret position inside a SwingUtilities.invokeLater call, so - // we need to wrap our change to the scroll position inside another invokeLater so it happens after - // the caret's own scrolling. - SwingUtilities.invokeLater(() -> { - gui.sourceScroller.getHorizontalScrollBar().setValue(Math.min(scrollX, gui.sourceScroller.getHorizontalScrollBar().getMaximum())); - gui.sourceScroller.getVerticalScrollBar().setValue(Math.min(newScrollY, gui.sourceScroller.getVerticalScrollBar().getMaximum())); - }); - }), mode); - } else { - loadClass(currentSource.getEntry(), () -> showReference(reference), mode); - } - } - } - - private void loadClass(ClassEntry classEntry, Runnable callback) { - loadClass(classEntry, callback, RefreshMode.MINIMAL); - } - - private void loadClass(ClassEntry classEntry, Runnable callback, RefreshMode mode) { - ClassEntry targetClass = classEntry.getOutermostClass(); - - boolean requiresDecompile = mode == RefreshMode.FULL || currentSource == null || !currentSource.getEntry().equals(targetClass); - if (requiresDecompile) { - currentSource = null; // Or the GUI may try to find a nonexistent token - gui.setEditorText(I18n.translate("info_panel.editor.class.decompiling")); - } - - DECOMPILER_SERVICE.submit(() -> { - try { - if (requiresDecompile || mode == RefreshMode.JAVADOCS) { - currentSource = decompileSource(targetClass, mode == RefreshMode.JAVADOCS); - } - - remapSource(project.getMapper().getDeobfuscator()); - callback.run(); - } catch (Throwable t) { - System.err.println("An exception was thrown while decompiling class " + classEntry.getFullName()); - t.printStackTrace(System.err); - } - }); - } - - private DecompiledClassSource decompileSource(ClassEntry targetClass, boolean onlyRefreshJavadocs) { - try { - if (!onlyRefreshJavadocs || currentSource == null || !currentSource.getEntry().equals(targetClass)) { - uncommentedSource = decompiler.getSource(targetClass.getFullName()); - } - - Source source = uncommentedSource.addJavadocs(project.getMapper()); - - if (source == null) { - gui.setEditorText(I18n.translate("info_panel.editor.class.not_found") + " " + targetClass); - return DecompiledClassSource.text(targetClass, "Unable to find class"); - } - - SourceIndex index = source.index(); - index.resolveReferences(project.getMapper().getObfResolver()); - - return new DecompiledClassSource(targetClass, index); - } catch (Throwable t) { - StringWriter traceWriter = new StringWriter(); - t.printStackTrace(new PrintWriter(traceWriter)); - - return DecompiledClassSource.text(targetClass, traceWriter.toString()); - } - } + public void onModifierChanged(ValidationContext vc, Entry entry, AccessModifier modifier) { + EntryRemapper mapper = project.getMapper(); - private void remapSource(Translator translator) { - if (currentSource == null) { - return; + EntryMapping mapping = mapper.getDeobfMapping(entry); + if (mapping != null) { + mapper.mapFromObf(vc, entry, new EntryMapping(mapping.getTargetName(), modifier)); + } else { + mapper.mapFromObf(vc, entry, new EntryMapping(entry.getName(), modifier)); } - currentSource.remapSource(project, translator); - - gui.setEditorTheme(Config.getInstance().lookAndFeel); - gui.setSource(currentSource); - } - - public void modifierChange(ItemEvent event) { - if (event.getStateChange() == ItemEvent.SELECTED) { - EntryRemapper mapper = project.getMapper(); - Entry entry = gui.cursorReference.entry; - AccessModifier modifier = (AccessModifier) event.getItem(); - - EntryMapping mapping = mapper.getDeobfMapping(entry); - if (mapping != null) { - mapper.mapFromObf(entry, new EntryMapping(mapping.getTargetName(), modifier)); - } else { - mapper.mapFromObf(entry, new EntryMapping(entry.getName(), modifier)); - } - - refreshCurrentClass(); - } + chp.invalidateMapped(); } public ClassInheritanceTreeNode getClassInheritance(ClassEntry entry) { @@ -557,72 +415,71 @@ public class GuiController implements ClientPacketHandler { return rootNode; } - public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree) { - rename(reference, newName, refreshClassTree, true); + @Override + public void rename(ValidationContext vc, EntryReference, Entry> reference, String newName, boolean refreshClassTree) { + rename(vc, reference, newName, refreshClassTree, false); } - @Override - public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean jumpToReference) { + public void rename(ValidationContext vc, EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean validateOnly) { Entry entry = reference.getNameableEntry(); - project.getMapper().mapFromObf(entry, new EntryMapping(newName)); + project.getMapper().mapFromObf(vc, entry, new EntryMapping(newName), true, validateOnly); - if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) - this.gui.moveClassTree(reference, newName); + if (validateOnly || !vc.canProceed()) return; - refreshCurrentClass(jumpToReference ? reference : null); - } + if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) + this.gui.moveClassTree(reference.entry, newName); - public void removeMapping(EntryReference, Entry> reference) { - removeMapping(reference, true); + chp.invalidateMapped(); } @Override - public void removeMapping(EntryReference, Entry> reference, boolean jumpToReference) { - project.getMapper().removeByObf(reference.getNameableEntry()); + public void removeMapping(ValidationContext vc, EntryReference, Entry> reference) { + project.getMapper().removeByObf(vc, reference.getNameableEntry()); + + if (!vc.canProceed()) return; if (reference.entry instanceof ClassEntry) - this.gui.moveClassTree(reference, false, true); - refreshCurrentClass(jumpToReference ? reference : null); - } + this.gui.moveClassTree(reference.entry, false, true); - public void changeDocs(EntryReference, Entry> reference, String updatedDocs) { - changeDocs(reference, updatedDocs, true); + chp.invalidateMapped(); } @Override - public void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference) { - changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs); - - refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS); + public void changeDocs(ValidationContext vc, EntryReference, Entry> reference, String updatedDocs) { + changeDocs(vc, reference, updatedDocs, false); } - private void changeDoc(Entry obfEntry, String newDoc) { - EntryRemapper mapper = project.getMapper(); - if (mapper.getDeobfMapping(obfEntry) == null) { - markAsDeobfuscated(obfEntry, false); // NPE - } - mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); + public void changeDocs(ValidationContext vc, EntryReference, Entry> reference, String updatedDocs, boolean validateOnly) { + changeDoc(vc, reference.entry, updatedDocs, validateOnly); + + if (validateOnly || !vc.canProceed()) return; + + chp.invalidateJavadoc(reference.getLocationClassEntry()); } - private void markAsDeobfuscated(Entry obfEntry, boolean renaming) { + private void changeDoc(ValidationContext vc, Entry obfEntry, String newDoc, boolean validateOnly) { EntryRemapper mapper = project.getMapper(); - mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming); - } - public void markAsDeobfuscated(EntryReference, Entry> reference) { - markAsDeobfuscated(reference, true); + EntryMapping deobfMapping = mapper.getDeobfMapping(obfEntry); + if (deobfMapping == null) { + deobfMapping = new EntryMapping(mapper.deobfuscate(obfEntry).getName()); + } + + mapper.mapFromObf(vc, obfEntry, deobfMapping.withDocs(newDoc), false, validateOnly); } @Override - public void markAsDeobfuscated(EntryReference, Entry> reference, boolean jumpToReference) { + public void markAsDeobfuscated(ValidationContext vc, EntryReference, Entry> reference) { EntryRemapper mapper = project.getMapper(); Entry entry = reference.getNameableEntry(); - mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName())); + mapper.mapFromObf(vc, entry, new EntryMapping(mapper.deobfuscate(entry).getName())); + + if (!vc.canProceed()) return; if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) - this.gui.moveClassTree(reference, true, false); + this.gui.moveClassTree(reference.entry, true, false); - refreshCurrentClass(jumpToReference ? reference : null); + chp.invalidateMapped(); } public void openStats(Set includedMembers) { @@ -635,7 +492,7 @@ public class GuiController implements ClientPacketHandler { try (FileWriter w = new FileWriter(statsFile)) { w.write( Utils.readResourceToString("/stats.html") - .replace("/*data*/", data) + .replace("/*data*/", data) ); } @@ -647,10 +504,11 @@ public class GuiController implements ClientPacketHandler { } public void setDecompiler(DecompilerService service) { - uncommentedSource = null; - decompilerService = service; - decompiler = project.createDecompiler(decompilerService); - refreshCurrentClass(null, RefreshMode.FULL); + chp.setDecompilerService(service); + } + + public ClassHandleProvider getClassHandleProvider() { + return chp; } public EnigmaClient getClient() { diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java deleted file mode 100644 index 87cb83b..0000000 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java +++ /dev/null @@ -1,7 +0,0 @@ -package cuchaz.enigma.gui; - -public enum RefreshMode { - MINIMAL, - JAVADOCS, - FULL -} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java index 10c418c..4ef0442 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java @@ -32,4 +32,5 @@ public class TokenListCellRenderer implements ListCellRenderer { label.setText(this.controller.getReadableToken(token).toString()); return label; } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java index 035b238..fd40cb7 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java @@ -1,25 +1,27 @@ package cuchaz.enigma.gui.config; import java.io.IOException; - -import javax.swing.SwingUtilities; +import java.util.HashSet; +import java.util.Set; import com.google.common.collect.ImmutableMap; import cuchaz.enigma.gui.EnigmaSyntaxKit; -import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.events.ThemeChangeListener; import cuchaz.enigma.gui.highlight.BoxHighlightPainter; -import cuchaz.enigma.gui.highlight.TokenHighlightType; import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.source.RenamableTokenType; import de.sciss.syntaxpane.DefaultSyntaxKit; public class Themes { - public static void setLookAndFeel(Gui gui, Config.LookAndFeel lookAndFeel) { + private static final Set listeners = new HashSet<>(); + + public static void setLookAndFeel(Config.LookAndFeel lookAndFeel) { Config.getInstance().lookAndFeel = lookAndFeel; - updateTheme(gui); + updateTheme(); } - public static void updateTheme(Gui gui) { + public static void updateTheme() { Config config = Config.getInstance(); config.lookAndFeel.setGlobalLAF(); config.lookAndFeel.apply(config); @@ -31,15 +33,26 @@ public class Themes { EnigmaSyntaxKit.invalidate(); DefaultSyntaxKit.initKit(); DefaultSyntaxKit.registerContentType("text/enigma-sources", EnigmaSyntaxKit.class.getName()); - gui.boxHighlightPainters = ImmutableMap.of( - TokenHighlightType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline), - TokenHighlightType.PROPOSED, BoxHighlightPainter.create(config.proposedColor, config.proposedColorOutline), - TokenHighlightType.DEOBFUSCATED, BoxHighlightPainter.create(config.deobfuscatedColor, config.deobfuscatedColorOutline) - ); - gui.setEditorTheme(config.lookAndFeel); - SwingUtilities.updateComponentTreeUI(gui.getFrame()); + ImmutableMap boxHighlightPainters = getBoxHighlightPainters(); + listeners.forEach(l -> l.onThemeChanged(config.lookAndFeel, boxHighlightPainters)); ScaleUtil.applyScaling(); } + public static ImmutableMap getBoxHighlightPainters() { + Config config = Config.getInstance(); + return ImmutableMap.of( + RenamableTokenType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline), + RenamableTokenType.PROPOSED, BoxHighlightPainter.create(config.proposedColor, config.proposedColorOutline), + RenamableTokenType.DEOBFUSCATED, BoxHighlightPainter.create(config.deobfuscatedColor, config.deobfuscatedColorOutline) + ); + } + + public static void addListener(ThemeChangeListener listener) { + listeners.add(listener); + } + + public static void removeListener(ThemeChangeListener listener) { + listeners.remove(listener); + } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java index d81460a..9fbe65a 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java @@ -19,34 +19,64 @@ import javax.swing.*; import javax.swing.text.html.HTML; import java.awt.*; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import javax.swing.*; + +import com.google.common.base.Strings; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.elements.ValidatableTextArea; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.network.packet.ChangeDocsC2SPacket; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.validation.Message; +import cuchaz.enigma.utils.validation.ValidationContext; + public class JavadocDialog { - private static JavadocDialog instance = null; + private final JDialog ui; + private final GuiController controller; + private final EntryReference, Entry> entry; - private JFrame frame; + private final ValidatableTextArea text; - private JavadocDialog(JFrame parent, JTextArea text, Callback callback) { - // init frame - frame = new JFrame(I18n.translate("javadocs.edit")); - final Container pane = frame.getContentPane(); - pane.setLayout(new BorderLayout()); + private final ValidationContext vc = new ValidationContext(); + + private JavadocDialog(JFrame parent, GuiController controller, EntryReference, Entry> entry, String preset) { + this.ui = new JDialog(parent, I18n.translate("javadocs.edit")); + this.controller = controller; + this.entry = entry; + this.text = new ValidatableTextArea(10, 40); + + // set up dialog + Container contentPane = ui.getContentPane(); + contentPane.setLayout(new BorderLayout()); // editor panel - text.setTabSize(2); - pane.add(new JScrollPane(text), BorderLayout.CENTER); - text.addKeyListener(new KeyAdapter() { + this.text.setText(preset); + this.text.setTabSize(2); + contentPane.add(new JScrollPane(this.text), BorderLayout.CENTER); + this.text.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { switch (event.getKeyCode()) { case KeyEvent.VK_ENTER: - if (event.isControlDown()) - callback.closeUi(frame, true); + if (event.isControlDown()) { + doSave(); + if (vc.canProceed()) { + close(); + } + } break; case KeyEvent.VK_ESCAPE: - callback.closeUi(frame, false); + close(); break; default: break; @@ -56,23 +86,15 @@ public class JavadocDialog { // buttons panel JPanel buttonsPanel = new JPanel(); - FlowLayout buttonsLayout = new FlowLayout(); - buttonsLayout.setAlignment(FlowLayout.RIGHT); - buttonsPanel.setLayout(buttonsLayout); + buttonsPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("javadocs.instruction")))); JButton cancelButton = new JButton(I18n.translate("javadocs.cancel")); - cancelButton.addActionListener(event -> { - // close (hide) the dialog - callback.closeUi(frame, false); - }); + cancelButton.addActionListener(event -> close()); buttonsPanel.add(cancelButton); JButton saveButton = new JButton(I18n.translate("javadocs.save")); - saveButton.addActionListener(event -> { - // exit enigma - callback.closeUi(frame, true); - }); + saveButton.addActionListener(event -> doSave()); buttonsPanel.add(saveButton); - pane.add(buttonsPanel, BorderLayout.SOUTH); + contentPane.add(buttonsPanel, BorderLayout.SOUTH); // tags panel JMenuBar tagsMenu = new JMenuBar(); @@ -116,22 +138,56 @@ public class JavadocDialog { }); tagsMenu.add(htmlList); - pane.add(tagsMenu, BorderLayout.NORTH); + contentPane.add(tagsMenu, BorderLayout.NORTH); // show the frame - frame.setSize(ScaleUtil.getDimension(600, 400)); - frame.setLocationRelativeTo(parent); - frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + this.ui.setSize(ScaleUtil.getDimension(600, 400)); + this.ui.setLocationRelativeTo(parent); + this.ui.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } - public static void init(JFrame parent, JTextArea area, Callback callback) { - instance = new JavadocDialog(parent, area, callback); - instance.frame.doLayout(); - instance.frame.setVisible(true); + // Called when the "Save" button gets clicked. + public void doSave() { + vc.reset(); + validate(); + if (!vc.canProceed()) return; + save(); + if (!vc.canProceed()) return; + close(); } - public interface Callback { - void closeUi(JFrame frame, boolean save); + public void close() { + this.ui.setVisible(false); + this.ui.dispose(); + } + + public void validate() { + vc.setActiveElement(text); + + if (text.getText().contains("*/")) { + vc.raise(Message.ILLEGAL_DOC_COMMENT_END); + } + + controller.changeDocs(vc, entry, text.getText(), true); + } + + public void save() { + vc.setActiveElement(text); + controller.changeDocs(vc, entry, text.getText()); + + if (!vc.canProceed()) return; + + controller.sendPacket(new ChangeDocsC2SPacket(entry.getNameableEntry(), text.getText())); + } + + public static void show(JFrame parent, GuiController controller, EntryReference, Entry> entry) { + EntryReference, Entry> translatedReference = controller.project.getMapper().deobfuscate(entry); + String text = Strings.nullToEmpty(translatedReference.entry.getJavadocs()); + + JavadocDialog dialog = new JavadocDialog(parent, controller, entry, text); + dialog.ui.doLayout(); + dialog.ui.setVisible(true); + dialog.text.grabFocus(); } private enum JavadocTag { @@ -156,4 +212,5 @@ public class JavadocDialog { return this.inline; } } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java new file mode 100644 index 0000000..6f28949 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java @@ -0,0 +1,170 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.GridLayout; +import java.awt.event.*; +import java.util.HashSet; +import java.util.Set; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.text.Document; + +import cuchaz.enigma.gui.events.ConvertingTextFieldListener; +import cuchaz.enigma.gui.util.GuiUtil; +import cuchaz.enigma.utils.validation.ParameterizedMessage; +import cuchaz.enigma.utils.validation.Validatable; + +/** + * A label that converts into an editable text field when you click it. + */ +public class ConvertingTextField implements Validatable { + + private final JPanel ui; + private final ValidatableTextField textField; + private final JLabel label; + private boolean isEditing = false; + + private final Set listeners = new HashSet<>(); + + public ConvertingTextField(String text) { + this.ui = new JPanel(); + this.ui.setLayout(new GridLayout(1, 1, 0, 0)); + this.textField = new ValidatableTextField(text); + this.label = GuiUtil.unboldLabel(new JLabel(text)); + this.label.setBorder(BorderFactory.createLoweredBevelBorder()); + + this.label.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + startEditing(); + } + }); + + this.textField.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (!hasChanges()) { + stopEditing(true); + } + } + }); + + this.textField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + stopEditing(true); + } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { + stopEditing(false); + } + } + }); + + this.ui.add(this.label); + } + + public void startEditing() { + if (isEditing) return; + this.ui.removeAll(); + this.ui.add(this.textField); + this.isEditing = true; + this.ui.validate(); + this.ui.repaint(); + this.textField.requestFocusInWindow(); + this.textField.selectAll(); + this.listeners.forEach(l -> l.onStartEditing(this)); + } + + public void stopEditing(boolean abort) { + if (!isEditing) return; + + if (!listeners.stream().allMatch(l -> l.tryStopEditing(this, abort))) return; + + if (abort) { + this.textField.setText(this.label.getText()); + } else { + this.label.setText(this.textField.getText()); + } + + this.ui.removeAll(); + this.ui.add(this.label); + this.isEditing = false; + this.ui.validate(); + this.ui.repaint(); + this.listeners.forEach(l -> l.onStopEditing(this, abort)); + } + + public void setText(String text) { + stopEditing(true); + this.label.setText(text); + this.textField.setText(text); + } + + public void setEditText(String text) { + if (!isEditing) return; + + this.textField.setText(text); + } + + public void selectAll() { + if (!isEditing) return; + + this.textField.selectAll(); + } + + public void selectSubstring(int startIndex) { + if (!isEditing) return; + + Document doc = this.textField.getDocument(); + if (doc != null) { + this.selectSubstring(startIndex, doc.getLength()); + } + } + + public void selectSubstring(int startIndex, int endIndex) { + if (!isEditing) return; + + this.textField.select(startIndex, endIndex); + } + + public String getText() { + if (isEditing) { + return this.textField.getText(); + } else { + return this.label.getText(); + } + } + + public String getPersistentText() { + return this.label.getText(); + } + + public boolean hasChanges() { + if (!isEditing) return false; + return !this.textField.getText().equals(this.label.getText()); + } + + @Override + public void addMessage(ParameterizedMessage message) { + textField.addMessage(message); + } + + @Override + public void clearMessages() { + textField.clearMessages(); + } + + public void addListener(ConvertingTextFieldListener listener) { + this.listeners.add(listener); + } + + public void removeListener(ConvertingTextFieldListener listener) { + this.listeners.remove(listener); + } + + public JPanel getUi() { + return ui; + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java new file mode 100644 index 0000000..e92e677 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java @@ -0,0 +1,58 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Component; +import java.awt.event.KeyEvent; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.KeyStroke; + +import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.panels.PanelEditor; +import cuchaz.enigma.utils.I18n; + +public class EditorTabPopupMenu { + + private final JPopupMenu ui; + private final JMenuItem close; + private final JMenuItem closeAll; + private final JMenuItem closeOthers; + private final JMenuItem closeLeft; + private final JMenuItem closeRight; + + private final Gui gui; + private PanelEditor editor; + + public EditorTabPopupMenu(Gui gui) { + this.gui = gui; + + this.ui = new JPopupMenu(); + + this.close = new JMenuItem(I18n.translate("popup_menu.editor_tab.close")); + this.close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, KeyEvent.CTRL_DOWN_MASK)); + this.close.addActionListener(a -> gui.closeEditor(editor)); + this.ui.add(this.close); + + this.closeAll = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_all")); + this.closeAll.addActionListener(a -> gui.closeAllEditorTabs()); + this.ui.add(this.closeAll); + + this.closeOthers = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_others")); + this.closeOthers.addActionListener(a -> gui.closeTabsExcept(editor)); + this.ui.add(this.closeOthers); + + this.closeLeft = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_left")); + this.closeLeft.addActionListener(a -> gui.closeTabsLeftOf(editor)); + this.ui.add(this.closeLeft); + + this.closeRight = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_right")); + this.closeRight.addActionListener(a -> gui.closeTabsRightOf(editor)); + this.ui.add(this.closeRight); + } + + public void show(Component invoker, int x, int y, PanelEditor panelEditor) { + this.editor = panelEditor; + ui.show(invoker, x, y); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/JMultiLineToolTip.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/JMultiLineToolTip.java new file mode 100644 index 0000000..533d1b3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/JMultiLineToolTip.java @@ -0,0 +1,132 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; + +import javax.swing.CellRendererPane; +import javax.swing.JComponent; +import javax.swing.JTextArea; +import javax.swing.JToolTip; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.basic.BasicToolTipUI; + +/** + * Implements a multi line tooltip for GUI components + * Copied from http://www.codeguru.com/java/articles/122.shtml + * + * @author Zafir Anjum + */ +public class JMultiLineToolTip extends JToolTip { + + private static final long serialVersionUID = 7813662474312183098L; + + public JMultiLineToolTip() { + updateUI(); + } + + public void updateUI() { + setUI(MultiLineToolTipUI.createUI(this)); + } + + public void setColumns(int columns) { + this.columns = columns; + this.fixedwidth = 0; + } + + public int getColumns() { + return columns; + } + + public void setFixedWidth(int width) { + this.fixedwidth = width; + this.columns = 0; + } + + public int getFixedWidth() { + return fixedwidth; + } + + protected int columns = 0; + protected int fixedwidth = 0; +} + +/** + * UI for multi line tool tip + */ +class MultiLineToolTipUI extends BasicToolTipUI { + + static MultiLineToolTipUI sharedInstance = new MultiLineToolTipUI(); + Font smallFont; + static JToolTip tip; + protected CellRendererPane rendererPane; + + private static JTextArea textArea; + + public static ComponentUI createUI(JComponent c) { + return sharedInstance; + } + + public MultiLineToolTipUI() { + super(); + } + + public void installUI(JComponent c) { + super.installUI(c); + tip = (JToolTip) c; + rendererPane = new CellRendererPane(); + c.add(rendererPane); + } + + public void uninstallUI(JComponent c) { + super.uninstallUI(c); + + c.remove(rendererPane); + rendererPane = null; + } + + public void paint(Graphics g, JComponent c) { + Dimension size = c.getSize(); + textArea.setBackground(c.getBackground()); + rendererPane.paintComponent(g, textArea, c, 1, 1, size.width - 1, size.height - 1, true); + } + + public Dimension getPreferredSize(JComponent c) { + String tipText = ((JToolTip) c).getTipText(); + if (tipText == null) return new Dimension(0, 0); + textArea = new JTextArea(tipText); + rendererPane.removeAll(); + rendererPane.add(textArea); + textArea.setWrapStyleWord(true); + int width = ((JMultiLineToolTip) c).getFixedWidth(); + int columns = ((JMultiLineToolTip) c).getColumns(); + + if (columns > 0) { + textArea.setColumns(columns); + textArea.setSize(0, 0); + textArea.setLineWrap(true); + textArea.setSize(textArea.getPreferredSize()); + } else if (width > 0) { + textArea.setLineWrap(true); + Dimension d = textArea.getPreferredSize(); + d.width = width; + d.height++; + textArea.setSize(d); + } else + textArea.setLineWrap(false); + + Dimension dim = textArea.getPreferredSize(); + + dim.height += 1; + dim.width += 1; + return dim; + } + + public Dimension getMinimumSize(JComponent c) { + return getPreferredSize(c); + } + + public Dimension getMaximumSize(JComponent c) { + return getPreferredSize(c); + } +} \ No newline at end of file diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java index 948798a..9b06d26 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java @@ -321,7 +321,7 @@ public class MenuBar { if (lookAndFeel.equals(Config.getInstance().lookAndFeel)) { themeButton.setSelected(true); } - themeButton.addActionListener(_e -> Themes.setLookAndFeel(gui, lookAndFeel)); + themeButton.addActionListener(_e -> Themes.setLookAndFeel(lookAndFeel)); themesMenu.add(themeButton); } } @@ -335,7 +335,12 @@ public class MenuBar { languageButton.setSelected(true); } languageButton.addActionListener(event -> { - I18n.setLanguage(lang); + Config.getInstance().language = lang; + try { + Config.getInstance().saveConfig(); + } catch (IOException e) { + e.printStackTrace(); + } ChangeDialog.show(gui); }); languagesMenu.add(languageButton); diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java index b92041c..2310cf3 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java @@ -1,6 +1,7 @@ package cuchaz.enigma.gui.elements; import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.panels.PanelEditor; import cuchaz.enigma.utils.I18n; import javax.swing.*; @@ -20,10 +21,10 @@ public class PopupMenuBar extends JPopupMenu { public final JMenuItem openNextMenu; public final JMenuItem toggleMappingMenu; - public PopupMenuBar(Gui gui) { + public PopupMenuBar(PanelEditor editor, Gui gui) { { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.rename")); - menu.addActionListener(event -> gui.startRename()); + menu.addActionListener(event -> gui.startRename(editor)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -31,7 +32,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.javadoc")); - menu.addActionListener(event -> gui.startDocChange()); + menu.addActionListener(event -> gui.startDocChange(editor)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -39,7 +40,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.inheritance")); - menu.addActionListener(event -> gui.showInheritance()); + menu.addActionListener(event -> gui.showInheritance(editor)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -47,7 +48,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.implementations")); - menu.addActionListener(event -> gui.showImplementations()); + menu.addActionListener(event -> gui.showImplementations(editor)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -55,7 +56,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls")); - menu.addActionListener(event -> gui.showCalls(true)); + menu.addActionListener(event -> gui.showCalls(editor, true)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -63,7 +64,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls.specific")); - menu.addActionListener(event -> gui.showCalls(false)); + menu.addActionListener(event -> gui.showCalls(editor, false)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -71,7 +72,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.declaration")); - menu.addActionListener(event -> gui.getController().navigateTo(gui.cursorReference.entry)); + menu.addActionListener(event -> gui.getController().navigateTo(editor.getCursorReference().entry)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -95,7 +96,7 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.mark_deobfuscated")); - menu.addActionListener(event -> gui.toggleMapping()); + menu.addActionListener(event -> gui.toggleMapping(editor)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); menu.setEnabled(false); this.add(menu); @@ -106,19 +107,19 @@ public class PopupMenuBar extends JPopupMenu { } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.in")); - menu.addActionListener(event -> gui.editor.offsetEditorZoom(2)); + menu.addActionListener(event -> editor.offsetEditorZoom(2)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK)); this.add(menu); } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.out")); - menu.addActionListener(event -> gui.editor.offsetEditorZoom(-2)); + menu.addActionListener(event -> editor.offsetEditorZoom(-2)); menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); this.add(menu); } { JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.reset")); - menu.addActionListener(event -> gui.editor.resetEditorZoom()); + menu.addActionListener(event -> editor.resetEditorZoom()); this.add(menu); } } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatablePasswordField.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatablePasswordField.java new file mode 100644 index 0000000..02e1bc3 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatablePasswordField.java @@ -0,0 +1,96 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Graphics; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JPasswordField; +import javax.swing.JToolTip; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import cuchaz.enigma.utils.validation.ParameterizedMessage; +import cuchaz.enigma.utils.validation.Validatable; + +public class ValidatablePasswordField extends JPasswordField implements Validatable { + + private List messages = new ArrayList<>(); + private String tooltipText = null; + + public ValidatablePasswordField() { + } + + public ValidatablePasswordField(String text) { + super(text); + } + + public ValidatablePasswordField(int columns) { + super(columns); + } + + public ValidatablePasswordField(String text, int columns) { + super(text, columns); + } + + public ValidatablePasswordField(Document doc, String txt, int columns) { + super(doc, txt, columns); + } + + { + getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + clearMessages(); + } + }); + } + + @Override + public JToolTip createToolTip() { + JMultiLineToolTip tooltip = new JMultiLineToolTip(); + tooltip.setComponent(this); + return tooltip; + } + + @Override + public void setToolTipText(String text) { + tooltipText = text; + setToolTipText0(); + } + + private void setToolTipText0() { + super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages)); + } + + @Override + public void clearMessages() { + messages.clear(); + setToolTipText0(); + repaint(); + } + + @Override + public void addMessage(ParameterizedMessage message) { + messages.add(message); + setToolTipText0(); + repaint(); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + ValidatableUi.drawMarker(this, g, messages); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextArea.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextArea.java new file mode 100644 index 0000000..7d1f866 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextArea.java @@ -0,0 +1,100 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Graphics; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JTextArea; +import javax.swing.JToolTip; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import cuchaz.enigma.utils.validation.ParameterizedMessage; +import cuchaz.enigma.utils.validation.Validatable; + +public class ValidatableTextArea extends JTextArea implements Validatable { + + private List messages = new ArrayList<>(); + private String tooltipText = null; + + public ValidatableTextArea() { + } + + public ValidatableTextArea(String text) { + super(text); + } + + public ValidatableTextArea(int rows, int columns) { + super(rows, columns); + } + + public ValidatableTextArea(String text, int rows, int columns) { + super(text, rows, columns); + } + + public ValidatableTextArea(Document doc) { + super(doc); + } + + public ValidatableTextArea(Document doc, String text, int rows, int columns) { + super(doc, text, rows, columns); + } + + { + getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + clearMessages(); + } + }); + } + + @Override + public JToolTip createToolTip() { + JMultiLineToolTip tooltip = new JMultiLineToolTip(); + tooltip.setComponent(this); + return tooltip; + } + + @Override + public void setToolTipText(String text) { + tooltipText = text; + setToolTipText0(); + } + + private void setToolTipText0() { + super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages)); + } + + @Override + public void clearMessages() { + messages.clear(); + setToolTipText0(); + repaint(); + } + + @Override + public void addMessage(ParameterizedMessage message) { + messages.add(message); + setToolTipText0(); + repaint(); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + ValidatableUi.drawMarker(this, g, messages); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextField.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextField.java new file mode 100644 index 0000000..c114dc1 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextField.java @@ -0,0 +1,96 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Graphics; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JTextField; +import javax.swing.JToolTip; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import cuchaz.enigma.utils.validation.ParameterizedMessage; +import cuchaz.enigma.utils.validation.Validatable; + +public class ValidatableTextField extends JTextField implements Validatable { + + private List messages = new ArrayList<>(); + private String tooltipText = null; + + public ValidatableTextField() { + } + + public ValidatableTextField(String text) { + super(text); + } + + public ValidatableTextField(int columns) { + super(columns); + } + + public ValidatableTextField(String text, int columns) { + super(text, columns); + } + + public ValidatableTextField(Document doc, String text, int columns) { + super(doc, text, columns); + } + + { + getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + clearMessages(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + clearMessages(); + } + }); + } + + @Override + public JToolTip createToolTip() { + JMultiLineToolTip tooltip = new JMultiLineToolTip(); + tooltip.setComponent(this); + return tooltip; + } + + @Override + public void setToolTipText(String text) { + tooltipText = text; + setToolTipText0(); + } + + private void setToolTipText0() { + super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages)); + } + + @Override + public void clearMessages() { + messages.clear(); + setToolTipText0(); + repaint(); + } + + @Override + public void addMessage(ParameterizedMessage message) { + messages.add(message); + setToolTipText0(); + repaint(); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + ValidatableUi.drawMarker(this, g, messages); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableUi.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableUi.java new file mode 100644 index 0000000..5df6348 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableUi.java @@ -0,0 +1,107 @@ +package cuchaz.enigma.gui.elements; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.utils.validation.ParameterizedMessage; + +public final class ValidatableUi { + + private ValidatableUi() { + } + + public static String getTooltipText(String tooltipText, List messages) { + List strings = new ArrayList<>(); + if (tooltipText != null) { + strings.add(tooltipText); + } + if (!messages.isEmpty()) { + strings.add("Error(s): "); + + messages.forEach(msg -> { + strings.add(String.format(" - %s", msg.getText())); + String longDesc = msg.getLongText(); + if (!longDesc.isEmpty()) { + Arrays.stream(longDesc.split("\n")).map(s -> String.format(" %s", s)).forEach(strings::add); + } + }); + } + if (strings.isEmpty()) { + return null; + } else { + return String.join("\n", strings); + } + } + + public static String formatMessages(List messages) { + List strings = new ArrayList<>(); + + if (!messages.isEmpty()) { + strings.add("Error(s): "); + + messages.forEach(msg -> { + strings.add(String.format(" - %s", msg.getText())); + String longDesc = msg.getLongText(); + if (!longDesc.isEmpty()) { + Arrays.stream(longDesc.split("\n")).map(s -> String.format(" %s", s)).forEach(strings::add); + } + }); + } + if (strings.isEmpty()) { + return null; + } else { + return String.join("\n", strings); + } + } + + public static void drawMarker(Component self, Graphics g, List messages) { + Color color = ValidatableUi.getMarkerColor(messages); + if (color != null) { + g.setColor(color); + int x1 = self.getWidth() - ScaleUtil.scale(8) - 1; + int x2 = self.getWidth() - ScaleUtil.scale(1) - 1; + int y1 = ScaleUtil.scale(1); + int y2 = ScaleUtil.scale(8); + g.fillPolygon(new int[]{x1, x2, x2}, new int[]{y1, y1, y2}, 3); + } + } + + @Nullable + public static Color getMarkerColor(List messages) { + int level = messages.stream() + .mapToInt(ValidatableUi::getMessageLevel) + .max().orElse(0); + + switch (level) { + case 0: + return null; + case 1: + return Color.BLUE; + case 2: + return Color.ORANGE; + case 3: + return Color.RED; + } + throw new IllegalStateException("unreachable"); + } + + private static int getMessageLevel(ParameterizedMessage message) { + switch (message.message.type) { + case INFO: + return 1; + case WARNING: + return 2; + case ERROR: + return 3; + } + throw new IllegalStateException("unreachable"); + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ConvertingTextFieldListener.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ConvertingTextFieldListener.java new file mode 100644 index 0000000..6e17fec --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ConvertingTextFieldListener.java @@ -0,0 +1,17 @@ +package cuchaz.enigma.gui.events; + +import cuchaz.enigma.gui.elements.ConvertingTextField; + +public interface ConvertingTextFieldListener { + + default void onStartEditing(ConvertingTextField field) { + } + + default boolean tryStopEditing(ConvertingTextField field, boolean abort) { + return true; + } + + default void onStopEditing(ConvertingTextField field, boolean abort) { + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/events/EditorActionListener.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/EditorActionListener.java new file mode 100644 index 0000000..8880731 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/EditorActionListener.java @@ -0,0 +1,20 @@ +package cuchaz.enigma.gui.events; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.panels.PanelEditor; +import cuchaz.enigma.classhandle.ClassHandle; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; + +public interface EditorActionListener { + + default void onCursorReferenceChanged(PanelEditor editor, EntryReference, Entry> ref) { + } + + default void onClassHandleChanged(PanelEditor editor, ClassEntry old, ClassHandle ch) { + } + + default void onTitleChanged(PanelEditor editor, String title) { + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ThemeChangeListener.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ThemeChangeListener.java new file mode 100644 index 0000000..d4962f7 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/events/ThemeChangeListener.java @@ -0,0 +1,13 @@ +package cuchaz.enigma.gui.events; + +import java.util.Map; + +import cuchaz.enigma.gui.config.Config.LookAndFeel; +import cuchaz.enigma.gui.highlight.BoxHighlightPainter; +import cuchaz.enigma.source.RenamableTokenType; + +public interface ThemeChangeListener { + + void onThemeChanged(LookAndFeel lookAndFeel, Map boxHighlightPainters); + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java index 2e4e462..c899e68 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java @@ -19,6 +19,8 @@ import java.awt.*; public class SelectionHighlightPainter implements Highlighter.HighlightPainter { + public static final SelectionHighlightPainter INSTANCE = new SelectionHighlightPainter(); + @Override public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) { // draw a thick border @@ -28,4 +30,5 @@ public class SelectionHighlightPainter implements Highlighter.HighlightPainter { g2d.setStroke(new BasicStroke(2.0f)); g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java deleted file mode 100644 index ae23f32..0000000 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java +++ /dev/null @@ -1,7 +0,0 @@ -package cuchaz.enigma.gui.highlight; - -public enum TokenHighlightType { - OBFUSCATED, - DEOBFUSCATED, - PROPOSED -} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/ClosableTabTitlePane.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/ClosableTabTitlePane.java new file mode 100644 index 0000000..fe5c857 --- /dev/null +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/ClosableTabTitlePane.java @@ -0,0 +1,133 @@ +package cuchaz.enigma.gui.panels; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.accessibility.AccessibleContext; +import javax.annotation.Nullable; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeListener; + +public class ClosableTabTitlePane { + + private final JPanel ui; + private final JButton closeButton; + private final JLabel label; + + private ChangeListener cachedChangeListener; + private JTabbedPane parent; + + public ClosableTabTitlePane(String text, Runnable onClose) { + this.ui = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2)); + this.ui.setOpaque(false); + this.label = new JLabel(text); + this.ui.add(this.label); + + // Adapted from javax.swing.plaf.metal.MetalTitlePane + this.closeButton = new JButton(); + this.closeButton.setFocusPainted(false); + this.closeButton.setFocusable(false); + this.closeButton.setOpaque(true); + this.closeButton.setIcon(UIManager.getIcon("InternalFrame.closeIcon")); + this.closeButton.putClientProperty("paintActive", Boolean.TRUE); + this.closeButton.setBorder(new EmptyBorder(0, 0, 0, 0)); + this.closeButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, "Close"); + this.closeButton.setMaximumSize(new Dimension(this.closeButton.getIcon().getIconWidth(), this.closeButton.getIcon().getIconHeight())); + this.ui.add(this.closeButton); + + // Use mouse listener here so that it also works for disabled buttons + closeButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isMiddleMouseButton(e)) { + onClose.run(); + } + } + }); + + this.ui.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isMiddleMouseButton(e)) { + onClose.run(); + } + } + + @Override + public void mousePressed(MouseEvent e) { + // for some reason registering a mouse listener on this makes + // events never go to the tabbed pane, so we have to redirect + // the event for tab selection and context menu to work + if (parent != null) { + Point pt = new Point(e.getXOnScreen(), e.getYOnScreen()); + SwingUtilities.convertPointFromScreen(pt, parent); + MouseEvent e1 = new MouseEvent( + parent, + e.getID(), + e.getWhen(), + e.getModifiersEx(), + (int) pt.getX(), + (int) pt.getY(), + e.getXOnScreen(), + e.getYOnScreen(), + e.getClickCount(), + e.isPopupTrigger(), + e.getButton() + ); + parent.dispatchEvent(e1); + } + } + }); + + this.ui.putClientProperty(ClosableTabTitlePane.class, this); + } + + public void setTabbedPane(JTabbedPane pane) { + if (this.parent != null) { + pane.removeChangeListener(cachedChangeListener); + } + if (pane != null) { + updateState(pane); + cachedChangeListener = e -> updateState(pane); + pane.addChangeListener(cachedChangeListener); + } + this.parent = pane; + } + + public void setText(String text) { + this.label.setText(text); + } + + public String getText() { + return this.label.getText(); + } + + private void updateState(JTabbedPane pane) { + int selectedIndex = pane.getSelectedIndex(); + boolean isActive = selectedIndex != -1 && pane.getTabComponentAt(selectedIndex) == this.ui; + this.closeButton.setEnabled(isActive); + this.closeButton.putClientProperty("paintActive", isActive); + this.ui.repaint(); + } + + public JPanel getUi() { + return ui; + } + + @Nullable + public static ClosableTabTitlePane byUi(Component c) { + if (c instanceof JComponent) { + Object prop = ((JComponent) c).getClientProperty(ClosableTabTitlePane.class); + if (prop instanceof ClosableTabTitlePane) { + return (ClosableTabTitlePane) prop; + } + } + return null; + } + +} diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java index 346d665..dd9971a 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java @@ -1,43 +1,124 @@ package cuchaz.enigma.gui.panels; +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Highlighter; +import javax.swing.text.Highlighter.HighlightPainter; + import cuchaz.enigma.EnigmaProject; import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.classhandle.ClassHandle; +import cuchaz.enigma.classhandle.ClassHandleError; +import cuchaz.enigma.events.ClassHandleListener; import cuchaz.enigma.gui.BrowserCaret; import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.gui.config.Config; +import cuchaz.enigma.gui.config.Themes; +import cuchaz.enigma.gui.elements.PopupMenuBar; +import cuchaz.enigma.gui.events.EditorActionListener; +import cuchaz.enigma.gui.events.ThemeChangeListener; +import cuchaz.enigma.gui.highlight.BoxHighlightPainter; +import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; +import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.source.DecompiledClassSource; +import cuchaz.enigma.source.RenamableTokenType; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; -import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.Result; +import de.sciss.syntaxpane.DefaultSyntaxKit; -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; +public class PanelEditor { -public class PanelEditor extends JEditorPane { + private final JPanel ui = new JPanel(); + private final JEditorPane editor = new JEditorPane(); + private final JScrollPane editorScrollPane = new JScrollPane(this.editor); + private final PopupMenuBar popupMenu; + + // progress UI + private final JLabel decompilingLabel = new JLabel(I18n.translate("editor.decompiling"), JLabel.CENTER); + private final JProgressBar decompilingProgressBar = new JProgressBar(0, 100); + + // error display UI + private final JLabel errorLabel = new JLabel(I18n.translate("editor.decompile_error")); + private final JTextArea errorTextArea = new JTextArea(); + private final JScrollPane errorScrollPane = new JScrollPane(this.errorTextArea); + private final JButton retryButton = new JButton(I18n.translate("general.retry")); + + private DisplayMode mode = DisplayMode.INACTIVE; + + private final GuiController controller; + private final Gui gui; + + private EntryReference, Entry> cursorReference; private boolean mouseIsPressed = false; - public int fontSize = 12; + private boolean shouldNavigateOnClick; + + public Config.LookAndFeel editorLaf; + private int fontSize = 12; + private Map boxHighlightPainters; + + private final List listeners = new ArrayList<>(); + + private final ThemeChangeListener themeChangeListener; + + private ClassHandle classHandle; + private DecompiledClassSource source; + private boolean settingSource; public PanelEditor(Gui gui) { - this.setEditable(false); - this.setSelectionColor(new Color(31, 46, 90)); - this.setCaret(new BrowserCaret()); - this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); - this.addCaretListener(event -> gui.onCaretMove(event.getDot(), mouseIsPressed)); - final PanelEditor self = this; - this.addMouseListener(new MouseAdapter() { + this.gui = gui; + this.controller = gui.getController(); + + this.editor.setEditable(false); + this.editor.setSelectionColor(new Color(31, 46, 90)); + this.editor.setCaret(new BrowserCaret()); + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); + this.editor.addCaretListener(event -> onCaretMove(event.getDot(), this.mouseIsPressed)); + this.editor.setCaretColor(new Color(Config.getInstance().caretColor)); + 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, gui); + this.editor.setComponentPopupMenu(this.popupMenu); + + this.decompilingLabel.setFont(ScaleUtil.getFont(this.decompilingLabel.getFont().getFontName(), Font.BOLD, 26)); + this.decompilingProgressBar.setIndeterminate(true); + this.errorTextArea.setEditable(false); + this.errorTextArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 10)); + + this.boxHighlightPainters = Themes.getBoxHighlightPainters(); + + this.editor.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) { - mouseIsPressed = true; + PanelEditor.this.mouseIsPressed = true; } @Override public void mouseReleased(MouseEvent e) { switch (e.getButton()) { case MouseEvent.BUTTON3: // Right click - self.setCaretPosition(self.viewToModel(e.getPoint())); + PanelEditor.this.editor.setCaretPosition(PanelEditor.this.editor.viewToModel(e.getPoint())); break; case 4: // Back navigation @@ -48,57 +129,59 @@ public class PanelEditor extends JEditorPane { gui.getController().openNextReference(); break; } - mouseIsPressed = false; + PanelEditor.this.mouseIsPressed = false; } }); - this.addKeyListener(new KeyAdapter() { + this.editor.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { if (event.isControlDown()) { - gui.setShouldNavigateOnClick(false); + PanelEditor.this.shouldNavigateOnClick = false; switch (event.getKeyCode()) { case KeyEvent.VK_I: - gui.popupMenu.showInheritanceMenu.doClick(); + PanelEditor.this.popupMenu.showInheritanceMenu.doClick(); break; case KeyEvent.VK_M: - gui.popupMenu.showImplementationsMenu.doClick(); + PanelEditor.this.popupMenu.showImplementationsMenu.doClick(); break; case KeyEvent.VK_N: - gui.popupMenu.openEntryMenu.doClick(); + PanelEditor.this.popupMenu.openEntryMenu.doClick(); break; case KeyEvent.VK_P: - gui.popupMenu.openPreviousMenu.doClick(); + PanelEditor.this.popupMenu.openPreviousMenu.doClick(); break; case KeyEvent.VK_E: - gui.popupMenu.openNextMenu.doClick(); + PanelEditor.this.popupMenu.openNextMenu.doClick(); break; case KeyEvent.VK_C: if (event.isShiftDown()) { - gui.popupMenu.showCallsSpecificMenu.doClick(); + PanelEditor.this.popupMenu.showCallsSpecificMenu.doClick(); } else { - gui.popupMenu.showCallsMenu.doClick(); + PanelEditor.this.popupMenu.showCallsMenu.doClick(); } break; case KeyEvent.VK_O: - gui.popupMenu.toggleMappingMenu.doClick(); + PanelEditor.this.popupMenu.toggleMappingMenu.doClick(); break; case KeyEvent.VK_R: - gui.popupMenu.renameMenu.doClick(); + PanelEditor.this.popupMenu.renameMenu.doClick(); break; case KeyEvent.VK_D: - gui.popupMenu.editJavadocMenu.doClick(); + PanelEditor.this.popupMenu.editJavadocMenu.doClick(); break; case KeyEvent.VK_F5: - gui.getController().refreshCurrentClass(); + if (PanelEditor.this.classHandle != null) { + PanelEditor.this.classHandle.invalidateMapped(); + } break; case KeyEvent.VK_F: @@ -108,15 +191,15 @@ public class PanelEditor extends JEditorPane { case KeyEvent.VK_ADD: case KeyEvent.VK_EQUALS: case KeyEvent.VK_PLUS: - self.offsetEditorZoom(2); + offsetEditorZoom(2); break; case KeyEvent.VK_SUBTRACT: case KeyEvent.VK_MINUS: - self.offsetEditorZoom(-2); + offsetEditorZoom(-2); break; default: - gui.setShouldNavigateOnClick(true); // CTRL + PanelEditor.this.shouldNavigateOnClick = true; // CTRL break; } } @@ -124,11 +207,11 @@ public class PanelEditor extends JEditorPane { @Override public void keyTyped(KeyEvent event) { - if (!gui.popupMenu.renameMenu.isEnabled()) return; + if (!PanelEditor.this.popupMenu.renameMenu.isEnabled()) return; if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) { EnigmaProject project = gui.getController().project; - EntryReference, Entry> reference = project.getMapper().deobfuscate(gui.cursorReference); + EntryReference, Entry> reference = project.getMapper().deobfuscate(PanelEditor.this.cursorReference); Entry entry = reference.getNameableEntry(); String name = String.valueOf(event.getKeyChar()); @@ -139,33 +222,429 @@ public class PanelEditor extends JEditorPane { } } - gui.popupMenu.renameMenu.doClick(); - gui.renameTextField.setText(name); + gui.startRename(PanelEditor.this, name); } } @Override public void keyReleased(KeyEvent event) { - gui.setShouldNavigateOnClick(event.isControlDown()); + PanelEditor.this.shouldNavigateOnClick = event.isControlDown(); + } + }); + + this.retryButton.addActionListener(_e -> redecompileClass()); + + this.themeChangeListener = (laf, boxHighlightPainters) -> { + if ((this.editorLaf == null || this.editorLaf != laf)) { + this.editor.updateUI(); + this.editor.setBackground(new Color(Config.getInstance().editorBackground)); + if (this.editorLaf != null) { + this.classHandle.invalidateMapped(); + } + + this.editorLaf = laf; + } + this.boxHighlightPainters = boxHighlightPainters; + }; + + this.ui.putClientProperty(PanelEditor.class, this); + } + + @Nullable + public static PanelEditor byUi(Component ui) { + if (ui instanceof JComponent) { + Object prop = ((JComponent) ui).getClientProperty(PanelEditor.class); + if (prop instanceof PanelEditor) { + return (PanelEditor) prop; + } + } + return null; + } + + public void setClassHandle(ClassHandle handle) { + ClassEntry old = null; + if (this.classHandle != null) { + old = this.classHandle.getRef(); + this.classHandle.close(); + } + setClassHandle0(old, handle); + } + + private void setClassHandle0(ClassEntry old, ClassHandle handle) { + this.setDisplayMode(DisplayMode.IN_PROGRESS); + setCursorReference(null); + + handle.addListener(new ClassHandleListener() { + @Override + public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { + SwingUtilities.invokeLater(() -> { + PanelEditor.this.listeners.forEach(l -> l.onTitleChanged(PanelEditor.this, getFileName())); + }); + } + + @Override + public void onMappedSourceChanged(ClassHandle h, Result res) { + handleDecompilerResult(res); + } + + @Override + public void onInvalidate(ClassHandle h, InvalidationType t) { + SwingUtilities.invokeLater(() -> { + if (t == InvalidationType.FULL) { + PanelEditor.this.setDisplayMode(DisplayMode.IN_PROGRESS); + } + }); + } + + @Override + public void onDeleted(ClassHandle h) { + SwingUtilities.invokeLater(() -> PanelEditor.this.gui.closeEditor(PanelEditor.this)); } }); + + handle.getSource().thenAcceptAsync(this::handleDecompilerResult, SwingUtilities::invokeLater); + + this.classHandle = handle; + this.listeners.forEach(l -> l.onClassHandleChanged(this, old, handle)); + } + + public void setup() { + Themes.addListener(this.themeChangeListener); + } + + public void destroy() { + Themes.removeListener(this.themeChangeListener); + this.classHandle.close(); + } + + private void redecompileClass() { + if (this.classHandle != null) { + this.classHandle.invalidate(); + } + } + + private void handleDecompilerResult(Result res) { + SwingUtilities.invokeLater(() -> { + if (res.isOk()) { + this.setSource(res.unwrap()); + } else { + this.displayError(res.unwrapErr()); + } + }); + } + + public void displayError(ClassHandleError t) { + this.setDisplayMode(DisplayMode.ERRORED); + this.errorTextArea.setText(t.getStackTrace()); + this.errorTextArea.setCaretPosition(0); + } + + public void setDisplayMode(DisplayMode mode) { + if (this.mode == mode) return; + this.ui.removeAll(); + switch (mode) { + case INACTIVE: + break; + case IN_PROGRESS: { + // make progress bar start from the left every time + this.decompilingProgressBar.setIndeterminate(false); + this.decompilingProgressBar.setIndeterminate(true); + + this.ui.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.insets = ScaleUtil.getInsets(2, 2, 2, 2); + c.anchor = GridBagConstraints.SOUTH; + this.ui.add(this.decompilingLabel, c); + c.gridy = 1; + c.anchor = GridBagConstraints.NORTH; + this.ui.add(this.decompilingProgressBar, c); + break; + } + case SUCCESS: { + this.ui.setLayout(new GridLayout(1, 1, 0, 0)); + this.ui.add(this.editorScrollPane); + break; + } + case ERRORED: { + this.ui.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.insets = ScaleUtil.getInsets(2, 2, 2, 2); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1.0; + c.anchor = GridBagConstraints.WEST; + this.ui.add(this.errorLabel, c); + c.gridy = 1; + c.fill = GridBagConstraints.HORIZONTAL; + this.ui.add(new JSeparator(JSeparator.HORIZONTAL), c); + c.gridy = 2; + c.fill = GridBagConstraints.BOTH; + c.weighty = 1.0; + this.ui.add(this.errorScrollPane, c); + c.gridy = 3; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.EAST; + c.weightx = 0.0; + c.weighty = 0.0; + this.ui.add(this.retryButton, c); + break; + } + } + this.ui.validate(); + this.ui.repaint(); + this.mode = mode; } public void offsetEditorZoom(int zoomAmount) { int newResult = this.fontSize + zoomAmount; if (newResult > 8 && newResult < 72) { this.fontSize = newResult; - this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); } } public void resetEditorZoom() { this.fontSize = 12; - this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); + this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize)); } - @Override - public Color getCaretColor() { - return new Color(Config.getInstance().caretColor); + public void onCaretMove(int pos, boolean fromClick) { + if (this.controller.project == null) return; + + EntryRemapper mapper = this.controller.project.getMapper(); + Token token = getToken(pos); + + if (this.settingSource) { + EntryReference, Entry> ref = getCursorReference(); + EntryReference, Entry> refAtCursor = getReference(token); + if (this.editor.getDocument().getLength() != 0 && ref != null && !ref.equals(refAtCursor)) { + showReference0(ref); + } + return; + } else { + setCursorReference(getReference(token)); + } + + Entry referenceEntry = this.cursorReference != null ? this.cursorReference.entry : null; + + if (referenceEntry != null && this.shouldNavigateOnClick && fromClick) { + this.shouldNavigateOnClick = false; + Entry navigationEntry = referenceEntry; + if (this.cursorReference.context == null) { + EntryResolver resolver = mapper.getObfResolver(); + navigationEntry = resolver.resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT); + } + this.controller.navigateTo(navigationEntry); + } + } + + private void setCursorReference(EntryReference, Entry> ref) { + this.cursorReference = ref; + + Entry referenceEntry = ref == null ? null : ref.entry; + + boolean isClassEntry = referenceEntry instanceof ClassEntry; + boolean isFieldEntry = referenceEntry instanceof FieldEntry; + boolean isMethodEntry = referenceEntry instanceof MethodEntry && !((MethodEntry) referenceEntry).isConstructor(); + boolean isConstructorEntry = referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor(); + boolean isRenamable = ref != null && this.controller.project.isRenamable(ref); + + 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 (referenceEntry != null && referenceEntry.equals(this.controller.project.getMapper().deobfuscate(referenceEntry))) { + this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.reset_obfuscated")); + } else { + this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.mark_deobfuscated")); + } + + this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref)); + } + + public Token getToken(int pos) { + if (this.source == null) { + return null; + } + return this.source.getIndex().getReferenceToken(pos); + } + + @Nullable + public EntryReference, Entry> getReference(Token token) { + if (this.source == null) { + return null; + } + return this.source.getIndex().getReference(token); } + + public void setSource(DecompiledClassSource source) { + this.setDisplayMode(DisplayMode.SUCCESS); + if (source == null) return; + try { + this.settingSource = true; + this.source = source; + this.editor.getHighlighter().removeAllHighlights(); + this.editor.setText(source.toString()); + setHighlightedTokens(source.getHighlightedTokens()); + } finally { + this.settingSource = false; + } + showReference0(getCursorReference()); + } + + public void setHighlightedTokens(Map> tokens) { + // remove any old highlighters + this.editor.getHighlighter().removeAllHighlights(); + + if (this.boxHighlightPainters != null) { + for (RenamableTokenType type : tokens.keySet()) { + BoxHighlightPainter painter = this.boxHighlightPainters.get(type); + if (painter != null) { + setHighlightedTokens(tokens.get(type), painter); + } + } + } + + this.editor.validate(); + this.editor.repaint(); + } + + 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); + } + } + } + + public EntryReference, Entry> getCursorReference() { + return this.cursorReference; + } + + public void showReference(EntryReference, Entry> reference) { + setCursorReference(reference); + showReference0(reference); + } + + /** + * Navigates to the reference without modifying history. Assumes the class is loaded. + * + * @param reference + */ + private void showReference0(EntryReference, Entry> reference) { + if (this.source == null) return; + if (reference == null) return; + + Collection tokens = this.controller.getTokensForReference(this.source, reference); + if (tokens.isEmpty()) { + // DEBUG + System.err.println(String.format("WARNING: no tokens found for %s in %s", reference, this.classHandle.getRef())); + } else { + this.gui.showTokens(this, tokens); + } + } + + public void navigateToToken(Token token) { + if (token == null) { + throw new IllegalArgumentException("Token cannot be null!"); + } + navigateToToken(token, SelectionHighlightPainter.INSTANCE); + } + + private void navigateToToken(Token token, HighlightPainter highlightPainter) { + // set the caret position to the token + Document document = this.editor.getDocument(); + int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength()); + + this.editor.setCaretPosition(clampedPosition); + this.editor.grabFocus(); + + try { + // make sure the token is visible in the scroll window + Rectangle start = this.editor.modelToView(token.start); + Rectangle end = this.editor.modelToView(token.end); + Rectangle show = start.union(end); + show.grow(start.width * 10, start.height * 6); + SwingUtilities.invokeLater(() -> this.editor.scrollRectToVisible(show)); + } catch (BadLocationException ex) { + if (!this.settingSource) { + throw new RuntimeException(ex); + } else { + return; + } + } + + // highlight the token momentarily + Timer timer = new Timer(200, new ActionListener() { + private int counter = 0; + private Object highlight = null; + + @Override + public void actionPerformed(ActionEvent event) { + if (this.counter % 2 == 0) { + try { + this.highlight = PanelEditor.this.editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); + } catch (BadLocationException ex) { + // don't care + } + } else if (this.highlight != null) { + PanelEditor.this.editor.getHighlighter().removeHighlight(this.highlight); + } + + if (this.counter++ > 6) { + Timer timer = (Timer) event.getSource(); + timer.stop(); + } + } + }); + timer.start(); + } + + public void addListener(EditorActionListener listener) { + this.listeners.add(listener); + } + + public void removeListener(EditorActionListener listener) { + this.listeners.remove(listener); + } + + public JPanel getUi() { + return this.ui; + } + + public JEditorPane getEditor() { + return this.editor; + } + + public DecompiledClassSource getSource() { + return this.source; + } + + public ClassHandle getClassHandle() { + return this.classHandle; + } + + public String getFileName() { + ClassEntry classEntry = this.classHandle.getDeobfRef() != null ? this.classHandle.getDeobfRef() : this.classHandle.getRef(); + return classEntry.getSimpleName(); + } + + private enum DisplayMode { + INACTIVE, + IN_PROGRESS, + SUCCESS, + ERRORED, + } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java index 8c19efb..bfba845 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java @@ -1,32 +1,255 @@ package cuchaz.enigma.gui.panels; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.util.function.Consumer; + +import javax.swing.BorderFactory; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.gui.Gui; +import cuchaz.enigma.gui.elements.ConvertingTextField; +import cuchaz.enigma.gui.events.ConvertingTextFieldListener; import cuchaz.enigma.gui.util.GuiUtil; -import cuchaz.enigma.utils.I18n; import cuchaz.enigma.gui.util.ScaleUtil; +import cuchaz.enigma.network.packet.RenameC2SPacket; +import cuchaz.enigma.translation.mapping.AccessModifier; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.*; +import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.validation.ValidationContext; -import javax.swing.*; -import java.awt.*; - -public class PanelIdentifier extends JPanel { +public class PanelIdentifier { private final Gui gui; + private final JPanel ui; + + private Entry entry; + private Entry deobfEntry; + + private ConvertingTextField nameField; + + private final ValidationContext vc = new ValidationContext(); + public PanelIdentifier(Gui gui) { this.gui = gui; - this.setLayout(new GridLayout(4, 1, 0, 0)); - this.setPreferredSize(ScaleUtil.getDimension(0, 100)); - this.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier"))); + this.ui = new JPanel(); + this.ui.setLayout(new GridBagLayout()); + this.ui.setPreferredSize(ScaleUtil.getDimension(0, 120)); + this.ui.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier"))); + this.ui.setEnabled(false); + } + + public void setReference(Entry entry) { + this.entry = entry; + refreshReference(); } - public void clearReference() { - this.removeAll(); - JLabel label = new JLabel(I18n.translate("info_panel.identifier.none")); - GuiUtil.unboldLabel(label); - label.setHorizontalAlignment(JLabel.CENTER); - this.add(label); + public boolean startRenaming() { + if (this.nameField == null) return false; - gui.redraw(); + this.nameField.startEditing(); + + return true; + } + + public boolean startRenaming(String text) { + if (this.nameField == null) return false; + + this.nameField.startEditing(); + this.nameField.setEditText(text); + + return true; + } + + private void onModifierChanged(AccessModifier modifier) { + gui.validateImmediateAction(vc -> this.gui.getController().onModifierChanged(vc, entry, modifier)); } + + public void refreshReference() { + this.deobfEntry = entry == null ? null : gui.getController().project.getMapper().deobfuscate(this.entry); + + this.nameField = null; + + TableHelper th = new TableHelper(this.ui, this.entry, this.gui.getController().project); + th.begin(); + if (this.entry == null) { + this.ui.setEnabled(false); + } else { + this.ui.setEnabled(true); + + if (deobfEntry instanceof ClassEntry) { + ClassEntry ce = (ClassEntry) deobfEntry; + this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.class"), ce.getFullName()); + th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged); + } else if (deobfEntry instanceof FieldEntry) { + FieldEntry fe = (FieldEntry) deobfEntry; + this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.field"), fe.getName()); + th.addStringRow(I18n.translate("info_panel.identifier.class"), fe.getParent().getFullName()); + th.addStringRow(I18n.translate("info_panel.identifier.type_descriptor"), fe.getDesc().toString()); + th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged); + } else if (deobfEntry instanceof MethodEntry) { + MethodEntry me = (MethodEntry) deobfEntry; + if (me.isConstructor()) { + th.addStringRow(I18n.translate("info_panel.identifier.constructor"), me.getParent().getFullName()); + } else { + this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.method"), me.getName()); + th.addStringRow(I18n.translate("info_panel.identifier.class"), me.getParent().getFullName()); + } + th.addStringRow(I18n.translate("info_panel.identifier.method_descriptor"), me.getDesc().toString()); + th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged); + } else if (deobfEntry instanceof LocalVariableEntry) { + LocalVariableEntry lve = (LocalVariableEntry) deobfEntry; + this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.variable"), lve.getName()); + th.addStringRow(I18n.translate("info_panel.identifier.class"), lve.getContainingClass().getFullName()); + th.addStringRow(I18n.translate("info_panel.identifier.method"), lve.getParent().getName()); + th.addStringRow(I18n.translate("info_panel.identifier.index"), Integer.toString(lve.getIndex())); + } else { + throw new IllegalStateException("unreachable"); + } + } + th.end(); + + if (this.nameField != null) { + this.nameField.addListener(new ConvertingTextFieldListener() { + @Override + public void onStartEditing(ConvertingTextField field) { + int i = field.getText().lastIndexOf('/'); + if (i != -1) { + field.selectSubstring(i + 1); + } + } + + @Override + public boolean tryStopEditing(ConvertingTextField field, boolean abort) { + if (abort) return true; + vc.reset(); + vc.setActiveElement(field); + validateRename(field.getText()); + return vc.canProceed(); + } + + @Override + public void onStopEditing(ConvertingTextField field, boolean abort) { + if (abort) return; + vc.reset(); + vc.setActiveElement(field); + doRename(field.getText()); + } + }); + } + + this.ui.validate(); + this.ui.repaint(); + } + + private void validateRename(String newName) { + gui.getController().rename(vc, new EntryReference<>(entry, deobfEntry.getName()), newName, true, true); + } + + private void doRename(String newName) { + gui.getController().rename(vc, new EntryReference<>(entry, deobfEntry.getName()), newName, true); + if (!vc.canProceed()) return; + gui.getController().sendPacket(new RenameC2SPacket(entry, newName, true)); + } + + public JPanel getUi() { + return ui; + } + + private static final class TableHelper { + + private final Container c; + private final Entry e; + private final EnigmaProject project; + private final GridBagConstraints col1; + private final GridBagConstraints col2; + + public TableHelper(Container c, Entry e, EnigmaProject project) { + this.c = c; + this.e = e; + this.project = project; + this.col1 = new GridBagConstraints(); + this.col2 = new GridBagConstraints(); + Insets insets = ScaleUtil.getInsets(2, 2, 2, 2); + this.col1.gridx = 0; + this.col1.gridy = 0; + this.col1.insets = insets; + this.col1.anchor = GridBagConstraints.WEST; + this.col2.gridx = 1; + this.col2.gridy = 0; + this.col2.weightx = 1.0; + this.col2.fill = GridBagConstraints.HORIZONTAL; + this.col2.insets = insets; + this.col2.anchor = GridBagConstraints.WEST; + } + + public void begin() { + c.removeAll(); + c.setLayout(new GridBagLayout()); + } + + public void addRow(Component c1, Component c2) { + c.add(c1, col1); + c.add(c2, col2); + + col1.gridy += 1; + col2.gridy += 1; + } + + public ConvertingTextField addCovertTextField(String c1, String c2) { + ConvertingTextField textField = new ConvertingTextField(c2); + addRow(new JLabel(c1), textField.getUi()); + return textField; + } + + public ConvertingTextField addRenameTextField(String c1, String c2) { + if (project.isRenamable(e)) { + return addCovertTextField(c1, c2); + } else { + addStringRow(c1, c2); + return null; + } + } + + public void addStringRow(String c1, String c2) { + addRow(new JLabel(c1), GuiUtil.unboldLabel(new JLabel(c2))); + } + + public JComboBox addModifierRow(String c1, Consumer changeListener) { + if (!project.isRenamable(e)) + return null; + JComboBox combo = new JComboBox<>(AccessModifier.values()); + EntryMapping mapping = project.getMapper().getDeobfMapping(e); + if (mapping != null) { + combo.setSelectedIndex(mapping.getAccessModifier().ordinal()); + } else { + combo.setSelectedIndex(AccessModifier.UNCHANGED.ordinal()); + } + combo.addItemListener(event -> { + if (event.getStateChange() == ItemEvent.SELECTED) { + AccessModifier modifier = (AccessModifier) event.getItem(); + changeListener.accept(modifier); + } + }); + + addRow(new JLabel(c1), combo); + + return combo; + } + + public void end() { + // Add an empty panel with y-weight=1 so that all the other elements get placed at the top edge + this.col1.weighty = 1.0; + c.add(new JPanel(), col1); + } + + } + } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java index 70172fe..3b8df61 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java @@ -40,17 +40,4 @@ public class GuiUtil { manager.setInitialDelay(oldDelay); } - public static Rectangle safeModelToView(JTextComponent component, int modelPos) { - if (modelPos < 0) { - modelPos = 0; - } else if (modelPos >= component.getText().length()) { - modelPos = component.getText().length(); - } - try { - return component.modelToView(modelPos); - } catch (BadLocationException e) { - throw new RuntimeException(e); - } - } - } diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java index e7ee565..985615a 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java @@ -2,6 +2,7 @@ package cuchaz.enigma.gui.util; import java.awt.Dimension; import java.awt.Font; +import java.awt.Insets; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; @@ -51,8 +52,12 @@ public class ScaleUtil { return new Dimension(scale(width), scale(height)); } - public static Font getFont(String fontName, int plain, int fontSize) { - return scaleFont(new Font(fontName, plain, fontSize)); + public static Insets getInsets(int top, int left, int bottom, int right) { + return new Insets(scale(top), scale(left), scale(bottom), scale(right)); + } + + public static Font getFont(String fontName, int style, int fontSize) { + return scaleFont(new Font(fontName, style, fontSize)); } public static Font scaleFont(Font font) { @@ -106,5 +111,4 @@ public class ScaleUtil { } return new BasicTweaker(dpiScaling); } - } -- cgit v1.2.3