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 --- .../cuchaz/enigma/network/ClientPacketHandler.java | 9 +- .../enigma/network/packet/ChangeDocsC2SPacket.java | 18 +- .../enigma/network/packet/ChangeDocsS2CPacket.java | 17 +- .../network/packet/MarkDeobfuscatedC2SPacket.java | 22 +- .../network/packet/MarkDeobfuscatedS2CPacket.java | 17 +- .../network/packet/RemoveMappingC2SPacket.java | 21 +- .../network/packet/RemoveMappingS2CPacket.java | 17 +- .../enigma/network/packet/RenameC2SPacket.java | 21 +- .../enigma/network/packet/RenameS2CPacket.java | 17 +- .../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 +- .../cuchaz/enigma/analysis/EntryReference.java | 12 +- .../cuchaz/enigma/classhandle/ClassHandle.java | 108 ++++ .../enigma/classhandle/ClassHandleError.java | 35 ++ .../enigma/classhandle/ClassHandleProvider.java | 445 +++++++++++++++ .../cuchaz/enigma/events/ClassHandleListener.java | 36 ++ .../enigma/source/DecompiledClassSource.java | 157 ++++++ .../cuchaz/enigma/source/RenamableTokenType.java | 7 + .../enigma/translation/LocalNameGenerator.java | 8 +- .../enigma/translation/mapping/EntryRemapper.java | 28 +- .../translation/mapping/IdentifierValidation.java | 79 +++ .../translation/mapping/IllegalNameException.java | 39 -- .../translation/mapping/MappingValidator.java | 24 +- .../enigma/translation/mapping/NameValidator.java | 50 -- .../representation/entry/ClassEntry.java | 19 +- .../translation/representation/entry/Entry.java | 15 +- enigma/src/main/java/cuchaz/enigma/utils/I18n.java | 55 +- .../src/main/java/cuchaz/enigma/utils/Result.java | 108 ++++ .../src/main/java/cuchaz/enigma/utils/Utils.java | 21 + .../cuchaz/enigma/utils/validation/Message.java | 48 ++ .../utils/validation/ParameterizedMessage.java | 45 ++ .../enigma/utils/validation/PrintValidatable.java | 37 ++ .../utils/validation/StandardValidation.java | 34 ++ .../enigma/utils/validation/Validatable.java | 9 + .../enigma/utils/validation/ValidationContext.java | 78 +++ enigma/src/main/resources/lang/de_de.json | 26 + enigma/src/main/resources/lang/en_us.json | 27 +- 64 files changed, 3692 insertions(+), 1255 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 create mode 100644 enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java create mode 100644 enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/Result.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java create mode 100644 enigma/src/main/resources/lang/de_de.json diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java b/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java index 720744bf..1b0191be 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java @@ -5,19 +5,20 @@ import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.translation.mapping.tree.EntryTree; import cuchaz.enigma.network.packet.Packet; import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.ValidationContext; import java.util.List; public interface ClientPacketHandler { void openMappings(EntryTree mappings); - void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean jumpToReference); + void rename(ValidationContext vc, EntryReference, Entry> reference, String newName, boolean refreshClassTree); - void removeMapping(EntryReference, Entry> reference, boolean jumpToReference); + void removeMapping(ValidationContext vc, EntryReference, Entry> reference); - void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference); + void changeDocs(ValidationContext vc, EntryReference, Entry> reference, String updatedDocs); - void markAsDeobfuscated(EntryReference, Entry> reference, boolean jumpToReference); + void markAsDeobfuscated(ValidationContext vc, EntryReference, Entry> reference); void disconnectIfConnected(String reason); diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java index 1b52cf14..23ffe993 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java @@ -1,15 +1,17 @@ package cuchaz.enigma.network.packet; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.network.EnigmaServer; import cuchaz.enigma.network.Message; import cuchaz.enigma.network.ServerPacketHandler; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.utils.Utils; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; public class ChangeDocsC2SPacket implements Packet { private Entry entry; @@ -37,6 +39,9 @@ public class ChangeDocsC2SPacket implements Packet { @Override public void handle(ServerPacketHandler handler) { + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry); boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); @@ -49,11 +54,12 @@ public class ChangeDocsC2SPacket implements Packet { if (mapping == null) { mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()); } - handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs)); + handler.getServer().getMappings().mapFromObf(vc, entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs)); + + if (!vc.canProceed()) return; int syncId = handler.getServer().lockEntry(handler.getClient(), entry); handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs)); handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry)); } - } diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java index 12a30253..78fa4fa9 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java @@ -1,13 +1,15 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.network.ClientPacketHandler; -import cuchaz.enigma.translation.representation.entry.Entry; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class ChangeDocsS2CPacket implements Packet { private int syncId; private Entry entry; @@ -38,7 +40,12 @@ public class ChangeDocsS2CPacket implements Packet { @Override public void handle(ClientPacketHandler controller) { - controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false); + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + + controller.changeDocs(vc, new EntryReference<>(entry, entry.getName()), newDocs); + + if (!vc.canProceed()) return; controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); } } diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java index a41c620f..732c7448 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java @@ -1,14 +1,16 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.network.ServerPacketHandler; -import cuchaz.enigma.translation.mapping.EntryMapping; -import cuchaz.enigma.translation.representation.entry.Entry; -import cuchaz.enigma.network.Message; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.network.Message; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class MarkDeobfuscatedC2SPacket implements Packet { private Entry entry; @@ -31,18 +33,24 @@ public class MarkDeobfuscatedC2SPacket implements Packet { @Override public void handle(ServerPacketHandler handler) { + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + if (!valid) { handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); return; } - handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName())); + handler.getServer().getMappings().mapFromObf(vc, entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName())); + + if (!vc.canProceed()) return; + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated"); int syncId = handler.getServer().lockEntry(handler.getClient(), entry); handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry)); handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry)); - } } diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java index 7504430d..969d13c5 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java @@ -1,13 +1,15 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.network.ClientPacketHandler; -import cuchaz.enigma.translation.representation.entry.Entry; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class MarkDeobfuscatedS2CPacket implements Packet { private int syncId; private Entry entry; @@ -34,7 +36,12 @@ public class MarkDeobfuscatedS2CPacket implements Packet { @Override public void handle(ClientPacketHandler controller) { - controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false); + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + + controller.markAsDeobfuscated(vc, new EntryReference<>(entry, entry.getName())); + + if (!vc.canProceed()) return; controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); } } diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java index 3f852285..298e674f 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java @@ -1,14 +1,15 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.network.ServerPacketHandler; -import cuchaz.enigma.translation.mapping.IllegalNameException; -import cuchaz.enigma.translation.representation.entry.Entry; -import cuchaz.enigma.network.Message; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.network.Message; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class RemoveMappingC2SPacket implements Packet { private Entry entry; @@ -31,14 +32,14 @@ public class RemoveMappingC2SPacket implements Packet { @Override public void handle(ServerPacketHandler handler) { + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); if (valid) { - try { - handler.getServer().getMappings().removeByObf(entry); - } catch (IllegalNameException e) { - valid = false; - } + handler.getServer().getMappings().removeByObf(vc, entry); + valid = vc.canProceed(); } if (!valid) { diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java index 70d803c1..e336c7b2 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java @@ -1,13 +1,15 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.network.ClientPacketHandler; -import cuchaz.enigma.translation.representation.entry.Entry; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class RemoveMappingS2CPacket implements Packet { private int syncId; private Entry entry; @@ -34,7 +36,12 @@ public class RemoveMappingS2CPacket implements Packet { @Override public void handle(ClientPacketHandler controller) { - controller.removeMapping(new EntryReference<>(entry, entry.getName()), false); + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + + controller.removeMapping(vc, new EntryReference<>(entry, entry.getName())); + + if (!vc.canProceed()) return; controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); } } diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java index e3e7e379..6a7d2fd1 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java @@ -1,14 +1,15 @@ package cuchaz.enigma.network.packet; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + import cuchaz.enigma.network.ServerPacketHandler; -import cuchaz.enigma.translation.mapping.IllegalNameException; import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.network.Message; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; public class RenameC2SPacket implements Packet { private Entry entry; @@ -40,14 +41,14 @@ public class RenameC2SPacket implements Packet { @Override public void handle(ServerPacketHandler handler) { + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); if (valid) { - try { - handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName)); - } catch (IllegalNameException e) { - valid = false; - } + handler.getServer().getMappings().mapFromObf(vc, entry, new EntryMapping(newName)); + valid = vc.canProceed(); } if (!valid) { diff --git a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java index 787e89e6..fdf06540 100644 --- a/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java +++ b/enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java @@ -1,13 +1,15 @@ package cuchaz.enigma.network.packet; -import cuchaz.enigma.analysis.EntryReference; -import cuchaz.enigma.network.ClientPacketHandler; -import cuchaz.enigma.translation.representation.entry.Entry; - import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.network.ClientPacketHandler; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.validation.PrintValidatable; +import cuchaz.enigma.utils.validation.ValidationContext; + public class RenameS2CPacket implements Packet { private int syncId; private Entry entry; @@ -42,7 +44,12 @@ public class RenameS2CPacket implements Packet { @Override public void handle(ClientPacketHandler controller) { - controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false); + ValidationContext vc = new ValidationContext(); + vc.setActiveElement(PrintValidatable.INSTANCE); + + controller.rename(vc, new EntryReference<>(entry, entry.getName()), newName, refreshClassTree); + + if (!vc.canProceed()) return; controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); } } 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 3d0e04c9..488d04ed 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 356656b9..00000000 --- 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 aca5d724..00000000 --- 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 2f08a269..d4a71f59 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 c67fc5b3..0a5d3f7d 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 94979e77..10f36b85 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 87cb83b2..00000000 --- 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 10c418c2..4ef04428 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 035b2381..fd40cb74 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 d81460ab..9fbe65af 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 00000000..6f289496 --- /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 00000000..e92e6773 --- /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 00000000..533d1b30 --- /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 948798ad..9b06d26f 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 b92041c3..2310cf32 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 00000000..02e1bc39 --- /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 00000000..7d1f8665 --- /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 00000000..c114dc17 --- /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 00000000..5df63486 --- /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 00000000..6e17fec1 --- /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 00000000..88807314 --- /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 00000000..d4962f7b --- /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 2e4e462a..c899e689 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 ae23f324..00000000 --- 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 00000000..fe5c8578 --- /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 346d6655..dd9971a7 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 8c19efb5..bfba8452 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 70172fe7..3b8df611 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 e7ee5657..985615a4 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); } - } diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java index 320f9450..9d6bc4e8 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java @@ -11,19 +11,19 @@ package cuchaz.enigma.analysis; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + import cuchaz.enigma.translation.Translatable; import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMap; import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.translation.mapping.EntryResolver; -import cuchaz.enigma.translation.mapping.EntryMap; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.translation.representation.entry.MethodEntry; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - public class EntryReference, C extends Entry> implements Translatable { private static final List CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static"); @@ -100,6 +100,8 @@ public class EntryReference, C extends Entry> implements T } public boolean equals(EntryReference other) { + if (other == null) return false; + // check entry first boolean isEntrySame = entry.equals(other.entry); if (!isEntrySame) { diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java new file mode 100644 index 00000000..326197db --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java @@ -0,0 +1,108 @@ +package cuchaz.enigma.classhandle; + +import java.util.concurrent.CompletableFuture; + +import javax.annotation.Nullable; + +import cuchaz.enigma.events.ClassHandleListener; +import cuchaz.enigma.source.DecompiledClassSource; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.Result; + +/** + * A handle to a class file. Can be treated similarly to a handle to a file. + * This type allows for accessing decompiled classes and being notified when + * mappings for the class it belongs to changes. + * + * @see ClassHandleProvider + */ +public interface ClassHandle extends AutoCloseable { + + /** + * Gets the reference to this class. This is always obfuscated, for example + * {@code net/minecraft/class_1000}. + * + * @return the obfuscated class reference + * @throws IllegalStateException if the class handle is closed + */ + ClassEntry getRef(); + + /** + * Gets the deobfuscated reference to this class, if any. + * + * @return the deobfuscated reference, or {@code null} if the class is not + * mapped + * @throws IllegalStateException if the class handle is closed + */ + @Nullable + ClassEntry getDeobfRef(); + + /** + * Gets the class source, or the error generated while decompiling the + * class, asynchronously. If this class has already finished decompiling, + * this will return an already completed future. + * + * @return the class source + * @throws IllegalStateException if the class handle is closed + */ + CompletableFuture> getSource(); + + /** + * Gets the class source without any decoration, or the error generated + * while decompiling the class, asynchronously. This is the raw source from + * the decompiler and will not be deobfuscated, and does not contain any + * Javadoc comments added via mappings. If this class has already finished + * decompiling, this will return an already completed future. + * + * @return the uncommented class source + * @throws IllegalStateException if the class handle is closed + * @see ClassHandle#getSource() + */ + CompletableFuture> getUncommentedSource(); + + void invalidate(); + + void invalidateMapped(); + + void invalidateJavadoc(); + + /** + * Adds a listener for this class handle. + * + * @param listener the listener to add + * @see ClassHandleListener + */ + void addListener(ClassHandleListener listener); + + /** + * Removes a previously added listener (with + * {@link ClassHandle#addListener(ClassHandleListener)}) from this class + * handle. + * + * @param listener the listener to remove + */ + void removeListener(ClassHandleListener listener); + + /** + * Copies this class handle. The new class handle points to the same class, + * but is independent from this class handle in every other aspect. + * Specifically, any listeners will not be copied to the new class handle. + * + * @return a copy of this class handle + * @throws IllegalStateException if the class handle is closed + */ + ClassHandle copy(); + + /** + * {@inheritDoc} + * + *

Specifically, for class handles, this means that most methods on the + * handle will throw an exception if called, that the handle will no longer + * receive any events over any added listeners, and the handle will no + * longer be able to be copied. + */ + @Override + void close(); + +} diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java new file mode 100644 index 00000000..a11b9dce --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java @@ -0,0 +1,35 @@ +package cuchaz.enigma.classhandle; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import javax.annotation.Nullable; + +public final class ClassHandleError { + + public final Type type; + public final Throwable cause; + + private ClassHandleError(Type type, Throwable cause) { + this.type = type; + this.cause = cause; + } + + @Nullable + public String getStackTrace() { + if (cause == null) return null; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(os); + cause.printStackTrace(ps); + return os.toString(); + } + + public static ClassHandleError decompile(Throwable cause) { + return new ClassHandleError(Type.DECOMPILE, cause); + } + + public enum Type { + DECOMPILE, + } + +} \ No newline at end of file diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java new file mode 100644 index 00000000..2d9b52d9 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java @@ -0,0 +1,445 @@ +package cuchaz.enigma.classhandle; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.annotation.Nullable; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.bytecode.translators.SourceFixVisitor; +import cuchaz.enigma.events.ClassHandleListener; +import cuchaz.enigma.events.ClassHandleListener.InvalidationType; +import cuchaz.enigma.source.*; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.Result; +import org.objectweb.asm.tree.ClassNode; + +import static cuchaz.enigma.utils.Utils.withLock; + +public final class ClassHandleProvider { + + private final EnigmaProject project; + + private final ExecutorService pool = Executors.newWorkStealingPool(); + private DecompilerService ds; + private Decompiler decompiler; + + private final Map handles = new HashMap<>(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ClassHandleProvider(EnigmaProject project, DecompilerService ds) { + this.project = project; + this.ds = ds; + this.decompiler = createDecompiler(); + } + + /** + * Open a class by entry. Schedules decompilation immediately if this is the + * only handle to the class. + * + * @param entry the entry of the class to open + * @return a handle to the class, {@code null} if a class by that name does + * not exist + */ + @Nullable + public ClassHandle openClass(ClassEntry entry) { + if (!project.getJarIndex().getEntryIndex().hasClass(entry)) return null; + + return withLock(lock.writeLock(), () -> { + Entry e = handles.computeIfAbsent(entry, entry1 -> new Entry(this, entry1)); + return e.createHandle(); + }); + } + + /** + * Set the decompiler service to use when decompiling classes. Invalidates + * all currently open classes. + * + *

If the current decompiler service equals the old one, no classes will + * be invalidated. + * + * @param ds the decompiler service to use + */ + public void setDecompilerService(DecompilerService ds) { + if (this.ds.equals(ds)) return; + + this.ds = ds; + this.decompiler = createDecompiler(); + withLock(lock.readLock(), () -> { + handles.values().forEach(Entry::invalidate); + }); + } + + /** + * Gets the current decompiler service in use. + * + * @return the current decompiler service + */ + public DecompilerService getDecompilerService() { + return ds; + } + + private Decompiler createDecompiler() { + return ds.create(name -> { + ClassNode node = project.getClassCache().getClassNode(name); + + if (node == null) { + return null; + } + + ClassNode fixedNode = new ClassNode(); + node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, project.getJarIndex())); + return fixedNode; + }, new SourceSettings(true, true)); + } + + /** + * Invalidates all mappings. This causes all open class handles to be + * re-remapped. + */ + public void invalidateMapped() { + withLock(lock.readLock(), () -> { + handles.values().forEach(Entry::invalidateMapped); + }); + } + + /** + * Invalidates mappings for a single class. Note that this does not + * invalidate any mappings of other classes where this class is used, so + * this should not be used to notify that the mapped name for this class has + * changed. + * + * @param entry the class entry to invalidate + */ + public void invalidateMapped(ClassEntry entry) { + withLock(lock.readLock(), () -> { + Entry e = handles.get(entry); + if (e != null) { + e.invalidateMapped(); + } + }); + } + + /** + * Invalidates javadoc for a single class. This also causes the class to be + * remapped again. + * + * @param entry the class entry to invalidate + */ + public void invalidateJavadoc(ClassEntry entry) { + withLock(lock.readLock(), () -> { + Entry e = handles.get(entry); + if (e != null) { + e.invalidateJavadoc(); + } + }); + } + + private void deleteEntry(Entry entry) { + withLock(lock.writeLock(), () -> { + handles.remove(entry.entry); + }); + } + + /** + * Destroy this class handle provider. The decompiler threads will try to + * shutdown cleanly, and then every open class handle will also be deleted. + * This causes {@link ClassHandleListener#onDeleted(ClassHandle)} to get + * called. + * + *

After this method is called, this class handle provider can no longer + * be used. + */ + public void destroy() { + pool.shutdown(); + try { + pool.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + withLock(lock.writeLock(), () -> { + handles.values().forEach(Entry::destroy); + handles.clear(); + }); + } + + private static final class Entry { + + private final ClassHandleProvider p; + private final ClassEntry entry; + private ClassEntry deobfRef; + private final List handles = new ArrayList<>(); + private Result uncommentedSource; + private Result source; + + private final List>> waitingUncommentedSources = Collections.synchronizedList(new ArrayList<>()); + private final List>> waitingSources = Collections.synchronizedList(new ArrayList<>()); + + private final AtomicInteger decompileVersion = new AtomicInteger(); + private final AtomicInteger javadocVersion = new AtomicInteger(); + private final AtomicInteger indexVersion = new AtomicInteger(); + private final AtomicInteger mappedVersion = new AtomicInteger(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private Entry(ClassHandleProvider p, ClassEntry entry) { + this.p = p; + this.entry = entry; + this.deobfRef = p.project.getMapper().deobfuscate(entry); + invalidate(); + } + + public ClassHandleImpl createHandle() { + ClassHandleImpl handle = new ClassHandleImpl(this); + withLock(lock.writeLock(), () -> { + handles.add(handle); + }); + return handle; + } + + @Nullable + public ClassEntry getDeobfRef() { + return deobfRef; + } + + private void checkDeobfRefForUpdate() { + ClassEntry newDeobf = p.project.getMapper().deobfuscate(entry); + if (!Objects.equals(deobfRef, newDeobf)) { + deobfRef = newDeobf; + // copy the list so we don't call event listener code with the lock active + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDeobfRefChanged(newDeobf)); + } + } + + public void invalidate() { + checkDeobfRefForUpdate(); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.FULL)); + continueMapSource(continueIndexSource(continueInsertJavadoc(decompile()))); + } + + public void invalidateJavadoc() { + checkDeobfRefForUpdate(); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.JAVADOC)); + continueMapSource(continueIndexSource(continueInsertJavadoc(CompletableFuture.completedFuture(uncommentedSource)))); + } + + public void invalidateMapped() { + checkDeobfRefForUpdate(); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.MAPPINGS)); + continueMapSource(CompletableFuture.completedFuture(source)); + } + + private CompletableFuture> decompile() { + int v = decompileVersion.incrementAndGet(); + return CompletableFuture.supplyAsync(() -> { + if (decompileVersion.get() != v) return null; + + Result _uncommentedSource; + try { + _uncommentedSource = Result.ok(p.decompiler.getSource(entry.getFullName())); + } catch (Throwable e) { + return Result.err(ClassHandleError.decompile(e)); + } + Result uncommentedSource = _uncommentedSource; + Entry.this.uncommentedSource = uncommentedSource; + Entry.this.waitingUncommentedSources.forEach(f -> f.complete(uncommentedSource)); + Entry.this.waitingUncommentedSources.clear(); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onUncommentedSourceChanged(uncommentedSource)); + return uncommentedSource; + }, p.pool); + } + + private CompletableFuture> continueInsertJavadoc(CompletableFuture> f) { + int v = javadocVersion.incrementAndGet(); + return f.thenApplyAsync(res -> { + if (res == null || javadocVersion.get() != v) return null; + Result jdSource = res.map(s -> s.addJavadocs(p.project.getMapper())); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDocsChanged(jdSource)); + return jdSource; + }, p.pool); + } + + private CompletableFuture> continueIndexSource(CompletableFuture> f) { + int v = indexVersion.incrementAndGet(); + return f.thenApplyAsync(res -> { + if (res == null || indexVersion.get() != v) return null; + return res.andThen(jdSource -> { + SourceIndex index = jdSource.index(); + index.resolveReferences(p.project.getMapper().getObfResolver()); + DecompiledClassSource source = new DecompiledClassSource(entry, index); + return Result.ok(source); + }); + }, p.pool); + } + + private void continueMapSource(CompletableFuture> f) { + int v = mappedVersion.incrementAndGet(); + f.thenAcceptAsync(res -> { + if (res == null || mappedVersion.get() != v) return; + res = res.map(source -> { + source.remapSource(p.project, p.project.getMapper().getDeobfuscator()); + return source; + }); + Entry.this.source = res; + Entry.this.waitingSources.forEach(s -> s.complete(source)); + Entry.this.waitingSources.clear(); + withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onMappedSourceChanged(source)); + }, p.pool); + } + + public void closeHandle(ClassHandleImpl classHandle) { + classHandle.destroy(); + withLock(lock.writeLock(), () -> { + handles.remove(classHandle); + if (handles.isEmpty()) { + p.deleteEntry(this); + } + }); + } + + public void destroy() { + withLock(lock.writeLock(), () -> { + handles.forEach(ClassHandleImpl::destroy); + handles.clear(); + }); + } + + public CompletableFuture> getUncommentedSourceAsync() { + if (uncommentedSource != null) { + return CompletableFuture.completedFuture(uncommentedSource); + } else { + CompletableFuture> f = new CompletableFuture<>(); + waitingUncommentedSources.add(f); + return f; + } + } + + public CompletableFuture> getSourceAsync() { + if (source != null) { + return CompletableFuture.completedFuture(source); + } else { + CompletableFuture> f = new CompletableFuture<>(); + waitingSources.add(f); + return f; + } + } + } + + private static final class ClassHandleImpl implements ClassHandle { + + private final Entry entry; + + private boolean valid = true; + + private final Set listeners = new HashSet<>(); + + private ClassHandleImpl(Entry entry) { + this.entry = entry; + } + + @Override + public ClassEntry getRef() { + checkValid(); + return entry.entry; + } + + @Nullable + @Override + public ClassEntry getDeobfRef() { + checkValid(); + // cache this? + return entry.getDeobfRef(); + } + + @Override + public CompletableFuture> getSource() { + checkValid(); + return entry.getSourceAsync(); + } + + @Override + public CompletableFuture> getUncommentedSource() { + checkValid(); + return entry.getUncommentedSourceAsync(); + } + + @Override + public void invalidate() { + checkValid(); + this.entry.invalidate(); + } + + @Override + public void invalidateMapped() { + checkValid(); + this.entry.invalidateMapped(); + } + + @Override + public void invalidateJavadoc() { + checkValid(); + this.entry.invalidateJavadoc(); + } + + public void onUncommentedSourceChanged(Result source) { + listeners.forEach(l -> l.onUncommentedSourceChanged(this, source)); + } + + public void onDocsChanged(Result source) { + listeners.forEach(l -> l.onDocsChanged(this, source)); + } + + public void onMappedSourceChanged(Result source) { + listeners.forEach(l -> l.onMappedSourceChanged(this, source)); + } + + public void onInvalidate(InvalidationType t) { + listeners.forEach(l -> l.onInvalidate(this, t)); + } + + public void onDeobfRefChanged(ClassEntry newDeobf) { + listeners.forEach(l -> l.onDeobfRefChanged(this, newDeobf)); + } + + @Override + public void addListener(ClassHandleListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(ClassHandleListener listener) { + listeners.remove(listener); + } + + @Override + public ClassHandle copy() { + checkValid(); + return entry.createHandle(); + } + + @Override + public void close() { + if (valid) entry.closeHandle(this); + } + + private void checkValid() { + if (!valid) throw new IllegalStateException("Class handle no longer valid"); + } + + public void destroy() { + listeners.forEach(l -> l.onDeleted(this)); + valid = false; + } + + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java b/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java new file mode 100644 index 00000000..61fea4ea --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java @@ -0,0 +1,36 @@ +package cuchaz.enigma.events; + +import cuchaz.enigma.classhandle.ClassHandle; +import cuchaz.enigma.classhandle.ClassHandleError; +import cuchaz.enigma.source.DecompiledClassSource; +import cuchaz.enigma.source.Source; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.utils.Result; + +public interface ClassHandleListener { + + default void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) { + } + + default void onUncommentedSourceChanged(ClassHandle h, Result res) { + } + + default void onDocsChanged(ClassHandle h, Result res) { + } + + default void onMappedSourceChanged(ClassHandle h, Result res) { + } + + default void onInvalidate(ClassHandle h, InvalidationType t) { + } + + default void onDeleted(ClassHandle h) { + } + + enum InvalidationType { + FULL, + JAVADOC, + MAPPINGS, + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java new file mode 100644 index 00000000..85fba505 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java @@ -0,0 +1,157 @@ +package cuchaz.enigma.source; + +import java.util.*; + +import javax.annotation.Nullable; + +import cuchaz.enigma.EnigmaProject; +import cuchaz.enigma.EnigmaServices; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.api.service.NameProposalService; +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; + +public class DecompiledClassSource { + private final ClassEntry classEntry; + + private final SourceIndex obfuscatedIndex; + private SourceIndex remappedIndex; + + private final Map> highlightedTokens = new EnumMap<>(RenamableTokenType.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, RenamableTokenType.DEOBFUSCATED); + return translatedEntry.getSourceRemapName(); + } else { + Optional proposedName = proposeName(project, entry); + if (proposedName.isPresent()) { + highlightToken(movedToken, RenamableTokenType.PROPOSED); + return proposedName.get(); + } + + highlightToken(movedToken, RenamableTokenType.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, RenamableTokenType 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/src/main/java/cuchaz/enigma/source/RenamableTokenType.java b/enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java new file mode 100644 index 00000000..c63aad91 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java @@ -0,0 +1,7 @@ +package cuchaz.enigma.source; + +public enum RenamableTokenType { + OBFUSCATED, + DEOBFUSCATED, + PROPOSED +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java index 18c966cd..dec75ff4 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java @@ -1,18 +1,18 @@ package cuchaz.enigma.translation; -import cuchaz.enigma.translation.mapping.NameValidator; -import cuchaz.enigma.translation.representation.TypeDescriptor; - import java.util.Collection; import java.util.Locale; +import cuchaz.enigma.translation.mapping.IdentifierValidation; +import cuchaz.enigma.translation.representation.TypeDescriptor; + public class LocalNameGenerator { public static String generateArgumentName(int index, TypeDescriptor desc, Collection arguments) { boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1; String translatedName; int nameIndex = index + 1; StringBuilder nameBuilder = new StringBuilder(getTypeName(desc)); - if (!uniqueType || NameValidator.isReserved(nameBuilder.toString())) { + if (!uniqueType || IdentifierValidation.isReservedMethodName(nameBuilder.toString())) { nameBuilder.append(nameIndex); } translatedName = nameBuilder.toString(); diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java index 1dd7eacc..932b5bb5 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java @@ -1,5 +1,10 @@ package cuchaz.enigma.translation.mapping; +import java.util.Collection; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.translation.MappingTranslator; import cuchaz.enigma.translation.Translatable; @@ -8,10 +13,7 @@ import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; import cuchaz.enigma.translation.mapping.tree.EntryTree; import cuchaz.enigma.translation.mapping.tree.HashEntryTree; import cuchaz.enigma.translation.representation.entry.Entry; - -import javax.annotation.Nullable; -import java.util.Collection; -import java.util.stream.Stream; +import cuchaz.enigma.utils.validation.ValidationContext; public class EntryRemapper { private final DeltaTrackingTree obfToDeobf; @@ -39,26 +41,32 @@ public class EntryRemapper { return new EntryRemapper(index, new HashEntryTree<>()); } - public > void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { - mapFromObf(obfuscatedEntry, deobfMapping, true); + public > void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { + mapFromObf(vc, obfuscatedEntry, deobfMapping, true); } - public > void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) { + public > void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) { + mapFromObf(vc, obfuscatedEntry, deobfMapping, renaming, false); + } + + public > void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming, boolean validateOnly) { Collection resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST); if (renaming && deobfMapping != null) { for (E resolvedEntry : resolvedEntries) { - validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); + validator.validateRename(vc, resolvedEntry, deobfMapping.getTargetName()); } } + if (validateOnly || !vc.canProceed()) return; + for (E resolvedEntry : resolvedEntries) { obfToDeobf.insert(resolvedEntry, deobfMapping); } } - public void removeByObf(Entry obfuscatedEntry) { - mapFromObf(obfuscatedEntry, null); + public void removeByObf(ValidationContext vc, Entry obfuscatedEntry) { + mapFromObf(vc, obfuscatedEntry, null); } @Nullable diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java new file mode 100644 index 00000000..097c9e90 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * 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.translation.mapping; + +import java.util.Arrays; +import java.util.List; + +import cuchaz.enigma.utils.validation.Message; +import cuchaz.enigma.utils.validation.StandardValidation; +import cuchaz.enigma.utils.validation.ValidationContext; + +public final class IdentifierValidation { + + private IdentifierValidation() { + } + + private static final List ILLEGAL_IDENTIFIERS = Arrays.asList( + "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", + "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", + "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", + "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", + "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_" + ); + + public static boolean validateClassName(ValidationContext vc, String name) { + if (!StandardValidation.notBlank(vc, name)) return false; + String[] parts = name.split("/"); + for (String part : parts) { + validateIdentifier(vc, part); + } + return true; + } + + public static boolean validateIdentifier(ValidationContext vc, String name) { + if (!StandardValidation.notBlank(vc, name)) return false; + if (checkForReservedName(vc, name)) return false; + + // Adapted from javax.lang.model.SourceVersion.isIdentifier + + int cp = name.codePointAt(0); + int position = 1; + if (!Character.isJavaIdentifierStart(cp)) { + vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position); + return false; + } + for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { + cp = name.codePointAt(i); + position += 1; + if (!Character.isJavaIdentifierPart(cp)) { + vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position); + return false; + } + } + + return true; + } + + private static boolean checkForReservedName(ValidationContext vc, String name) { + if (isReservedMethodName(name)) { + vc.raise(Message.RESERVED_IDENTIFIER); + return true; + } + return false; + } + + public static boolean isReservedMethodName(String name) { + return ILLEGAL_IDENTIFIERS.contains(name); + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java deleted file mode 100644 index a7f83cd7..00000000 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java +++ /dev/null @@ -1,39 +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.translation.mapping; - -public class IllegalNameException extends RuntimeException { - - private String name; - private String reason; - - public IllegalNameException(String name, String reason) { - this.name = name; - this.reason = reason; - } - - public String getReason() { - return this.reason; - } - - @Override - public String getMessage() { - StringBuilder buf = new StringBuilder(); - buf.append("Illegal name: "); - buf.append(this.name); - if (this.reason != null) { - buf.append(" because "); - buf.append(this.reason); - } - return buf.toString(); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java index ae615da4..f9f3b88e 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java @@ -1,17 +1,20 @@ package cuchaz.enigma.translation.mapping; +import java.util.Collection; +import java.util.HashSet; +import java.util.stream.Collectors; + import cuchaz.enigma.analysis.index.InheritanceIndex; import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.mapping.tree.EntryTree; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; - -import java.util.Collection; -import java.util.HashSet; -import java.util.stream.Collectors; +import cuchaz.enigma.utils.validation.Message; +import cuchaz.enigma.utils.validation.ValidationContext; public class MappingValidator { + private final EntryTree obfToDeobf; private final Translator deobfuscator; private final JarIndex index; @@ -22,15 +25,15 @@ public class MappingValidator { this.index = index; } - public void validateRename(Entry entry, String name) throws IllegalNameException { + public void validateRename(ValidationContext vc, Entry entry, String name) { Collection> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry); for (Entry equivalentEntry : equivalentEntries) { - equivalentEntry.validateName(name); - validateUnique(equivalentEntry, name); + equivalentEntry.validateName(vc, name); + validateUnique(vc, equivalentEntry, name); } } - private void validateUnique(Entry entry, String name) { + private void validateUnique(ValidationContext vc, Entry entry, String name) { ClassEntry containingClass = entry.getContainingClass(); Collection relatedClasses = getRelatedClasses(containingClass); @@ -45,9 +48,9 @@ public class MappingValidator { if (!isUnique(translatedEntry, translatedSiblings, name)) { Entry parent = translatedEntry.getParent(); if (parent != null) { - throw new IllegalNameException(name, "Name is not unique in " + parent + "!"); + vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent); } else { - throw new IllegalNameException(name, "Name is not unique!"); + vc.raise(Message.NONUNIQUE_NAME, name); } } } @@ -72,4 +75,5 @@ public class MappingValidator { } return true; } + } diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java deleted file mode 100644 index 74ba633d..00000000 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java +++ /dev/null @@ -1,50 +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.translation.mapping; - -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -public class NameValidator { - private static final Pattern IDENTIFIER_PATTERN; - private static final Pattern CLASS_PATTERN; - private static final List ILLEGAL_IDENTIFIERS = Arrays.asList( - "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", - "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", - "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", - "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", - "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_" - ); - - static { - String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*"; - IDENTIFIER_PATTERN = Pattern.compile(identifierRegex); - CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex)); - } - - public static void validateClassName(String name) { - if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { - throw new IllegalNameException(name, "This doesn't look like a legal class name"); - } - } - - public static void validateIdentifier(String name) { - if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) { - throw new IllegalNameException(name, "This doesn't look like a legal identifier"); - } - } - - public static boolean isReserved(String name) { - return ILLEGAL_IDENTIFIERS.contains(name); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java index 7d4b2ba4..15b0a9b4 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java @@ -11,16 +11,17 @@ package cuchaz.enigma.translation.representation.entry; -import cuchaz.enigma.translation.mapping.IllegalNameException; -import cuchaz.enigma.translation.Translator; -import cuchaz.enigma.translation.mapping.EntryMapping; -import cuchaz.enigma.translation.mapping.NameValidator; -import cuchaz.enigma.translation.representation.TypeDescriptor; +import java.util.List; +import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.List; -import java.util.Objects; + +import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.IdentifierValidation; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.utils.validation.ValidationContext; public class ClassEntry extends ParentedEntry implements Comparable { private final String fullName; @@ -97,8 +98,8 @@ public class ClassEntry extends ParentedEntry implements Comparable< } @Override - public void validateName(String name) throws IllegalNameException { - NameValidator.validateClassName(name); + public void validateName(ValidationContext vc, String name) { + IdentifierValidation.validateClassName(vc, name); } @Override diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java index 40bff31d..ff392fee 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java @@ -11,14 +11,15 @@ package cuchaz.enigma.translation.representation.entry; -import cuchaz.enigma.translation.mapping.IllegalNameException; -import cuchaz.enigma.translation.Translatable; -import cuchaz.enigma.translation.mapping.NameValidator; - -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; + +import cuchaz.enigma.translation.Translatable; +import cuchaz.enigma.translation.mapping.IdentifierValidation; +import cuchaz.enigma.utils.validation.ValidationContext; + public interface Entry

> extends Translatable { String getName(); @@ -92,8 +93,8 @@ public interface Entry

> extends Translatable { return withParent((P) parent.replaceAncestor(target, replacement)); } - default void validateName(String name) throws IllegalNameException { - NameValidator.validateIdentifier(name); + default void validateName(ValidationContext vc, String name) { + IdentifierValidation.validateIdentifier(vc, name); } @SuppressWarnings("unchecked") diff --git a/enigma/src/main/java/cuchaz/enigma/utils/I18n.java b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java index e18532b6..cb498e05 100644 --- a/enigma/src/main/java/cuchaz/enigma/utils/I18n.java +++ b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java @@ -5,9 +5,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.google.common.collect.ImmutableList; @@ -22,12 +21,12 @@ public class I18n { private static Map translations = Maps.newHashMap(); private static Map defaultTranslations = Maps.newHashMap(); private static Map languageNames = Maps.newHashMap(); - + static { defaultTranslations = load(DEFAULT_LANGUAGE); translations = defaultTranslations; } - + @SuppressWarnings("unchecked") public static Map load(String language) { try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) { @@ -41,30 +40,50 @@ public class I18n { } return Collections.emptyMap(); } - - public static String translate(String key) { + + public static String translateOrNull(String key) { String value = translations.get(key); - if (value != null) { - return value; + if (value != null) return value; + + return defaultTranslations.get(key); + } + + public static String translate(String key) { + String tr = translateOrNull(key); + return tr != null ? tr : key; + } + + public static String translateOrEmpty(String key, Object... args) { + String text = translateOrNull(key); + if (text != null) { + return String.format(text, args); + } else { + return ""; } - value = defaultTranslations.get(key); - if (value != null) { - return value; + } + + public static String translateFormatted(String key, Object... args) { + String text = translateOrNull(key); + if (text != null) { + return String.format(text, args); + } else if (args.length == 0) { + return key; + } else { + return key + Arrays.stream(args).map(Objects::toString).collect(Collectors.joining(", ", "[", "]")); } - return key; } - + public static String getLanguageName(String language) { return languageNames.get(language); } - + public static void setLanguage(String language) { translations = load(language); } - + public static ArrayList getAvailableLanguages() { ArrayList list = new ArrayList(); - + try { ImmutableList resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList(); Stream dirStream = resources.stream(); @@ -81,7 +100,7 @@ public class I18n { } return list; } - + private static void loadLanguageName(String fileName) { try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Result.java b/enigma/src/main/java/cuchaz/enigma/utils/Result.java new file mode 100644 index 00000000..dcaabd58 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/Result.java @@ -0,0 +1,108 @@ +package cuchaz.enigma.utils; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +public final class Result { + + private final T ok; + private final E err; + + private Result(T ok, E err) { + this.ok = ok; + this.err = err; + } + + public static Result ok(T ok) { + return new Result<>(Objects.requireNonNull(ok), null); + } + + public static Result err(E err) { + return new Result<>(null, Objects.requireNonNull(err)); + } + + public boolean isOk() { + return this.ok != null; + } + + public boolean isErr() { + return this.err != null; + } + + public Optional ok() { + return Optional.ofNullable(this.ok); + } + + public Optional err() { + return Optional.ofNullable(this.err); + } + + public T unwrap() { + if (this.isOk()) return this.ok; + throw new IllegalStateException(String.format("Called Result.unwrap on an Err value: %s", this.err)); + } + + public E unwrapErr() { + if (this.isErr()) return this.err; + throw new IllegalStateException(String.format("Called Result.unwrapErr on an Ok value: %s", this.ok)); + } + + public T unwrapOr(T fallback) { + if (this.isOk()) return this.ok; + return fallback; + } + + public T unwrapOrElse(Function fn) { + if (this.isOk()) return this.ok; + return fn.apply(this.err); + } + + @SuppressWarnings("unchecked") + public Result map(Function op) { + if (!this.isOk()) return (Result) this; + return Result.ok(op.apply(this.ok)); + } + + @SuppressWarnings("unchecked") + public Result mapErr(Function op) { + if (!this.isErr()) return (Result) this; + return Result.err(op.apply(this.err)); + } + + @SuppressWarnings("unchecked") + public Result and(Result next) { + if (this.isErr()) return (Result) this; + return next; + } + + @SuppressWarnings("unchecked") + public Result andThen(Function> op) { + if (this.isErr()) return (Result) this; + return op.apply(this.ok); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Result result = (Result) o; + return Objects.equals(ok, result.ok) && + Objects.equals(err, result.err); + } + + @Override + public int hashCode() { + return Objects.hash(ok, err); + } + + @Override + public String toString() { + if (this.isOk()) { + return String.format("Result.Ok(%s)", this.ok); + } else { + return String.format("Result.Err(%s)", this.err); + } + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Utils.java b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java index 26640993..8beaaae6 100644 --- a/enigma/src/main/java/cuchaz/enigma/utils/Utils.java +++ b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -78,6 +80,25 @@ public class Utils { return digest.digest(); } + public static void withLock(Lock l, Runnable op) { + try { + l.lock(); + op.run(); + } finally { + l.unlock(); + } + } + + public static R withLock(Lock l, Supplier op) { + try { + l.lock(); + return op.get(); + } finally { + l.unlock(); + } + } + + public static boolean isBlank(String input) { if (input == null) { return true; diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java new file mode 100644 index 00000000..dca74bc8 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.utils.validation; + +import cuchaz.enigma.utils.I18n; + +public class Message { + + public static final Message EMPTY_FIELD = create(Type.ERROR, "empty_field"); + public static final Message NOT_INT = create(Type.ERROR, "not_int"); + public static final Message FIELD_OUT_OF_RANGE_INT = create(Type.ERROR, "field_out_of_range_int"); + public static final Message FIELD_LENGTH_OUT_OF_RANGE = create(Type.ERROR, "field_length_out_of_range"); + public static final Message NONUNIQUE_NAME_CLASS = create(Type.ERROR, "nonunique_name_class"); + public static final Message NONUNIQUE_NAME = create(Type.ERROR, "nonunique_name"); + public static final Message ILLEGAL_CLASS_NAME = create(Type.ERROR, "illegal_class_name"); + public static final Message ILLEGAL_IDENTIFIER = create(Type.ERROR, "illegal_identifier"); + public static final Message RESERVED_IDENTIFIER = create(Type.ERROR, "reserved_identifier"); + public static final Message ILLEGAL_DOC_COMMENT_END = create(Type.ERROR, "illegal_doc_comment_end"); + + public static final Message STYLE_VIOLATION = create(Type.WARNING, "style_violation"); + + public final Type type; + public final String textKey; + public final String longTextKey; + + private Message(Type type, String textKey, String longTextKey) { + this.type = type; + this.textKey = textKey; + this.longTextKey = longTextKey; + } + + public String format(Object[] args) { + return I18n.translateFormatted(textKey, args); + } + + public String formatDetails(Object[] args) { + return I18n.translateOrEmpty(longTextKey, args); + } + + public static Message create(Type type, String name) { + return new Message(type, String.format("validation.message.%s", name), String.format("validation.message.%s.long", name)); + } + + public enum Type { + INFO, + WARNING, + ERROR, + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java new file mode 100644 index 00000000..56b0ecc5 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java @@ -0,0 +1,45 @@ +package cuchaz.enigma.utils.validation; + +import java.util.Arrays; +import java.util.Objects; + +public class ParameterizedMessage { + + public final Message message; + private final Object[] params; + + public ParameterizedMessage(Message message, Object[] params) { + this.message = message; + this.params = params; + } + + public String getText() { + return message.format(params); + } + + public String getLongText() { + return message.formatDetails(params); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ParameterizedMessage that = (ParameterizedMessage) o; + return Objects.equals(message, that.message) && + Arrays.equals(params, that.params); + } + + @Override + public int hashCode() { + int result = Objects.hash(message); + result = 31 * result + Arrays.hashCode(params); + return result; + } + + @Override + public String toString() { + return String.format("ParameterizedMessage { message: %s, params: %s }", message, Arrays.toString(params)); + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java new file mode 100644 index 00000000..fe91cc11 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java @@ -0,0 +1,37 @@ +package cuchaz.enigma.utils.validation; + +import java.util.Arrays; + +public class PrintValidatable implements Validatable { + + public static final PrintValidatable INSTANCE = new PrintValidatable(); + + @Override + public void addMessage(ParameterizedMessage message) { + String text = message.getText(); + String longText = message.getLongText(); + String type; + switch (message.message.type) { + case INFO: + type = "info"; + break; + case WARNING: + type = "warning"; + break; + case ERROR: + type = "error"; + break; + default: + throw new IllegalStateException("unreachable"); + } + System.out.printf("%s: %s\n", type, text); + if (!longText.isEmpty()) { + Arrays.stream(longText.split("\n")).forEach(s -> System.out.printf(" %s\n", s)); + } + } + + @Override + public void clearMessages() { + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java new file mode 100644 index 00000000..871b59d7 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java @@ -0,0 +1,34 @@ +package cuchaz.enigma.utils.validation; + +public class StandardValidation { + + public static boolean notBlank(ValidationContext vc, String value) { + if (value.trim().isEmpty()) { + vc.raise(Message.EMPTY_FIELD); + return false; + } + return true; + } + + public static boolean isInt(ValidationContext vc, String value) { + if (!notBlank(vc, value)) return false; + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + vc.raise(Message.NOT_INT); + return false; + } + } + + public static boolean isIntInRange(ValidationContext vc, String value, int min, int max) { + if (!isInt(vc, value)) return false; + int intVal = Integer.parseInt(value); + if (intVal < min || intVal > max) { + vc.raise(Message.FIELD_OUT_OF_RANGE_INT, min, max); + return false; + } + return true; + } + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java new file mode 100644 index 00000000..765ee084 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java @@ -0,0 +1,9 @@ +package cuchaz.enigma.utils.validation; + +public interface Validatable { + + void addMessage(ParameterizedMessage message); + + void clearMessages(); + +} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java new file mode 100644 index 00000000..d38fc213 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java @@ -0,0 +1,78 @@ +package cuchaz.enigma.utils.validation; + +import java.util.*; + +import javax.annotation.Nullable; + +import cuchaz.enigma.utils.validation.Message.Type; + +/** + * A context for user input validation. Handles collecting error messages and + * displaying the errors on the relevant input fields. UIs using validation + * often have two stages of applying changes: validating all the input fields, + * then checking if there's any errors or unconfirmed warnings, and if not, + * then actually applying the changes. This allows for easily collecting + * multiple errors and displaying them to the user at the same time. + */ +public class ValidationContext { + + private Validatable activeElement = null; + private final Set elements = new HashSet<>(); + private final List messages = new ArrayList<>(); + + /** + * Sets the currently active element (such as an input field). Any messages + * raised while this is set get displayed on this element. + * + * @param v the active element to set, or {@code null} to unset + */ + public void setActiveElement(@Nullable Validatable v) { + if (v != null) { + elements.add(v); + } + activeElement = v; + } + + /** + * Raises a message. If there's a currently active element, also notifies + * that element about the message. + * + * @param message the message to raise + * @param args the arguments used when formatting the message text + */ + public void raise(Message message, Object... args) { + ParameterizedMessage pm = new ParameterizedMessage(message, args); + if (activeElement != null) { + activeElement.addMessage(pm); + } + messages.add(pm); + } + + /** + * Returns whether the validation context currently has no messages that + * block executing actions, such as errors and unconfirmed warnings. + * + * @return whether the program can proceed executing and the UI is in a + * valid state + */ + public boolean canProceed() { + // TODO on warnings, wait until user confirms + return messages.stream().noneMatch(m -> m.message.type == Type.ERROR); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + /** + * Clears all currently pending messages. This should be called whenever the + * interface starts getting validated, to get rid of old messages. + */ + public void reset() { + activeElement = null; + elements.forEach(Validatable::clearMessages); + elements.clear(); + messages.clear(); + } + +} diff --git a/enigma/src/main/resources/lang/de_de.json b/enigma/src/main/resources/lang/de_de.json new file mode 100644 index 00000000..ef41da15 --- /dev/null +++ b/enigma/src/main/resources/lang/de_de.json @@ -0,0 +1,26 @@ +{ + "language": "German", + + "general.retry": "Wiederholen", + + "popup_menu.editor_tab.close": "Schließen", + "popup_menu.editor_tab.close_all": "Alle schließen", + "popup_menu.editor_tab.close_others": "Andere schließen", + "popup_menu.editor_tab.close_left": "Alle links hiervon schließen", + "popup_menu.editor_tab.close_right": "Alle rechts hiervon schließen", + + "editor.decompiling": "Dekompiliere...", + "editor.decompile_error": "Ein Fehler ist während des Dekompilierens aufgetreten.", + + "validation.message.empty_field": "Dieses Feld muss ausgefüllt werden.", + "validation.message.not_int": "Wert muss eine ganze Zahl sein.", + "validation.message.field_out_of_range_int": "Wert muss eine ganze Zahl zwischen %d und %d sein.", + "validation.message.field_length_out_of_range": "Wert muss kürzer als %d Zeichen sein.", + "validation.message.nonunique_name_class": "Name „%s“ ist in „%s“ nicht eindeutig.", + "validation.message.nonunique_name": "Name „%s“ ist nicht eindeutig.", + "validation.message.illegal_class_name": "„%s“ ist kein gültiger Klassenname.", + "validation.message.illegal_identifier": "„%s“ ist kein gültiger Name.", + "validation.message.illegal_identifier.long": "Ungültiges Zeichen „%2$s“ an Position %3$d.", + "validation.message.illegal_doc_comment_end": "Javadoc-Kommentar darf die Zeichenfolge „*/“ nicht enthalten.", + "validation.message.reserved_identifier": "„%s“ ist ein reservierter Name." +} \ No newline at end of file diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index dbf4b935..b8db4b06 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -1,6 +1,8 @@ { "language": "English", + "general.retry": "Retry", + "mapping_format.enigma_file": "Enigma File", "mapping_format.enigma_directory": "Enigma Directory", "mapping_format.enigma_zip": "Enigma ZIP", @@ -69,6 +71,14 @@ "popup_menu.zoom.in": "Zoom in", "popup_menu.zoom.out": "Zoom out", "popup_menu.zoom.reset": "Reset zoom", + "popup_menu.editor_tab.close": "Close", + "popup_menu.editor_tab.close_all": "Close All", + "popup_menu.editor_tab.close_others": "Close Others", + "popup_menu.editor_tab.close_left": "Close All to the Left", + "popup_menu.editor_tab.close_right": "Close All to the Right", + + "editor.decompiling": "Decompiling...", + "editor.decompile_error": "An error was encountered while decompiling.", "info_panel.classes.obfuscated": "Obfuscated Classes", "info_panel.classes.deobfuscated": "De-obfuscated Classes", @@ -79,8 +89,8 @@ "info_panel.identifier.method": "Method", "info_panel.identifier.constructor": "Constructor", "info_panel.identifier.class": "Class", - "info_panel.identifier.type_descriptor": "TypeDescriptor", - "info_panel.identifier.method_descriptor": "MethodDescriptor", + "info_panel.identifier.type_descriptor": "Type Descriptor", + "info_panel.identifier.method_descriptor": "Method Descriptor", "info_panel.identifier.modifier": "Modifier", "info_panel.identifier.index": "Index", "info_panel.editor.class.decompiling": "(decompiling...)", @@ -149,12 +159,23 @@ "message.mark_deobf.text": "%s marked %s as deobfuscated", "message.remove_mapping.text": "%s removed mappings for %s", "message.rename.text": "%s renamed %s to %s", - "status.disconnected": "Disconnected.", "status.connected": "Connected.", "status.connected_user_count": "Connected (%d users).", "status.ready": "Ready.", + "validation.message.empty_field": "This field is required.", + "validation.message.not_int": "Value must be an integer.", + "validation.message.field_out_of_range_int": "Value must be an integer between %d and %d.", + "validation.message.field_length_out_of_range": "Value must be less than %d characters long.", + "validation.message.nonunique_name_class": "Name '%s' is not unique in '%s'.", + "validation.message.nonunique_name": "Name '%s' is not unique.", + "validation.message.illegal_class_name": "'%s' is not a valid class name.", + "validation.message.illegal_identifier": "'%s' is not a valid identifier.", + "validation.message.illegal_identifier.long": "Invalid character '%2$s' at position %3$d.", + "validation.message.illegal_doc_comment_end": "Javadoc comment cannot contain the character sequence '*/'.", + "validation.message.reserved_identifier": "'%s' is a reserved identifier.", + "crash.title": "%s - Crash Report", "crash.summary": "%s has crashed! =(", "crash.export": "Export", -- cgit v1.2.3