summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar 2xsaiko2020-06-03 20:16:10 +0200
committerGravatar GitHub2020-06-03 19:16:10 +0100
commit5a286d58e740f1aa5944488c602f5abc1318f6ca (patch)
treedfde9eff0c744906b3571390af0f6a6e3be92a91
parentRefactor MenuBar (#251) (diff)
downloadenigma-5a286d58e740f1aa5944488c602f5abc1318f6ca.tar.gz
enigma-5a286d58e740f1aa5944488c602f5abc1318f6ca.tar.xz
enigma-5a286d58e740f1aa5944488c602f5abc1318f6ca.zip
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 <runemoro1@gmail.com>
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/ClientPacketHandler.java9
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java18
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java17
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java22
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java17
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java21
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java17
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java21
-rw-r--r--enigma-server/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java17
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java25
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java73
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java16
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java601
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java354
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java7
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java1
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java41
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java127
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ConvertingTextField.java170
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/EditorTabPopupMenu.java58
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/JMultiLineToolTip.java132
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java9
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java25
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatablePasswordField.java96
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextArea.java100
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableTextField.java96
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/ValidatableUi.java107
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/events/ConvertingTextFieldListener.java17
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/events/EditorActionListener.java20
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/events/ThemeChangeListener.java13
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java3
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java7
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/ClosableTabTitlePane.java133
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java571
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java253
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java13
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java10
-rw-r--r--enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java12
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java108
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java35
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java445
-rw-r--r--enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java36
-rw-r--r--enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java (renamed from enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java)25
-rw-r--r--enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java7
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java8
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java28
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java79
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java39
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java24
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java50
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java19
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java15
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/I18n.java55
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/Result.java108
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/Utils.java21
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java48
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java45
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java37
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java34
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java9
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java78
-rw-r--r--enigma/src/main/resources/lang/de_de.json26
-rw-r--r--enigma/src/main/resources/lang/en_us.json27
63 files changed, 3546 insertions, 1109 deletions
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;
5import cuchaz.enigma.translation.mapping.tree.EntryTree; 5import cuchaz.enigma.translation.mapping.tree.EntryTree;
6import cuchaz.enigma.network.packet.Packet; 6import cuchaz.enigma.network.packet.Packet;
7import cuchaz.enigma.translation.representation.entry.Entry; 7import cuchaz.enigma.translation.representation.entry.Entry;
8import cuchaz.enigma.utils.validation.ValidationContext;
8 9
9import java.util.List; 10import java.util.List;
10 11
11public interface ClientPacketHandler { 12public interface ClientPacketHandler {
12 void openMappings(EntryTree<EntryMapping> mappings); 13 void openMappings(EntryTree<EntryMapping> mappings);
13 14
14 void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean jumpToReference); 15 void rename(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree);
15 16
16 void removeMapping(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference); 17 void removeMapping(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference);
17 18
18 void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean jumpToReference); 19 void changeDocs(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs);
19 20
20 void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference); 21 void markAsDeobfuscated(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference);
21 22
22 void disconnectIfConnected(String reason); 23 void disconnectIfConnected(String reason);
23 24
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
3import cuchaz.enigma.translation.mapping.EntryMapping; 7import cuchaz.enigma.translation.mapping.EntryMapping;
4import cuchaz.enigma.network.EnigmaServer; 8import cuchaz.enigma.network.EnigmaServer;
5import cuchaz.enigma.network.Message; 9import cuchaz.enigma.network.Message;
6import cuchaz.enigma.network.ServerPacketHandler; 10import cuchaz.enigma.network.ServerPacketHandler;
7import cuchaz.enigma.translation.representation.entry.Entry; 11import cuchaz.enigma.translation.representation.entry.Entry;
8import cuchaz.enigma.utils.Utils; 12import cuchaz.enigma.utils.Utils;
9 13import cuchaz.enigma.utils.validation.PrintValidatable;
10import java.io.DataInput; 14import cuchaz.enigma.utils.validation.ValidationContext;
11import java.io.DataOutput;
12import java.io.IOException;
13 15
14public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> { 16public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> {
15 private Entry<?> entry; 17 private Entry<?> entry;
@@ -37,6 +39,9 @@ public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> {
37 39
38 @Override 40 @Override
39 public void handle(ServerPacketHandler handler) { 41 public void handle(ServerPacketHandler handler) {
42 ValidationContext vc = new ValidationContext();
43 vc.setActiveElement(PrintValidatable.INSTANCE);
44
40 EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry); 45 EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry);
41 46
42 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); 47 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
@@ -49,11 +54,12 @@ public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> {
49 if (mapping == null) { 54 if (mapping == null) {
50 mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()); 55 mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName());
51 } 56 }
52 handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs)); 57 handler.getServer().getMappings().mapFromObf(vc, entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs));
58
59 if (!vc.canProceed()) return;
53 60
54 int syncId = handler.getServer().lockEntry(handler.getClient(), entry); 61 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
55 handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs)); 62 handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs));
56 handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry)); 63 handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry));
57 } 64 }
58
59} 65}
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput; 3import java.io.DataInput;
8import java.io.DataOutput; 4import java.io.DataOutput;
9import java.io.IOException; 5import java.io.IOException;
10 6
7import cuchaz.enigma.analysis.EntryReference;
8import cuchaz.enigma.network.ClientPacketHandler;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.validation.PrintValidatable;
11import cuchaz.enigma.utils.validation.ValidationContext;
12
11public class ChangeDocsS2CPacket implements Packet<ClientPacketHandler> { 13public class ChangeDocsS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId; 14 private int syncId;
13 private Entry<?> entry; 15 private Entry<?> entry;
@@ -38,7 +40,12 @@ public class ChangeDocsS2CPacket implements Packet<ClientPacketHandler> {
38 40
39 @Override 41 @Override
40 public void handle(ClientPacketHandler controller) { 42 public void handle(ClientPacketHandler controller) {
41 controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false); 43 ValidationContext vc = new ValidationContext();
44 vc.setActiveElement(PrintValidatable.INSTANCE);
45
46 controller.changeDocs(vc, new EntryReference<>(entry, entry.getName()), newDocs);
47
48 if (!vc.canProceed()) return;
42 controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); 49 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
43 } 50 }
44} 51}
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.network.Message;
7
8import java.io.DataInput; 3import java.io.DataInput;
9import java.io.DataOutput; 4import java.io.DataOutput;
10import java.io.IOException; 5import java.io.IOException;
11 6
7import cuchaz.enigma.network.Message;
8import cuchaz.enigma.network.ServerPacketHandler;
9import cuchaz.enigma.translation.mapping.EntryMapping;
10import cuchaz.enigma.translation.representation.entry.Entry;
11import cuchaz.enigma.utils.validation.PrintValidatable;
12import cuchaz.enigma.utils.validation.ValidationContext;
13
12public class MarkDeobfuscatedC2SPacket implements Packet<ServerPacketHandler> { 14public class MarkDeobfuscatedC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry; 15 private Entry<?> entry;
14 16
@@ -31,18 +33,24 @@ public class MarkDeobfuscatedC2SPacket implements Packet<ServerPacketHandler> {
31 33
32 @Override 34 @Override
33 public void handle(ServerPacketHandler handler) { 35 public void handle(ServerPacketHandler handler) {
36 ValidationContext vc = new ValidationContext();
37 vc.setActiveElement(PrintValidatable.INSTANCE);
38
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); 39 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
40
35 if (!valid) { 41 if (!valid) {
36 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); 42 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true);
37 return; 43 return;
38 } 44 }
39 45
40 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName())); 46 handler.getServer().getMappings().mapFromObf(vc, entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()));
47
48 if (!vc.canProceed()) return;
49
41 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated"); 50 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated");
42 51
43 int syncId = handler.getServer().lockEntry(handler.getClient(), entry); 52 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
44 handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry)); 53 handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry));
45 handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry)); 54 handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry));
46
47 } 55 }
48} 56}
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput; 3import java.io.DataInput;
8import java.io.DataOutput; 4import java.io.DataOutput;
9import java.io.IOException; 5import java.io.IOException;
10 6
7import cuchaz.enigma.analysis.EntryReference;
8import cuchaz.enigma.network.ClientPacketHandler;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.validation.PrintValidatable;
11import cuchaz.enigma.utils.validation.ValidationContext;
12
11public class MarkDeobfuscatedS2CPacket implements Packet<ClientPacketHandler> { 13public class MarkDeobfuscatedS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId; 14 private int syncId;
13 private Entry<?> entry; 15 private Entry<?> entry;
@@ -34,7 +36,12 @@ public class MarkDeobfuscatedS2CPacket implements Packet<ClientPacketHandler> {
34 36
35 @Override 37 @Override
36 public void handle(ClientPacketHandler controller) { 38 public void handle(ClientPacketHandler controller) {
37 controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false); 39 ValidationContext vc = new ValidationContext();
40 vc.setActiveElement(PrintValidatable.INSTANCE);
41
42 controller.markAsDeobfuscated(vc, new EntryReference<>(entry, entry.getName()));
43
44 if (!vc.canProceed()) return;
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); 45 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 } 46 }
40} 47}
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.IllegalNameException;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.network.Message;
7
8import java.io.DataInput; 3import java.io.DataInput;
9import java.io.DataOutput; 4import java.io.DataOutput;
10import java.io.IOException; 5import java.io.IOException;
11 6
7import cuchaz.enigma.network.Message;
8import cuchaz.enigma.network.ServerPacketHandler;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.validation.PrintValidatable;
11import cuchaz.enigma.utils.validation.ValidationContext;
12
12public class RemoveMappingC2SPacket implements Packet<ServerPacketHandler> { 13public class RemoveMappingC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry; 14 private Entry<?> entry;
14 15
@@ -31,14 +32,14 @@ public class RemoveMappingC2SPacket implements Packet<ServerPacketHandler> {
31 32
32 @Override 33 @Override
33 public void handle(ServerPacketHandler handler) { 34 public void handle(ServerPacketHandler handler) {
35 ValidationContext vc = new ValidationContext();
36 vc.setActiveElement(PrintValidatable.INSTANCE);
37
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); 38 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
35 39
36 if (valid) { 40 if (valid) {
37 try { 41 handler.getServer().getMappings().removeByObf(vc, entry);
38 handler.getServer().getMappings().removeByObf(entry); 42 valid = vc.canProceed();
39 } catch (IllegalNameException e) {
40 valid = false;
41 }
42 } 43 }
43 44
44 if (!valid) { 45 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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput; 3import java.io.DataInput;
8import java.io.DataOutput; 4import java.io.DataOutput;
9import java.io.IOException; 5import java.io.IOException;
10 6
7import cuchaz.enigma.analysis.EntryReference;
8import cuchaz.enigma.network.ClientPacketHandler;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.validation.PrintValidatable;
11import cuchaz.enigma.utils.validation.ValidationContext;
12
11public class RemoveMappingS2CPacket implements Packet<ClientPacketHandler> { 13public class RemoveMappingS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId; 14 private int syncId;
13 private Entry<?> entry; 15 private Entry<?> entry;
@@ -34,7 +36,12 @@ public class RemoveMappingS2CPacket implements Packet<ClientPacketHandler> {
34 36
35 @Override 37 @Override
36 public void handle(ClientPacketHandler controller) { 38 public void handle(ClientPacketHandler controller) {
37 controller.removeMapping(new EntryReference<>(entry, entry.getName()), false); 39 ValidationContext vc = new ValidationContext();
40 vc.setActiveElement(PrintValidatable.INSTANCE);
41
42 controller.removeMapping(vc, new EntryReference<>(entry, entry.getName()));
43
44 if (!vc.canProceed()) return;
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); 45 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 } 46 }
40} 47}
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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
3import cuchaz.enigma.network.ServerPacketHandler; 7import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.IllegalNameException;
5import cuchaz.enigma.translation.mapping.EntryMapping; 8import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.representation.entry.Entry; 9import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.network.Message; 10import cuchaz.enigma.network.Message;
8 11import cuchaz.enigma.utils.validation.PrintValidatable;
9import java.io.DataInput; 12import cuchaz.enigma.utils.validation.ValidationContext;
10import java.io.DataOutput;
11import java.io.IOException;
12 13
13public class RenameC2SPacket implements Packet<ServerPacketHandler> { 14public class RenameC2SPacket implements Packet<ServerPacketHandler> {
14 private Entry<?> entry; 15 private Entry<?> entry;
@@ -40,14 +41,14 @@ public class RenameC2SPacket implements Packet<ServerPacketHandler> {
40 41
41 @Override 42 @Override
42 public void handle(ServerPacketHandler handler) { 43 public void handle(ServerPacketHandler handler) {
44 ValidationContext vc = new ValidationContext();
45 vc.setActiveElement(PrintValidatable.INSTANCE);
46
43 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); 47 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
44 48
45 if (valid) { 49 if (valid) {
46 try { 50 handler.getServer().getMappings().mapFromObf(vc, entry, new EntryMapping(newName));
47 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName)); 51 valid = vc.canProceed();
48 } catch (IllegalNameException e) {
49 valid = false;
50 }
51 } 52 }
52 53
53 if (!valid) { 54 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 @@
1package cuchaz.enigma.network.packet; 1package cuchaz.enigma.network.packet;
2 2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.network.ClientPacketHandler;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput; 3import java.io.DataInput;
8import java.io.DataOutput; 4import java.io.DataOutput;
9import java.io.IOException; 5import java.io.IOException;
10 6
7import cuchaz.enigma.analysis.EntryReference;
8import cuchaz.enigma.network.ClientPacketHandler;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.utils.validation.PrintValidatable;
11import cuchaz.enigma.utils.validation.ValidationContext;
12
11public class RenameS2CPacket implements Packet<ClientPacketHandler> { 13public class RenameS2CPacket implements Packet<ClientPacketHandler> {
12 private int syncId; 14 private int syncId;
13 private Entry<?> entry; 15 private Entry<?> entry;
@@ -42,7 +44,12 @@ public class RenameS2CPacket implements Packet<ClientPacketHandler> {
42 44
43 @Override 45 @Override
44 public void handle(ClientPacketHandler controller) { 46 public void handle(ClientPacketHandler controller) {
45 controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false); 47 ValidationContext vc = new ValidationContext();
48 vc.setActiveElement(PrintValidatable.INSTANCE);
49
50 controller.rename(vc, new EntryReference<>(entry, entry.getName()), newName, refreshClassTree);
51
52 if (!vc.canProceed()) return;
46 controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); 53 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
47 } 54 }
48} 55}
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;
16import java.util.*; 16import java.util.*;
17 17
18import javax.annotation.Nullable; 18import javax.annotation.Nullable;
19import javax.swing.JOptionPane;
20import javax.swing.JTree; 19import javax.swing.JTree;
21import javax.swing.event.CellEditorListener; 20import javax.swing.event.CellEditorListener;
22import javax.swing.event.ChangeEvent; 21import javax.swing.event.ChangeEvent;
@@ -28,9 +27,9 @@ import com.google.common.collect.Maps;
28import com.google.common.collect.Multimap; 27import com.google.common.collect.Multimap;
29import cuchaz.enigma.gui.node.ClassSelectorClassNode; 28import cuchaz.enigma.gui.node.ClassSelectorClassNode;
30import cuchaz.enigma.gui.node.ClassSelectorPackageNode; 29import cuchaz.enigma.gui.node.ClassSelectorPackageNode;
31import cuchaz.enigma.translation.mapping.IllegalNameException;
32import cuchaz.enigma.translation.Translator; 30import cuchaz.enigma.translation.Translator;
33import cuchaz.enigma.translation.representation.entry.ClassEntry; 31import cuchaz.enigma.translation.representation.entry.ClassEntry;
32import cuchaz.enigma.utils.validation.ValidationContext;
34 33
35public class ClassSelector extends JTree { 34public class ClassSelector extends JTree {
36 35
@@ -103,18 +102,18 @@ public class ClassSelector extends JTree {
103 if (allowEdit && renameSelectionListener != null) { 102 if (allowEdit && renameSelectionListener != null) {
104 Object prevData = node.getUserObject(); 103 Object prevData = node.getUserObject();
105 Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data; 104 Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data;
106 try { 105 gui.validateImmediateAction(vc -> {
107 renameSelectionListener.onSelectionRename(node.getUserObject(), objectData, node); 106 renameSelectionListener.onSelectionRename(vc, node.getUserObject(), objectData, node);
108 node.setUserObject(objectData); // Make sure that it's modified 107 if (vc.canProceed()) {
109 } catch (IllegalNameException ex) { 108 node.setUserObject(objectData); // Make sure that it's modified
110 JOptionPane.showOptionDialog(gui.getFrame(), ex.getMessage(), "Enigma - Error", JOptionPane.OK_OPTION, 109 } else {
111 JOptionPane.ERROR_MESSAGE, null, new String[]{"Ok"}, "OK"); 110 editor.cancelCellEditing();
112 editor.cancelCellEditing(); 111 }
113 } 112 });
114 } else 113 } else {
115 editor.cancelCellEditing(); 114 editor.cancelCellEditing();
115 }
116 } 116 }
117
118 } 117 }
119 118
120 @Override 119 @Override
@@ -527,6 +526,6 @@ public class ClassSelector extends JTree {
527 } 526 }
528 527
529 public interface RenameSelectionListener { 528 public interface RenameSelectionListener {
530 void onSelectionRename(Object prevData, Object data, DefaultMutableTreeNode node); 529 void onSelectionRename(ValidationContext vc, Object prevData, Object data, DefaultMutableTreeNode node);
531 } 530 }
532} 531}
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 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.gui;
13
14import cuchaz.enigma.source.Token;
15
16import javax.swing.*;
17import javax.swing.text.BadLocationException;
18import javax.swing.text.Document;
19import javax.swing.text.Highlighter.HighlightPainter;
20import java.awt.*;
21import java.awt.event.ActionEvent;
22import java.awt.event.ActionListener;
23
24public class CodeReader extends JEditorPane {
25 private static final long serialVersionUID = 3673180950485748810L;
26
27 // HACKHACK: someday we can update the main GUI to use this code reader
28 public static void navigateToToken(final JEditorPane editor, final Token token, final HighlightPainter highlightPainter) {
29
30 // set the caret position to the token
31 Document document = editor.getDocument();
32 int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength());
33
34 editor.setCaretPosition(clampedPosition);
35 editor.grabFocus();
36
37 try {
38 // make sure the token is visible in the scroll window
39 Rectangle start = editor.modelToView(token.start);
40 Rectangle end = editor.modelToView(token.end);
41 final Rectangle show = start.union(end);
42 show.grow(start.width * 10, start.height * 6);
43 SwingUtilities.invokeLater(() -> editor.scrollRectToVisible(show));
44 } catch (BadLocationException ex) {
45 throw new Error(ex);
46 }
47
48 // highlight the token momentarily
49 final Timer timer = new Timer(200, new ActionListener() {
50 private int counter = 0;
51 private Object highlight = null;
52
53 @Override
54 public void actionPerformed(ActionEvent event) {
55 if (counter % 2 == 0) {
56 try {
57 highlight = editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter);
58 } catch (BadLocationException ex) {
59 // don't care
60 }
61 } else if (highlight != null) {
62 editor.getHighlighter().removeHighlight(highlight);
63 }
64
65 if (counter++ > 6) {
66 Timer timer = (Timer) event.getSource();
67 timer.stop();
68 }
69 }
70 });
71 timer.start();
72 }
73}
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 @@
1package cuchaz.enigma.gui; 1package cuchaz.enigma.gui;
2 2
3import cuchaz.enigma.gui.config.Config; 3import cuchaz.enigma.gui.config.Config;
4import de.sciss.syntaxpane.DefaultSyntaxKit;
4import de.sciss.syntaxpane.components.LineNumbersRuler; 5import de.sciss.syntaxpane.components.LineNumbersRuler;
5import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit; 6import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit;
6import de.sciss.syntaxpane.util.Configuration; 7import de.sciss.syntaxpane.util.Configuration;
7 8
8public class EnigmaSyntaxKit extends JavaSyntaxKit { 9public class EnigmaSyntaxKit extends JavaSyntaxKit {
10
9 private static Configuration configuration = null; 11 private static Configuration configuration = null;
10 12
11 @Override 13 @Override
12 public Configuration getConfig() { 14 public Configuration getConfig() {
13 if(configuration == null){ 15 if (configuration == null) {
14 initConfig(super.getConfig(JavaSyntaxKit.class)); 16 initConfig(DefaultSyntaxKit.getConfig(JavaSyntaxKit.class));
15 } 17 }
16 return configuration; 18 return configuration;
17 } 19 }
18 20
19 public void initConfig(Configuration baseConfig){ 21 public void initConfig(Configuration baseConfig) {
20 configuration = baseConfig; 22 configuration = baseConfig;
21 //See de.sciss.syntaxpane.TokenType 23 //See de.sciss.syntaxpane.TokenType
22 configuration.put("Style.KEYWORD", Config.getInstance().highlightColor + ", 0"); 24 configuration.put("Style.KEYWORD", Config.getInstance().highlightColor + ", 0");
@@ -36,9 +38,15 @@ public class EnigmaSyntaxKit extends JavaSyntaxKit {
36 configuration.put("RightMarginColumn", "999"); //No need to have a right margin, if someone wants it add a config 38 configuration.put("RightMarginColumn", "999"); //No need to have a right margin, if someone wants it add a config
37 39
38 configuration.put("Action.quick-find", "cuchaz.enigma.gui.QuickFindAction, menu F"); 40 configuration.put("Action.quick-find", "cuchaz.enigma.gui.QuickFindAction, menu F");
41
42 // This is an action written in javascript that is useless for enigma's
43 // use case, and removing it causes the editor to load way faster the
44 // first time
45 configuration.remove("Action.insert-date");
39 } 46 }
40 47
41 public static void invalidate(){ 48 public static void invalidate() {
42 configuration = null; 49 configuration = null;
43 } 50 }
51
44} 52}
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 @@
11 11
12package cuchaz.enigma.gui; 12package cuchaz.enigma.gui;
13 13
14import java.awt.*; 14import java.awt.BorderLayout;
15import java.awt.Container;
16import java.awt.FileDialog;
15import java.awt.event.*; 17import java.awt.event.*;
16import java.nio.file.Path; 18import java.nio.file.Path;
17import java.util.List;
18import java.util.*; 19import java.util.*;
20import java.util.function.Consumer;
19import java.util.function.Function; 21import java.util.function.Function;
20 22
23import javax.annotation.Nullable;
21import javax.swing.*; 24import javax.swing.*;
22import javax.swing.text.BadLocationException;
23import javax.swing.text.Highlighter;
24import javax.swing.tree.*; 25import javax.swing.tree.*;
25 26
26import com.google.common.base.Strings; 27import com.google.common.collect.HashBiMap;
27import com.google.common.collect.Lists; 28import com.google.common.collect.Lists;
28import cuchaz.enigma.Enigma; 29import cuchaz.enigma.Enigma;
29import cuchaz.enigma.EnigmaProfile; 30import cuchaz.enigma.EnigmaProfile;
30import cuchaz.enigma.analysis.*; 31import cuchaz.enigma.analysis.*;
32import cuchaz.enigma.classhandle.ClassHandle;
31import cuchaz.enigma.gui.config.Config; 33import cuchaz.enigma.gui.config.Config;
32import cuchaz.enigma.gui.config.Themes; 34import cuchaz.enigma.gui.config.Themes;
33import cuchaz.enigma.gui.dialog.CrashDialog; 35import cuchaz.enigma.gui.dialog.CrashDialog;
34import cuchaz.enigma.gui.dialog.JavadocDialog; 36import cuchaz.enigma.gui.dialog.JavadocDialog;
35import cuchaz.enigma.gui.dialog.SearchDialog; 37import cuchaz.enigma.gui.dialog.SearchDialog;
36import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; 38import cuchaz.enigma.gui.elements.CollapsibleTabbedPane;
39import cuchaz.enigma.gui.elements.EditorTabPopupMenu;
37import cuchaz.enigma.gui.elements.MenuBar; 40import cuchaz.enigma.gui.elements.MenuBar;
38import cuchaz.enigma.gui.elements.PopupMenuBar; 41import cuchaz.enigma.gui.elements.ValidatableUi;
42import cuchaz.enigma.gui.events.EditorActionListener;
39import cuchaz.enigma.gui.filechooser.FileChooserAny; 43import cuchaz.enigma.gui.filechooser.FileChooserAny;
40import cuchaz.enigma.gui.filechooser.FileChooserFolder; 44import cuchaz.enigma.gui.filechooser.FileChooserFolder;
41import cuchaz.enigma.gui.highlight.BoxHighlightPainter; 45import cuchaz.enigma.gui.panels.*;
42import cuchaz.enigma.gui.highlight.SelectionHighlightPainter;
43import cuchaz.enigma.gui.highlight.TokenHighlightType;
44import cuchaz.enigma.gui.panels.PanelDeobf;
45import cuchaz.enigma.gui.panels.PanelEditor;
46import cuchaz.enigma.gui.panels.PanelIdentifier;
47import cuchaz.enigma.gui.panels.PanelObf;
48import cuchaz.enigma.gui.util.GuiUtil;
49import cuchaz.enigma.gui.util.History; 46import cuchaz.enigma.gui.util.History;
50import cuchaz.enigma.network.packet.*;
51import cuchaz.enigma.source.Token;
52import cuchaz.enigma.translation.mapping.IllegalNameException;
53import cuchaz.enigma.translation.mapping.*;
54import cuchaz.enigma.translation.representation.entry.*;
55import cuchaz.enigma.network.Message;
56import cuchaz.enigma.gui.util.ScaleUtil; 47import cuchaz.enigma.gui.util.ScaleUtil;
48import cuchaz.enigma.network.Message;
49import cuchaz.enigma.network.packet.MarkDeobfuscatedC2SPacket;
50import cuchaz.enigma.network.packet.MessageC2SPacket;
51import cuchaz.enigma.network.packet.RemoveMappingC2SPacket;
52import cuchaz.enigma.network.packet.RenameC2SPacket;
53import cuchaz.enigma.source.Token;
54import cuchaz.enigma.translation.mapping.EntryRemapper;
55import cuchaz.enigma.translation.representation.entry.ClassEntry;
56import cuchaz.enigma.translation.representation.entry.Entry;
57import cuchaz.enigma.translation.representation.entry.FieldEntry;
58import cuchaz.enigma.translation.representation.entry.MethodEntry;
57import cuchaz.enigma.utils.I18n; 59import cuchaz.enigma.utils.I18n;
58import de.sciss.syntaxpane.DefaultSyntaxKit; 60import cuchaz.enigma.utils.validation.ParameterizedMessage;
61import cuchaz.enigma.utils.validation.ValidationContext;
59 62
60public class Gui { 63public class Gui {
61 64
62 public final PopupMenuBar popupMenu;
63 private final PanelObf obfPanel; 65 private final PanelObf obfPanel;
64 private final PanelDeobf deobfPanel; 66 private final PanelDeobf deobfPanel;
65 67
66 private final MenuBar menuBar; 68 private final MenuBar menuBar;
69
67 // state 70 // state
68 public History<EntryReference<Entry<?>, Entry<?>>> referenceHistory; 71 public History<EntryReference<Entry<?>, Entry<?>>> referenceHistory;
69 public EntryReference<Entry<?>, Entry<?>> renamingReference;
70 public EntryReference<Entry<?>, Entry<?>> cursorReference;
71 private boolean shouldNavigateOnClick;
72 private ConnectionState connectionState; 72 private ConnectionState connectionState;
73 private boolean isJarOpen; 73 private boolean isJarOpen;
74 74
@@ -80,14 +80,9 @@ public class Gui {
80 public FileDialog exportJarFileChooser; 80 public FileDialog exportJarFileChooser;
81 private GuiController controller; 81 private GuiController controller;
82 private JFrame frame; 82 private JFrame frame;
83 public Config.LookAndFeel editorFeel;
84 public PanelEditor editor;
85 public JScrollPane sourceScroller;
86 private JPanel classesPanel; 83 private JPanel classesPanel;
87 private JSplitPane splitClasses; 84 private JSplitPane splitClasses;
88 private PanelIdentifier infoPanel; 85 private PanelIdentifier infoPanel;
89 public Map<TokenHighlightType, BoxHighlightPainter> boxHighlightPainters;
90 private SelectionHighlightPainter selectionHighlightPainter;
91 private JTree inheritanceTree; 86 private JTree inheritanceTree;
92 private JTree implementationsTree; 87 private JTree implementationsTree;
93 private JTree callsTree; 88 private JTree callsTree;
@@ -108,20 +103,9 @@ public class Gui {
108 private JLabel connectionStatusLabel; 103 private JLabel connectionStatusLabel;
109 private JLabel statusLabel; 104 private JLabel statusLabel;
110 105
111 public JTextField renameTextField; 106 private final EditorTabPopupMenu editorTabPopupMenu;
112 public JTextArea javadocTextArea; 107 private final JTabbedPane openFiles;
113 108 private final HashBiMap<ClassEntry, PanelEditor> editors = HashBiMap.create();
114 public void setEditorTheme(Config.LookAndFeel feel) {
115 if (editor != null && (editorFeel == null || editorFeel != feel)) {
116 editor.updateUI();
117 editor.setBackground(new Color(Config.getInstance().editorBackground));
118 if (editorFeel != null) {
119 getController().refreshCurrentClass();
120 }
121
122 editorFeel = feel;
123 }
124 }
125 109
126 public Gui(EnigmaProfile profile) { 110 public Gui(EnigmaProfile profile) {
127 Config.getInstance().lookAndFeel.setGlobalLAF(); 111 Config.getInstance().lookAndFeel.setGlobalLAF();
@@ -144,7 +128,9 @@ public class Gui {
144 128
145 this.controller = new GuiController(this, profile); 129 this.controller = new GuiController(this, profile);
146 130
147 Themes.updateTheme(this); 131 Themes.addListener((lookAndFeel, boxHighlightPainters) -> SwingUtilities.updateComponentTreeUI(getFrame()));
132
133 Themes.updateTheme();
148 134
149 // init file choosers 135 // init file choosers
150 this.jarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.jar.open"), FileDialog.LOAD); 136 this.jarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.jar.open"), FileDialog.LOAD);
@@ -166,20 +152,6 @@ public class Gui {
166 152
167 // init info panel 153 // init info panel
168 infoPanel = new PanelIdentifier(this); 154 infoPanel = new PanelIdentifier(this);
169 infoPanel.clearReference();
170
171 // init editor
172 selectionHighlightPainter = new SelectionHighlightPainter();
173 this.editor = new PanelEditor(this);
174 this.sourceScroller = new JScrollPane(this.editor);
175 this.editor.setContentType("text/enigma-sources");
176 this.editor.setBackground(new Color(Config.getInstance().editorBackground));
177 DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit();
178 kit.toggleComponent(this.editor, "de.sciss.syntaxpane.components.TokenMarker");
179
180 // init editor popup menu
181 this.popupMenu = new PopupMenuBar(this);
182 this.editor.setComponentPopupMenu(this.popupMenu);
183 155
184 // init inheritance panel 156 // init inheritance panel
185 inheritanceTree = new JTree(); 157 inheritanceTree = new JTree();
@@ -269,7 +241,7 @@ public class Gui {
269 } 241 }
270 }); 242 });
271 tokens = new JList<>(); 243 tokens = new JList<>();
272 tokens.setCellRenderer(new TokenListCellRenderer(this.controller)); 244 tokens.setCellRenderer(new TokenListCellRenderer(controller));
273 tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 245 tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
274 tokens.setLayoutOrientation(JList.VERTICAL); 246 tokens.setLayoutOrientation(JList.VERTICAL);
275 tokens.addMouseListener(new MouseAdapter() { 247 tokens.addMouseListener(new MouseAdapter() {
@@ -278,7 +250,7 @@ public class Gui {
278 if (event.getClickCount() == 2) { 250 if (event.getClickCount() == 2) {
279 Token selected = tokens.getSelectedValue(); 251 Token selected = tokens.getSelectedValue();
280 if (selected != null) { 252 if (selected != null) {
281 showToken(selected); 253 openClass(controller.getTokenHandle().getRef()).navigateToToken(selected);
282 } 254 }
283 } 255 }
284 } 256 }
@@ -294,11 +266,25 @@ public class Gui {
294 callPanel.setResizeWeight(1); // let the top side take all the slack 266 callPanel.setResizeWeight(1); // let the top side take all the slack
295 callPanel.resetToPreferredSizes(); 267 callPanel.resetToPreferredSizes();
296 268
269 editorTabPopupMenu = new EditorTabPopupMenu(this);
270 openFiles = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
271 openFiles.addMouseListener(new MouseAdapter() {
272 @Override
273 public void mousePressed(MouseEvent e) {
274 if (SwingUtilities.isRightMouseButton(e)) {
275 int i = openFiles.getUI().tabForCoordinate(openFiles, e.getX(), e.getY());
276 if (i != -1) {
277 editorTabPopupMenu.show(openFiles, e.getX(), e.getY(), PanelEditor.byUi(openFiles.getComponentAt(i)));
278 }
279 }
280 }
281 });
282
297 // layout controls 283 // layout controls
298 JPanel centerPanel = new JPanel(); 284 JPanel centerPanel = new JPanel();
299 centerPanel.setLayout(new BorderLayout()); 285 centerPanel.setLayout(new BorderLayout());
300 centerPanel.add(infoPanel, BorderLayout.NORTH); 286 centerPanel.add(infoPanel.getUi(), BorderLayout.NORTH);
301 centerPanel.add(sourceScroller, BorderLayout.CENTER); 287 centerPanel.add(openFiles, BorderLayout.CENTER);
302 tabs = new JTabbedPane(); 288 tabs = new JTabbedPane();
303 tabs.setPreferredSize(ScaleUtil.getDimension(250, 0)); 289 tabs.setPreferredSize(ScaleUtil.getDimension(250, 0));
304 tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel); 290 tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel);
@@ -389,7 +375,7 @@ public class Gui {
389 this.frame.setTitle(Enigma.NAME + " - " + jarName); 375 this.frame.setTitle(Enigma.NAME + " - " + jarName);
390 this.classesPanel.removeAll(); 376 this.classesPanel.removeAll();
391 this.classesPanel.add(splitClasses); 377 this.classesPanel.add(splitClasses);
392 setEditorText(null); 378 closeAllEditorTabs();
393 379
394 // update menu 380 // update menu
395 isJarOpen = true; 381 isJarOpen = true;
@@ -404,7 +390,7 @@ public class Gui {
404 this.frame.setTitle(Enigma.NAME); 390 this.frame.setTitle(Enigma.NAME);
405 setObfClasses(null); 391 setObfClasses(null);
406 setDeobfClasses(null); 392 setDeobfClasses(null);
407 setEditorText(null); 393 closeAllEditorTabs();
408 this.classesPanel.removeAll(); 394 this.classesPanel.removeAll();
409 395
410 // update menu 396 // update menu
@@ -415,6 +401,54 @@ public class Gui {
415 redraw(); 401 redraw();
416 } 402 }
417 403
404 public PanelEditor openClass(ClassEntry entry) {
405 PanelEditor panelEditor = editors.computeIfAbsent(entry, e -> {
406 ClassHandle ch = controller.getClassHandleProvider().openClass(entry);
407 if (ch == null) return null;
408 PanelEditor ed = new PanelEditor(this);
409 ed.setup();
410 ed.setClassHandle(ch);
411 openFiles.addTab(ed.getFileName(), ed.getUi());
412
413 ClosableTabTitlePane titlePane = new ClosableTabTitlePane(ed.getFileName(), () -> closeEditor(ed));
414 openFiles.setTabComponentAt(openFiles.indexOfComponent(ed.getUi()), titlePane.getUi());
415 titlePane.setTabbedPane(openFiles);
416
417 ed.addListener(new EditorActionListener() {
418 @Override
419 public void onCursorReferenceChanged(PanelEditor editor, EntryReference<Entry<?>, Entry<?>> ref) {
420 updateSelectedReference(editor, ref);
421 }
422
423 @Override
424 public void onClassHandleChanged(PanelEditor editor, ClassEntry old, ClassHandle ch) {
425 editors.remove(old);
426 editors.put(ch.getRef(), editor);
427 }
428
429 @Override
430 public void onTitleChanged(PanelEditor editor, String title) {
431 titlePane.setText(editor.getFileName());
432 }
433 });
434
435 ed.getEditor().addKeyListener(new KeyAdapter() {
436 @Override
437 public void keyPressed(KeyEvent e) {
438 if (e.getKeyCode() == KeyEvent.VK_4 && (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
439 closeEditor(ed);
440 }
441 }
442 });
443
444 return ed;
445 });
446 if (panelEditor != null) {
447 openFiles.setSelectedComponent(editors.get(entry).getUi());
448 }
449 return panelEditor;
450 }
451
418 public void setObfClasses(Collection<ClassEntry> obfClasses) { 452 public void setObfClasses(Collection<ClassEntry> obfClasses) {
419 this.obfPanel.obfClasses.setClasses(obfClasses); 453 this.obfPanel.obfClasses.setClasses(obfClasses);
420 } 454 }
@@ -428,29 +462,49 @@ public class Gui {
428 updateUiState(); 462 updateUiState();
429 } 463 }
430 464
431 public void setEditorText(String source) { 465 public void closeEditor(PanelEditor ed) {
432 this.editor.getHighlighter().removeAllHighlights(); 466 openFiles.remove(ed.getUi());
433 this.editor.setText(source); 467 editors.inverse().remove(ed);
468 ed.destroy();
434 } 469 }
435 470
436 public void setSource(DecompiledClassSource source) { 471 public void closeAllEditorTabs() {
437 editor.setText(source.toString()); 472 for (Iterator<PanelEditor> iter = editors.values().iterator(); iter.hasNext(); ) {
438 setHighlightedTokens(source.getHighlightedTokens()); 473 PanelEditor e = iter.next();
474 openFiles.remove(e.getUi());
475 e.destroy();
476 iter.remove();
477 }
439 } 478 }
440 479
441 public void showToken(final Token token) { 480 public void closeTabsLeftOf(PanelEditor ed) {
442 if (token == null) { 481 int index = openFiles.indexOfComponent(ed.getUi());
443 throw new IllegalArgumentException("Token cannot be null!"); 482 for (int i = index - 1; i >= 0; i--) {
483 closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i)));
484 }
485 }
486
487 public void closeTabsRightOf(PanelEditor ed) {
488 int index = openFiles.indexOfComponent(ed.getUi());
489 for (int i = openFiles.getTabCount() - 1; i > index; i--) {
490 closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i)));
491 }
492 }
493
494 public void closeTabsExcept(PanelEditor ed) {
495 int index = openFiles.indexOfComponent(ed.getUi());
496 for (int i = openFiles.getTabCount() - 1; i >= 0; i--) {
497 if (i == index) continue;
498 closeEditor(PanelEditor.byUi(openFiles.getComponentAt(i)));
444 } 499 }
445 CodeReader.navigateToToken(this.editor, token, selectionHighlightPainter);
446 redraw();
447 } 500 }
448 501
449 public void showTokens(Collection<Token> tokens) { 502 public void showTokens(PanelEditor editor, Collection<Token> tokens) {
450 Vector<Token> sortedTokens = new Vector<>(tokens); 503 Vector<Token> sortedTokens = new Vector<>(tokens);
451 Collections.sort(sortedTokens); 504 Collections.sort(sortedTokens);
452 if (sortedTokens.size() > 1) { 505 if (sortedTokens.size() > 1) {
453 // sort the tokens and update the tokens panel 506 // sort the tokens and update the tokens panel
507 this.controller.setTokenHandle(editor.getClassHandle().copy());
454 this.tokens.setListData(sortedTokens); 508 this.tokens.setListData(sortedTokens);
455 this.tokens.setSelectedIndex(0); 509 this.tokens.setSelectedIndex(0);
456 } else { 510 } else {
@@ -458,311 +512,51 @@ public class Gui {
458 } 512 }
459 513
460 // show the first token 514 // show the first token
461 showToken(sortedTokens.get(0)); 515 editor.navigateToToken(sortedTokens.get(0));
462 } 516 }
463 517
464 public void setHighlightedTokens(Map<TokenHighlightType, Collection<Token>> tokens) { 518 private void updateSelectedReference(PanelEditor editor, EntryReference<Entry<?>, Entry<?>> ref) {
465 // remove any old highlighters 519 if (editor != getActiveEditor()) return;
466 this.editor.getHighlighter().removeAllHighlights();
467 520
468 if (boxHighlightPainters != null) { 521 showCursorReference(ref);
469 for (TokenHighlightType type : tokens.keySet()) {
470 BoxHighlightPainter painter = boxHighlightPainters.get(type);
471 if (painter != null) {
472 setHighlightedTokens(tokens.get(type), painter);
473 }
474 }
475 }
476
477 redraw();
478 }
479
480 private void setHighlightedTokens(Iterable<Token> tokens, Highlighter.HighlightPainter painter) {
481 for (Token token : tokens) {
482 try {
483 this.editor.getHighlighter().addHighlight(token.start, token.end, painter);
484 } catch (BadLocationException ex) {
485 throw new IllegalArgumentException(ex);
486 }
487 }
488 } 522 }
489 523
490 private void showCursorReference(EntryReference<Entry<?>, Entry<?>> reference) { 524 private void showCursorReference(EntryReference<Entry<?>, Entry<?>> reference) {
491 if (reference == null) { 525 infoPanel.setReference(reference == null ? null : reference.entry);
492 infoPanel.clearReference();
493 return;
494 }
495
496 this.cursorReference = reference;
497
498 EntryReference<Entry<?>, Entry<?>> translatedReference = controller.project.getMapper().deobfuscate(reference);
499
500 infoPanel.removeAll();
501 if (translatedReference.entry instanceof ClassEntry) {
502 showClassEntry((ClassEntry) translatedReference.entry);
503 } else if (translatedReference.entry instanceof FieldEntry) {
504 showFieldEntry((FieldEntry) translatedReference.entry);
505 } else if (translatedReference.entry instanceof MethodEntry) {
506 showMethodEntry((MethodEntry) translatedReference.entry);
507 } else if (translatedReference.entry instanceof LocalVariableEntry) {
508 showLocalVariableEntry((LocalVariableEntry) translatedReference.entry);
509 } else {
510 throw new Error("Unknown entry desc: " + translatedReference.entry.getClass().getName());
511 }
512
513 redraw();
514 }
515
516 private void showLocalVariableEntry(LocalVariableEntry entry) {
517 addNameValue(infoPanel, I18n.translate("info_panel.identifier.variable"), entry.getName());
518 addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getContainingClass().getFullName());
519 addNameValue(infoPanel, I18n.translate("info_panel.identifier.method"), entry.getParent().getName());
520 addNameValue(infoPanel, I18n.translate("info_panel.identifier.index"), Integer.toString(entry.getIndex()));
521 }
522
523 private void showClassEntry(ClassEntry entry) {
524 addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getFullName());
525 addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry);
526 }
527
528 private void showFieldEntry(FieldEntry entry) {
529 addNameValue(infoPanel, I18n.translate("info_panel.identifier.field"), entry.getName());
530 addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getParent().getFullName());
531 addNameValue(infoPanel, I18n.translate("info_panel.identifier.type_descriptor"), entry.getDesc().toString());
532 addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry);
533 }
534
535 private void showMethodEntry(MethodEntry entry) {
536 if (entry.isConstructor()) {
537 addNameValue(infoPanel, I18n.translate("info_panel.identifier.constructor"), entry.getParent().getFullName());
538 } else {
539 addNameValue(infoPanel, I18n.translate("info_panel.identifier.method"), entry.getName());
540 addNameValue(infoPanel, I18n.translate("info_panel.identifier.class"), entry.getParent().getFullName());
541 }
542 addNameValue(infoPanel, I18n.translate("info_panel.identifier.method_descriptor"), entry.getDesc().toString());
543 addModifierComboBox(infoPanel, I18n.translate("info_panel.identifier.modifier"), entry);
544 } 526 }
545 527
546 private void addNameValue(JPanel container, String name, String value) { 528 @Nullable
547 JPanel panel = new JPanel(); 529 public PanelEditor getActiveEditor() {
548 panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0)); 530 return PanelEditor.byUi(openFiles.getSelectedComponent());
549
550 JLabel label = new JLabel(name + ":", JLabel.RIGHT);
551 label.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height)));
552 panel.add(label);
553
554 panel.add(GuiUtil.unboldLabel(new JLabel(value, JLabel.LEFT)));
555
556 container.add(panel);
557 } 531 }
558 532
559 private JComboBox<AccessModifier> addModifierComboBox(JPanel container, String name, Entry<?> entry) { 533 @Nullable
560 if (!getController().project.isRenamable(entry)) 534 public EntryReference<Entry<?>, Entry<?>> getCursorReference() {
561 return null; 535 PanelEditor activeEditor = getActiveEditor();
562 JPanel panel = new JPanel(); 536 return activeEditor == null ? null : activeEditor.getCursorReference();
563 panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0));
564 JLabel label = new JLabel(name + ":", JLabel.RIGHT);
565 label.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height)));
566 panel.add(label);
567 JComboBox<AccessModifier> combo = new JComboBox<>(AccessModifier.values());
568 ((JLabel) combo.getRenderer()).setHorizontalAlignment(JLabel.LEFT);
569 combo.setPreferredSize(ScaleUtil.getDimension(100, ScaleUtil.invert(label.getPreferredSize().height)));
570
571 EntryMapping mapping = controller.project.getMapper().getDeobfMapping(entry);
572 if (mapping != null) {
573 combo.setSelectedIndex(mapping.getAccessModifier().ordinal());
574 } else {
575 combo.setSelectedIndex(AccessModifier.UNCHANGED.ordinal());
576 }
577
578 combo.addItemListener(controller::modifierChange);
579
580 panel.add(combo);
581
582 container.add(panel);
583
584 return combo;
585 } 537 }
586 538
587 public void onCaretMove(int pos, boolean fromClick) { 539 public void startDocChange(PanelEditor editor) {
588 if (controller.project == null) 540 EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference();
589 return; 541 if (cursorReference == null) return;
590 EntryRemapper mapper = controller.project.getMapper(); 542 JavadocDialog.show(frame, getController(), cursorReference);
591 Token token = this.controller.getToken(pos);
592 boolean isToken = token != null;
593
594 cursorReference = this.controller.getReference(token);
595 Entry<?> referenceEntry = cursorReference != null ? cursorReference.entry : null;
596
597 if (referenceEntry != null && shouldNavigateOnClick && fromClick) {
598 shouldNavigateOnClick = false;
599 Entry<?> navigationEntry = referenceEntry;
600 if (cursorReference.context == null) {
601 EntryResolver resolver = mapper.getObfResolver();
602 navigationEntry = resolver.resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT);
603 }
604 controller.navigateTo(navigationEntry);
605 return;
606 }
607
608 boolean isClassEntry = isToken && referenceEntry instanceof ClassEntry;
609 boolean isFieldEntry = isToken && referenceEntry instanceof FieldEntry;
610 boolean isMethodEntry = isToken && referenceEntry instanceof MethodEntry && !((MethodEntry) referenceEntry).isConstructor();
611 boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor();
612 boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference);
613
614 if (!isRenaming()) {
615 if (isToken) {
616 showCursorReference(cursorReference);
617 } else {
618 infoPanel.clearReference();
619 }
620 }
621
622 this.popupMenu.renameMenu.setEnabled(isRenamable);
623 this.popupMenu.editJavadocMenu.setEnabled(isRenamable);
624 this.popupMenu.showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry);
625 this.popupMenu.showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry);
626 this.popupMenu.showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry);
627 this.popupMenu.showCallsSpecificMenu.setEnabled(isMethodEntry);
628 this.popupMenu.openEntryMenu.setEnabled(isRenamable && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry));
629 this.popupMenu.openPreviousMenu.setEnabled(this.controller.hasPreviousReference());
630 this.popupMenu.openNextMenu.setEnabled(this.controller.hasNextReference());
631 this.popupMenu.toggleMappingMenu.setEnabled(isRenamable);
632
633 if (isToken && !Objects.equals(referenceEntry, mapper.deobfuscate(referenceEntry))) {
634 this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.reset_obfuscated"));
635 } else {
636 this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.mark_deobfuscated"));
637 }
638 }
639
640 public void startDocChange() {
641 EntryReference<Entry<?>, Entry<?>> curReference = cursorReference;
642 if (isRenaming()) {
643 finishRename(false);
644 }
645 renamingReference = curReference;
646
647 // init the text box
648 javadocTextArea = new JTextArea(10, 40);
649
650 EntryReference<Entry<?>, Entry<?>> translatedReference = controller.project.getMapper().deobfuscate(cursorReference);
651 javadocTextArea.setText(Strings.nullToEmpty(translatedReference.entry.getJavadocs()));
652
653 JavadocDialog.init(frame, javadocTextArea, this::finishDocChange);
654 javadocTextArea.grabFocus();
655
656 redraw();
657 }
658
659 private void finishDocChange(JFrame ui, boolean saveName) {
660 String newName = javadocTextArea.getText();
661 if (saveName) {
662 try {
663 this.controller.changeDocs(renamingReference, newName);
664 this.controller.sendPacket(new ChangeDocsC2SPacket(renamingReference.getNameableEntry(), newName));
665 } catch (IllegalNameException ex) {
666 javadocTextArea.setBorder(BorderFactory.createLineBorder(Color.red, 1));
667 javadocTextArea.setToolTipText(ex.getReason());
668 GuiUtil.showToolTipNow(javadocTextArea);
669 return;
670 }
671
672 ui.setVisible(false);
673 showCursorReference(cursorReference);
674 return;
675 }
676
677 // abort the jd change
678 javadocTextArea = null;
679 ui.setVisible(false);
680 showCursorReference(cursorReference);
681
682 this.editor.grabFocus();
683
684 redraw();
685 } 543 }
686 544
687 public void startRename() { 545 public void startRename(PanelEditor editor, String text) {
546 if (editor != getActiveEditor()) return;
688 547
689 // init the text box 548 infoPanel.startRenaming(text);
690 renameTextField = new JTextField();
691
692 EntryReference<Entry<?>, Entry<?>> translatedReference = controller.project.getMapper().deobfuscate(cursorReference);
693 renameTextField.setText(translatedReference.getNameableName());
694
695 renameTextField.setPreferredSize(ScaleUtil.getDimension(360, ScaleUtil.invert(renameTextField.getPreferredSize().height)));
696 renameTextField.addKeyListener(new KeyAdapter() {
697 @Override
698 public void keyPressed(KeyEvent event) {
699 switch (event.getKeyCode()) {
700 case KeyEvent.VK_ENTER:
701 finishRename(true);
702 break;
703
704 case KeyEvent.VK_ESCAPE:
705 finishRename(false);
706 break;
707 default:
708 break;
709 }
710 }
711 });
712
713 // find the label with the name and replace it with the text box
714 JPanel panel = (JPanel) infoPanel.getComponent(0);
715 panel.remove(panel.getComponentCount() - 1);
716 panel.add(renameTextField);
717 renameTextField.grabFocus();
718
719 int offset = renameTextField.getText().lastIndexOf('/') + 1;
720 // If it's a class and isn't in the default package, assume that it's deobfuscated.
721 if (translatedReference.getNameableEntry() instanceof ClassEntry && renameTextField.getText().contains("/") && offset != 0)
722 renameTextField.select(offset, renameTextField.getText().length());
723 else
724 renameTextField.selectAll();
725
726 renamingReference = cursorReference;
727
728 redraw();
729 } 549 }
730 550
731 private void finishRename(boolean saveName) { 551 public void startRename(PanelEditor editor) {
732 String newName = renameTextField.getText(); 552 if (editor != getActiveEditor()) return;
733
734 if (saveName && newName != null && !newName.isEmpty()) {
735 try {
736 this.controller.rename(renamingReference, newName, true);
737 this.controller.sendPacket(new RenameC2SPacket(renamingReference.getNameableEntry(), newName, true));
738 renameTextField = null;
739 } catch (IllegalNameException ex) {
740 renameTextField.setBorder(BorderFactory.createLineBorder(Color.red, 1));
741 renameTextField.setToolTipText(ex.getReason());
742 GuiUtil.showToolTipNow(renameTextField);
743 }
744 return;
745 }
746
747 renameTextField = null;
748 553
749 // abort the rename 554 infoPanel.startRenaming();
750 showCursorReference(cursorReference);
751
752 this.editor.grabFocus();
753
754 redraw();
755 } 555 }
756 556
757 private boolean isRenaming() { 557 public void showInheritance(PanelEditor editor) {
758 return renameTextField != null; 558 EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference();
759 } 559 if (cursorReference == null) return;
760
761 public void showInheritance() {
762
763 if (cursorReference == null) {
764 return;
765 }
766 560
767 inheritanceTree.setModel(null); 561 inheritanceTree.setModel(null);
768 562
@@ -791,11 +585,9 @@ public class Gui {
791 redraw(); 585 redraw();
792 } 586 }
793 587
794 public void showImplementations() { 588 public void showImplementations(PanelEditor editor) {
795 589 EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference();
796 if (cursorReference == null) { 590 if (cursorReference == null) return;
797 return;
798 }
799 591
800 implementationsTree.setModel(null); 592 implementationsTree.setModel(null);
801 593
@@ -821,10 +613,9 @@ public class Gui {
821 redraw(); 613 redraw();
822 } 614 }
823 615
824 public void showCalls(boolean recurse) { 616 public void showCalls(PanelEditor editor, boolean recurse) {
825 if (cursorReference == null) { 617 EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference();
826 return; 618 if (cursorReference == null) return;
827 }
828 619
829 if (cursorReference.entry instanceof ClassEntry) { 620 if (cursorReference.entry instanceof ClassEntry) {
830 ClassReferenceTreeNode node = this.controller.getClassReferences((ClassEntry) cursorReference.entry); 621 ClassReferenceTreeNode node = this.controller.getClassReferences((ClassEntry) cursorReference.entry);
@@ -842,15 +633,18 @@ public class Gui {
842 redraw(); 633 redraw();
843 } 634 }
844 635
845 public void toggleMapping() { 636 public void toggleMapping(PanelEditor editor) {
637 EntryReference<Entry<?>, Entry<?>> cursorReference = editor.getCursorReference();
638 if (cursorReference == null) return;
639
846 Entry<?> obfEntry = cursorReference.entry; 640 Entry<?> obfEntry = cursorReference.entry;
847 Entry<?> deobfEntry = controller.project.getMapper().deobfuscate(obfEntry); 641 Entry<?> deobfEntry = controller.project.getMapper().deobfuscate(obfEntry);
848 642
849 if (!Objects.equals(obfEntry, deobfEntry)) { 643 if (!Objects.equals(obfEntry, deobfEntry)) {
850 this.controller.removeMapping(cursorReference); 644 if (!validateImmediateAction(vc -> this.controller.removeMapping(vc, cursorReference))) return;
851 this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry())); 645 this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry()));
852 } else { 646 } else {
853 this.controller.markAsDeobfuscated(cursorReference); 647 if (!validateImmediateAction(vc -> this.controller.markAsDeobfuscated(vc, cursorReference))) return;
854 this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry())); 648 this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry()));
855 } 649 }
856 } 650 }
@@ -909,37 +703,51 @@ public class Gui {
909 this.frame.repaint(); 703 this.frame.repaint();
910 } 704 }
911 705
912 public void onPanelRename(Object prevData, Object data, DefaultMutableTreeNode node) throws IllegalNameException { 706 public void onPanelRename(ValidationContext vc, Object prevData, Object data, DefaultMutableTreeNode node) {
913 // package rename
914 if (data instanceof String) { 707 if (data instanceof String) {
708 // package rename
915 for (int i = 0; i < node.getChildCount(); i++) { 709 for (int i = 0; i < node.getChildCount(); i++) {
916 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt(i); 710 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt(i);
917 ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject(); 711 ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject();
918 ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName()); 712 ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName());
919 this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false); 713 this.controller.rename(vc, new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false);
714 if (!vc.canProceed()) return;
920 this.controller.sendPacket(new RenameC2SPacket(prevDataChild, dataChild.getFullName(), false)); 715 this.controller.sendPacket(new RenameC2SPacket(prevDataChild, dataChild.getFullName(), false));
921 childNode.setUserObject(dataChild); 716 childNode.setUserObject(dataChild);
922 } 717 }
923 node.setUserObject(data); 718 node.setUserObject(data);
924 // Ob package will never be modified, just reload deob view 719 // Ob package will never be modified, just reload deob view
925 this.deobfPanel.deobfClasses.reload(); 720 this.deobfPanel.deobfClasses.reload();
926 } 721 } else if (data instanceof ClassEntry) {
927 // class rename 722 // class rename
928 else if (data instanceof ClassEntry) { 723
929 this.controller.rename(new EntryReference<>((ClassEntry) prevData, ((ClassEntry) prevData).getFullName()), ((ClassEntry) data).getFullName(), false); 724 // assume this is deobf since the obf tree doesn't allow renaming in
930 this.controller.sendPacket(new RenameC2SPacket((ClassEntry) prevData, ((ClassEntry) data).getFullName(), false)); 725 // the first place
726 // TODO optimize reverse class lookup, although it looks like it's
727 // fast enough for now
728 EntryRemapper mapper = this.controller.project.getMapper();
729 ClassEntry deobf = (ClassEntry) prevData;
730 ClassEntry obf = mapper.getObfToDeobf().getAllEntries()
731 .filter(e -> e instanceof ClassEntry)
732 .map(e -> (ClassEntry) e)
733 .filter(e -> mapper.deobfuscate(e).equals(deobf))
734 .findAny().get();
735
736 this.controller.rename(vc, new EntryReference<>(obf, obf.getFullName()), ((ClassEntry) data).getFullName(), false);
737 if (!vc.canProceed()) return;
738 this.controller.sendPacket(new RenameC2SPacket(obf, ((ClassEntry) data).getFullName(), false));
931 } 739 }
932 } 740 }
933 741
934 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, String newName) { 742 public void moveClassTree(Entry<?> obfEntry, String newName) {
935 String oldEntry = obfReference.entry.getContainingClass().getPackageName(); 743 String oldEntry = obfEntry.getContainingClass().getPackageName();
936 String newEntry = new ClassEntry(newName).getPackageName(); 744 String newEntry = new ClassEntry(newName).getPackageName();
937 moveClassTree(obfReference, oldEntry == null, newEntry == null); 745 moveClassTree(obfEntry, oldEntry == null, newEntry == null);
938 } 746 }
939 747
940 // TODO: getExpansionState will *not* actually update itself based on name changes! 748 // TODO: getExpansionState will *not* actually update itself based on name changes!
941 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, boolean isOldOb, boolean isNewOb) { 749 public void moveClassTree(Entry<?> obfEntry, boolean isOldOb, boolean isNewOb) {
942 ClassEntry classEntry = obfReference.entry.getContainingClass(); 750 ClassEntry classEntry = obfEntry.getContainingClass();
943 751
944 List<ClassSelector.StateEntry> stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(this.deobfPanel.deobfClasses); 752 List<ClassSelector.StateEntry> stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(this.deobfPanel.deobfClasses);
945 List<ClassSelector.StateEntry> stateObf = this.obfPanel.obfClasses.getExpansionState(this.obfPanel.obfClasses); 753 List<ClassSelector.StateEntry> stateObf = this.obfPanel.obfClasses.getExpansionState(this.obfPanel.obfClasses);
@@ -979,10 +787,6 @@ public class Gui {
979 return deobfPanel; 787 return deobfPanel;
980 } 788 }
981 789
982 public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) {
983 this.shouldNavigateOnClick = shouldNavigateOnClick;
984 }
985
986 public SearchDialog getSearchDialog() { 790 public SearchDialog getSearchDialog() {
987 if (searchDialog == null) { 791 if (searchDialog == null) {
988 searchDialog = new SearchDialog(this); 792 searchDialog = new SearchDialog(this);
@@ -1052,4 +856,19 @@ public class Gui {
1052 return this.connectionState; 856 return this.connectionState;
1053 } 857 }
1054 858
859 public PanelIdentifier getInfoPanel() {
860 return infoPanel;
861 }
862
863 public boolean validateImmediateAction(Consumer<ValidationContext> op) {
864 ValidationContext vc = new ValidationContext();
865 op.accept(vc);
866 if (!vc.canProceed()) {
867 List<ParameterizedMessage> messages = vc.getMessages();
868 String text = ValidatableUi.formatMessages(messages);
869 JOptionPane.showMessageDialog(this.getFrame(), text, String.format("%d message(s)", messages.size()), JOptionPane.ERROR_MESSAGE);
870 }
871 return vc.canProceed();
872 }
873
1055} 874}
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 @@
11 11
12package cuchaz.enigma.gui; 12package cuchaz.enigma.gui;
13 13
14import java.awt.Desktop;
15import java.io.File;
16import java.io.FileWriter;
17import java.io.IOException;
18import java.nio.file.Path;
19import java.util.Collection;
20import java.util.List;
21import java.util.Set;
22import java.util.concurrent.CompletableFuture;
23import java.util.concurrent.ExecutionException;
24import java.util.stream.Collectors;
25import java.util.stream.Stream;
26
27import javax.swing.JOptionPane;
28import javax.swing.SwingUtilities;
29
14import com.google.common.collect.Lists; 30import com.google.common.collect.Lists;
15import com.google.common.util.concurrent.ThreadFactoryBuilder;
16import cuchaz.enigma.Enigma; 31import cuchaz.enigma.Enigma;
17import cuchaz.enigma.EnigmaProfile; 32import cuchaz.enigma.EnigmaProfile;
18import cuchaz.enigma.EnigmaProject; 33import cuchaz.enigma.EnigmaProject;
19import cuchaz.enigma.analysis.*; 34import cuchaz.enigma.analysis.*;
20import cuchaz.enigma.api.service.ObfuscationTestService; 35import cuchaz.enigma.api.service.ObfuscationTestService;
36import cuchaz.enigma.classhandle.ClassHandle;
37import cuchaz.enigma.classhandle.ClassHandleProvider;
21import cuchaz.enigma.gui.config.Config; 38import cuchaz.enigma.gui.config.Config;
22import cuchaz.enigma.gui.dialog.ProgressDialog; 39import cuchaz.enigma.gui.dialog.ProgressDialog;
23import cuchaz.enigma.gui.stats.StatsGenerator; 40import cuchaz.enigma.gui.stats.StatsGenerator;
24import cuchaz.enigma.gui.stats.StatsMember; 41import cuchaz.enigma.gui.stats.StatsMember;
25import cuchaz.enigma.gui.util.GuiUtil;
26import cuchaz.enigma.gui.util.History; 42import cuchaz.enigma.gui.util.History;
27import cuchaz.enigma.network.*; 43import cuchaz.enigma.network.*;
28import cuchaz.enigma.network.packet.LoginC2SPacket; 44import cuchaz.enigma.network.packet.LoginC2SPacket;
29import cuchaz.enigma.network.packet.Packet; 45import cuchaz.enigma.network.packet.Packet;
30import cuchaz.enigma.source.*; 46import cuchaz.enigma.source.DecompiledClassSource;
31import cuchaz.enigma.translation.mapping.serde.MappingParseException; 47import cuchaz.enigma.source.DecompilerService;
48import cuchaz.enigma.source.SourceIndex;
49import cuchaz.enigma.source.Token;
32import cuchaz.enigma.translation.Translator; 50import cuchaz.enigma.translation.Translator;
33import cuchaz.enigma.translation.mapping.*; 51import cuchaz.enigma.translation.mapping.*;
34import cuchaz.enigma.translation.mapping.serde.MappingFormat; 52import cuchaz.enigma.translation.mapping.serde.MappingFormat;
53import cuchaz.enigma.translation.mapping.serde.MappingParseException;
35import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; 54import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters;
36import cuchaz.enigma.translation.mapping.tree.EntryTree; 55import cuchaz.enigma.translation.mapping.tree.EntryTree;
37import cuchaz.enigma.translation.mapping.tree.HashEntryTree; 56import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
@@ -41,44 +60,21 @@ import cuchaz.enigma.translation.representation.entry.FieldEntry;
41import cuchaz.enigma.translation.representation.entry.MethodEntry; 60import cuchaz.enigma.translation.representation.entry.MethodEntry;
42import cuchaz.enigma.utils.I18n; 61import cuchaz.enigma.utils.I18n;
43import cuchaz.enigma.utils.Utils; 62import cuchaz.enigma.utils.Utils;
44 63import cuchaz.enigma.utils.validation.ValidationContext;
45import javax.annotation.Nullable;
46import javax.swing.JOptionPane;
47import javax.swing.SwingUtilities;
48import java.awt.*;
49import java.awt.event.ItemEvent;
50import java.io.*;
51import java.nio.file.Path;
52import java.util.Collection;
53import java.util.List;
54import java.util.Set;
55import java.util.concurrent.CompletableFuture;
56import java.util.concurrent.ExecutorService;
57import java.util.concurrent.Executors;
58import java.util.stream.Collectors;
59import java.util.stream.Stream;
60 64
61public class GuiController implements ClientPacketHandler { 65public class GuiController implements ClientPacketHandler {
62 private static final ExecutorService DECOMPILER_SERVICE = Executors.newSingleThreadExecutor(
63 new ThreadFactoryBuilder()
64 .setDaemon(true)
65 .setNameFormat("decompiler-thread")
66 .build()
67 );
68
69 private final Gui gui; 66 private final Gui gui;
70 public final Enigma enigma; 67 public final Enigma enigma;
71 68
72 public EnigmaProject project; 69 public EnigmaProject project;
73 private DecompilerService decompilerService;
74 private Decompiler decompiler;
75 private IndexTreeBuilder indexTreeBuilder; 70 private IndexTreeBuilder indexTreeBuilder;
76 71
77 private Path loadedMappingPath; 72 private Path loadedMappingPath;
78 private MappingFormat loadedMappingFormat; 73 private MappingFormat loadedMappingFormat;
79 74
80 private DecompiledClassSource currentSource; 75 private ClassHandleProvider chp;
81 private Source uncommentedSource; 76
77 private ClassHandle tokenHandle;
82 78
83 private EnigmaClient client; 79 private EnigmaClient client;
84 private EnigmaServer server; 80 private EnigmaServer server;
@@ -88,8 +84,6 @@ public class GuiController implements ClientPacketHandler {
88 this.enigma = Enigma.builder() 84 this.enigma = Enigma.builder()
89 .setProfile(profile) 85 .setProfile(profile)
90 .build(); 86 .build();
91
92 decompilerService = Config.getInstance().decompiler.service;
93 } 87 }
94 88
95 public boolean isDirty() { 89 public boolean isDirty() {
@@ -102,13 +96,15 @@ public class GuiController implements ClientPacketHandler {
102 return ProgressDialog.runOffThread(gui.getFrame(), progress -> { 96 return ProgressDialog.runOffThread(gui.getFrame(), progress -> {
103 project = enigma.openJar(jarPath, progress); 97 project = enigma.openJar(jarPath, progress);
104 indexTreeBuilder = new IndexTreeBuilder(project.getJarIndex()); 98 indexTreeBuilder = new IndexTreeBuilder(project.getJarIndex());
105 decompiler = project.createDecompiler(decompilerService); 99 chp = new ClassHandleProvider(project, Config.getInstance().decompiler.service);
106 gui.onFinishOpenJar(jarPath.getFileName().toString()); 100 gui.onFinishOpenJar(jarPath.getFileName().toString());
107 refreshClasses(); 101 refreshClasses();
108 }); 102 });
109 } 103 }
110 104
111 public void closeJar() { 105 public void closeJar() {
106 this.chp.destroy();
107 this.chp = null;
112 this.project = null; 108 this.project = null;
113 this.gui.onCloseJar(); 109 this.gui.onCloseJar();
114 } 110 }
@@ -129,7 +125,7 @@ public class GuiController implements ClientPacketHandler {
129 loadedMappingPath = path; 125 loadedMappingPath = path;
130 126
131 refreshClasses(); 127 refreshClasses();
132 refreshCurrentClass(); 128 chp.invalidateMapped();
133 } catch (MappingParseException e) { 129 } catch (MappingParseException e) {
134 JOptionPane.showMessageDialog(gui.getFrame(), e.getMessage()); 130 JOptionPane.showMessageDialog(gui.getFrame(), e.getMessage());
135 } 131 }
@@ -142,7 +138,7 @@ public class GuiController implements ClientPacketHandler {
142 138
143 project.setMappings(mappings); 139 project.setMappings(mappings);
144 refreshClasses(); 140 refreshClasses();
145 refreshCurrentClass(); 141 chp.invalidateMapped();
146 } 142 }
147 143
148 public CompletableFuture<Void> saveMappings(Path path) { 144 public CompletableFuture<Void> saveMappings(Path path) {
@@ -177,7 +173,7 @@ public class GuiController implements ClientPacketHandler {
177 173
178 this.gui.setMappingsFile(null); 174 this.gui.setMappingsFile(null);
179 refreshClasses(); 175 refreshClasses();
180 refreshCurrentClass(); 176 chp.invalidateMapped();
181 } 177 }
182 178
183 public CompletableFuture<Void> dropMappings() { 179 public CompletableFuture<Void> dropMappings() {
@@ -191,7 +187,7 @@ public class GuiController implements ClientPacketHandler {
191 187
192 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> { 188 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> {
193 EnigmaProject.JarExport jar = project.exportRemappedJar(progress); 189 EnigmaProject.JarExport jar = project.exportRemappedJar(progress);
194 EnigmaProject.SourceExport source = jar.decompile(progress, decompilerService); 190 EnigmaProject.SourceExport source = jar.decompile(progress, chp.getDecompilerService());
195 191
196 source.write(path, progress); 192 source.write(path, progress);
197 }); 193 });
@@ -206,32 +202,34 @@ public class GuiController implements ClientPacketHandler {
206 }); 202 });
207 } 203 }
208 204
209 public Token getToken(int pos) { 205 public void setTokenHandle(ClassHandle handle) {
210 if (this.currentSource == null) { 206 if (tokenHandle != null) {
211 return null; 207 tokenHandle.close();
212 } 208 }
213 return this.currentSource.getIndex().getReferenceToken(pos); 209
210 tokenHandle = handle;
214 } 211 }
215 212
216 @Nullable 213 public ClassHandle getTokenHandle() {
217 public EntryReference<Entry<?>, Entry<?>> getReference(Token token) { 214 return tokenHandle;
218 if (this.currentSource == null) {
219 return null;
220 }
221 return this.currentSource.getIndex().getReference(token);
222 } 215 }
223 216
224 public ReadableToken getReadableToken(Token token) { 217 public ReadableToken getReadableToken(Token token) {
225 if (this.currentSource == null) { 218 if (tokenHandle == null) {
226 return null; 219 return null;
227 } 220 }
228 221
229 SourceIndex index = this.currentSource.getIndex(); 222 try {
230 return new ReadableToken( 223 return tokenHandle.getSource().get()
231 index.getLineNumber(token.start), 224 .map(DecompiledClassSource::getIndex)
232 index.getColumnNumber(token.start), 225 .map(index -> new ReadableToken(
233 index.getColumnNumber(token.end) 226 index.getLineNumber(token.start),
234 ); 227 index.getColumnNumber(token.start),
228 index.getColumnNumber(token.end)))
229 .unwrapOr(null);
230 } catch (InterruptedException | ExecutionException e) {
231 throw new RuntimeException(e);
232 }
235 } 233 }
236 234
237 /** 235 /**
@@ -271,39 +269,13 @@ public class GuiController implements ClientPacketHandler {
271 * @param reference the reference 269 * @param reference the reference
272 */ 270 */
273 private void setReference(EntryReference<Entry<?>, Entry<?>> reference) { 271 private void setReference(EntryReference<Entry<?>, Entry<?>> reference) {
274 // get the reference target class 272 gui.openClass(reference.getLocationClassEntry()).showReference(reference);
275 ClassEntry classEntry = reference.getLocationClassEntry();
276 if (!project.isRenamable(classEntry)) {
277 throw new IllegalArgumentException("Obfuscated class " + classEntry + " was not found in the jar!");
278 }
279
280 if (this.currentSource == null || !this.currentSource.getEntry().equals(classEntry)) {
281 // deobfuscate the class, then navigate to the reference
282 loadClass(classEntry, () -> showReference(reference));
283 } else {
284 showReference(reference);
285 }
286 }
287
288 /**
289 * Navigates to the reference without modifying history. Assumes the class is loaded.
290 *
291 * @param reference
292 */
293 private void showReference(EntryReference<Entry<?>, Entry<?>> reference) {
294 Collection<Token> tokens = getTokensForReference(reference);
295 if (tokens.isEmpty()) {
296 // DEBUG
297 System.err.println(String.format("WARNING: no tokens found for %s in %s", reference, this.currentSource.getEntry()));
298 } else {
299 this.gui.showTokens(tokens);
300 }
301 } 273 }
302 274
303 public Collection<Token> getTokensForReference(EntryReference<Entry<?>, Entry<?>> reference) { 275 public Collection<Token> getTokensForReference(DecompiledClassSource source, EntryReference<Entry<?>, Entry<?>> reference) {
304 EntryRemapper mapper = this.project.getMapper(); 276 EntryRemapper mapper = this.project.getMapper();
305 277
306 SourceIndex index = this.currentSource.getIndex(); 278 SourceIndex index = source.getIndex();
307 return mapper.getObfResolver().resolveReference(reference, ResolutionStrategy.RESOLVE_CLOSEST) 279 return mapper.getObfResolver().resolveReference(reference, ResolutionStrategy.RESOLVE_CLOSEST)
308 .stream() 280 .stream()
309 .flatMap(r -> index.getReferenceTokens(r).stream()) 281 .flatMap(r -> index.getReferenceTokens(r).stream())
@@ -380,131 +352,17 @@ public class GuiController implements ClientPacketHandler {
380 }); 352 });
381 } 353 }
382 354
383 public void refreshCurrentClass() { 355 public void onModifierChanged(ValidationContext vc, Entry<?> entry, AccessModifier modifier) {
384 refreshCurrentClass(null); 356 EntryRemapper mapper = project.getMapper();
385 }
386
387 private void refreshCurrentClass(EntryReference<Entry<?>, Entry<?>> reference) {
388 refreshCurrentClass(reference, RefreshMode.MINIMAL);
389 }
390
391 private void refreshCurrentClass(EntryReference<Entry<?>, Entry<?>> reference, RefreshMode mode) {
392 if (currentSource != null) {
393 if (reference == null) {
394 int obfSelectionStart = currentSource.getObfuscatedOffset(gui.editor.getSelectionStart());
395 int obfSelectionEnd = currentSource.getObfuscatedOffset(gui.editor.getSelectionEnd());
396
397 Rectangle viewportBounds = gui.sourceScroller.getViewport().getViewRect();
398 // 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
399 int anchorModelPos = gui.editor.getSelectionStart();
400 Rectangle anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos);
401 if (anchorViewPos.y < viewportBounds.y || anchorViewPos.y >= viewportBounds.y + viewportBounds.height) {
402 anchorModelPos = gui.editor.viewToModel(new Point(0, viewportBounds.y));
403 anchorViewPos = GuiUtil.safeModelToView(gui.editor, anchorModelPos);
404 }
405 int obfAnchorPos = currentSource.getObfuscatedOffset(anchorModelPos);
406 Rectangle anchorViewPos_f = anchorViewPos;
407 int scrollX = gui.sourceScroller.getHorizontalScrollBar().getValue();
408
409 loadClass(currentSource.getEntry(), () -> SwingUtilities.invokeLater(() -> {
410 int newAnchorModelPos = currentSource.getDeobfuscatedOffset(obfAnchorPos);
411 Rectangle newAnchorViewPos = GuiUtil.safeModelToView(gui.editor, newAnchorModelPos);
412 int newScrollY = newAnchorViewPos.y - (anchorViewPos_f.y - viewportBounds.y);
413
414 gui.editor.select(currentSource.getDeobfuscatedOffset(obfSelectionStart), currentSource.getDeobfuscatedOffset(obfSelectionEnd));
415 // Changing the selection scrolls to the caret position inside a SwingUtilities.invokeLater call, so
416 // we need to wrap our change to the scroll position inside another invokeLater so it happens after
417 // the caret's own scrolling.
418 SwingUtilities.invokeLater(() -> {
419 gui.sourceScroller.getHorizontalScrollBar().setValue(Math.min(scrollX, gui.sourceScroller.getHorizontalScrollBar().getMaximum()));
420 gui.sourceScroller.getVerticalScrollBar().setValue(Math.min(newScrollY, gui.sourceScroller.getVerticalScrollBar().getMaximum()));
421 });
422 }), mode);
423 } else {
424 loadClass(currentSource.getEntry(), () -> showReference(reference), mode);
425 }
426 }
427 }
428
429 private void loadClass(ClassEntry classEntry, Runnable callback) {
430 loadClass(classEntry, callback, RefreshMode.MINIMAL);
431 }
432
433 private void loadClass(ClassEntry classEntry, Runnable callback, RefreshMode mode) {
434 ClassEntry targetClass = classEntry.getOutermostClass();
435
436 boolean requiresDecompile = mode == RefreshMode.FULL || currentSource == null || !currentSource.getEntry().equals(targetClass);
437 if (requiresDecompile) {
438 currentSource = null; // Or the GUI may try to find a nonexistent token
439 gui.setEditorText(I18n.translate("info_panel.editor.class.decompiling"));
440 }
441
442 DECOMPILER_SERVICE.submit(() -> {
443 try {
444 if (requiresDecompile || mode == RefreshMode.JAVADOCS) {
445 currentSource = decompileSource(targetClass, mode == RefreshMode.JAVADOCS);
446 }
447
448 remapSource(project.getMapper().getDeobfuscator());
449 callback.run();
450 } catch (Throwable t) {
451 System.err.println("An exception was thrown while decompiling class " + classEntry.getFullName());
452 t.printStackTrace(System.err);
453 }
454 });
455 }
456
457 private DecompiledClassSource decompileSource(ClassEntry targetClass, boolean onlyRefreshJavadocs) {
458 try {
459 if (!onlyRefreshJavadocs || currentSource == null || !currentSource.getEntry().equals(targetClass)) {
460 uncommentedSource = decompiler.getSource(targetClass.getFullName());
461 }
462
463 Source source = uncommentedSource.addJavadocs(project.getMapper());
464
465 if (source == null) {
466 gui.setEditorText(I18n.translate("info_panel.editor.class.not_found") + " " + targetClass);
467 return DecompiledClassSource.text(targetClass, "Unable to find class");
468 }
469
470 SourceIndex index = source.index();
471 index.resolveReferences(project.getMapper().getObfResolver());
472
473 return new DecompiledClassSource(targetClass, index);
474 } catch (Throwable t) {
475 StringWriter traceWriter = new StringWriter();
476 t.printStackTrace(new PrintWriter(traceWriter));
477
478 return DecompiledClassSource.text(targetClass, traceWriter.toString());
479 }
480 }
481 357
482 private void remapSource(Translator translator) { 358 EntryMapping mapping = mapper.getDeobfMapping(entry);
483 if (currentSource == null) { 359 if (mapping != null) {
484 return; 360 mapper.mapFromObf(vc, entry, new EntryMapping(mapping.getTargetName(), modifier));
361 } else {
362 mapper.mapFromObf(vc, entry, new EntryMapping(entry.getName(), modifier));
485 } 363 }
486 364
487 currentSource.remapSource(project, translator); 365 chp.invalidateMapped();
488
489 gui.setEditorTheme(Config.getInstance().lookAndFeel);
490 gui.setSource(currentSource);
491 }
492
493 public void modifierChange(ItemEvent event) {
494 if (event.getStateChange() == ItemEvent.SELECTED) {
495 EntryRemapper mapper = project.getMapper();
496 Entry<?> entry = gui.cursorReference.entry;
497 AccessModifier modifier = (AccessModifier) event.getItem();
498
499 EntryMapping mapping = mapper.getDeobfMapping(entry);
500 if (mapping != null) {
501 mapper.mapFromObf(entry, new EntryMapping(mapping.getTargetName(), modifier));
502 } else {
503 mapper.mapFromObf(entry, new EntryMapping(entry.getName(), modifier));
504 }
505
506 refreshCurrentClass();
507 }
508 } 366 }
509 367
510 public ClassInheritanceTreeNode getClassInheritance(ClassEntry entry) { 368 public ClassInheritanceTreeNode getClassInheritance(ClassEntry entry) {
@@ -557,72 +415,71 @@ public class GuiController implements ClientPacketHandler {
557 return rootNode; 415 return rootNode;
558 } 416 }
559 417
560 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree) { 418 @Override
561 rename(reference, newName, refreshClassTree, true); 419 public void rename(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree) {
420 rename(vc, reference, newName, refreshClassTree, false);
562 } 421 }
563 422
564 @Override 423 public void rename(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean validateOnly) {
565 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean jumpToReference) {
566 Entry<?> entry = reference.getNameableEntry(); 424 Entry<?> entry = reference.getNameableEntry();
567 project.getMapper().mapFromObf(entry, new EntryMapping(newName)); 425 project.getMapper().mapFromObf(vc, entry, new EntryMapping(newName), true, validateOnly);
568 426
569 if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) 427 if (validateOnly || !vc.canProceed()) return;
570 this.gui.moveClassTree(reference, newName);
571 428
572 refreshCurrentClass(jumpToReference ? reference : null); 429 if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
573 } 430 this.gui.moveClassTree(reference.entry, newName);
574 431
575 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference) { 432 chp.invalidateMapped();
576 removeMapping(reference, true);
577 } 433 }
578 434
579 @Override 435 @Override
580 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) { 436 public void removeMapping(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference) {
581 project.getMapper().removeByObf(reference.getNameableEntry()); 437 project.getMapper().removeByObf(vc, reference.getNameableEntry());
438
439 if (!vc.canProceed()) return;
582 440
583 if (reference.entry instanceof ClassEntry) 441 if (reference.entry instanceof ClassEntry)
584 this.gui.moveClassTree(reference, false, true); 442 this.gui.moveClassTree(reference.entry, false, true);
585 refreshCurrentClass(jumpToReference ? reference : null);
586 }
587 443
588 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs) { 444 chp.invalidateMapped();
589 changeDocs(reference, updatedDocs, true);
590 } 445 }
591 446
592 @Override 447 @Override
593 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean jumpToReference) { 448 public void changeDocs(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs) {
594 changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs); 449 changeDocs(vc, reference, updatedDocs, false);
595
596 refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS);
597 } 450 }
598 451
599 private void changeDoc(Entry<?> obfEntry, String newDoc) { 452 public void changeDocs(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean validateOnly) {
600 EntryRemapper mapper = project.getMapper(); 453 changeDoc(vc, reference.entry, updatedDocs, validateOnly);
601 if (mapper.getDeobfMapping(obfEntry) == null) { 454
602 markAsDeobfuscated(obfEntry, false); // NPE 455 if (validateOnly || !vc.canProceed()) return;
603 } 456
604 mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); 457 chp.invalidateJavadoc(reference.getLocationClassEntry());
605 } 458 }
606 459
607 private void markAsDeobfuscated(Entry<?> obfEntry, boolean renaming) { 460 private void changeDoc(ValidationContext vc, Entry<?> obfEntry, String newDoc, boolean validateOnly) {
608 EntryRemapper mapper = project.getMapper(); 461 EntryRemapper mapper = project.getMapper();
609 mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming);
610 }
611 462
612 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference) { 463 EntryMapping deobfMapping = mapper.getDeobfMapping(obfEntry);
613 markAsDeobfuscated(reference, true); 464 if (deobfMapping == null) {
465 deobfMapping = new EntryMapping(mapper.deobfuscate(obfEntry).getName());
466 }
467
468 mapper.mapFromObf(vc, obfEntry, deobfMapping.withDocs(newDoc), false, validateOnly);
614 } 469 }
615 470
616 @Override 471 @Override
617 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) { 472 public void markAsDeobfuscated(ValidationContext vc, EntryReference<Entry<?>, Entry<?>> reference) {
618 EntryRemapper mapper = project.getMapper(); 473 EntryRemapper mapper = project.getMapper();
619 Entry<?> entry = reference.getNameableEntry(); 474 Entry<?> entry = reference.getNameableEntry();
620 mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName())); 475 mapper.mapFromObf(vc, entry, new EntryMapping(mapper.deobfuscate(entry).getName()));
476
477 if (!vc.canProceed()) return;
621 478
622 if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) 479 if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
623 this.gui.moveClassTree(reference, true, false); 480 this.gui.moveClassTree(reference.entry, true, false);
624 481
625 refreshCurrentClass(jumpToReference ? reference : null); 482 chp.invalidateMapped();
626 } 483 }
627 484
628 public void openStats(Set<StatsMember> includedMembers) { 485 public void openStats(Set<StatsMember> includedMembers) {
@@ -635,7 +492,7 @@ public class GuiController implements ClientPacketHandler {
635 try (FileWriter w = new FileWriter(statsFile)) { 492 try (FileWriter w = new FileWriter(statsFile)) {
636 w.write( 493 w.write(
637 Utils.readResourceToString("/stats.html") 494 Utils.readResourceToString("/stats.html")
638 .replace("/*data*/", data) 495 .replace("/*data*/", data)
639 ); 496 );
640 } 497 }
641 498
@@ -647,10 +504,11 @@ public class GuiController implements ClientPacketHandler {
647 } 504 }
648 505
649 public void setDecompiler(DecompilerService service) { 506 public void setDecompiler(DecompilerService service) {
650 uncommentedSource = null; 507 chp.setDecompilerService(service);
651 decompilerService = service; 508 }
652 decompiler = project.createDecompiler(decompilerService); 509
653 refreshCurrentClass(null, RefreshMode.FULL); 510 public ClassHandleProvider getClassHandleProvider() {
511 return chp;
654 } 512 }
655 513
656 public EnigmaClient getClient() { 514 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 @@
1package cuchaz.enigma.gui;
2
3public enum RefreshMode {
4 MINIMAL,
5 JAVADOCS,
6 FULL
7}
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<Token> {
32 label.setText(this.controller.getReadableToken(token).toString()); 32 label.setText(this.controller.getReadableToken(token).toString());
33 return label; 33 return label;
34 } 34 }
35
35} 36}
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 @@
1package cuchaz.enigma.gui.config; 1package cuchaz.enigma.gui.config;
2 2
3import java.io.IOException; 3import java.io.IOException;
4 4import java.util.HashSet;
5import javax.swing.SwingUtilities; 5import java.util.Set;
6 6
7import com.google.common.collect.ImmutableMap; 7import com.google.common.collect.ImmutableMap;
8import cuchaz.enigma.gui.EnigmaSyntaxKit; 8import cuchaz.enigma.gui.EnigmaSyntaxKit;
9import cuchaz.enigma.gui.Gui; 9import cuchaz.enigma.gui.events.ThemeChangeListener;
10import cuchaz.enigma.gui.highlight.BoxHighlightPainter; 10import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
11import cuchaz.enigma.gui.highlight.TokenHighlightType;
12import cuchaz.enigma.gui.util.ScaleUtil; 11import cuchaz.enigma.gui.util.ScaleUtil;
12import cuchaz.enigma.source.RenamableTokenType;
13import de.sciss.syntaxpane.DefaultSyntaxKit; 13import de.sciss.syntaxpane.DefaultSyntaxKit;
14 14
15public class Themes { 15public class Themes {
16 16
17 public static void setLookAndFeel(Gui gui, Config.LookAndFeel lookAndFeel) { 17 private static final Set<ThemeChangeListener> listeners = new HashSet<>();
18
19 public static void setLookAndFeel(Config.LookAndFeel lookAndFeel) {
18 Config.getInstance().lookAndFeel = lookAndFeel; 20 Config.getInstance().lookAndFeel = lookAndFeel;
19 updateTheme(gui); 21 updateTheme();
20 } 22 }
21 23
22 public static void updateTheme(Gui gui) { 24 public static void updateTheme() {
23 Config config = Config.getInstance(); 25 Config config = Config.getInstance();
24 config.lookAndFeel.setGlobalLAF(); 26 config.lookAndFeel.setGlobalLAF();
25 config.lookAndFeel.apply(config); 27 config.lookAndFeel.apply(config);
@@ -31,15 +33,26 @@ public class Themes {
31 EnigmaSyntaxKit.invalidate(); 33 EnigmaSyntaxKit.invalidate();
32 DefaultSyntaxKit.initKit(); 34 DefaultSyntaxKit.initKit();
33 DefaultSyntaxKit.registerContentType("text/enigma-sources", EnigmaSyntaxKit.class.getName()); 35 DefaultSyntaxKit.registerContentType("text/enigma-sources", EnigmaSyntaxKit.class.getName());
34 gui.boxHighlightPainters = ImmutableMap.of( 36 ImmutableMap<RenamableTokenType, BoxHighlightPainter> boxHighlightPainters = getBoxHighlightPainters();
35 TokenHighlightType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline), 37 listeners.forEach(l -> l.onThemeChanged(config.lookAndFeel, boxHighlightPainters));
36 TokenHighlightType.PROPOSED, BoxHighlightPainter.create(config.proposedColor, config.proposedColorOutline),
37 TokenHighlightType.DEOBFUSCATED, BoxHighlightPainter.create(config.deobfuscatedColor, config.deobfuscatedColorOutline)
38 );
39 gui.setEditorTheme(config.lookAndFeel);
40 SwingUtilities.updateComponentTreeUI(gui.getFrame());
41 ScaleUtil.applyScaling(); 38 ScaleUtil.applyScaling();
42 } 39 }
43 40
41 public static ImmutableMap<RenamableTokenType, BoxHighlightPainter> getBoxHighlightPainters() {
42 Config config = Config.getInstance();
43 return ImmutableMap.of(
44 RenamableTokenType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline),
45 RenamableTokenType.PROPOSED, BoxHighlightPainter.create(config.proposedColor, config.proposedColorOutline),
46 RenamableTokenType.DEOBFUSCATED, BoxHighlightPainter.create(config.deobfuscatedColor, config.deobfuscatedColorOutline)
47 );
48 }
49
50 public static void addListener(ThemeChangeListener listener) {
51 listeners.add(listener);
52 }
53
54 public static void removeListener(ThemeChangeListener listener) {
55 listeners.remove(listener);
56 }
44 57
45} 58}
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.*;
19import javax.swing.text.html.HTML; 19import javax.swing.text.html.HTML;
20 20
21import java.awt.*; 21import java.awt.*;
22import java.awt.BorderLayout;
23import java.awt.Container;
24import java.awt.FlowLayout;
22import java.awt.event.KeyAdapter; 25import java.awt.event.KeyAdapter;
23import java.awt.event.KeyEvent; 26import java.awt.event.KeyEvent;
24 27
28import javax.swing.*;
29
30import com.google.common.base.Strings;
31import cuchaz.enigma.analysis.EntryReference;
32import cuchaz.enigma.gui.GuiController;
33import cuchaz.enigma.gui.elements.ValidatableTextArea;
34import cuchaz.enigma.gui.util.GuiUtil;
35import cuchaz.enigma.gui.util.ScaleUtil;
36import cuchaz.enigma.network.packet.ChangeDocsC2SPacket;
37import cuchaz.enigma.translation.representation.entry.Entry;
38import cuchaz.enigma.utils.I18n;
39import cuchaz.enigma.utils.validation.Message;
40import cuchaz.enigma.utils.validation.ValidationContext;
41
25public class JavadocDialog { 42public class JavadocDialog {
26 43
27 private static JavadocDialog instance = null; 44 private final JDialog ui;
45 private final GuiController controller;
46 private final EntryReference<Entry<?>, Entry<?>> entry;
28 47
29 private JFrame frame; 48 private final ValidatableTextArea text;
30 49
31 private JavadocDialog(JFrame parent, JTextArea text, Callback callback) { 50 private final ValidationContext vc = new ValidationContext();
32 // init frame 51
33 frame = new JFrame(I18n.translate("javadocs.edit")); 52 private JavadocDialog(JFrame parent, GuiController controller, EntryReference<Entry<?>, Entry<?>> entry, String preset) {
34 final Container pane = frame.getContentPane(); 53 this.ui = new JDialog(parent, I18n.translate("javadocs.edit"));
35 pane.setLayout(new BorderLayout()); 54 this.controller = controller;
55 this.entry = entry;
56 this.text = new ValidatableTextArea(10, 40);
57
58 // set up dialog
59 Container contentPane = ui.getContentPane();
60 contentPane.setLayout(new BorderLayout());
36 61
37 // editor panel 62 // editor panel
38 text.setTabSize(2); 63 this.text.setText(preset);
39 pane.add(new JScrollPane(text), BorderLayout.CENTER); 64 this.text.setTabSize(2);
40 text.addKeyListener(new KeyAdapter() { 65 contentPane.add(new JScrollPane(this.text), BorderLayout.CENTER);
66 this.text.addKeyListener(new KeyAdapter() {
41 @Override 67 @Override
42 public void keyPressed(KeyEvent event) { 68 public void keyPressed(KeyEvent event) {
43 switch (event.getKeyCode()) { 69 switch (event.getKeyCode()) {
44 case KeyEvent.VK_ENTER: 70 case KeyEvent.VK_ENTER:
45 if (event.isControlDown()) 71 if (event.isControlDown()) {
46 callback.closeUi(frame, true); 72 doSave();
73 if (vc.canProceed()) {
74 close();
75 }
76 }
47 break; 77 break;
48 case KeyEvent.VK_ESCAPE: 78 case KeyEvent.VK_ESCAPE:
49 callback.closeUi(frame, false); 79 close();
50 break; 80 break;
51 default: 81 default:
52 break; 82 break;
@@ -56,23 +86,15 @@ public class JavadocDialog {
56 86
57 // buttons panel 87 // buttons panel
58 JPanel buttonsPanel = new JPanel(); 88 JPanel buttonsPanel = new JPanel();
59 FlowLayout buttonsLayout = new FlowLayout(); 89 buttonsPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
60 buttonsLayout.setAlignment(FlowLayout.RIGHT);
61 buttonsPanel.setLayout(buttonsLayout);
62 buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("javadocs.instruction")))); 90 buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("javadocs.instruction"))));
63 JButton cancelButton = new JButton(I18n.translate("javadocs.cancel")); 91 JButton cancelButton = new JButton(I18n.translate("javadocs.cancel"));
64 cancelButton.addActionListener(event -> { 92 cancelButton.addActionListener(event -> close());
65 // close (hide) the dialog
66 callback.closeUi(frame, false);
67 });
68 buttonsPanel.add(cancelButton); 93 buttonsPanel.add(cancelButton);
69 JButton saveButton = new JButton(I18n.translate("javadocs.save")); 94 JButton saveButton = new JButton(I18n.translate("javadocs.save"));
70 saveButton.addActionListener(event -> { 95 saveButton.addActionListener(event -> doSave());
71 // exit enigma
72 callback.closeUi(frame, true);
73 });
74 buttonsPanel.add(saveButton); 96 buttonsPanel.add(saveButton);
75 pane.add(buttonsPanel, BorderLayout.SOUTH); 97 contentPane.add(buttonsPanel, BorderLayout.SOUTH);
76 98
77 // tags panel 99 // tags panel
78 JMenuBar tagsMenu = new JMenuBar(); 100 JMenuBar tagsMenu = new JMenuBar();
@@ -116,22 +138,56 @@ public class JavadocDialog {
116 }); 138 });
117 tagsMenu.add(htmlList); 139 tagsMenu.add(htmlList);
118 140
119 pane.add(tagsMenu, BorderLayout.NORTH); 141 contentPane.add(tagsMenu, BorderLayout.NORTH);
120 142
121 // show the frame 143 // show the frame
122 frame.setSize(ScaleUtil.getDimension(600, 400)); 144 this.ui.setSize(ScaleUtil.getDimension(600, 400));
123 frame.setLocationRelativeTo(parent); 145 this.ui.setLocationRelativeTo(parent);
124 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 146 this.ui.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
125 } 147 }
126 148
127 public static void init(JFrame parent, JTextArea area, Callback callback) { 149 // Called when the "Save" button gets clicked.
128 instance = new JavadocDialog(parent, area, callback); 150 public void doSave() {
129 instance.frame.doLayout(); 151 vc.reset();
130 instance.frame.setVisible(true); 152 validate();
153 if (!vc.canProceed()) return;
154 save();
155 if (!vc.canProceed()) return;
156 close();
131 } 157 }
132 158
133 public interface Callback { 159 public void close() {
134 void closeUi(JFrame frame, boolean save); 160 this.ui.setVisible(false);
161 this.ui.dispose();
162 }
163
164 public void validate() {
165 vc.setActiveElement(text);
166
167 if (text.getText().contains("*/")) {
168 vc.raise(Message.ILLEGAL_DOC_COMMENT_END);
169 }
170
171 controller.changeDocs(vc, entry, text.getText(), true);
172 }
173
174 public void save() {
175 vc.setActiveElement(text);
176 controller.changeDocs(vc, entry, text.getText());
177
178 if (!vc.canProceed()) return;
179
180 controller.sendPacket(new ChangeDocsC2SPacket(entry.getNameableEntry(), text.getText()));
181 }
182
183 public static void show(JFrame parent, GuiController controller, EntryReference<Entry<?>, Entry<?>> entry) {
184 EntryReference<Entry<?>, Entry<?>> translatedReference = controller.project.getMapper().deobfuscate(entry);
185 String text = Strings.nullToEmpty(translatedReference.entry.getJavadocs());
186
187 JavadocDialog dialog = new JavadocDialog(parent, controller, entry, text);
188 dialog.ui.doLayout();
189 dialog.ui.setVisible(true);
190 dialog.text.grabFocus();
135 } 191 }
136 192
137 private enum JavadocTag { 193 private enum JavadocTag {
@@ -156,4 +212,5 @@ public class JavadocDialog {
156 return this.inline; 212 return this.inline;
157 } 213 }
158 } 214 }
215
159} 216}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.GridLayout;
4import java.awt.event.*;
5import java.util.HashSet;
6import java.util.Set;
7
8import javax.swing.BorderFactory;
9import javax.swing.JLabel;
10import javax.swing.JPanel;
11import javax.swing.text.Document;
12
13import cuchaz.enigma.gui.events.ConvertingTextFieldListener;
14import cuchaz.enigma.gui.util.GuiUtil;
15import cuchaz.enigma.utils.validation.ParameterizedMessage;
16import cuchaz.enigma.utils.validation.Validatable;
17
18/**
19 * A label that converts into an editable text field when you click it.
20 */
21public class ConvertingTextField implements Validatable {
22
23 private final JPanel ui;
24 private final ValidatableTextField textField;
25 private final JLabel label;
26 private boolean isEditing = false;
27
28 private final Set<ConvertingTextFieldListener> listeners = new HashSet<>();
29
30 public ConvertingTextField(String text) {
31 this.ui = new JPanel();
32 this.ui.setLayout(new GridLayout(1, 1, 0, 0));
33 this.textField = new ValidatableTextField(text);
34 this.label = GuiUtil.unboldLabel(new JLabel(text));
35 this.label.setBorder(BorderFactory.createLoweredBevelBorder());
36
37 this.label.addMouseListener(new MouseAdapter() {
38 @Override
39 public void mouseClicked(MouseEvent e) {
40 startEditing();
41 }
42 });
43
44 this.textField.addFocusListener(new FocusAdapter() {
45 @Override
46 public void focusLost(FocusEvent e) {
47 if (!hasChanges()) {
48 stopEditing(true);
49 }
50 }
51 });
52
53 this.textField.addKeyListener(new KeyAdapter() {
54 @Override
55 public void keyPressed(KeyEvent e) {
56 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
57 stopEditing(true);
58 } else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
59 stopEditing(false);
60 }
61 }
62 });
63
64 this.ui.add(this.label);
65 }
66
67 public void startEditing() {
68 if (isEditing) return;
69 this.ui.removeAll();
70 this.ui.add(this.textField);
71 this.isEditing = true;
72 this.ui.validate();
73 this.ui.repaint();
74 this.textField.requestFocusInWindow();
75 this.textField.selectAll();
76 this.listeners.forEach(l -> l.onStartEditing(this));
77 }
78
79 public void stopEditing(boolean abort) {
80 if (!isEditing) return;
81
82 if (!listeners.stream().allMatch(l -> l.tryStopEditing(this, abort))) return;
83
84 if (abort) {
85 this.textField.setText(this.label.getText());
86 } else {
87 this.label.setText(this.textField.getText());
88 }
89
90 this.ui.removeAll();
91 this.ui.add(this.label);
92 this.isEditing = false;
93 this.ui.validate();
94 this.ui.repaint();
95 this.listeners.forEach(l -> l.onStopEditing(this, abort));
96 }
97
98 public void setText(String text) {
99 stopEditing(true);
100 this.label.setText(text);
101 this.textField.setText(text);
102 }
103
104 public void setEditText(String text) {
105 if (!isEditing) return;
106
107 this.textField.setText(text);
108 }
109
110 public void selectAll() {
111 if (!isEditing) return;
112
113 this.textField.selectAll();
114 }
115
116 public void selectSubstring(int startIndex) {
117 if (!isEditing) return;
118
119 Document doc = this.textField.getDocument();
120 if (doc != null) {
121 this.selectSubstring(startIndex, doc.getLength());
122 }
123 }
124
125 public void selectSubstring(int startIndex, int endIndex) {
126 if (!isEditing) return;
127
128 this.textField.select(startIndex, endIndex);
129 }
130
131 public String getText() {
132 if (isEditing) {
133 return this.textField.getText();
134 } else {
135 return this.label.getText();
136 }
137 }
138
139 public String getPersistentText() {
140 return this.label.getText();
141 }
142
143 public boolean hasChanges() {
144 if (!isEditing) return false;
145 return !this.textField.getText().equals(this.label.getText());
146 }
147
148 @Override
149 public void addMessage(ParameterizedMessage message) {
150 textField.addMessage(message);
151 }
152
153 @Override
154 public void clearMessages() {
155 textField.clearMessages();
156 }
157
158 public void addListener(ConvertingTextFieldListener listener) {
159 this.listeners.add(listener);
160 }
161
162 public void removeListener(ConvertingTextFieldListener listener) {
163 this.listeners.remove(listener);
164 }
165
166 public JPanel getUi() {
167 return ui;
168 }
169
170}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Component;
4import java.awt.event.KeyEvent;
5
6import javax.swing.JMenuItem;
7import javax.swing.JPopupMenu;
8import javax.swing.KeyStroke;
9
10import cuchaz.enigma.gui.Gui;
11import cuchaz.enigma.gui.panels.PanelEditor;
12import cuchaz.enigma.utils.I18n;
13
14public class EditorTabPopupMenu {
15
16 private final JPopupMenu ui;
17 private final JMenuItem close;
18 private final JMenuItem closeAll;
19 private final JMenuItem closeOthers;
20 private final JMenuItem closeLeft;
21 private final JMenuItem closeRight;
22
23 private final Gui gui;
24 private PanelEditor editor;
25
26 public EditorTabPopupMenu(Gui gui) {
27 this.gui = gui;
28
29 this.ui = new JPopupMenu();
30
31 this.close = new JMenuItem(I18n.translate("popup_menu.editor_tab.close"));
32 this.close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_4, KeyEvent.CTRL_DOWN_MASK));
33 this.close.addActionListener(a -> gui.closeEditor(editor));
34 this.ui.add(this.close);
35
36 this.closeAll = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_all"));
37 this.closeAll.addActionListener(a -> gui.closeAllEditorTabs());
38 this.ui.add(this.closeAll);
39
40 this.closeOthers = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_others"));
41 this.closeOthers.addActionListener(a -> gui.closeTabsExcept(editor));
42 this.ui.add(this.closeOthers);
43
44 this.closeLeft = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_left"));
45 this.closeLeft.addActionListener(a -> gui.closeTabsLeftOf(editor));
46 this.ui.add(this.closeLeft);
47
48 this.closeRight = new JMenuItem(I18n.translate("popup_menu.editor_tab.close_right"));
49 this.closeRight.addActionListener(a -> gui.closeTabsRightOf(editor));
50 this.ui.add(this.closeRight);
51 }
52
53 public void show(Component invoker, int x, int y, PanelEditor panelEditor) {
54 this.editor = panelEditor;
55 ui.show(invoker, x, y);
56 }
57
58}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Dimension;
4import java.awt.Font;
5import java.awt.Graphics;
6
7import javax.swing.CellRendererPane;
8import javax.swing.JComponent;
9import javax.swing.JTextArea;
10import javax.swing.JToolTip;
11import javax.swing.plaf.ComponentUI;
12import javax.swing.plaf.basic.BasicToolTipUI;
13
14/**
15 * Implements a multi line tooltip for GUI components
16 * Copied from http://www.codeguru.com/java/articles/122.shtml
17 *
18 * @author Zafir Anjum
19 */
20public class JMultiLineToolTip extends JToolTip {
21
22 private static final long serialVersionUID = 7813662474312183098L;
23
24 public JMultiLineToolTip() {
25 updateUI();
26 }
27
28 public void updateUI() {
29 setUI(MultiLineToolTipUI.createUI(this));
30 }
31
32 public void setColumns(int columns) {
33 this.columns = columns;
34 this.fixedwidth = 0;
35 }
36
37 public int getColumns() {
38 return columns;
39 }
40
41 public void setFixedWidth(int width) {
42 this.fixedwidth = width;
43 this.columns = 0;
44 }
45
46 public int getFixedWidth() {
47 return fixedwidth;
48 }
49
50 protected int columns = 0;
51 protected int fixedwidth = 0;
52}
53
54/**
55 * UI for multi line tool tip
56 */
57class MultiLineToolTipUI extends BasicToolTipUI {
58
59 static MultiLineToolTipUI sharedInstance = new MultiLineToolTipUI();
60 Font smallFont;
61 static JToolTip tip;
62 protected CellRendererPane rendererPane;
63
64 private static JTextArea textArea;
65
66 public static ComponentUI createUI(JComponent c) {
67 return sharedInstance;
68 }
69
70 public MultiLineToolTipUI() {
71 super();
72 }
73
74 public void installUI(JComponent c) {
75 super.installUI(c);
76 tip = (JToolTip) c;
77 rendererPane = new CellRendererPane();
78 c.add(rendererPane);
79 }
80
81 public void uninstallUI(JComponent c) {
82 super.uninstallUI(c);
83
84 c.remove(rendererPane);
85 rendererPane = null;
86 }
87
88 public void paint(Graphics g, JComponent c) {
89 Dimension size = c.getSize();
90 textArea.setBackground(c.getBackground());
91 rendererPane.paintComponent(g, textArea, c, 1, 1, size.width - 1, size.height - 1, true);
92 }
93
94 public Dimension getPreferredSize(JComponent c) {
95 String tipText = ((JToolTip) c).getTipText();
96 if (tipText == null) return new Dimension(0, 0);
97 textArea = new JTextArea(tipText);
98 rendererPane.removeAll();
99 rendererPane.add(textArea);
100 textArea.setWrapStyleWord(true);
101 int width = ((JMultiLineToolTip) c).getFixedWidth();
102 int columns = ((JMultiLineToolTip) c).getColumns();
103
104 if (columns > 0) {
105 textArea.setColumns(columns);
106 textArea.setSize(0, 0);
107 textArea.setLineWrap(true);
108 textArea.setSize(textArea.getPreferredSize());
109 } else if (width > 0) {
110 textArea.setLineWrap(true);
111 Dimension d = textArea.getPreferredSize();
112 d.width = width;
113 d.height++;
114 textArea.setSize(d);
115 } else
116 textArea.setLineWrap(false);
117
118 Dimension dim = textArea.getPreferredSize();
119
120 dim.height += 1;
121 dim.width += 1;
122 return dim;
123 }
124
125 public Dimension getMinimumSize(JComponent c) {
126 return getPreferredSize(c);
127 }
128
129 public Dimension getMaximumSize(JComponent c) {
130 return getPreferredSize(c);
131 }
132} \ 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 {
321 if (lookAndFeel.equals(Config.getInstance().lookAndFeel)) { 321 if (lookAndFeel.equals(Config.getInstance().lookAndFeel)) {
322 themeButton.setSelected(true); 322 themeButton.setSelected(true);
323 } 323 }
324 themeButton.addActionListener(_e -> Themes.setLookAndFeel(gui, lookAndFeel)); 324 themeButton.addActionListener(_e -> Themes.setLookAndFeel(lookAndFeel));
325 themesMenu.add(themeButton); 325 themesMenu.add(themeButton);
326 } 326 }
327 } 327 }
@@ -335,7 +335,12 @@ public class MenuBar {
335 languageButton.setSelected(true); 335 languageButton.setSelected(true);
336 } 336 }
337 languageButton.addActionListener(event -> { 337 languageButton.addActionListener(event -> {
338 I18n.setLanguage(lang); 338 Config.getInstance().language = lang;
339 try {
340 Config.getInstance().saveConfig();
341 } catch (IOException e) {
342 e.printStackTrace();
343 }
339 ChangeDialog.show(gui); 344 ChangeDialog.show(gui);
340 }); 345 });
341 languagesMenu.add(languageButton); 346 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 @@
1package cuchaz.enigma.gui.elements; 1package cuchaz.enigma.gui.elements;
2 2
3import cuchaz.enigma.gui.Gui; 3import cuchaz.enigma.gui.Gui;
4import cuchaz.enigma.gui.panels.PanelEditor;
4import cuchaz.enigma.utils.I18n; 5import cuchaz.enigma.utils.I18n;
5 6
6import javax.swing.*; 7import javax.swing.*;
@@ -20,10 +21,10 @@ public class PopupMenuBar extends JPopupMenu {
20 public final JMenuItem openNextMenu; 21 public final JMenuItem openNextMenu;
21 public final JMenuItem toggleMappingMenu; 22 public final JMenuItem toggleMappingMenu;
22 23
23 public PopupMenuBar(Gui gui) { 24 public PopupMenuBar(PanelEditor editor, Gui gui) {
24 { 25 {
25 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.rename")); 26 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.rename"));
26 menu.addActionListener(event -> gui.startRename()); 27 menu.addActionListener(event -> gui.startRename(editor));
27 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK)); 28 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK));
28 menu.setEnabled(false); 29 menu.setEnabled(false);
29 this.add(menu); 30 this.add(menu);
@@ -31,7 +32,7 @@ public class PopupMenuBar extends JPopupMenu {
31 } 32 }
32 { 33 {
33 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.javadoc")); 34 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.javadoc"));
34 menu.addActionListener(event -> gui.startDocChange()); 35 menu.addActionListener(event -> gui.startDocChange(editor));
35 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK)); 36 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK));
36 menu.setEnabled(false); 37 menu.setEnabled(false);
37 this.add(menu); 38 this.add(menu);
@@ -39,7 +40,7 @@ public class PopupMenuBar extends JPopupMenu {
39 } 40 }
40 { 41 {
41 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.inheritance")); 42 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.inheritance"));
42 menu.addActionListener(event -> gui.showInheritance()); 43 menu.addActionListener(event -> gui.showInheritance(editor));
43 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); 44 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK));
44 menu.setEnabled(false); 45 menu.setEnabled(false);
45 this.add(menu); 46 this.add(menu);
@@ -47,7 +48,7 @@ public class PopupMenuBar extends JPopupMenu {
47 } 48 }
48 { 49 {
49 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.implementations")); 50 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.implementations"));
50 menu.addActionListener(event -> gui.showImplementations()); 51 menu.addActionListener(event -> gui.showImplementations(editor));
51 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK)); 52 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK));
52 menu.setEnabled(false); 53 menu.setEnabled(false);
53 this.add(menu); 54 this.add(menu);
@@ -55,7 +56,7 @@ public class PopupMenuBar extends JPopupMenu {
55 } 56 }
56 { 57 {
57 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls")); 58 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls"));
58 menu.addActionListener(event -> gui.showCalls(true)); 59 menu.addActionListener(event -> gui.showCalls(editor, true));
59 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK)); 60 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
60 menu.setEnabled(false); 61 menu.setEnabled(false);
61 this.add(menu); 62 this.add(menu);
@@ -63,7 +64,7 @@ public class PopupMenuBar extends JPopupMenu {
63 } 64 }
64 { 65 {
65 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls.specific")); 66 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls.specific"));
66 menu.addActionListener(event -> gui.showCalls(false)); 67 menu.addActionListener(event -> gui.showCalls(editor, false));
67 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK)); 68 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK));
68 menu.setEnabled(false); 69 menu.setEnabled(false);
69 this.add(menu); 70 this.add(menu);
@@ -71,7 +72,7 @@ public class PopupMenuBar extends JPopupMenu {
71 } 72 }
72 { 73 {
73 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.declaration")); 74 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.declaration"));
74 menu.addActionListener(event -> gui.getController().navigateTo(gui.cursorReference.entry)); 75 menu.addActionListener(event -> gui.getController().navigateTo(editor.getCursorReference().entry));
75 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); 76 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
76 menu.setEnabled(false); 77 menu.setEnabled(false);
77 this.add(menu); 78 this.add(menu);
@@ -95,7 +96,7 @@ public class PopupMenuBar extends JPopupMenu {
95 } 96 }
96 { 97 {
97 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.mark_deobfuscated")); 98 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.mark_deobfuscated"));
98 menu.addActionListener(event -> gui.toggleMapping()); 99 menu.addActionListener(event -> gui.toggleMapping(editor));
99 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); 100 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
100 menu.setEnabled(false); 101 menu.setEnabled(false);
101 this.add(menu); 102 this.add(menu);
@@ -106,19 +107,19 @@ public class PopupMenuBar extends JPopupMenu {
106 } 107 }
107 { 108 {
108 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.in")); 109 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.in"));
109 menu.addActionListener(event -> gui.editor.offsetEditorZoom(2)); 110 menu.addActionListener(event -> editor.offsetEditorZoom(2));
110 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK)); 111 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK));
111 this.add(menu); 112 this.add(menu);
112 } 113 }
113 { 114 {
114 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.out")); 115 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.out"));
115 menu.addActionListener(event -> gui.editor.offsetEditorZoom(-2)); 116 menu.addActionListener(event -> editor.offsetEditorZoom(-2));
116 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); 117 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK));
117 this.add(menu); 118 this.add(menu);
118 } 119 }
119 { 120 {
120 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.reset")); 121 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.reset"));
121 menu.addActionListener(event -> gui.editor.resetEditorZoom()); 122 menu.addActionListener(event -> editor.resetEditorZoom());
122 this.add(menu); 123 this.add(menu);
123 } 124 }
124 } 125 }
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Graphics;
4import java.util.ArrayList;
5import java.util.List;
6
7import javax.swing.JPasswordField;
8import javax.swing.JToolTip;
9import javax.swing.event.DocumentEvent;
10import javax.swing.event.DocumentListener;
11import javax.swing.text.Document;
12
13import cuchaz.enigma.utils.validation.ParameterizedMessage;
14import cuchaz.enigma.utils.validation.Validatable;
15
16public class ValidatablePasswordField extends JPasswordField implements Validatable {
17
18 private List<ParameterizedMessage> messages = new ArrayList<>();
19 private String tooltipText = null;
20
21 public ValidatablePasswordField() {
22 }
23
24 public ValidatablePasswordField(String text) {
25 super(text);
26 }
27
28 public ValidatablePasswordField(int columns) {
29 super(columns);
30 }
31
32 public ValidatablePasswordField(String text, int columns) {
33 super(text, columns);
34 }
35
36 public ValidatablePasswordField(Document doc, String txt, int columns) {
37 super(doc, txt, columns);
38 }
39
40 {
41 getDocument().addDocumentListener(new DocumentListener() {
42 @Override
43 public void insertUpdate(DocumentEvent e) {
44 clearMessages();
45 }
46
47 @Override
48 public void removeUpdate(DocumentEvent e) {
49 clearMessages();
50 }
51
52 @Override
53 public void changedUpdate(DocumentEvent e) {
54 clearMessages();
55 }
56 });
57 }
58
59 @Override
60 public JToolTip createToolTip() {
61 JMultiLineToolTip tooltip = new JMultiLineToolTip();
62 tooltip.setComponent(this);
63 return tooltip;
64 }
65
66 @Override
67 public void setToolTipText(String text) {
68 tooltipText = text;
69 setToolTipText0();
70 }
71
72 private void setToolTipText0() {
73 super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages));
74 }
75
76 @Override
77 public void clearMessages() {
78 messages.clear();
79 setToolTipText0();
80 repaint();
81 }
82
83 @Override
84 public void addMessage(ParameterizedMessage message) {
85 messages.add(message);
86 setToolTipText0();
87 repaint();
88 }
89
90 @Override
91 public void paint(Graphics g) {
92 super.paint(g);
93 ValidatableUi.drawMarker(this, g, messages);
94 }
95
96}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Graphics;
4import java.util.ArrayList;
5import java.util.List;
6
7import javax.swing.JTextArea;
8import javax.swing.JToolTip;
9import javax.swing.event.DocumentEvent;
10import javax.swing.event.DocumentListener;
11import javax.swing.text.Document;
12
13import cuchaz.enigma.utils.validation.ParameterizedMessage;
14import cuchaz.enigma.utils.validation.Validatable;
15
16public class ValidatableTextArea extends JTextArea implements Validatable {
17
18 private List<ParameterizedMessage> messages = new ArrayList<>();
19 private String tooltipText = null;
20
21 public ValidatableTextArea() {
22 }
23
24 public ValidatableTextArea(String text) {
25 super(text);
26 }
27
28 public ValidatableTextArea(int rows, int columns) {
29 super(rows, columns);
30 }
31
32 public ValidatableTextArea(String text, int rows, int columns) {
33 super(text, rows, columns);
34 }
35
36 public ValidatableTextArea(Document doc) {
37 super(doc);
38 }
39
40 public ValidatableTextArea(Document doc, String text, int rows, int columns) {
41 super(doc, text, rows, columns);
42 }
43
44 {
45 getDocument().addDocumentListener(new DocumentListener() {
46 @Override
47 public void insertUpdate(DocumentEvent e) {
48 clearMessages();
49 }
50
51 @Override
52 public void removeUpdate(DocumentEvent e) {
53 clearMessages();
54 }
55
56 @Override
57 public void changedUpdate(DocumentEvent e) {
58 clearMessages();
59 }
60 });
61 }
62
63 @Override
64 public JToolTip createToolTip() {
65 JMultiLineToolTip tooltip = new JMultiLineToolTip();
66 tooltip.setComponent(this);
67 return tooltip;
68 }
69
70 @Override
71 public void setToolTipText(String text) {
72 tooltipText = text;
73 setToolTipText0();
74 }
75
76 private void setToolTipText0() {
77 super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages));
78 }
79
80 @Override
81 public void clearMessages() {
82 messages.clear();
83 setToolTipText0();
84 repaint();
85 }
86
87 @Override
88 public void addMessage(ParameterizedMessage message) {
89 messages.add(message);
90 setToolTipText0();
91 repaint();
92 }
93
94 @Override
95 public void paint(Graphics g) {
96 super.paint(g);
97 ValidatableUi.drawMarker(this, g, messages);
98 }
99
100}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Graphics;
4import java.util.ArrayList;
5import java.util.List;
6
7import javax.swing.JTextField;
8import javax.swing.JToolTip;
9import javax.swing.event.DocumentEvent;
10import javax.swing.event.DocumentListener;
11import javax.swing.text.Document;
12
13import cuchaz.enigma.utils.validation.ParameterizedMessage;
14import cuchaz.enigma.utils.validation.Validatable;
15
16public class ValidatableTextField extends JTextField implements Validatable {
17
18 private List<ParameterizedMessage> messages = new ArrayList<>();
19 private String tooltipText = null;
20
21 public ValidatableTextField() {
22 }
23
24 public ValidatableTextField(String text) {
25 super(text);
26 }
27
28 public ValidatableTextField(int columns) {
29 super(columns);
30 }
31
32 public ValidatableTextField(String text, int columns) {
33 super(text, columns);
34 }
35
36 public ValidatableTextField(Document doc, String text, int columns) {
37 super(doc, text, columns);
38 }
39
40 {
41 getDocument().addDocumentListener(new DocumentListener() {
42 @Override
43 public void insertUpdate(DocumentEvent e) {
44 clearMessages();
45 }
46
47 @Override
48 public void removeUpdate(DocumentEvent e) {
49 clearMessages();
50 }
51
52 @Override
53 public void changedUpdate(DocumentEvent e) {
54 clearMessages();
55 }
56 });
57 }
58
59 @Override
60 public JToolTip createToolTip() {
61 JMultiLineToolTip tooltip = new JMultiLineToolTip();
62 tooltip.setComponent(this);
63 return tooltip;
64 }
65
66 @Override
67 public void setToolTipText(String text) {
68 tooltipText = text;
69 setToolTipText0();
70 }
71
72 private void setToolTipText0() {
73 super.setToolTipText(ValidatableUi.getTooltipText(tooltipText, messages));
74 }
75
76 @Override
77 public void clearMessages() {
78 messages.clear();
79 setToolTipText0();
80 repaint();
81 }
82
83 @Override
84 public void addMessage(ParameterizedMessage message) {
85 messages.add(message);
86 setToolTipText0();
87 repaint();
88 }
89
90 @Override
91 public void paint(Graphics g) {
92 super.paint(g);
93 ValidatableUi.drawMarker(this, g, messages);
94 }
95
96}
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 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.Color;
4import java.awt.Component;
5import java.awt.Graphics;
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.List;
9
10import javax.annotation.Nullable;
11
12import cuchaz.enigma.gui.util.ScaleUtil;
13import cuchaz.enigma.utils.validation.ParameterizedMessage;
14
15public final class ValidatableUi {
16
17 private ValidatableUi() {
18 }
19
20 public static String getTooltipText(String tooltipText, List<ParameterizedMessage> messages) {
21 List<String> strings = new ArrayList<>();
22 if (tooltipText != null) {
23 strings.add(tooltipText);
24 }
25 if (!messages.isEmpty()) {
26 strings.add("Error(s): ");
27
28 messages.forEach(msg -> {
29 strings.add(String.format(" - %s", msg.getText()));
30 String longDesc = msg.getLongText();
31 if (!longDesc.isEmpty()) {
32 Arrays.stream(longDesc.split("\n")).map(s -> String.format(" %s", s)).forEach(strings::add);
33 }
34 });
35 }
36 if (strings.isEmpty()) {
37 return null;
38 } else {
39 return String.join("\n", strings);
40 }
41 }
42
43 public static String formatMessages(List<ParameterizedMessage> messages) {
44 List<String> strings = new ArrayList<>();
45
46 if (!messages.isEmpty()) {
47 strings.add("Error(s): ");
48
49 messages.forEach(msg -> {
50 strings.add(String.format(" - %s", msg.getText()));
51 String longDesc = msg.getLongText();
52 if (!longDesc.isEmpty()) {
53 Arrays.stream(longDesc.split("\n")).map(s -> String.format(" %s", s)).forEach(strings::add);
54 }
55 });
56 }
57 if (strings.isEmpty()) {
58 return null;
59 } else {
60 return String.join("\n", strings);
61 }
62 }
63
64 public static void drawMarker(Component self, Graphics g, List<ParameterizedMessage> messages) {
65 Color color = ValidatableUi.getMarkerColor(messages);
66 if (color != null) {
67 g.setColor(color);
68 int x1 = self.getWidth() - ScaleUtil.scale(8) - 1;
69 int x2 = self.getWidth() - ScaleUtil.scale(1) - 1;
70 int y1 = ScaleUtil.scale(1);
71 int y2 = ScaleUtil.scale(8);
72 g.fillPolygon(new int[]{x1, x2, x2}, new int[]{y1, y1, y2}, 3);
73 }
74 }
75
76 @Nullable
77 public static Color getMarkerColor(List<ParameterizedMessage> messages) {
78 int level = messages.stream()
79 .mapToInt(ValidatableUi::getMessageLevel)
80 .max().orElse(0);
81
82 switch (level) {
83 case 0:
84 return null;
85 case 1:
86 return Color.BLUE;
87 case 2:
88 return Color.ORANGE;
89 case 3:
90 return Color.RED;
91 }
92 throw new IllegalStateException("unreachable");
93 }
94
95 private static int getMessageLevel(ParameterizedMessage message) {
96 switch (message.message.type) {
97 case INFO:
98 return 1;
99 case WARNING:
100 return 2;
101 case ERROR:
102 return 3;
103 }
104 throw new IllegalStateException("unreachable");
105 }
106
107}
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 @@
1package cuchaz.enigma.gui.events;
2
3import cuchaz.enigma.gui.elements.ConvertingTextField;
4
5public interface ConvertingTextFieldListener {
6
7 default void onStartEditing(ConvertingTextField field) {
8 }
9
10 default boolean tryStopEditing(ConvertingTextField field, boolean abort) {
11 return true;
12 }
13
14 default void onStopEditing(ConvertingTextField field, boolean abort) {
15 }
16
17}
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 @@
1package cuchaz.enigma.gui.events;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.gui.panels.PanelEditor;
5import cuchaz.enigma.classhandle.ClassHandle;
6import cuchaz.enigma.translation.representation.entry.ClassEntry;
7import cuchaz.enigma.translation.representation.entry.Entry;
8
9public interface EditorActionListener {
10
11 default void onCursorReferenceChanged(PanelEditor editor, EntryReference<Entry<?>, Entry<?>> ref) {
12 }
13
14 default void onClassHandleChanged(PanelEditor editor, ClassEntry old, ClassHandle ch) {
15 }
16
17 default void onTitleChanged(PanelEditor editor, String title) {
18 }
19
20}
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 @@
1package cuchaz.enigma.gui.events;
2
3import java.util.Map;
4
5import cuchaz.enigma.gui.config.Config.LookAndFeel;
6import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
7import cuchaz.enigma.source.RenamableTokenType;
8
9public interface ThemeChangeListener {
10
11 void onThemeChanged(LookAndFeel lookAndFeel, Map<RenamableTokenType, BoxHighlightPainter> boxHighlightPainters);
12
13}
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.*;
19 19
20public class SelectionHighlightPainter implements Highlighter.HighlightPainter { 20public class SelectionHighlightPainter implements Highlighter.HighlightPainter {
21 21
22 public static final SelectionHighlightPainter INSTANCE = new SelectionHighlightPainter();
23
22 @Override 24 @Override
23 public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) { 25 public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) {
24 // draw a thick border 26 // draw a thick border
@@ -28,4 +30,5 @@ public class SelectionHighlightPainter implements Highlighter.HighlightPainter {
28 g2d.setStroke(new BasicStroke(2.0f)); 30 g2d.setStroke(new BasicStroke(2.0f));
29 g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); 31 g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
30 } 32 }
33
31} 34}
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 @@
1package cuchaz.enigma.gui.highlight;
2
3public enum TokenHighlightType {
4 OBFUSCATED,
5 DEOBFUSCATED,
6 PROPOSED
7}
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 @@
1package cuchaz.enigma.gui.panels;
2
3import java.awt.Component;
4import java.awt.Dimension;
5import java.awt.FlowLayout;
6import java.awt.Point;
7import java.awt.event.MouseAdapter;
8import java.awt.event.MouseEvent;
9
10import javax.accessibility.AccessibleContext;
11import javax.annotation.Nullable;
12import javax.swing.*;
13import javax.swing.border.EmptyBorder;
14import javax.swing.event.ChangeListener;
15
16public class ClosableTabTitlePane {
17
18 private final JPanel ui;
19 private final JButton closeButton;
20 private final JLabel label;
21
22 private ChangeListener cachedChangeListener;
23 private JTabbedPane parent;
24
25 public ClosableTabTitlePane(String text, Runnable onClose) {
26 this.ui = new JPanel(new FlowLayout(FlowLayout.CENTER, 2, 2));
27 this.ui.setOpaque(false);
28 this.label = new JLabel(text);
29 this.ui.add(this.label);
30
31 // Adapted from javax.swing.plaf.metal.MetalTitlePane
32 this.closeButton = new JButton();
33 this.closeButton.setFocusPainted(false);
34 this.closeButton.setFocusable(false);
35 this.closeButton.setOpaque(true);
36 this.closeButton.setIcon(UIManager.getIcon("InternalFrame.closeIcon"));
37 this.closeButton.putClientProperty("paintActive", Boolean.TRUE);
38 this.closeButton.setBorder(new EmptyBorder(0, 0, 0, 0));
39 this.closeButton.putClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY, "Close");
40 this.closeButton.setMaximumSize(new Dimension(this.closeButton.getIcon().getIconWidth(), this.closeButton.getIcon().getIconHeight()));
41 this.ui.add(this.closeButton);
42
43 // Use mouse listener here so that it also works for disabled buttons
44 closeButton.addMouseListener(new MouseAdapter() {
45 @Override
46 public void mouseClicked(MouseEvent e) {
47 if (SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isMiddleMouseButton(e)) {
48 onClose.run();
49 }
50 }
51 });
52
53 this.ui.addMouseListener(new MouseAdapter() {
54 @Override
55 public void mouseClicked(MouseEvent e) {
56 if (SwingUtilities.isMiddleMouseButton(e)) {
57 onClose.run();
58 }
59 }
60
61 @Override
62 public void mousePressed(MouseEvent e) {
63 // for some reason registering a mouse listener on this makes
64 // events never go to the tabbed pane, so we have to redirect
65 // the event for tab selection and context menu to work
66 if (parent != null) {
67 Point pt = new Point(e.getXOnScreen(), e.getYOnScreen());
68 SwingUtilities.convertPointFromScreen(pt, parent);
69 MouseEvent e1 = new MouseEvent(
70 parent,
71 e.getID(),
72 e.getWhen(),
73 e.getModifiersEx(),
74 (int) pt.getX(),
75 (int) pt.getY(),
76 e.getXOnScreen(),
77 e.getYOnScreen(),
78 e.getClickCount(),
79 e.isPopupTrigger(),
80 e.getButton()
81 );
82 parent.dispatchEvent(e1);
83 }
84 }
85 });
86
87 this.ui.putClientProperty(ClosableTabTitlePane.class, this);
88 }
89
90 public void setTabbedPane(JTabbedPane pane) {
91 if (this.parent != null) {
92 pane.removeChangeListener(cachedChangeListener);
93 }
94 if (pane != null) {
95 updateState(pane);
96 cachedChangeListener = e -> updateState(pane);
97 pane.addChangeListener(cachedChangeListener);
98 }
99 this.parent = pane;
100 }
101
102 public void setText(String text) {
103 this.label.setText(text);
104 }
105
106 public String getText() {
107 return this.label.getText();
108 }
109
110 private void updateState(JTabbedPane pane) {
111 int selectedIndex = pane.getSelectedIndex();
112 boolean isActive = selectedIndex != -1 && pane.getTabComponentAt(selectedIndex) == this.ui;
113 this.closeButton.setEnabled(isActive);
114 this.closeButton.putClientProperty("paintActive", isActive);
115 this.ui.repaint();
116 }
117
118 public JPanel getUi() {
119 return ui;
120 }
121
122 @Nullable
123 public static ClosableTabTitlePane byUi(Component c) {
124 if (c instanceof JComponent) {
125 Object prop = ((JComponent) c).getClientProperty(ClosableTabTitlePane.class);
126 if (prop instanceof ClosableTabTitlePane) {
127 return (ClosableTabTitlePane) prop;
128 }
129 }
130 return null;
131 }
132
133}
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 @@
1package cuchaz.enigma.gui.panels; 1package cuchaz.enigma.gui.panels;
2 2
3import java.awt.*;
4import java.awt.event.*;
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.List;
8import java.util.Map;
9
10import javax.annotation.Nullable;
11import javax.swing.*;
12import javax.swing.text.BadLocationException;
13import javax.swing.text.Document;
14import javax.swing.text.Highlighter;
15import javax.swing.text.Highlighter.HighlightPainter;
16
3import cuchaz.enigma.EnigmaProject; 17import cuchaz.enigma.EnigmaProject;
4import cuchaz.enigma.analysis.EntryReference; 18import cuchaz.enigma.analysis.EntryReference;
5import cuchaz.enigma.gui.config.Config; 19import cuchaz.enigma.classhandle.ClassHandle;
20import cuchaz.enigma.classhandle.ClassHandleError;
21import cuchaz.enigma.events.ClassHandleListener;
6import cuchaz.enigma.gui.BrowserCaret; 22import cuchaz.enigma.gui.BrowserCaret;
7import cuchaz.enigma.gui.Gui; 23import cuchaz.enigma.gui.Gui;
24import cuchaz.enigma.gui.GuiController;
25import cuchaz.enigma.gui.config.Config;
26import cuchaz.enigma.gui.config.Themes;
27import cuchaz.enigma.gui.elements.PopupMenuBar;
28import cuchaz.enigma.gui.events.EditorActionListener;
29import cuchaz.enigma.gui.events.ThemeChangeListener;
30import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
31import cuchaz.enigma.gui.highlight.SelectionHighlightPainter;
32import cuchaz.enigma.gui.util.ScaleUtil;
33import cuchaz.enigma.source.DecompiledClassSource;
34import cuchaz.enigma.source.RenamableTokenType;
35import cuchaz.enigma.source.Token;
36import cuchaz.enigma.translation.mapping.EntryRemapper;
37import cuchaz.enigma.translation.mapping.EntryResolver;
38import cuchaz.enigma.translation.mapping.ResolutionStrategy;
8import cuchaz.enigma.translation.representation.entry.ClassEntry; 39import cuchaz.enigma.translation.representation.entry.ClassEntry;
9import cuchaz.enigma.translation.representation.entry.Entry; 40import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.gui.util.ScaleUtil; 41import cuchaz.enigma.translation.representation.entry.FieldEntry;
42import cuchaz.enigma.translation.representation.entry.MethodEntry;
43import cuchaz.enigma.utils.I18n;
44import cuchaz.enigma.utils.Result;
45import de.sciss.syntaxpane.DefaultSyntaxKit;
11 46
12import javax.swing.*; 47public class PanelEditor {
13import java.awt.*;
14import java.awt.event.KeyAdapter;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18 48
19public class PanelEditor extends JEditorPane { 49 private final JPanel ui = new JPanel();
50 private final JEditorPane editor = new JEditorPane();
51 private final JScrollPane editorScrollPane = new JScrollPane(this.editor);
52 private final PopupMenuBar popupMenu;
53
54 // progress UI
55 private final JLabel decompilingLabel = new JLabel(I18n.translate("editor.decompiling"), JLabel.CENTER);
56 private final JProgressBar decompilingProgressBar = new JProgressBar(0, 100);
57
58 // error display UI
59 private final JLabel errorLabel = new JLabel(I18n.translate("editor.decompile_error"));
60 private final JTextArea errorTextArea = new JTextArea();
61 private final JScrollPane errorScrollPane = new JScrollPane(this.errorTextArea);
62 private final JButton retryButton = new JButton(I18n.translate("general.retry"));
63
64 private DisplayMode mode = DisplayMode.INACTIVE;
65
66 private final GuiController controller;
67 private final Gui gui;
68
69 private EntryReference<Entry<?>, Entry<?>> cursorReference;
20 private boolean mouseIsPressed = false; 70 private boolean mouseIsPressed = false;
21 public int fontSize = 12; 71 private boolean shouldNavigateOnClick;
72
73 public Config.LookAndFeel editorLaf;
74 private int fontSize = 12;
75 private Map<RenamableTokenType, BoxHighlightPainter> boxHighlightPainters;
76
77 private final List<EditorActionListener> listeners = new ArrayList<>();
78
79 private final ThemeChangeListener themeChangeListener;
80
81 private ClassHandle classHandle;
82 private DecompiledClassSource source;
83 private boolean settingSource;
22 84
23 public PanelEditor(Gui gui) { 85 public PanelEditor(Gui gui) {
24 this.setEditable(false); 86 this.gui = gui;
25 this.setSelectionColor(new Color(31, 46, 90)); 87 this.controller = gui.getController();
26 this.setCaret(new BrowserCaret()); 88
27 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); 89 this.editor.setEditable(false);
28 this.addCaretListener(event -> gui.onCaretMove(event.getDot(), mouseIsPressed)); 90 this.editor.setSelectionColor(new Color(31, 46, 90));
29 final PanelEditor self = this; 91 this.editor.setCaret(new BrowserCaret());
30 this.addMouseListener(new MouseAdapter() { 92 this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize));
93 this.editor.addCaretListener(event -> onCaretMove(event.getDot(), this.mouseIsPressed));
94 this.editor.setCaretColor(new Color(Config.getInstance().caretColor));
95 this.editor.setContentType("text/enigma-sources");
96 this.editor.setBackground(new Color(Config.getInstance().editorBackground));
97 DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit();
98 kit.toggleComponent(this.editor, "de.sciss.syntaxpane.components.TokenMarker");
99
100 // init editor popup menu
101 this.popupMenu = new PopupMenuBar(this, gui);
102 this.editor.setComponentPopupMenu(this.popupMenu);
103
104 this.decompilingLabel.setFont(ScaleUtil.getFont(this.decompilingLabel.getFont().getFontName(), Font.BOLD, 26));
105 this.decompilingProgressBar.setIndeterminate(true);
106 this.errorTextArea.setEditable(false);
107 this.errorTextArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 10));
108
109 this.boxHighlightPainters = Themes.getBoxHighlightPainters();
110
111 this.editor.addMouseListener(new MouseAdapter() {
31 @Override 112 @Override
32 public void mousePressed(MouseEvent mouseEvent) { 113 public void mousePressed(MouseEvent mouseEvent) {
33 mouseIsPressed = true; 114 PanelEditor.this.mouseIsPressed = true;
34 } 115 }
35 116
36 @Override 117 @Override
37 public void mouseReleased(MouseEvent e) { 118 public void mouseReleased(MouseEvent e) {
38 switch (e.getButton()) { 119 switch (e.getButton()) {
39 case MouseEvent.BUTTON3: // Right click 120 case MouseEvent.BUTTON3: // Right click
40 self.setCaretPosition(self.viewToModel(e.getPoint())); 121 PanelEditor.this.editor.setCaretPosition(PanelEditor.this.editor.viewToModel(e.getPoint()));
41 break; 122 break;
42 123
43 case 4: // Back navigation 124 case 4: // Back navigation
@@ -48,57 +129,59 @@ public class PanelEditor extends JEditorPane {
48 gui.getController().openNextReference(); 129 gui.getController().openNextReference();
49 break; 130 break;
50 } 131 }
51 mouseIsPressed = false; 132 PanelEditor.this.mouseIsPressed = false;
52 } 133 }
53 }); 134 });
54 this.addKeyListener(new KeyAdapter() { 135 this.editor.addKeyListener(new KeyAdapter() {
55 @Override 136 @Override
56 public void keyPressed(KeyEvent event) { 137 public void keyPressed(KeyEvent event) {
57 if (event.isControlDown()) { 138 if (event.isControlDown()) {
58 gui.setShouldNavigateOnClick(false); 139 PanelEditor.this.shouldNavigateOnClick = false;
59 switch (event.getKeyCode()) { 140 switch (event.getKeyCode()) {
60 case KeyEvent.VK_I: 141 case KeyEvent.VK_I:
61 gui.popupMenu.showInheritanceMenu.doClick(); 142 PanelEditor.this.popupMenu.showInheritanceMenu.doClick();
62 break; 143 break;
63 144
64 case KeyEvent.VK_M: 145 case KeyEvent.VK_M:
65 gui.popupMenu.showImplementationsMenu.doClick(); 146 PanelEditor.this.popupMenu.showImplementationsMenu.doClick();
66 break; 147 break;
67 148
68 case KeyEvent.VK_N: 149 case KeyEvent.VK_N:
69 gui.popupMenu.openEntryMenu.doClick(); 150 PanelEditor.this.popupMenu.openEntryMenu.doClick();
70 break; 151 break;
71 152
72 case KeyEvent.VK_P: 153 case KeyEvent.VK_P:
73 gui.popupMenu.openPreviousMenu.doClick(); 154 PanelEditor.this.popupMenu.openPreviousMenu.doClick();
74 break; 155 break;
75 156
76 case KeyEvent.VK_E: 157 case KeyEvent.VK_E:
77 gui.popupMenu.openNextMenu.doClick(); 158 PanelEditor.this.popupMenu.openNextMenu.doClick();
78 break; 159 break;
79 160
80 case KeyEvent.VK_C: 161 case KeyEvent.VK_C:
81 if (event.isShiftDown()) { 162 if (event.isShiftDown()) {
82 gui.popupMenu.showCallsSpecificMenu.doClick(); 163 PanelEditor.this.popupMenu.showCallsSpecificMenu.doClick();
83 } else { 164 } else {
84 gui.popupMenu.showCallsMenu.doClick(); 165 PanelEditor.this.popupMenu.showCallsMenu.doClick();
85 } 166 }
86 break; 167 break;
87 168
88 case KeyEvent.VK_O: 169 case KeyEvent.VK_O:
89 gui.popupMenu.toggleMappingMenu.doClick(); 170 PanelEditor.this.popupMenu.toggleMappingMenu.doClick();
90 break; 171 break;
91 172
92 case KeyEvent.VK_R: 173 case KeyEvent.VK_R:
93 gui.popupMenu.renameMenu.doClick(); 174 PanelEditor.this.popupMenu.renameMenu.doClick();
94 break; 175 break;
95 176
96 case KeyEvent.VK_D: 177 case KeyEvent.VK_D:
97 gui.popupMenu.editJavadocMenu.doClick(); 178 PanelEditor.this.popupMenu.editJavadocMenu.doClick();
98 break; 179 break;
99 180
100 case KeyEvent.VK_F5: 181 case KeyEvent.VK_F5:
101 gui.getController().refreshCurrentClass(); 182 if (PanelEditor.this.classHandle != null) {
183 PanelEditor.this.classHandle.invalidateMapped();
184 }
102 break; 185 break;
103 186
104 case KeyEvent.VK_F: 187 case KeyEvent.VK_F:
@@ -108,15 +191,15 @@ public class PanelEditor extends JEditorPane {
108 case KeyEvent.VK_ADD: 191 case KeyEvent.VK_ADD:
109 case KeyEvent.VK_EQUALS: 192 case KeyEvent.VK_EQUALS:
110 case KeyEvent.VK_PLUS: 193 case KeyEvent.VK_PLUS:
111 self.offsetEditorZoom(2); 194 offsetEditorZoom(2);
112 break; 195 break;
113 case KeyEvent.VK_SUBTRACT: 196 case KeyEvent.VK_SUBTRACT:
114 case KeyEvent.VK_MINUS: 197 case KeyEvent.VK_MINUS:
115 self.offsetEditorZoom(-2); 198 offsetEditorZoom(-2);
116 break; 199 break;
117 200
118 default: 201 default:
119 gui.setShouldNavigateOnClick(true); // CTRL 202 PanelEditor.this.shouldNavigateOnClick = true; // CTRL
120 break; 203 break;
121 } 204 }
122 } 205 }
@@ -124,11 +207,11 @@ public class PanelEditor extends JEditorPane {
124 207
125 @Override 208 @Override
126 public void keyTyped(KeyEvent event) { 209 public void keyTyped(KeyEvent event) {
127 if (!gui.popupMenu.renameMenu.isEnabled()) return; 210 if (!PanelEditor.this.popupMenu.renameMenu.isEnabled()) return;
128 211
129 if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) { 212 if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) {
130 EnigmaProject project = gui.getController().project; 213 EnigmaProject project = gui.getController().project;
131 EntryReference<Entry<?>, Entry<?>> reference = project.getMapper().deobfuscate(gui.cursorReference); 214 EntryReference<Entry<?>, Entry<?>> reference = project.getMapper().deobfuscate(PanelEditor.this.cursorReference);
132 Entry<?> entry = reference.getNameableEntry(); 215 Entry<?> entry = reference.getNameableEntry();
133 216
134 String name = String.valueOf(event.getKeyChar()); 217 String name = String.valueOf(event.getKeyChar());
@@ -139,33 +222,429 @@ public class PanelEditor extends JEditorPane {
139 } 222 }
140 } 223 }
141 224
142 gui.popupMenu.renameMenu.doClick(); 225 gui.startRename(PanelEditor.this, name);
143 gui.renameTextField.setText(name);
144 } 226 }
145 } 227 }
146 228
147 @Override 229 @Override
148 public void keyReleased(KeyEvent event) { 230 public void keyReleased(KeyEvent event) {
149 gui.setShouldNavigateOnClick(event.isControlDown()); 231 PanelEditor.this.shouldNavigateOnClick = event.isControlDown();
232 }
233 });
234
235 this.retryButton.addActionListener(_e -> redecompileClass());
236
237 this.themeChangeListener = (laf, boxHighlightPainters) -> {
238 if ((this.editorLaf == null || this.editorLaf != laf)) {
239 this.editor.updateUI();
240 this.editor.setBackground(new Color(Config.getInstance().editorBackground));
241 if (this.editorLaf != null) {
242 this.classHandle.invalidateMapped();
243 }
244
245 this.editorLaf = laf;
246 }
247 this.boxHighlightPainters = boxHighlightPainters;
248 };
249
250 this.ui.putClientProperty(PanelEditor.class, this);
251 }
252
253 @Nullable
254 public static PanelEditor byUi(Component ui) {
255 if (ui instanceof JComponent) {
256 Object prop = ((JComponent) ui).getClientProperty(PanelEditor.class);
257 if (prop instanceof PanelEditor) {
258 return (PanelEditor) prop;
259 }
260 }
261 return null;
262 }
263
264 public void setClassHandle(ClassHandle handle) {
265 ClassEntry old = null;
266 if (this.classHandle != null) {
267 old = this.classHandle.getRef();
268 this.classHandle.close();
269 }
270 setClassHandle0(old, handle);
271 }
272
273 private void setClassHandle0(ClassEntry old, ClassHandle handle) {
274 this.setDisplayMode(DisplayMode.IN_PROGRESS);
275 setCursorReference(null);
276
277 handle.addListener(new ClassHandleListener() {
278 @Override
279 public void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) {
280 SwingUtilities.invokeLater(() -> {
281 PanelEditor.this.listeners.forEach(l -> l.onTitleChanged(PanelEditor.this, getFileName()));
282 });
283 }
284
285 @Override
286 public void onMappedSourceChanged(ClassHandle h, Result<DecompiledClassSource, ClassHandleError> res) {
287 handleDecompilerResult(res);
288 }
289
290 @Override
291 public void onInvalidate(ClassHandle h, InvalidationType t) {
292 SwingUtilities.invokeLater(() -> {
293 if (t == InvalidationType.FULL) {
294 PanelEditor.this.setDisplayMode(DisplayMode.IN_PROGRESS);
295 }
296 });
297 }
298
299 @Override
300 public void onDeleted(ClassHandle h) {
301 SwingUtilities.invokeLater(() -> PanelEditor.this.gui.closeEditor(PanelEditor.this));
150 } 302 }
151 }); 303 });
304
305 handle.getSource().thenAcceptAsync(this::handleDecompilerResult, SwingUtilities::invokeLater);
306
307 this.classHandle = handle;
308 this.listeners.forEach(l -> l.onClassHandleChanged(this, old, handle));
309 }
310
311 public void setup() {
312 Themes.addListener(this.themeChangeListener);
313 }
314
315 public void destroy() {
316 Themes.removeListener(this.themeChangeListener);
317 this.classHandle.close();
318 }
319
320 private void redecompileClass() {
321 if (this.classHandle != null) {
322 this.classHandle.invalidate();
323 }
324 }
325
326 private void handleDecompilerResult(Result<DecompiledClassSource, ClassHandleError> res) {
327 SwingUtilities.invokeLater(() -> {
328 if (res.isOk()) {
329 this.setSource(res.unwrap());
330 } else {
331 this.displayError(res.unwrapErr());
332 }
333 });
334 }
335
336 public void displayError(ClassHandleError t) {
337 this.setDisplayMode(DisplayMode.ERRORED);
338 this.errorTextArea.setText(t.getStackTrace());
339 this.errorTextArea.setCaretPosition(0);
340 }
341
342 public void setDisplayMode(DisplayMode mode) {
343 if (this.mode == mode) return;
344 this.ui.removeAll();
345 switch (mode) {
346 case INACTIVE:
347 break;
348 case IN_PROGRESS: {
349 // make progress bar start from the left every time
350 this.decompilingProgressBar.setIndeterminate(false);
351 this.decompilingProgressBar.setIndeterminate(true);
352
353 this.ui.setLayout(new GridBagLayout());
354 GridBagConstraints c = new GridBagConstraints();
355 c.gridx = 0;
356 c.gridy = 0;
357 c.insets = ScaleUtil.getInsets(2, 2, 2, 2);
358 c.anchor = GridBagConstraints.SOUTH;
359 this.ui.add(this.decompilingLabel, c);
360 c.gridy = 1;
361 c.anchor = GridBagConstraints.NORTH;
362 this.ui.add(this.decompilingProgressBar, c);
363 break;
364 }
365 case SUCCESS: {
366 this.ui.setLayout(new GridLayout(1, 1, 0, 0));
367 this.ui.add(this.editorScrollPane);
368 break;
369 }
370 case ERRORED: {
371 this.ui.setLayout(new GridBagLayout());
372 GridBagConstraints c = new GridBagConstraints();
373 c.insets = ScaleUtil.getInsets(2, 2, 2, 2);
374 c.gridx = 0;
375 c.gridy = 0;
376 c.weightx = 1.0;
377 c.anchor = GridBagConstraints.WEST;
378 this.ui.add(this.errorLabel, c);
379 c.gridy = 1;
380 c.fill = GridBagConstraints.HORIZONTAL;
381 this.ui.add(new JSeparator(JSeparator.HORIZONTAL), c);
382 c.gridy = 2;
383 c.fill = GridBagConstraints.BOTH;
384 c.weighty = 1.0;
385 this.ui.add(this.errorScrollPane, c);
386 c.gridy = 3;
387 c.fill = GridBagConstraints.NONE;
388 c.anchor = GridBagConstraints.EAST;
389 c.weightx = 0.0;
390 c.weighty = 0.0;
391 this.ui.add(this.retryButton, c);
392 break;
393 }
394 }
395 this.ui.validate();
396 this.ui.repaint();
397 this.mode = mode;
152 } 398 }
153 399
154 public void offsetEditorZoom(int zoomAmount) { 400 public void offsetEditorZoom(int zoomAmount) {
155 int newResult = this.fontSize + zoomAmount; 401 int newResult = this.fontSize + zoomAmount;
156 if (newResult > 8 && newResult < 72) { 402 if (newResult > 8 && newResult < 72) {
157 this.fontSize = newResult; 403 this.fontSize = newResult;
158 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); 404 this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize));
159 } 405 }
160 } 406 }
161 407
162 public void resetEditorZoom() { 408 public void resetEditorZoom() {
163 this.fontSize = 12; 409 this.fontSize = 12;
164 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize)); 410 this.editor.setFont(ScaleUtil.getFont(this.editor.getFont().getFontName(), Font.PLAIN, this.fontSize));
165 } 411 }
166 412
167 @Override 413 public void onCaretMove(int pos, boolean fromClick) {
168 public Color getCaretColor() { 414 if (this.controller.project == null) return;
169 return new Color(Config.getInstance().caretColor); 415
416 EntryRemapper mapper = this.controller.project.getMapper();
417 Token token = getToken(pos);
418
419 if (this.settingSource) {
420 EntryReference<Entry<?>, Entry<?>> ref = getCursorReference();
421 EntryReference<Entry<?>, Entry<?>> refAtCursor = getReference(token);
422 if (this.editor.getDocument().getLength() != 0 && ref != null && !ref.equals(refAtCursor)) {
423 showReference0(ref);
424 }
425 return;
426 } else {
427 setCursorReference(getReference(token));
428 }
429
430 Entry<?> referenceEntry = this.cursorReference != null ? this.cursorReference.entry : null;
431
432 if (referenceEntry != null && this.shouldNavigateOnClick && fromClick) {
433 this.shouldNavigateOnClick = false;
434 Entry<?> navigationEntry = referenceEntry;
435 if (this.cursorReference.context == null) {
436 EntryResolver resolver = mapper.getObfResolver();
437 navigationEntry = resolver.resolveFirstEntry(referenceEntry, ResolutionStrategy.RESOLVE_ROOT);
438 }
439 this.controller.navigateTo(navigationEntry);
440 }
441 }
442
443 private void setCursorReference(EntryReference<Entry<?>, Entry<?>> ref) {
444 this.cursorReference = ref;
445
446 Entry<?> referenceEntry = ref == null ? null : ref.entry;
447
448 boolean isClassEntry = referenceEntry instanceof ClassEntry;
449 boolean isFieldEntry = referenceEntry instanceof FieldEntry;
450 boolean isMethodEntry = referenceEntry instanceof MethodEntry && !((MethodEntry) referenceEntry).isConstructor();
451 boolean isConstructorEntry = referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor();
452 boolean isRenamable = ref != null && this.controller.project.isRenamable(ref);
453
454 this.popupMenu.renameMenu.setEnabled(isRenamable);
455 this.popupMenu.editJavadocMenu.setEnabled(isRenamable);
456 this.popupMenu.showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry);
457 this.popupMenu.showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry);
458 this.popupMenu.showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry);
459 this.popupMenu.showCallsSpecificMenu.setEnabled(isMethodEntry);
460 this.popupMenu.openEntryMenu.setEnabled(isRenamable && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry));
461 this.popupMenu.openPreviousMenu.setEnabled(this.controller.hasPreviousReference());
462 this.popupMenu.openNextMenu.setEnabled(this.controller.hasNextReference());
463 this.popupMenu.toggleMappingMenu.setEnabled(isRenamable);
464
465 if (referenceEntry != null && referenceEntry.equals(this.controller.project.getMapper().deobfuscate(referenceEntry))) {
466 this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.reset_obfuscated"));
467 } else {
468 this.popupMenu.toggleMappingMenu.setText(I18n.translate("popup_menu.mark_deobfuscated"));
469 }
470
471 this.listeners.forEach(l -> l.onCursorReferenceChanged(this, ref));
472 }
473
474 public Token getToken(int pos) {
475 if (this.source == null) {
476 return null;
477 }
478 return this.source.getIndex().getReferenceToken(pos);
479 }
480
481 @Nullable
482 public EntryReference<Entry<?>, Entry<?>> getReference(Token token) {
483 if (this.source == null) {
484 return null;
485 }
486 return this.source.getIndex().getReference(token);
170 } 487 }
488
489 public void setSource(DecompiledClassSource source) {
490 this.setDisplayMode(DisplayMode.SUCCESS);
491 if (source == null) return;
492 try {
493 this.settingSource = true;
494 this.source = source;
495 this.editor.getHighlighter().removeAllHighlights();
496 this.editor.setText(source.toString());
497 setHighlightedTokens(source.getHighlightedTokens());
498 } finally {
499 this.settingSource = false;
500 }
501 showReference0(getCursorReference());
502 }
503
504 public void setHighlightedTokens(Map<RenamableTokenType, Collection<Token>> tokens) {
505 // remove any old highlighters
506 this.editor.getHighlighter().removeAllHighlights();
507
508 if (this.boxHighlightPainters != null) {
509 for (RenamableTokenType type : tokens.keySet()) {
510 BoxHighlightPainter painter = this.boxHighlightPainters.get(type);
511 if (painter != null) {
512 setHighlightedTokens(tokens.get(type), painter);
513 }
514 }
515 }
516
517 this.editor.validate();
518 this.editor.repaint();
519 }
520
521 private void setHighlightedTokens(Iterable<Token> tokens, Highlighter.HighlightPainter painter) {
522 for (Token token : tokens) {
523 try {
524 this.editor.getHighlighter().addHighlight(token.start, token.end, painter);
525 } catch (BadLocationException ex) {
526 throw new IllegalArgumentException(ex);
527 }
528 }
529 }
530
531 public EntryReference<Entry<?>, Entry<?>> getCursorReference() {
532 return this.cursorReference;
533 }
534
535 public void showReference(EntryReference<Entry<?>, Entry<?>> reference) {
536 setCursorReference(reference);
537 showReference0(reference);
538 }
539
540 /**
541 * Navigates to the reference without modifying history. Assumes the class is loaded.
542 *
543 * @param reference
544 */
545 private void showReference0(EntryReference<Entry<?>, Entry<?>> reference) {
546 if (this.source == null) return;
547 if (reference == null) return;
548
549 Collection<Token> tokens = this.controller.getTokensForReference(this.source, reference);
550 if (tokens.isEmpty()) {
551 // DEBUG
552 System.err.println(String.format("WARNING: no tokens found for %s in %s", reference, this.classHandle.getRef()));
553 } else {
554 this.gui.showTokens(this, tokens);
555 }
556 }
557
558 public void navigateToToken(Token token) {
559 if (token == null) {
560 throw new IllegalArgumentException("Token cannot be null!");
561 }
562 navigateToToken(token, SelectionHighlightPainter.INSTANCE);
563 }
564
565 private void navigateToToken(Token token, HighlightPainter highlightPainter) {
566 // set the caret position to the token
567 Document document = this.editor.getDocument();
568 int clampedPosition = Math.min(Math.max(token.start, 0), document.getLength());
569
570 this.editor.setCaretPosition(clampedPosition);
571 this.editor.grabFocus();
572
573 try {
574 // make sure the token is visible in the scroll window
575 Rectangle start = this.editor.modelToView(token.start);
576 Rectangle end = this.editor.modelToView(token.end);
577 Rectangle show = start.union(end);
578 show.grow(start.width * 10, start.height * 6);
579 SwingUtilities.invokeLater(() -> this.editor.scrollRectToVisible(show));
580 } catch (BadLocationException ex) {
581 if (!this.settingSource) {
582 throw new RuntimeException(ex);
583 } else {
584 return;
585 }
586 }
587
588 // highlight the token momentarily
589 Timer timer = new Timer(200, new ActionListener() {
590 private int counter = 0;
591 private Object highlight = null;
592
593 @Override
594 public void actionPerformed(ActionEvent event) {
595 if (this.counter % 2 == 0) {
596 try {
597 this.highlight = PanelEditor.this.editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter);
598 } catch (BadLocationException ex) {
599 // don't care
600 }
601 } else if (this.highlight != null) {
602 PanelEditor.this.editor.getHighlighter().removeHighlight(this.highlight);
603 }
604
605 if (this.counter++ > 6) {
606 Timer timer = (Timer) event.getSource();
607 timer.stop();
608 }
609 }
610 });
611 timer.start();
612 }
613
614 public void addListener(EditorActionListener listener) {
615 this.listeners.add(listener);
616 }
617
618 public void removeListener(EditorActionListener listener) {
619 this.listeners.remove(listener);
620 }
621
622 public JPanel getUi() {
623 return this.ui;
624 }
625
626 public JEditorPane getEditor() {
627 return this.editor;
628 }
629
630 public DecompiledClassSource getSource() {
631 return this.source;
632 }
633
634 public ClassHandle getClassHandle() {
635 return this.classHandle;
636 }
637
638 public String getFileName() {
639 ClassEntry classEntry = this.classHandle.getDeobfRef() != null ? this.classHandle.getDeobfRef() : this.classHandle.getRef();
640 return classEntry.getSimpleName();
641 }
642
643 private enum DisplayMode {
644 INACTIVE,
645 IN_PROGRESS,
646 SUCCESS,
647 ERRORED,
648 }
649
171} 650}
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 @@
1package cuchaz.enigma.gui.panels; 1package cuchaz.enigma.gui.panels;
2 2
3import java.awt.*;
4import java.awt.event.ItemEvent;
5import java.util.function.Consumer;
6
7import javax.swing.BorderFactory;
8import javax.swing.JComboBox;
9import javax.swing.JLabel;
10import javax.swing.JPanel;
11
12import cuchaz.enigma.EnigmaProject;
13import cuchaz.enigma.analysis.EntryReference;
3import cuchaz.enigma.gui.Gui; 14import cuchaz.enigma.gui.Gui;
15import cuchaz.enigma.gui.elements.ConvertingTextField;
16import cuchaz.enigma.gui.events.ConvertingTextFieldListener;
4import cuchaz.enigma.gui.util.GuiUtil; 17import cuchaz.enigma.gui.util.GuiUtil;
5import cuchaz.enigma.utils.I18n;
6import cuchaz.enigma.gui.util.ScaleUtil; 18import cuchaz.enigma.gui.util.ScaleUtil;
19import cuchaz.enigma.network.packet.RenameC2SPacket;
20import cuchaz.enigma.translation.mapping.AccessModifier;
21import cuchaz.enigma.translation.mapping.EntryMapping;
22import cuchaz.enigma.translation.representation.entry.*;
23import cuchaz.enigma.utils.I18n;
24import cuchaz.enigma.utils.validation.ValidationContext;
7 25
8import javax.swing.*; 26public class PanelIdentifier {
9import java.awt.*;
10
11public class PanelIdentifier extends JPanel {
12 27
13 private final Gui gui; 28 private final Gui gui;
14 29
30 private final JPanel ui;
31
32 private Entry<?> entry;
33 private Entry<?> deobfEntry;
34
35 private ConvertingTextField nameField;
36
37 private final ValidationContext vc = new ValidationContext();
38
15 public PanelIdentifier(Gui gui) { 39 public PanelIdentifier(Gui gui) {
16 this.gui = gui; 40 this.gui = gui;
17 41
18 this.setLayout(new GridLayout(4, 1, 0, 0)); 42 this.ui = new JPanel();
19 this.setPreferredSize(ScaleUtil.getDimension(0, 100)); 43 this.ui.setLayout(new GridBagLayout());
20 this.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier"))); 44 this.ui.setPreferredSize(ScaleUtil.getDimension(0, 120));
45 this.ui.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier")));
46 this.ui.setEnabled(false);
47 }
48
49 public void setReference(Entry<?> entry) {
50 this.entry = entry;
51 refreshReference();
21 } 52 }
22 53
23 public void clearReference() { 54 public boolean startRenaming() {
24 this.removeAll(); 55 if (this.nameField == null) return false;
25 JLabel label = new JLabel(I18n.translate("info_panel.identifier.none"));
26 GuiUtil.unboldLabel(label);
27 label.setHorizontalAlignment(JLabel.CENTER);
28 this.add(label);
29 56
30 gui.redraw(); 57 this.nameField.startEditing();
58
59 return true;
60 }
61
62 public boolean startRenaming(String text) {
63 if (this.nameField == null) return false;
64
65 this.nameField.startEditing();
66 this.nameField.setEditText(text);
67
68 return true;
69 }
70
71 private void onModifierChanged(AccessModifier modifier) {
72 gui.validateImmediateAction(vc -> this.gui.getController().onModifierChanged(vc, entry, modifier));
31 } 73 }
74
75 public void refreshReference() {
76 this.deobfEntry = entry == null ? null : gui.getController().project.getMapper().deobfuscate(this.entry);
77
78 this.nameField = null;
79
80 TableHelper th = new TableHelper(this.ui, this.entry, this.gui.getController().project);
81 th.begin();
82 if (this.entry == null) {
83 this.ui.setEnabled(false);
84 } else {
85 this.ui.setEnabled(true);
86
87 if (deobfEntry instanceof ClassEntry) {
88 ClassEntry ce = (ClassEntry) deobfEntry;
89 this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.class"), ce.getFullName());
90 th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged);
91 } else if (deobfEntry instanceof FieldEntry) {
92 FieldEntry fe = (FieldEntry) deobfEntry;
93 this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.field"), fe.getName());
94 th.addStringRow(I18n.translate("info_panel.identifier.class"), fe.getParent().getFullName());
95 th.addStringRow(I18n.translate("info_panel.identifier.type_descriptor"), fe.getDesc().toString());
96 th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged);
97 } else if (deobfEntry instanceof MethodEntry) {
98 MethodEntry me = (MethodEntry) deobfEntry;
99 if (me.isConstructor()) {
100 th.addStringRow(I18n.translate("info_panel.identifier.constructor"), me.getParent().getFullName());
101 } else {
102 this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.method"), me.getName());
103 th.addStringRow(I18n.translate("info_panel.identifier.class"), me.getParent().getFullName());
104 }
105 th.addStringRow(I18n.translate("info_panel.identifier.method_descriptor"), me.getDesc().toString());
106 th.addModifierRow(I18n.translate("info_panel.identifier.modifier"), this::onModifierChanged);
107 } else if (deobfEntry instanceof LocalVariableEntry) {
108 LocalVariableEntry lve = (LocalVariableEntry) deobfEntry;
109 this.nameField = th.addRenameTextField(I18n.translate("info_panel.identifier.variable"), lve.getName());
110 th.addStringRow(I18n.translate("info_panel.identifier.class"), lve.getContainingClass().getFullName());
111 th.addStringRow(I18n.translate("info_panel.identifier.method"), lve.getParent().getName());
112 th.addStringRow(I18n.translate("info_panel.identifier.index"), Integer.toString(lve.getIndex()));
113 } else {
114 throw new IllegalStateException("unreachable");
115 }
116 }
117 th.end();
118
119 if (this.nameField != null) {
120 this.nameField.addListener(new ConvertingTextFieldListener() {
121 @Override
122 public void onStartEditing(ConvertingTextField field) {
123 int i = field.getText().lastIndexOf('/');
124 if (i != -1) {
125 field.selectSubstring(i + 1);
126 }
127 }
128
129 @Override
130 public boolean tryStopEditing(ConvertingTextField field, boolean abort) {
131 if (abort) return true;
132 vc.reset();
133 vc.setActiveElement(field);
134 validateRename(field.getText());
135 return vc.canProceed();
136 }
137
138 @Override
139 public void onStopEditing(ConvertingTextField field, boolean abort) {
140 if (abort) return;
141 vc.reset();
142 vc.setActiveElement(field);
143 doRename(field.getText());
144 }
145 });
146 }
147
148 this.ui.validate();
149 this.ui.repaint();
150 }
151
152 private void validateRename(String newName) {
153 gui.getController().rename(vc, new EntryReference<>(entry, deobfEntry.getName()), newName, true, true);
154 }
155
156 private void doRename(String newName) {
157 gui.getController().rename(vc, new EntryReference<>(entry, deobfEntry.getName()), newName, true);
158 if (!vc.canProceed()) return;
159 gui.getController().sendPacket(new RenameC2SPacket(entry, newName, true));
160 }
161
162 public JPanel getUi() {
163 return ui;
164 }
165
166 private static final class TableHelper {
167
168 private final Container c;
169 private final Entry<?> e;
170 private final EnigmaProject project;
171 private final GridBagConstraints col1;
172 private final GridBagConstraints col2;
173
174 public TableHelper(Container c, Entry<?> e, EnigmaProject project) {
175 this.c = c;
176 this.e = e;
177 this.project = project;
178 this.col1 = new GridBagConstraints();
179 this.col2 = new GridBagConstraints();
180 Insets insets = ScaleUtil.getInsets(2, 2, 2, 2);
181 this.col1.gridx = 0;
182 this.col1.gridy = 0;
183 this.col1.insets = insets;
184 this.col1.anchor = GridBagConstraints.WEST;
185 this.col2.gridx = 1;
186 this.col2.gridy = 0;
187 this.col2.weightx = 1.0;
188 this.col2.fill = GridBagConstraints.HORIZONTAL;
189 this.col2.insets = insets;
190 this.col2.anchor = GridBagConstraints.WEST;
191 }
192
193 public void begin() {
194 c.removeAll();
195 c.setLayout(new GridBagLayout());
196 }
197
198 public void addRow(Component c1, Component c2) {
199 c.add(c1, col1);
200 c.add(c2, col2);
201
202 col1.gridy += 1;
203 col2.gridy += 1;
204 }
205
206 public ConvertingTextField addCovertTextField(String c1, String c2) {
207 ConvertingTextField textField = new ConvertingTextField(c2);
208 addRow(new JLabel(c1), textField.getUi());
209 return textField;
210 }
211
212 public ConvertingTextField addRenameTextField(String c1, String c2) {
213 if (project.isRenamable(e)) {
214 return addCovertTextField(c1, c2);
215 } else {
216 addStringRow(c1, c2);
217 return null;
218 }
219 }
220
221 public void addStringRow(String c1, String c2) {
222 addRow(new JLabel(c1), GuiUtil.unboldLabel(new JLabel(c2)));
223 }
224
225 public JComboBox<AccessModifier> addModifierRow(String c1, Consumer<AccessModifier> changeListener) {
226 if (!project.isRenamable(e))
227 return null;
228 JComboBox<AccessModifier> combo = new JComboBox<>(AccessModifier.values());
229 EntryMapping mapping = project.getMapper().getDeobfMapping(e);
230 if (mapping != null) {
231 combo.setSelectedIndex(mapping.getAccessModifier().ordinal());
232 } else {
233 combo.setSelectedIndex(AccessModifier.UNCHANGED.ordinal());
234 }
235 combo.addItemListener(event -> {
236 if (event.getStateChange() == ItemEvent.SELECTED) {
237 AccessModifier modifier = (AccessModifier) event.getItem();
238 changeListener.accept(modifier);
239 }
240 });
241
242 addRow(new JLabel(c1), combo);
243
244 return combo;
245 }
246
247 public void end() {
248 // Add an empty panel with y-weight=1 so that all the other elements get placed at the top edge
249 this.col1.weighty = 1.0;
250 c.add(new JPanel(), col1);
251 }
252
253 }
254
32} 255}
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 {
40 manager.setInitialDelay(oldDelay); 40 manager.setInitialDelay(oldDelay);
41 } 41 }
42 42
43 public static Rectangle safeModelToView(JTextComponent component, int modelPos) {
44 if (modelPos < 0) {
45 modelPos = 0;
46 } else if (modelPos >= component.getText().length()) {
47 modelPos = component.getText().length();
48 }
49 try {
50 return component.modelToView(modelPos);
51 } catch (BadLocationException e) {
52 throw new RuntimeException(e);
53 }
54 }
55
56} 43}
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;
2 2
3import java.awt.Dimension; 3import java.awt.Dimension;
4import java.awt.Font; 4import java.awt.Font;
5import java.awt.Insets;
5import java.io.IOException; 6import java.io.IOException;
6import java.lang.reflect.Field; 7import java.lang.reflect.Field;
7import java.util.ArrayList; 8import java.util.ArrayList;
@@ -51,8 +52,12 @@ public class ScaleUtil {
51 return new Dimension(scale(width), scale(height)); 52 return new Dimension(scale(width), scale(height));
52 } 53 }
53 54
54 public static Font getFont(String fontName, int plain, int fontSize) { 55 public static Insets getInsets(int top, int left, int bottom, int right) {
55 return scaleFont(new Font(fontName, plain, fontSize)); 56 return new Insets(scale(top), scale(left), scale(bottom), scale(right));
57 }
58
59 public static Font getFont(String fontName, int style, int fontSize) {
60 return scaleFont(new Font(fontName, style, fontSize));
56 } 61 }
57 62
58 public static Font scaleFont(Font font) { 63 public static Font scaleFont(Font font) {
@@ -106,5 +111,4 @@ public class ScaleUtil {
106 } 111 }
107 return new BasicTweaker(dpiScaling); 112 return new BasicTweaker(dpiScaling);
108 } 113 }
109
110} 114}
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 @@
11 11
12package cuchaz.enigma.analysis; 12package cuchaz.enigma.analysis;
13 13
14import java.util.Arrays;
15import java.util.List;
16import java.util.Objects;
17
14import cuchaz.enigma.translation.Translatable; 18import cuchaz.enigma.translation.Translatable;
15import cuchaz.enigma.translation.Translator; 19import cuchaz.enigma.translation.Translator;
20import cuchaz.enigma.translation.mapping.EntryMap;
16import cuchaz.enigma.translation.mapping.EntryMapping; 21import cuchaz.enigma.translation.mapping.EntryMapping;
17import cuchaz.enigma.translation.mapping.EntryResolver; 22import cuchaz.enigma.translation.mapping.EntryResolver;
18import cuchaz.enigma.translation.mapping.EntryMap;
19import cuchaz.enigma.translation.representation.entry.ClassEntry; 23import cuchaz.enigma.translation.representation.entry.ClassEntry;
20import cuchaz.enigma.translation.representation.entry.Entry; 24import cuchaz.enigma.translation.representation.entry.Entry;
21import cuchaz.enigma.translation.representation.entry.MethodEntry; 25import cuchaz.enigma.translation.representation.entry.MethodEntry;
22 26
23import java.util.Arrays;
24import java.util.List;
25import java.util.Objects;
26
27public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements Translatable { 27public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements Translatable {
28 28
29 private static final List<String> CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static"); 29 private static final List<String> CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static");
@@ -100,6 +100,8 @@ public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements T
100 } 100 }
101 101
102 public boolean equals(EntryReference<?, ?> other) { 102 public boolean equals(EntryReference<?, ?> other) {
103 if (other == null) return false;
104
103 // check entry first 105 // check entry first
104 boolean isEntrySame = entry.equals(other.entry); 106 boolean isEntrySame = entry.equals(other.entry);
105 if (!isEntrySame) { 107 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 @@
1package cuchaz.enigma.classhandle;
2
3import java.util.concurrent.CompletableFuture;
4
5import javax.annotation.Nullable;
6
7import cuchaz.enigma.events.ClassHandleListener;
8import cuchaz.enigma.source.DecompiledClassSource;
9import cuchaz.enigma.source.Source;
10import cuchaz.enigma.translation.representation.entry.ClassEntry;
11import cuchaz.enigma.utils.Result;
12
13/**
14 * A handle to a class file. Can be treated similarly to a handle to a file.
15 * This type allows for accessing decompiled classes and being notified when
16 * mappings for the class it belongs to changes.
17 *
18 * @see ClassHandleProvider
19 */
20public interface ClassHandle extends AutoCloseable {
21
22 /**
23 * Gets the reference to this class. This is always obfuscated, for example
24 * {@code net/minecraft/class_1000}.
25 *
26 * @return the obfuscated class reference
27 * @throws IllegalStateException if the class handle is closed
28 */
29 ClassEntry getRef();
30
31 /**
32 * Gets the deobfuscated reference to this class, if any.
33 *
34 * @return the deobfuscated reference, or {@code null} if the class is not
35 * mapped
36 * @throws IllegalStateException if the class handle is closed
37 */
38 @Nullable
39 ClassEntry getDeobfRef();
40
41 /**
42 * Gets the class source, or the error generated while decompiling the
43 * class, asynchronously. If this class has already finished decompiling,
44 * this will return an already completed future.
45 *
46 * @return the class source
47 * @throws IllegalStateException if the class handle is closed
48 */
49 CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSource();
50
51 /**
52 * Gets the class source without any decoration, or the error generated
53 * while decompiling the class, asynchronously. This is the raw source from
54 * the decompiler and will not be deobfuscated, and does not contain any
55 * Javadoc comments added via mappings. If this class has already finished
56 * decompiling, this will return an already completed future.
57 *
58 * @return the uncommented class source
59 * @throws IllegalStateException if the class handle is closed
60 * @see ClassHandle#getSource()
61 */
62 CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSource();
63
64 void invalidate();
65
66 void invalidateMapped();
67
68 void invalidateJavadoc();
69
70 /**
71 * Adds a listener for this class handle.
72 *
73 * @param listener the listener to add
74 * @see ClassHandleListener
75 */
76 void addListener(ClassHandleListener listener);
77
78 /**
79 * Removes a previously added listener (with
80 * {@link ClassHandle#addListener(ClassHandleListener)}) from this class
81 * handle.
82 *
83 * @param listener the listener to remove
84 */
85 void removeListener(ClassHandleListener listener);
86
87 /**
88 * Copies this class handle. The new class handle points to the same class,
89 * but is independent from this class handle in every other aspect.
90 * Specifically, any listeners will not be copied to the new class handle.
91 *
92 * @return a copy of this class handle
93 * @throws IllegalStateException if the class handle is closed
94 */
95 ClassHandle copy();
96
97 /**
98 * {@inheritDoc}
99 *
100 * <p>Specifically, for class handles, this means that most methods on the
101 * handle will throw an exception if called, that the handle will no longer
102 * receive any events over any added listeners, and the handle will no
103 * longer be able to be copied.
104 */
105 @Override
106 void close();
107
108}
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 @@
1package cuchaz.enigma.classhandle;
2
3import java.io.ByteArrayOutputStream;
4import java.io.PrintStream;
5
6import javax.annotation.Nullable;
7
8public final class ClassHandleError {
9
10 public final Type type;
11 public final Throwable cause;
12
13 private ClassHandleError(Type type, Throwable cause) {
14 this.type = type;
15 this.cause = cause;
16 }
17
18 @Nullable
19 public String getStackTrace() {
20 if (cause == null) return null;
21 ByteArrayOutputStream os = new ByteArrayOutputStream();
22 PrintStream ps = new PrintStream(os);
23 cause.printStackTrace(ps);
24 return os.toString();
25 }
26
27 public static ClassHandleError decompile(Throwable cause) {
28 return new ClassHandleError(Type.DECOMPILE, cause);
29 }
30
31 public enum Type {
32 DECOMPILE,
33 }
34
35} \ 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 @@
1package cuchaz.enigma.classhandle;
2
3import java.util.*;
4import java.util.concurrent.CompletableFuture;
5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.Executors;
7import java.util.concurrent.TimeUnit;
8import java.util.concurrent.atomic.AtomicInteger;
9import java.util.concurrent.locks.ReadWriteLock;
10import java.util.concurrent.locks.ReentrantReadWriteLock;
11
12import javax.annotation.Nullable;
13
14import cuchaz.enigma.Enigma;
15import cuchaz.enigma.EnigmaProject;
16import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
17import cuchaz.enigma.events.ClassHandleListener;
18import cuchaz.enigma.events.ClassHandleListener.InvalidationType;
19import cuchaz.enigma.source.*;
20import cuchaz.enigma.translation.representation.entry.ClassEntry;
21import cuchaz.enigma.utils.Result;
22import org.objectweb.asm.tree.ClassNode;
23
24import static cuchaz.enigma.utils.Utils.withLock;
25
26public final class ClassHandleProvider {
27
28 private final EnigmaProject project;
29
30 private final ExecutorService pool = Executors.newWorkStealingPool();
31 private DecompilerService ds;
32 private Decompiler decompiler;
33
34 private final Map<ClassEntry, Entry> handles = new HashMap<>();
35
36 private final ReadWriteLock lock = new ReentrantReadWriteLock();
37
38 public ClassHandleProvider(EnigmaProject project, DecompilerService ds) {
39 this.project = project;
40 this.ds = ds;
41 this.decompiler = createDecompiler();
42 }
43
44 /**
45 * Open a class by entry. Schedules decompilation immediately if this is the
46 * only handle to the class.
47 *
48 * @param entry the entry of the class to open
49 * @return a handle to the class, {@code null} if a class by that name does
50 * not exist
51 */
52 @Nullable
53 public ClassHandle openClass(ClassEntry entry) {
54 if (!project.getJarIndex().getEntryIndex().hasClass(entry)) return null;
55
56 return withLock(lock.writeLock(), () -> {
57 Entry e = handles.computeIfAbsent(entry, entry1 -> new Entry(this, entry1));
58 return e.createHandle();
59 });
60 }
61
62 /**
63 * Set the decompiler service to use when decompiling classes. Invalidates
64 * all currently open classes.
65 *
66 * <p>If the current decompiler service equals the old one, no classes will
67 * be invalidated.
68 *
69 * @param ds the decompiler service to use
70 */
71 public void setDecompilerService(DecompilerService ds) {
72 if (this.ds.equals(ds)) return;
73
74 this.ds = ds;
75 this.decompiler = createDecompiler();
76 withLock(lock.readLock(), () -> {
77 handles.values().forEach(Entry::invalidate);
78 });
79 }
80
81 /**
82 * Gets the current decompiler service in use.
83 *
84 * @return the current decompiler service
85 */
86 public DecompilerService getDecompilerService() {
87 return ds;
88 }
89
90 private Decompiler createDecompiler() {
91 return ds.create(name -> {
92 ClassNode node = project.getClassCache().getClassNode(name);
93
94 if (node == null) {
95 return null;
96 }
97
98 ClassNode fixedNode = new ClassNode();
99 node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, project.getJarIndex()));
100 return fixedNode;
101 }, new SourceSettings(true, true));
102 }
103
104 /**
105 * Invalidates all mappings. This causes all open class handles to be
106 * re-remapped.
107 */
108 public void invalidateMapped() {
109 withLock(lock.readLock(), () -> {
110 handles.values().forEach(Entry::invalidateMapped);
111 });
112 }
113
114 /**
115 * Invalidates mappings for a single class. Note that this does not
116 * invalidate any mappings of other classes where this class is used, so
117 * this should not be used to notify that the mapped name for this class has
118 * changed.
119 *
120 * @param entry the class entry to invalidate
121 */
122 public void invalidateMapped(ClassEntry entry) {
123 withLock(lock.readLock(), () -> {
124 Entry e = handles.get(entry);
125 if (e != null) {
126 e.invalidateMapped();
127 }
128 });
129 }
130
131 /**
132 * Invalidates javadoc for a single class. This also causes the class to be
133 * remapped again.
134 *
135 * @param entry the class entry to invalidate
136 */
137 public void invalidateJavadoc(ClassEntry entry) {
138 withLock(lock.readLock(), () -> {
139 Entry e = handles.get(entry);
140 if (e != null) {
141 e.invalidateJavadoc();
142 }
143 });
144 }
145
146 private void deleteEntry(Entry entry) {
147 withLock(lock.writeLock(), () -> {
148 handles.remove(entry.entry);
149 });
150 }
151
152 /**
153 * Destroy this class handle provider. The decompiler threads will try to
154 * shutdown cleanly, and then every open class handle will also be deleted.
155 * This causes {@link ClassHandleListener#onDeleted(ClassHandle)} to get
156 * called.
157 *
158 * <p>After this method is called, this class handle provider can no longer
159 * be used.
160 */
161 public void destroy() {
162 pool.shutdown();
163 try {
164 pool.awaitTermination(30, TimeUnit.SECONDS);
165 } catch (InterruptedException e) {
166 throw new RuntimeException(e);
167 }
168
169 withLock(lock.writeLock(), () -> {
170 handles.values().forEach(Entry::destroy);
171 handles.clear();
172 });
173 }
174
175 private static final class Entry {
176
177 private final ClassHandleProvider p;
178 private final ClassEntry entry;
179 private ClassEntry deobfRef;
180 private final List<ClassHandleImpl> handles = new ArrayList<>();
181 private Result<Source, ClassHandleError> uncommentedSource;
182 private Result<DecompiledClassSource, ClassHandleError> source;
183
184 private final List<CompletableFuture<Result<Source, ClassHandleError>>> waitingUncommentedSources = Collections.synchronizedList(new ArrayList<>());
185 private final List<CompletableFuture<Result<DecompiledClassSource, ClassHandleError>>> waitingSources = Collections.synchronizedList(new ArrayList<>());
186
187 private final AtomicInteger decompileVersion = new AtomicInteger();
188 private final AtomicInteger javadocVersion = new AtomicInteger();
189 private final AtomicInteger indexVersion = new AtomicInteger();
190 private final AtomicInteger mappedVersion = new AtomicInteger();
191
192 private final ReadWriteLock lock = new ReentrantReadWriteLock();
193
194 private Entry(ClassHandleProvider p, ClassEntry entry) {
195 this.p = p;
196 this.entry = entry;
197 this.deobfRef = p.project.getMapper().deobfuscate(entry);
198 invalidate();
199 }
200
201 public ClassHandleImpl createHandle() {
202 ClassHandleImpl handle = new ClassHandleImpl(this);
203 withLock(lock.writeLock(), () -> {
204 handles.add(handle);
205 });
206 return handle;
207 }
208
209 @Nullable
210 public ClassEntry getDeobfRef() {
211 return deobfRef;
212 }
213
214 private void checkDeobfRefForUpdate() {
215 ClassEntry newDeobf = p.project.getMapper().deobfuscate(entry);
216 if (!Objects.equals(deobfRef, newDeobf)) {
217 deobfRef = newDeobf;
218 // copy the list so we don't call event listener code with the lock active
219 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDeobfRefChanged(newDeobf));
220 }
221 }
222
223 public void invalidate() {
224 checkDeobfRefForUpdate();
225 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.FULL));
226 continueMapSource(continueIndexSource(continueInsertJavadoc(decompile())));
227 }
228
229 public void invalidateJavadoc() {
230 checkDeobfRefForUpdate();
231 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.JAVADOC));
232 continueMapSource(continueIndexSource(continueInsertJavadoc(CompletableFuture.completedFuture(uncommentedSource))));
233 }
234
235 public void invalidateMapped() {
236 checkDeobfRefForUpdate();
237 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.MAPPINGS));
238 continueMapSource(CompletableFuture.completedFuture(source));
239 }
240
241 private CompletableFuture<Result<Source, ClassHandleError>> decompile() {
242 int v = decompileVersion.incrementAndGet();
243 return CompletableFuture.supplyAsync(() -> {
244 if (decompileVersion.get() != v) return null;
245
246 Result<Source, ClassHandleError> _uncommentedSource;
247 try {
248 _uncommentedSource = Result.ok(p.decompiler.getSource(entry.getFullName()));
249 } catch (Throwable e) {
250 return Result.err(ClassHandleError.decompile(e));
251 }
252 Result<Source, ClassHandleError> uncommentedSource = _uncommentedSource;
253 Entry.this.uncommentedSource = uncommentedSource;
254 Entry.this.waitingUncommentedSources.forEach(f -> f.complete(uncommentedSource));
255 Entry.this.waitingUncommentedSources.clear();
256 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onUncommentedSourceChanged(uncommentedSource));
257 return uncommentedSource;
258 }, p.pool);
259 }
260
261 private CompletableFuture<Result<Source, ClassHandleError>> continueInsertJavadoc(CompletableFuture<Result<Source, ClassHandleError>> f) {
262 int v = javadocVersion.incrementAndGet();
263 return f.thenApplyAsync(res -> {
264 if (res == null || javadocVersion.get() != v) return null;
265 Result<Source, ClassHandleError> jdSource = res.map(s -> s.addJavadocs(p.project.getMapper()));
266 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDocsChanged(jdSource));
267 return jdSource;
268 }, p.pool);
269 }
270
271 private CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> continueIndexSource(CompletableFuture<Result<Source, ClassHandleError>> f) {
272 int v = indexVersion.incrementAndGet();
273 return f.thenApplyAsync(res -> {
274 if (res == null || indexVersion.get() != v) return null;
275 return res.andThen(jdSource -> {
276 SourceIndex index = jdSource.index();
277 index.resolveReferences(p.project.getMapper().getObfResolver());
278 DecompiledClassSource source = new DecompiledClassSource(entry, index);
279 return Result.ok(source);
280 });
281 }, p.pool);
282 }
283
284 private void continueMapSource(CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> f) {
285 int v = mappedVersion.incrementAndGet();
286 f.thenAcceptAsync(res -> {
287 if (res == null || mappedVersion.get() != v) return;
288 res = res.map(source -> {
289 source.remapSource(p.project, p.project.getMapper().getDeobfuscator());
290 return source;
291 });
292 Entry.this.source = res;
293 Entry.this.waitingSources.forEach(s -> s.complete(source));
294 Entry.this.waitingSources.clear();
295 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onMappedSourceChanged(source));
296 }, p.pool);
297 }
298
299 public void closeHandle(ClassHandleImpl classHandle) {
300 classHandle.destroy();
301 withLock(lock.writeLock(), () -> {
302 handles.remove(classHandle);
303 if (handles.isEmpty()) {
304 p.deleteEntry(this);
305 }
306 });
307 }
308
309 public void destroy() {
310 withLock(lock.writeLock(), () -> {
311 handles.forEach(ClassHandleImpl::destroy);
312 handles.clear();
313 });
314 }
315
316 public CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSourceAsync() {
317 if (uncommentedSource != null) {
318 return CompletableFuture.completedFuture(uncommentedSource);
319 } else {
320 CompletableFuture<Result<Source, ClassHandleError>> f = new CompletableFuture<>();
321 waitingUncommentedSources.add(f);
322 return f;
323 }
324 }
325
326 public CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSourceAsync() {
327 if (source != null) {
328 return CompletableFuture.completedFuture(source);
329 } else {
330 CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> f = new CompletableFuture<>();
331 waitingSources.add(f);
332 return f;
333 }
334 }
335 }
336
337 private static final class ClassHandleImpl implements ClassHandle {
338
339 private final Entry entry;
340
341 private boolean valid = true;
342
343 private final Set<ClassHandleListener> listeners = new HashSet<>();
344
345 private ClassHandleImpl(Entry entry) {
346 this.entry = entry;
347 }
348
349 @Override
350 public ClassEntry getRef() {
351 checkValid();
352 return entry.entry;
353 }
354
355 @Nullable
356 @Override
357 public ClassEntry getDeobfRef() {
358 checkValid();
359 // cache this?
360 return entry.getDeobfRef();
361 }
362
363 @Override
364 public CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSource() {
365 checkValid();
366 return entry.getSourceAsync();
367 }
368
369 @Override
370 public CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSource() {
371 checkValid();
372 return entry.getUncommentedSourceAsync();
373 }
374
375 @Override
376 public void invalidate() {
377 checkValid();
378 this.entry.invalidate();
379 }
380
381 @Override
382 public void invalidateMapped() {
383 checkValid();
384 this.entry.invalidateMapped();
385 }
386
387 @Override
388 public void invalidateJavadoc() {
389 checkValid();
390 this.entry.invalidateJavadoc();
391 }
392
393 public void onUncommentedSourceChanged(Result<Source, ClassHandleError> source) {
394 listeners.forEach(l -> l.onUncommentedSourceChanged(this, source));
395 }
396
397 public void onDocsChanged(Result<Source, ClassHandleError> source) {
398 listeners.forEach(l -> l.onDocsChanged(this, source));
399 }
400
401 public void onMappedSourceChanged(Result<DecompiledClassSource, ClassHandleError> source) {
402 listeners.forEach(l -> l.onMappedSourceChanged(this, source));
403 }
404
405 public void onInvalidate(InvalidationType t) {
406 listeners.forEach(l -> l.onInvalidate(this, t));
407 }
408
409 public void onDeobfRefChanged(ClassEntry newDeobf) {
410 listeners.forEach(l -> l.onDeobfRefChanged(this, newDeobf));
411 }
412
413 @Override
414 public void addListener(ClassHandleListener listener) {
415 listeners.add(listener);
416 }
417
418 @Override
419 public void removeListener(ClassHandleListener listener) {
420 listeners.remove(listener);
421 }
422
423 @Override
424 public ClassHandle copy() {
425 checkValid();
426 return entry.createHandle();
427 }
428
429 @Override
430 public void close() {
431 if (valid) entry.closeHandle(this);
432 }
433
434 private void checkValid() {
435 if (!valid) throw new IllegalStateException("Class handle no longer valid");
436 }
437
438 public void destroy() {
439 listeners.forEach(l -> l.onDeleted(this));
440 valid = false;
441 }
442
443 }
444
445}
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 @@
1package cuchaz.enigma.events;
2
3import cuchaz.enigma.classhandle.ClassHandle;
4import cuchaz.enigma.classhandle.ClassHandleError;
5import cuchaz.enigma.source.DecompiledClassSource;
6import cuchaz.enigma.source.Source;
7import cuchaz.enigma.translation.representation.entry.ClassEntry;
8import cuchaz.enigma.utils.Result;
9
10public interface ClassHandleListener {
11
12 default void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) {
13 }
14
15 default void onUncommentedSourceChanged(ClassHandle h, Result<Source, ClassHandleError> res) {
16 }
17
18 default void onDocsChanged(ClassHandle h, Result<Source, ClassHandleError> res) {
19 }
20
21 default void onMappedSourceChanged(ClassHandle h, Result<DecompiledClassSource, ClassHandleError> res) {
22 }
23
24 default void onInvalidate(ClassHandle h, InvalidationType t) {
25 }
26
27 default void onDeleted(ClassHandle h) {
28 }
29
30 enum InvalidationType {
31 FULL,
32 JAVADOC,
33 MAPPINGS,
34 }
35
36}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java
index aca5d724..85fba505 100644
--- a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
+++ b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java
@@ -1,13 +1,13 @@
1package cuchaz.enigma.gui; 1package cuchaz.enigma.source;
2
3import java.util.*;
4
5import javax.annotation.Nullable;
2 6
3import cuchaz.enigma.EnigmaProject; 7import cuchaz.enigma.EnigmaProject;
4import cuchaz.enigma.EnigmaServices; 8import cuchaz.enigma.EnigmaServices;
5import cuchaz.enigma.analysis.EntryReference; 9import cuchaz.enigma.analysis.EntryReference;
6import cuchaz.enigma.source.Token;
7import cuchaz.enigma.api.service.NameProposalService; 10import cuchaz.enigma.api.service.NameProposalService;
8import cuchaz.enigma.gui.highlight.TokenHighlightType;
9import cuchaz.enigma.source.SourceIndex;
10import cuchaz.enigma.source.SourceRemapper;
11import cuchaz.enigma.translation.LocalNameGenerator; 11import cuchaz.enigma.translation.LocalNameGenerator;
12import cuchaz.enigma.translation.Translator; 12import cuchaz.enigma.translation.Translator;
13import cuchaz.enigma.translation.mapping.EntryRemapper; 13import cuchaz.enigma.translation.mapping.EntryRemapper;
@@ -17,16 +17,13 @@ import cuchaz.enigma.translation.representation.entry.ClassEntry;
17import cuchaz.enigma.translation.representation.entry.Entry; 17import cuchaz.enigma.translation.representation.entry.Entry;
18import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry; 18import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry;
19 19
20import javax.annotation.Nullable;
21import java.util.*;
22
23public class DecompiledClassSource { 20public class DecompiledClassSource {
24 private final ClassEntry classEntry; 21 private final ClassEntry classEntry;
25 22
26 private final SourceIndex obfuscatedIndex; 23 private final SourceIndex obfuscatedIndex;
27 private SourceIndex remappedIndex; 24 private SourceIndex remappedIndex;
28 25
29 private final Map<TokenHighlightType, Collection<Token>> highlightedTokens = new EnumMap<>(TokenHighlightType.class); 26 private final Map<RenamableTokenType, Collection<Token>> highlightedTokens = new EnumMap<>(RenamableTokenType.class);
30 27
31 public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) { 28 public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) {
32 this.classEntry = classEntry; 29 this.classEntry = classEntry;
@@ -55,16 +52,16 @@ public class DecompiledClassSource {
55 52
56 if (project.isRenamable(reference)) { 53 if (project.isRenamable(reference)) {
57 if (isDeobfuscated(entry, translatedEntry)) { 54 if (isDeobfuscated(entry, translatedEntry)) {
58 highlightToken(movedToken, TokenHighlightType.DEOBFUSCATED); 55 highlightToken(movedToken, RenamableTokenType.DEOBFUSCATED);
59 return translatedEntry.getSourceRemapName(); 56 return translatedEntry.getSourceRemapName();
60 } else { 57 } else {
61 Optional<String> proposedName = proposeName(project, entry); 58 Optional<String> proposedName = proposeName(project, entry);
62 if (proposedName.isPresent()) { 59 if (proposedName.isPresent()) {
63 highlightToken(movedToken, TokenHighlightType.PROPOSED); 60 highlightToken(movedToken, RenamableTokenType.PROPOSED);
64 return proposedName.get(); 61 return proposedName.get();
65 } 62 }
66 63
67 highlightToken(movedToken, TokenHighlightType.OBFUSCATED); 64 highlightToken(movedToken, RenamableTokenType.OBFUSCATED);
68 } 65 }
69 } 66 }
70 67
@@ -119,11 +116,11 @@ public class DecompiledClassSource {
119 return remappedIndex; 116 return remappedIndex;
120 } 117 }
121 118
122 public Map<TokenHighlightType, Collection<Token>> getHighlightedTokens() { 119 public Map<RenamableTokenType, Collection<Token>> getHighlightedTokens() {
123 return highlightedTokens; 120 return highlightedTokens;
124 } 121 }
125 122
126 private void highlightToken(Token token, TokenHighlightType highlightType) { 123 private void highlightToken(Token token, RenamableTokenType highlightType) {
127 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token); 124 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token);
128 } 125 }
129 126
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 @@
1package cuchaz.enigma.source;
2
3public enum RenamableTokenType {
4 OBFUSCATED,
5 DEOBFUSCATED,
6 PROPOSED
7}
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 @@
1package cuchaz.enigma.translation; 1package cuchaz.enigma.translation;
2 2
3import cuchaz.enigma.translation.mapping.NameValidator;
4import cuchaz.enigma.translation.representation.TypeDescriptor;
5
6import java.util.Collection; 3import java.util.Collection;
7import java.util.Locale; 4import java.util.Locale;
8 5
6import cuchaz.enigma.translation.mapping.IdentifierValidation;
7import cuchaz.enigma.translation.representation.TypeDescriptor;
8
9public class LocalNameGenerator { 9public class LocalNameGenerator {
10 public static String generateArgumentName(int index, TypeDescriptor desc, Collection<TypeDescriptor> arguments) { 10 public static String generateArgumentName(int index, TypeDescriptor desc, Collection<TypeDescriptor> arguments) {
11 boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1; 11 boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1;
12 String translatedName; 12 String translatedName;
13 int nameIndex = index + 1; 13 int nameIndex = index + 1;
14 StringBuilder nameBuilder = new StringBuilder(getTypeName(desc)); 14 StringBuilder nameBuilder = new StringBuilder(getTypeName(desc));
15 if (!uniqueType || NameValidator.isReserved(nameBuilder.toString())) { 15 if (!uniqueType || IdentifierValidation.isReservedMethodName(nameBuilder.toString())) {
16 nameBuilder.append(nameIndex); 16 nameBuilder.append(nameIndex);
17 } 17 }
18 translatedName = nameBuilder.toString(); 18 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 @@
1package cuchaz.enigma.translation.mapping; 1package cuchaz.enigma.translation.mapping;
2 2
3import java.util.Collection;
4import java.util.stream.Stream;
5
6import javax.annotation.Nullable;
7
3import cuchaz.enigma.analysis.index.JarIndex; 8import cuchaz.enigma.analysis.index.JarIndex;
4import cuchaz.enigma.translation.MappingTranslator; 9import cuchaz.enigma.translation.MappingTranslator;
5import cuchaz.enigma.translation.Translatable; 10import cuchaz.enigma.translation.Translatable;
@@ -8,10 +13,7 @@ import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
8import cuchaz.enigma.translation.mapping.tree.EntryTree; 13import cuchaz.enigma.translation.mapping.tree.EntryTree;
9import cuchaz.enigma.translation.mapping.tree.HashEntryTree; 14import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
10import cuchaz.enigma.translation.representation.entry.Entry; 15import cuchaz.enigma.translation.representation.entry.Entry;
11 16import cuchaz.enigma.utils.validation.ValidationContext;
12import javax.annotation.Nullable;
13import java.util.Collection;
14import java.util.stream.Stream;
15 17
16public class EntryRemapper { 18public class EntryRemapper {
17 private final DeltaTrackingTree<EntryMapping> obfToDeobf; 19 private final DeltaTrackingTree<EntryMapping> obfToDeobf;
@@ -39,26 +41,32 @@ public class EntryRemapper {
39 return new EntryRemapper(index, new HashEntryTree<>()); 41 return new EntryRemapper(index, new HashEntryTree<>());
40 } 42 }
41 43
42 public <E extends Entry<?>> void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { 44 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping) {
43 mapFromObf(obfuscatedEntry, deobfMapping, true); 45 mapFromObf(vc, obfuscatedEntry, deobfMapping, true);
44 } 46 }
45 47
46 public <E extends Entry<?>> void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) { 48 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) {
49 mapFromObf(vc, obfuscatedEntry, deobfMapping, renaming, false);
50 }
51
52 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming, boolean validateOnly) {
47 Collection<E> resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST); 53 Collection<E> resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST);
48 54
49 if (renaming && deobfMapping != null) { 55 if (renaming && deobfMapping != null) {
50 for (E resolvedEntry : resolvedEntries) { 56 for (E resolvedEntry : resolvedEntries) {
51 validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); 57 validator.validateRename(vc, resolvedEntry, deobfMapping.getTargetName());
52 } 58 }
53 } 59 }
54 60
61 if (validateOnly || !vc.canProceed()) return;
62
55 for (E resolvedEntry : resolvedEntries) { 63 for (E resolvedEntry : resolvedEntries) {
56 obfToDeobf.insert(resolvedEntry, deobfMapping); 64 obfToDeobf.insert(resolvedEntry, deobfMapping);
57 } 65 }
58 } 66 }
59 67
60 public void removeByObf(Entry<?> obfuscatedEntry) { 68 public void removeByObf(ValidationContext vc, Entry<?> obfuscatedEntry) {
61 mapFromObf(obfuscatedEntry, null); 69 mapFromObf(vc, obfuscatedEntry, null);
62 } 70 }
63 71
64 @Nullable 72 @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 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.translation.mapping;
13
14import java.util.Arrays;
15import java.util.List;
16
17import cuchaz.enigma.utils.validation.Message;
18import cuchaz.enigma.utils.validation.StandardValidation;
19import cuchaz.enigma.utils.validation.ValidationContext;
20
21public final class IdentifierValidation {
22
23 private IdentifierValidation() {
24 }
25
26 private static final List<String> ILLEGAL_IDENTIFIERS = Arrays.asList(
27 "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized",
28 "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte",
29 "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch",
30 "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally",
31 "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_"
32 );
33
34 public static boolean validateClassName(ValidationContext vc, String name) {
35 if (!StandardValidation.notBlank(vc, name)) return false;
36 String[] parts = name.split("/");
37 for (String part : parts) {
38 validateIdentifier(vc, part);
39 }
40 return true;
41 }
42
43 public static boolean validateIdentifier(ValidationContext vc, String name) {
44 if (!StandardValidation.notBlank(vc, name)) return false;
45 if (checkForReservedName(vc, name)) return false;
46
47 // Adapted from javax.lang.model.SourceVersion.isIdentifier
48
49 int cp = name.codePointAt(0);
50 int position = 1;
51 if (!Character.isJavaIdentifierStart(cp)) {
52 vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position);
53 return false;
54 }
55 for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
56 cp = name.codePointAt(i);
57 position += 1;
58 if (!Character.isJavaIdentifierPart(cp)) {
59 vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position);
60 return false;
61 }
62 }
63
64 return true;
65 }
66
67 private static boolean checkForReservedName(ValidationContext vc, String name) {
68 if (isReservedMethodName(name)) {
69 vc.raise(Message.RESERVED_IDENTIFIER);
70 return true;
71 }
72 return false;
73 }
74
75 public static boolean isReservedMethodName(String name) {
76 return ILLEGAL_IDENTIFIERS.contains(name);
77 }
78
79}
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 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.translation.mapping;
13
14public class IllegalNameException extends RuntimeException {
15
16 private String name;
17 private String reason;
18
19 public IllegalNameException(String name, String reason) {
20 this.name = name;
21 this.reason = reason;
22 }
23
24 public String getReason() {
25 return this.reason;
26 }
27
28 @Override
29 public String getMessage() {
30 StringBuilder buf = new StringBuilder();
31 buf.append("Illegal name: ");
32 buf.append(this.name);
33 if (this.reason != null) {
34 buf.append(" because ");
35 buf.append(this.reason);
36 }
37 return buf.toString();
38 }
39}
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 @@
1package cuchaz.enigma.translation.mapping; 1package cuchaz.enigma.translation.mapping;
2 2
3import java.util.Collection;
4import java.util.HashSet;
5import java.util.stream.Collectors;
6
3import cuchaz.enigma.analysis.index.InheritanceIndex; 7import cuchaz.enigma.analysis.index.InheritanceIndex;
4import cuchaz.enigma.analysis.index.JarIndex; 8import cuchaz.enigma.analysis.index.JarIndex;
5import cuchaz.enigma.translation.Translator; 9import cuchaz.enigma.translation.Translator;
6import cuchaz.enigma.translation.mapping.tree.EntryTree; 10import cuchaz.enigma.translation.mapping.tree.EntryTree;
7import cuchaz.enigma.translation.representation.entry.ClassEntry; 11import cuchaz.enigma.translation.representation.entry.ClassEntry;
8import cuchaz.enigma.translation.representation.entry.Entry; 12import cuchaz.enigma.translation.representation.entry.Entry;
9 13import cuchaz.enigma.utils.validation.Message;
10import java.util.Collection; 14import cuchaz.enigma.utils.validation.ValidationContext;
11import java.util.HashSet;
12import java.util.stream.Collectors;
13 15
14public class MappingValidator { 16public class MappingValidator {
17
15 private final EntryTree<EntryMapping> obfToDeobf; 18 private final EntryTree<EntryMapping> obfToDeobf;
16 private final Translator deobfuscator; 19 private final Translator deobfuscator;
17 private final JarIndex index; 20 private final JarIndex index;
@@ -22,15 +25,15 @@ public class MappingValidator {
22 this.index = index; 25 this.index = index;
23 } 26 }
24 27
25 public void validateRename(Entry<?> entry, String name) throws IllegalNameException { 28 public void validateRename(ValidationContext vc, Entry<?> entry, String name) {
26 Collection<Entry<?>> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry); 29 Collection<Entry<?>> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry);
27 for (Entry<?> equivalentEntry : equivalentEntries) { 30 for (Entry<?> equivalentEntry : equivalentEntries) {
28 equivalentEntry.validateName(name); 31 equivalentEntry.validateName(vc, name);
29 validateUnique(equivalentEntry, name); 32 validateUnique(vc, equivalentEntry, name);
30 } 33 }
31 } 34 }
32 35
33 private void validateUnique(Entry<?> entry, String name) { 36 private void validateUnique(ValidationContext vc, Entry<?> entry, String name) {
34 ClassEntry containingClass = entry.getContainingClass(); 37 ClassEntry containingClass = entry.getContainingClass();
35 Collection<ClassEntry> relatedClasses = getRelatedClasses(containingClass); 38 Collection<ClassEntry> relatedClasses = getRelatedClasses(containingClass);
36 39
@@ -45,9 +48,9 @@ public class MappingValidator {
45 if (!isUnique(translatedEntry, translatedSiblings, name)) { 48 if (!isUnique(translatedEntry, translatedSiblings, name)) {
46 Entry<?> parent = translatedEntry.getParent(); 49 Entry<?> parent = translatedEntry.getParent();
47 if (parent != null) { 50 if (parent != null) {
48 throw new IllegalNameException(name, "Name is not unique in " + parent + "!"); 51 vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent);
49 } else { 52 } else {
50 throw new IllegalNameException(name, "Name is not unique!"); 53 vc.raise(Message.NONUNIQUE_NAME, name);
51 } 54 }
52 } 55 }
53 } 56 }
@@ -72,4 +75,5 @@ public class MappingValidator {
72 } 75 }
73 return true; 76 return true;
74 } 77 }
78
75} 79}
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 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.translation.mapping;
13
14import java.util.Arrays;
15import java.util.List;
16import java.util.regex.Pattern;
17
18public class NameValidator {
19 private static final Pattern IDENTIFIER_PATTERN;
20 private static final Pattern CLASS_PATTERN;
21 private static final List<String> ILLEGAL_IDENTIFIERS = Arrays.asList(
22 "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized",
23 "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte",
24 "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch",
25 "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally",
26 "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_"
27 );
28
29 static {
30 String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*";
31 IDENTIFIER_PATTERN = Pattern.compile(identifierRegex);
32 CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex));
33 }
34
35 public static void validateClassName(String name) {
36 if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) {
37 throw new IllegalNameException(name, "This doesn't look like a legal class name");
38 }
39 }
40
41 public static void validateIdentifier(String name) {
42 if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) {
43 throw new IllegalNameException(name, "This doesn't look like a legal identifier");
44 }
45 }
46
47 public static boolean isReserved(String name) {
48 return ILLEGAL_IDENTIFIERS.contains(name);
49 }
50}
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 @@
11 11
12package cuchaz.enigma.translation.representation.entry; 12package cuchaz.enigma.translation.representation.entry;
13 13
14import cuchaz.enigma.translation.mapping.IllegalNameException; 14import java.util.List;
15import cuchaz.enigma.translation.Translator; 15import java.util.Objects;
16import cuchaz.enigma.translation.mapping.EntryMapping;
17import cuchaz.enigma.translation.mapping.NameValidator;
18import cuchaz.enigma.translation.representation.TypeDescriptor;
19 16
20import javax.annotation.Nonnull; 17import javax.annotation.Nonnull;
21import javax.annotation.Nullable; 18import javax.annotation.Nullable;
22import java.util.List; 19
23import java.util.Objects; 20import cuchaz.enigma.translation.Translator;
21import cuchaz.enigma.translation.mapping.EntryMapping;
22import cuchaz.enigma.translation.mapping.IdentifierValidation;
23import cuchaz.enigma.translation.representation.TypeDescriptor;
24import cuchaz.enigma.utils.validation.ValidationContext;
24 25
25public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<ClassEntry> { 26public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<ClassEntry> {
26 private final String fullName; 27 private final String fullName;
@@ -97,8 +98,8 @@ public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<
97 } 98 }
98 99
99 @Override 100 @Override
100 public void validateName(String name) throws IllegalNameException { 101 public void validateName(ValidationContext vc, String name) {
101 NameValidator.validateClassName(name); 102 IdentifierValidation.validateClassName(vc, name);
102 } 103 }
103 104
104 @Override 105 @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 @@
11 11
12package cuchaz.enigma.translation.representation.entry; 12package cuchaz.enigma.translation.representation.entry;
13 13
14import cuchaz.enigma.translation.mapping.IllegalNameException;
15import cuchaz.enigma.translation.Translatable;
16import cuchaz.enigma.translation.mapping.NameValidator;
17
18import javax.annotation.Nullable;
19import java.util.ArrayList; 14import java.util.ArrayList;
20import java.util.List; 15import java.util.List;
21 16
17import javax.annotation.Nullable;
18
19import cuchaz.enigma.translation.Translatable;
20import cuchaz.enigma.translation.mapping.IdentifierValidation;
21import cuchaz.enigma.utils.validation.ValidationContext;
22
22public interface Entry<P extends Entry<?>> extends Translatable { 23public interface Entry<P extends Entry<?>> extends Translatable {
23 String getName(); 24 String getName();
24 25
@@ -92,8 +93,8 @@ public interface Entry<P extends Entry<?>> extends Translatable {
92 return withParent((P) parent.replaceAncestor(target, replacement)); 93 return withParent((P) parent.replaceAncestor(target, replacement));
93 } 94 }
94 95
95 default void validateName(String name) throws IllegalNameException { 96 default void validateName(ValidationContext vc, String name) {
96 NameValidator.validateIdentifier(name); 97 IdentifierValidation.validateIdentifier(vc, name);
97 } 98 }
98 99
99 @SuppressWarnings("unchecked") 100 @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;
5import java.io.InputStream; 5import java.io.InputStream;
6import java.io.InputStreamReader; 6import java.io.InputStreamReader;
7import java.nio.charset.StandardCharsets; 7import java.nio.charset.StandardCharsets;
8import java.util.ArrayList; 8import java.util.*;
9import java.util.Collections; 9import java.util.stream.Collectors;
10import java.util.Map;
11import java.util.stream.Stream; 10import java.util.stream.Stream;
12 11
13import com.google.common.collect.ImmutableList; 12import com.google.common.collect.ImmutableList;
@@ -22,12 +21,12 @@ public class I18n {
22 private static Map<String, String> translations = Maps.newHashMap(); 21 private static Map<String, String> translations = Maps.newHashMap();
23 private static Map<String, String> defaultTranslations = Maps.newHashMap(); 22 private static Map<String, String> defaultTranslations = Maps.newHashMap();
24 private static Map<String, String> languageNames = Maps.newHashMap(); 23 private static Map<String, String> languageNames = Maps.newHashMap();
25 24
26 static { 25 static {
27 defaultTranslations = load(DEFAULT_LANGUAGE); 26 defaultTranslations = load(DEFAULT_LANGUAGE);
28 translations = defaultTranslations; 27 translations = defaultTranslations;
29 } 28 }
30 29
31 @SuppressWarnings("unchecked") 30 @SuppressWarnings("unchecked")
32 public static Map<String, String> load(String language) { 31 public static Map<String, String> load(String language) {
33 try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) { 32 try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) {
@@ -41,30 +40,50 @@ public class I18n {
41 } 40 }
42 return Collections.emptyMap(); 41 return Collections.emptyMap();
43 } 42 }
44 43
45 public static String translate(String key) { 44 public static String translateOrNull(String key) {
46 String value = translations.get(key); 45 String value = translations.get(key);
47 if (value != null) { 46 if (value != null) return value;
48 return value; 47
48 return defaultTranslations.get(key);
49 }
50
51 public static String translate(String key) {
52 String tr = translateOrNull(key);
53 return tr != null ? tr : key;
54 }
55
56 public static String translateOrEmpty(String key, Object... args) {
57 String text = translateOrNull(key);
58 if (text != null) {
59 return String.format(text, args);
60 } else {
61 return "";
49 } 62 }
50 value = defaultTranslations.get(key); 63 }
51 if (value != null) { 64
52 return value; 65 public static String translateFormatted(String key, Object... args) {
66 String text = translateOrNull(key);
67 if (text != null) {
68 return String.format(text, args);
69 } else if (args.length == 0) {
70 return key;
71 } else {
72 return key + Arrays.stream(args).map(Objects::toString).collect(Collectors.joining(", ", "[", "]"));
53 } 73 }
54 return key;
55 } 74 }
56 75
57 public static String getLanguageName(String language) { 76 public static String getLanguageName(String language) {
58 return languageNames.get(language); 77 return languageNames.get(language);
59 } 78 }
60 79
61 public static void setLanguage(String language) { 80 public static void setLanguage(String language) {
62 translations = load(language); 81 translations = load(language);
63 } 82 }
64 83
65 public static ArrayList<String> getAvailableLanguages() { 84 public static ArrayList<String> getAvailableLanguages() {
66 ArrayList<String> list = new ArrayList<String>(); 85 ArrayList<String> list = new ArrayList<String>();
67 86
68 try { 87 try {
69 ImmutableList<ResourceInfo> resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList(); 88 ImmutableList<ResourceInfo> resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList();
70 Stream<ResourceInfo> dirStream = resources.stream(); 89 Stream<ResourceInfo> dirStream = resources.stream();
@@ -81,7 +100,7 @@ public class I18n {
81 } 100 }
82 return list; 101 return list;
83 } 102 }
84 103
85 private static void loadLanguageName(String fileName) { 104 private static void loadLanguageName(String fileName) {
86 try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) { 105 try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) {
87 try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { 106 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 @@
1package cuchaz.enigma.utils;
2
3import java.util.Objects;
4import java.util.Optional;
5import java.util.function.Function;
6
7public final class Result<T, E> {
8
9 private final T ok;
10 private final E err;
11
12 private Result(T ok, E err) {
13 this.ok = ok;
14 this.err = err;
15 }
16
17 public static <T, E> Result<T, E> ok(T ok) {
18 return new Result<>(Objects.requireNonNull(ok), null);
19 }
20
21 public static <T, E> Result<T, E> err(E err) {
22 return new Result<>(null, Objects.requireNonNull(err));
23 }
24
25 public boolean isOk() {
26 return this.ok != null;
27 }
28
29 public boolean isErr() {
30 return this.err != null;
31 }
32
33 public Optional<T> ok() {
34 return Optional.ofNullable(this.ok);
35 }
36
37 public Optional<E> err() {
38 return Optional.ofNullable(this.err);
39 }
40
41 public T unwrap() {
42 if (this.isOk()) return this.ok;
43 throw new IllegalStateException(String.format("Called Result.unwrap on an Err value: %s", this.err));
44 }
45
46 public E unwrapErr() {
47 if (this.isErr()) return this.err;
48 throw new IllegalStateException(String.format("Called Result.unwrapErr on an Ok value: %s", this.ok));
49 }
50
51 public T unwrapOr(T fallback) {
52 if (this.isOk()) return this.ok;
53 return fallback;
54 }
55
56 public T unwrapOrElse(Function<E, T> fn) {
57 if (this.isOk()) return this.ok;
58 return fn.apply(this.err);
59 }
60
61 @SuppressWarnings("unchecked")
62 public <U> Result<U, E> map(Function<T, U> op) {
63 if (!this.isOk()) return (Result<U, E>) this;
64 return Result.ok(op.apply(this.ok));
65 }
66
67 @SuppressWarnings("unchecked")
68 public <F> Result<T, F> mapErr(Function<E, F> op) {
69 if (!this.isErr()) return (Result<T, F>) this;
70 return Result.err(op.apply(this.err));
71 }
72
73 @SuppressWarnings("unchecked")
74 public <U> Result<U, E> and(Result<U, E> next) {
75 if (this.isErr()) return (Result<U, E>) this;
76 return next;
77 }
78
79 @SuppressWarnings("unchecked")
80 public <U> Result<U, E> andThen(Function<T, Result<U, E>> op) {
81 if (this.isErr()) return (Result<U, E>) this;
82 return op.apply(this.ok);
83 }
84
85 @Override
86 public boolean equals(Object o) {
87 if (this == o) return true;
88 if (o == null || getClass() != o.getClass()) return false;
89 Result<?, ?> result = (Result<?, ?>) o;
90 return Objects.equals(ok, result.ok) &&
91 Objects.equals(err, result.err);
92 }
93
94 @Override
95 public int hashCode() {
96 return Objects.hash(ok, err);
97 }
98
99 @Override
100 public String toString() {
101 if (this.isOk()) {
102 return String.format("Result.Ok(%s)", this.ok);
103 } else {
104 return String.format("Result.Err(%s)", this.err);
105 }
106 }
107
108}
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;
25import java.util.Comparator; 25import java.util.Comparator;
26import java.util.List; 26import java.util.List;
27import java.util.Locale; 27import java.util.Locale;
28import java.util.concurrent.locks.Lock;
29import java.util.function.Supplier;
28import java.util.stream.Collectors; 30import java.util.stream.Collectors;
29import java.util.zip.ZipEntry; 31import java.util.zip.ZipEntry;
30import java.util.zip.ZipFile; 32import java.util.zip.ZipFile;
@@ -78,6 +80,25 @@ public class Utils {
78 return digest.digest(); 80 return digest.digest();
79 } 81 }
80 82
83 public static void withLock(Lock l, Runnable op) {
84 try {
85 l.lock();
86 op.run();
87 } finally {
88 l.unlock();
89 }
90 }
91
92 public static <R> R withLock(Lock l, Supplier<R> op) {
93 try {
94 l.lock();
95 return op.get();
96 } finally {
97 l.unlock();
98 }
99 }
100
101
81 public static boolean isBlank(String input) { 102 public static boolean isBlank(String input) {
82 if (input == null) { 103 if (input == null) {
83 return true; 104 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 @@
1package cuchaz.enigma.utils.validation;
2
3import cuchaz.enigma.utils.I18n;
4
5public class Message {
6
7 public static final Message EMPTY_FIELD = create(Type.ERROR, "empty_field");
8 public static final Message NOT_INT = create(Type.ERROR, "not_int");
9 public static final Message FIELD_OUT_OF_RANGE_INT = create(Type.ERROR, "field_out_of_range_int");
10 public static final Message FIELD_LENGTH_OUT_OF_RANGE = create(Type.ERROR, "field_length_out_of_range");
11 public static final Message NONUNIQUE_NAME_CLASS = create(Type.ERROR, "nonunique_name_class");
12 public static final Message NONUNIQUE_NAME = create(Type.ERROR, "nonunique_name");
13 public static final Message ILLEGAL_CLASS_NAME = create(Type.ERROR, "illegal_class_name");
14 public static final Message ILLEGAL_IDENTIFIER = create(Type.ERROR, "illegal_identifier");
15 public static final Message RESERVED_IDENTIFIER = create(Type.ERROR, "reserved_identifier");
16 public static final Message ILLEGAL_DOC_COMMENT_END = create(Type.ERROR, "illegal_doc_comment_end");
17
18 public static final Message STYLE_VIOLATION = create(Type.WARNING, "style_violation");
19
20 public final Type type;
21 public final String textKey;
22 public final String longTextKey;
23
24 private Message(Type type, String textKey, String longTextKey) {
25 this.type = type;
26 this.textKey = textKey;
27 this.longTextKey = longTextKey;
28 }
29
30 public String format(Object[] args) {
31 return I18n.translateFormatted(textKey, args);
32 }
33
34 public String formatDetails(Object[] args) {
35 return I18n.translateOrEmpty(longTextKey, args);
36 }
37
38 public static Message create(Type type, String name) {
39 return new Message(type, String.format("validation.message.%s", name), String.format("validation.message.%s.long", name));
40 }
41
42 public enum Type {
43 INFO,
44 WARNING,
45 ERROR,
46 }
47
48}
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 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.Arrays;
4import java.util.Objects;
5
6public class ParameterizedMessage {
7
8 public final Message message;
9 private final Object[] params;
10
11 public ParameterizedMessage(Message message, Object[] params) {
12 this.message = message;
13 this.params = params;
14 }
15
16 public String getText() {
17 return message.format(params);
18 }
19
20 public String getLongText() {
21 return message.formatDetails(params);
22 }
23
24 @Override
25 public boolean equals(Object o) {
26 if (this == o) return true;
27 if (o == null || getClass() != o.getClass()) return false;
28 ParameterizedMessage that = (ParameterizedMessage) o;
29 return Objects.equals(message, that.message) &&
30 Arrays.equals(params, that.params);
31 }
32
33 @Override
34 public int hashCode() {
35 int result = Objects.hash(message);
36 result = 31 * result + Arrays.hashCode(params);
37 return result;
38 }
39
40 @Override
41 public String toString() {
42 return String.format("ParameterizedMessage { message: %s, params: %s }", message, Arrays.toString(params));
43 }
44
45}
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 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.Arrays;
4
5public class PrintValidatable implements Validatable {
6
7 public static final PrintValidatable INSTANCE = new PrintValidatable();
8
9 @Override
10 public void addMessage(ParameterizedMessage message) {
11 String text = message.getText();
12 String longText = message.getLongText();
13 String type;
14 switch (message.message.type) {
15 case INFO:
16 type = "info";
17 break;
18 case WARNING:
19 type = "warning";
20 break;
21 case ERROR:
22 type = "error";
23 break;
24 default:
25 throw new IllegalStateException("unreachable");
26 }
27 System.out.printf("%s: %s\n", type, text);
28 if (!longText.isEmpty()) {
29 Arrays.stream(longText.split("\n")).forEach(s -> System.out.printf(" %s\n", s));
30 }
31 }
32
33 @Override
34 public void clearMessages() {
35 }
36
37}
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 @@
1package cuchaz.enigma.utils.validation;
2
3public class StandardValidation {
4
5 public static boolean notBlank(ValidationContext vc, String value) {
6 if (value.trim().isEmpty()) {
7 vc.raise(Message.EMPTY_FIELD);
8 return false;
9 }
10 return true;
11 }
12
13 public static boolean isInt(ValidationContext vc, String value) {
14 if (!notBlank(vc, value)) return false;
15 try {
16 Integer.parseInt(value);
17 return true;
18 } catch (NumberFormatException e) {
19 vc.raise(Message.NOT_INT);
20 return false;
21 }
22 }
23
24 public static boolean isIntInRange(ValidationContext vc, String value, int min, int max) {
25 if (!isInt(vc, value)) return false;
26 int intVal = Integer.parseInt(value);
27 if (intVal < min || intVal > max) {
28 vc.raise(Message.FIELD_OUT_OF_RANGE_INT, min, max);
29 return false;
30 }
31 return true;
32 }
33
34}
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 @@
1package cuchaz.enigma.utils.validation;
2
3public interface Validatable {
4
5 void addMessage(ParameterizedMessage message);
6
7 void clearMessages();
8
9}
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 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.*;
4
5import javax.annotation.Nullable;
6
7import cuchaz.enigma.utils.validation.Message.Type;
8
9/**
10 * A context for user input validation. Handles collecting error messages and
11 * displaying the errors on the relevant input fields. UIs using validation
12 * often have two stages of applying changes: validating all the input fields,
13 * then checking if there's any errors or unconfirmed warnings, and if not,
14 * then actually applying the changes. This allows for easily collecting
15 * multiple errors and displaying them to the user at the same time.
16 */
17public class ValidationContext {
18
19 private Validatable activeElement = null;
20 private final Set<Validatable> elements = new HashSet<>();
21 private final List<ParameterizedMessage> messages = new ArrayList<>();
22
23 /**
24 * Sets the currently active element (such as an input field). Any messages
25 * raised while this is set get displayed on this element.
26 *
27 * @param v the active element to set, or {@code null} to unset
28 */
29 public void setActiveElement(@Nullable Validatable v) {
30 if (v != null) {
31 elements.add(v);
32 }
33 activeElement = v;
34 }
35
36 /**
37 * Raises a message. If there's a currently active element, also notifies
38 * that element about the message.
39 *
40 * @param message the message to raise
41 * @param args the arguments used when formatting the message text
42 */
43 public void raise(Message message, Object... args) {
44 ParameterizedMessage pm = new ParameterizedMessage(message, args);
45 if (activeElement != null) {
46 activeElement.addMessage(pm);
47 }
48 messages.add(pm);
49 }
50
51 /**
52 * Returns whether the validation context currently has no messages that
53 * block executing actions, such as errors and unconfirmed warnings.
54 *
55 * @return whether the program can proceed executing and the UI is in a
56 * valid state
57 */
58 public boolean canProceed() {
59 // TODO on warnings, wait until user confirms
60 return messages.stream().noneMatch(m -> m.message.type == Type.ERROR);
61 }
62
63 public List<ParameterizedMessage> getMessages() {
64 return Collections.unmodifiableList(messages);
65 }
66
67 /**
68 * Clears all currently pending messages. This should be called whenever the
69 * interface starts getting validated, to get rid of old messages.
70 */
71 public void reset() {
72 activeElement = null;
73 elements.forEach(Validatable::clearMessages);
74 elements.clear();
75 messages.clear();
76 }
77
78}
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 @@
1{
2 "language": "German",
3
4 "general.retry": "Wiederholen",
5
6 "popup_menu.editor_tab.close": "Schließen",
7 "popup_menu.editor_tab.close_all": "Alle schließen",
8 "popup_menu.editor_tab.close_others": "Andere schließen",
9 "popup_menu.editor_tab.close_left": "Alle links hiervon schließen",
10 "popup_menu.editor_tab.close_right": "Alle rechts hiervon schließen",
11
12 "editor.decompiling": "Dekompiliere...",
13 "editor.decompile_error": "Ein Fehler ist während des Dekompilierens aufgetreten.",
14
15 "validation.message.empty_field": "Dieses Feld muss ausgefüllt werden.",
16 "validation.message.not_int": "Wert muss eine ganze Zahl sein.",
17 "validation.message.field_out_of_range_int": "Wert muss eine ganze Zahl zwischen %d und %d sein.",
18 "validation.message.field_length_out_of_range": "Wert muss kürzer als %d Zeichen sein.",
19 "validation.message.nonunique_name_class": "Name „%s“ ist in „%s“ nicht eindeutig.",
20 "validation.message.nonunique_name": "Name „%s“ ist nicht eindeutig.",
21 "validation.message.illegal_class_name": "„%s“ ist kein gültiger Klassenname.",
22 "validation.message.illegal_identifier": "„%s“ ist kein gültiger Name.",
23 "validation.message.illegal_identifier.long": "Ungültiges Zeichen „%2$s“ an Position %3$d.",
24 "validation.message.illegal_doc_comment_end": "Javadoc-Kommentar darf die Zeichenfolge „*/“ nicht enthalten.",
25 "validation.message.reserved_identifier": "„%s“ ist ein reservierter Name."
26} \ 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 @@
1{ 1{
2 "language": "English", 2 "language": "English",
3 3
4 "general.retry": "Retry",
5
4 "mapping_format.enigma_file": "Enigma File", 6 "mapping_format.enigma_file": "Enigma File",
5 "mapping_format.enigma_directory": "Enigma Directory", 7 "mapping_format.enigma_directory": "Enigma Directory",
6 "mapping_format.enigma_zip": "Enigma ZIP", 8 "mapping_format.enigma_zip": "Enigma ZIP",
@@ -69,6 +71,14 @@
69 "popup_menu.zoom.in": "Zoom in", 71 "popup_menu.zoom.in": "Zoom in",
70 "popup_menu.zoom.out": "Zoom out", 72 "popup_menu.zoom.out": "Zoom out",
71 "popup_menu.zoom.reset": "Reset zoom", 73 "popup_menu.zoom.reset": "Reset zoom",
74 "popup_menu.editor_tab.close": "Close",
75 "popup_menu.editor_tab.close_all": "Close All",
76 "popup_menu.editor_tab.close_others": "Close Others",
77 "popup_menu.editor_tab.close_left": "Close All to the Left",
78 "popup_menu.editor_tab.close_right": "Close All to the Right",
79
80 "editor.decompiling": "Decompiling...",
81 "editor.decompile_error": "An error was encountered while decompiling.",
72 82
73 "info_panel.classes.obfuscated": "Obfuscated Classes", 83 "info_panel.classes.obfuscated": "Obfuscated Classes",
74 "info_panel.classes.deobfuscated": "De-obfuscated Classes", 84 "info_panel.classes.deobfuscated": "De-obfuscated Classes",
@@ -79,8 +89,8 @@
79 "info_panel.identifier.method": "Method", 89 "info_panel.identifier.method": "Method",
80 "info_panel.identifier.constructor": "Constructor", 90 "info_panel.identifier.constructor": "Constructor",
81 "info_panel.identifier.class": "Class", 91 "info_panel.identifier.class": "Class",
82 "info_panel.identifier.type_descriptor": "TypeDescriptor", 92 "info_panel.identifier.type_descriptor": "Type Descriptor",
83 "info_panel.identifier.method_descriptor": "MethodDescriptor", 93 "info_panel.identifier.method_descriptor": "Method Descriptor",
84 "info_panel.identifier.modifier": "Modifier", 94 "info_panel.identifier.modifier": "Modifier",
85 "info_panel.identifier.index": "Index", 95 "info_panel.identifier.index": "Index",
86 "info_panel.editor.class.decompiling": "(decompiling...)", 96 "info_panel.editor.class.decompiling": "(decompiling...)",
@@ -149,12 +159,23 @@
149 "message.mark_deobf.text": "%s marked %s as deobfuscated", 159 "message.mark_deobf.text": "%s marked %s as deobfuscated",
150 "message.remove_mapping.text": "%s removed mappings for %s", 160 "message.remove_mapping.text": "%s removed mappings for %s",
151 "message.rename.text": "%s renamed %s to %s", 161 "message.rename.text": "%s renamed %s to %s",
152
153 "status.disconnected": "Disconnected.", 162 "status.disconnected": "Disconnected.",
154 "status.connected": "Connected.", 163 "status.connected": "Connected.",
155 "status.connected_user_count": "Connected (%d users).", 164 "status.connected_user_count": "Connected (%d users).",
156 "status.ready": "Ready.", 165 "status.ready": "Ready.",
157 166
167 "validation.message.empty_field": "This field is required.",
168 "validation.message.not_int": "Value must be an integer.",
169 "validation.message.field_out_of_range_int": "Value must be an integer between %d and %d.",
170 "validation.message.field_length_out_of_range": "Value must be less than %d characters long.",
171 "validation.message.nonunique_name_class": "Name '%s' is not unique in '%s'.",
172 "validation.message.nonunique_name": "Name '%s' is not unique.",
173 "validation.message.illegal_class_name": "'%s' is not a valid class name.",
174 "validation.message.illegal_identifier": "'%s' is not a valid identifier.",
175 "validation.message.illegal_identifier.long": "Invalid character '%2$s' at position %3$d.",
176 "validation.message.illegal_doc_comment_end": "Javadoc comment cannot contain the character sequence '*/'.",
177 "validation.message.reserved_identifier": "'%s' is a reserved identifier.",
178
158 "crash.title": "%s - Crash Report", 179 "crash.title": "%s - Crash Report",
159 "crash.summary": "%s has crashed! =(", 180 "crash.summary": "%s has crashed! =(",
160 "crash.export": "Export", 181 "crash.export": "Export",