summaryrefslogtreecommitdiff
path: root/enigma-swing
diff options
context:
space:
mode:
Diffstat (limited to 'enigma-swing')
-rw-r--r--enigma-swing/build.gradle23
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java28
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java532
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java73
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java7
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java160
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java90
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java44
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java35
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java1058
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java719
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java118
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java24
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java42
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java45
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java30
-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.java35
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java261
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java45
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java70
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java50
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java82
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java105
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java65
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java159
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java109
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java261
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java82
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java40
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java386
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java125
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java10
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java8
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java11
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java69
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java31
-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/node/ClassSelectorClassNode.java72
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java58
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java26
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java171
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java32
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java37
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java17
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java268
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java197
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java8
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java77
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java56
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java49
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java8
-rw-r--r--enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java110
-rw-r--r--enigma-swing/src/main/resources/about.html6
-rw-r--r--enigma-swing/src/main/resources/stats.html34
55 files changed, 6272 insertions, 0 deletions
diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle
new file mode 100644
index 00000000..a1bcafc5
--- /dev/null
+++ b/enigma-swing/build.gradle
@@ -0,0 +1,23 @@
1plugins {
2 id 'com.github.johnrengelman.shadow' version '5.2.0'
3}
4
5dependencies {
6 implementation project(':enigma')
7 implementation project(':enigma-server')
8
9 implementation 'net.sf.jopt-simple:jopt-simple:6.0-alpha-3'
10 implementation 'com.bulenkov:darcula:1.0.0-bobbylight'
11 implementation 'de.sciss:syntaxpane:1.2.0'
12 implementation 'com.github.lukeu:swing-dpi:0.6'
13}
14
15jar.manifest.attributes 'Main-Class': 'cuchaz.enigma.gui.Main'
16
17publishing {
18 publications {
19 shadow(MavenPublication) { publication ->
20 project.shadow.component(publication)
21 }
22 }
23} \ No newline at end of file
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java
new file mode 100644
index 00000000..af105dbd
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/BrowserCaret.java
@@ -0,0 +1,28 @@
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 javax.swing.text.DefaultCaret;
15
16public class BrowserCaret extends DefaultCaret {
17
18 @Override
19 public boolean isSelectionVisible() {
20 return true;
21 }
22
23 @Override
24 public boolean isVisible() {
25 return true;
26 }
27
28}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java
new file mode 100644
index 00000000..3d0e04c9
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ClassSelector.java
@@ -0,0 +1,532 @@
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 java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.util.*;
17
18import javax.annotation.Nullable;
19import javax.swing.JOptionPane;
20import javax.swing.JTree;
21import javax.swing.event.CellEditorListener;
22import javax.swing.event.ChangeEvent;
23import javax.swing.tree.*;
24
25import com.google.common.collect.ArrayListMultimap;
26import com.google.common.collect.Lists;
27import com.google.common.collect.Maps;
28import com.google.common.collect.Multimap;
29import cuchaz.enigma.gui.node.ClassSelectorClassNode;
30import cuchaz.enigma.gui.node.ClassSelectorPackageNode;
31import cuchaz.enigma.translation.mapping.IllegalNameException;
32import cuchaz.enigma.translation.Translator;
33import cuchaz.enigma.translation.representation.entry.ClassEntry;
34
35public class ClassSelector extends JTree {
36
37 public static final Comparator<ClassEntry> DEOBF_CLASS_COMPARATOR = Comparator.comparing(ClassEntry::getFullName);
38
39 private final GuiController controller;
40
41 private DefaultMutableTreeNode rootNodes;
42 private ClassSelectionListener selectionListener;
43 private RenameSelectionListener renameSelectionListener;
44 private Comparator<ClassEntry> comparator;
45
46 private final Map<ClassEntry, ClassEntry> displayedObfToDeobf = new HashMap<>();
47
48 public ClassSelector(Gui gui, Comparator<ClassEntry> comparator, boolean isRenamable) {
49 this.comparator = comparator;
50 this.controller = gui.getController();
51
52 // configure the tree control
53 setEditable(true);
54 setRootVisible(false);
55 setShowsRootHandles(false);
56 setModel(null);
57
58 // hook events
59 addMouseListener(new MouseAdapter() {
60 @Override
61 public void mouseClicked(MouseEvent event) {
62 if (selectionListener != null && event.getClickCount() == 2) {
63 // get the selected node
64 TreePath path = getSelectionPath();
65 if (path != null && path.getLastPathComponent() instanceof ClassSelectorClassNode) {
66 ClassSelectorClassNode node = (ClassSelectorClassNode) path.getLastPathComponent();
67 selectionListener.onSelectClass(node.getObfEntry());
68 }
69 }
70 }
71 });
72
73 final JTree tree = this;
74
75 final DefaultTreeCellEditor editor = new DefaultTreeCellEditor(tree,
76 (DefaultTreeCellRenderer) tree.getCellRenderer()) {
77 @Override
78 public boolean isCellEditable(EventObject event) {
79 return isRenamable && !(event instanceof MouseEvent) && super.isCellEditable(event);
80 }
81 };
82 this.setCellEditor(editor);
83 editor.addCellEditorListener(new CellEditorListener() {
84 @Override
85 public void editingStopped(ChangeEvent e) {
86 String data = editor.getCellEditorValue().toString();
87 TreePath path = getSelectionPath();
88
89 Object realPath = path.getLastPathComponent();
90 if (realPath != null && realPath instanceof DefaultMutableTreeNode && data != null) {
91 DefaultMutableTreeNode node = (DefaultMutableTreeNode) realPath;
92 TreeNode parentNode = node.getParent();
93 if (parentNode == null)
94 return;
95 boolean allowEdit = true;
96 for (int i = 0; i < parentNode.getChildCount(); i++) {
97 TreeNode childNode = parentNode.getChildAt(i);
98 if (childNode != null && childNode.toString().equals(data) && childNode != node) {
99 allowEdit = false;
100 break;
101 }
102 }
103 if (allowEdit && renameSelectionListener != null) {
104 Object prevData = node.getUserObject();
105 Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data;
106 try {
107 renameSelectionListener.onSelectionRename(node.getUserObject(), objectData, node);
108 node.setUserObject(objectData); // Make sure that it's modified
109 } catch (IllegalNameException ex) {
110 JOptionPane.showOptionDialog(gui.getFrame(), ex.getMessage(), "Enigma - Error", JOptionPane.OK_OPTION,
111 JOptionPane.ERROR_MESSAGE, null, new String[]{"Ok"}, "OK");
112 editor.cancelCellEditing();
113 }
114 } else
115 editor.cancelCellEditing();
116 }
117
118 }
119
120 @Override
121 public void editingCanceled(ChangeEvent e) {
122 // NOP
123 }
124 });
125 // init defaults
126 this.selectionListener = null;
127 this.renameSelectionListener = null;
128 }
129
130 public boolean isDuplicate(Object[] nodes, String data) {
131 int count = 0;
132
133 for (Object node : nodes) {
134 if (node.toString().equals(data)) {
135 count++;
136 if (count == 2)
137 return true;
138 }
139 }
140 return false;
141 }
142
143 public void setSelectionListener(ClassSelectionListener val) {
144 this.selectionListener = val;
145 }
146
147 public void setRenameSelectionListener(RenameSelectionListener renameSelectionListener) {
148 this.renameSelectionListener = renameSelectionListener;
149 }
150
151 public void setClasses(Collection<ClassEntry> classEntries) {
152 displayedObfToDeobf.clear();
153
154 List<StateEntry> state = getExpansionState(this);
155 if (classEntries == null) {
156 setModel(null);
157 return;
158 }
159
160 Translator translator = controller.project.getMapper().getDeobfuscator();
161
162 // build the package names
163 Map<String, ClassSelectorPackageNode> packages = Maps.newHashMap();
164 for (ClassEntry obfClass : classEntries) {
165 ClassEntry deobfClass = translator.translate(obfClass);
166 packages.put(deobfClass.getPackageName(), null);
167 }
168
169 // sort the packages
170 List<String> sortedPackageNames = Lists.newArrayList(packages.keySet());
171 sortedPackageNames.sort((a, b) ->
172 {
173 // I can never keep this rule straight when writing these damn things...
174 // a < b => -1, a == b => 0, a > b => +1
175
176 if (b == null || a == null) {
177 return 0;
178 }
179
180 String[] aparts = a.split("/");
181 String[] bparts = b.split("/");
182 for (int i = 0; true; i++) {
183 if (i >= aparts.length) {
184 return -1;
185 } else if (i >= bparts.length) {
186 return 1;
187 }
188
189 int result = aparts[i].compareTo(bparts[i]);
190 if (result != 0) {
191 return result;
192 }
193 }
194 });
195
196 // create the rootNodes node and the package nodes
197 rootNodes = new DefaultMutableTreeNode();
198 for (String packageName : sortedPackageNames) {
199 ClassSelectorPackageNode node = new ClassSelectorPackageNode(packageName);
200 packages.put(packageName, node);
201 rootNodes.add(node);
202 }
203
204 // put the classes into packages
205 Multimap<String, ClassEntry> packagedClassEntries = ArrayListMultimap.create();
206 for (ClassEntry obfClass : classEntries) {
207 ClassEntry deobfClass = translator.translate(obfClass);
208 packagedClassEntries.put(deobfClass.getPackageName(), obfClass);
209 }
210
211 // build the class nodes
212 for (String packageName : packagedClassEntries.keySet()) {
213 // sort the class entries
214 List<ClassEntry> classEntriesInPackage = Lists.newArrayList(packagedClassEntries.get(packageName));
215 classEntriesInPackage.sort((o1, o2) -> comparator.compare(translator.translate(o1), translator.translate(o2)));
216
217 // create the nodes in order
218 for (ClassEntry obfClass : classEntriesInPackage) {
219 ClassEntry deobfClass = translator.translate(obfClass);
220 ClassSelectorPackageNode node = packages.get(packageName);
221 ClassSelectorClassNode classNode = new ClassSelectorClassNode(obfClass, deobfClass);
222 displayedObfToDeobf.put(obfClass, deobfClass);
223 node.add(classNode);
224 }
225 }
226
227 // finally, update the tree control
228 setModel(new DefaultTreeModel(rootNodes));
229
230 restoreExpansionState(this, state);
231 }
232
233 public ClassEntry getSelectedClass() {
234 if (!isSelectionEmpty()) {
235 Object selectedNode = getSelectionPath().getLastPathComponent();
236 if (selectedNode instanceof ClassSelectorClassNode) {
237 ClassSelectorClassNode classNode = (ClassSelectorClassNode) selectedNode;
238 return classNode.getClassEntry();
239 }
240 }
241 return null;
242 }
243
244 public String getSelectedPackage() {
245 if (!isSelectionEmpty()) {
246 Object selectedNode = getSelectionPath().getLastPathComponent();
247 if (selectedNode instanceof ClassSelectorPackageNode) {
248 ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode) selectedNode;
249 return packageNode.getPackageName();
250 } else if (selectedNode instanceof ClassSelectorClassNode) {
251 ClassSelectorClassNode classNode = (ClassSelectorClassNode) selectedNode;
252 return classNode.getClassEntry().getPackageName();
253 }
254 }
255 return null;
256 }
257
258 public boolean isDescendant(TreePath path1, TreePath path2) {
259 int count1 = path1.getPathCount();
260 int count2 = path2.getPathCount();
261 if (count1 <= count2) {
262 return false;
263 }
264 while (count1 != count2) {
265 path1 = path1.getParentPath();
266 count1--;
267 }
268 return path1.equals(path2);
269 }
270
271 public enum State {
272 EXPANDED,
273 SELECTED
274 }
275
276 public static class StateEntry {
277 public final State state;
278 public final TreePath path;
279
280 public StateEntry(State state, TreePath path) {
281 this.state = state;
282 this.path = path;
283 }
284 }
285
286 public List<StateEntry> getExpansionState(JTree tree) {
287 List<StateEntry> state = new ArrayList<>();
288 int rowCount = tree.getRowCount();
289 for (int i = 0; i < rowCount; i++) {
290 TreePath path = tree.getPathForRow(i);
291 if (tree.isPathSelected(path)) {
292 state.add(new StateEntry(State.SELECTED, path));
293 }
294 if (tree.isExpanded(path)) {
295 state.add(new StateEntry(State.EXPANDED, path));
296 }
297 }
298 return state;
299 }
300
301 public void restoreExpansionState(JTree tree, List<StateEntry> expansionState) {
302 tree.clearSelection();
303
304 for (StateEntry entry : expansionState) {
305 switch (entry.state) {
306 case SELECTED:
307 tree.addSelectionPath(entry.path);
308 break;
309 case EXPANDED:
310 tree.expandPath(entry.path);
311 break;
312 }
313 }
314 }
315
316 public List<ClassSelectorPackageNode> packageNodes() {
317 List<ClassSelectorPackageNode> nodes = Lists.newArrayList();
318 DefaultMutableTreeNode root = (DefaultMutableTreeNode) getModel().getRoot();
319 Enumeration<?> children = root.children();
320 while (children.hasMoreElements()) {
321 ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode) children.nextElement();
322 nodes.add(packageNode);
323 }
324 return nodes;
325 }
326
327 public List<ClassSelectorClassNode> classNodes(ClassSelectorPackageNode packageNode) {
328 List<ClassSelectorClassNode> nodes = Lists.newArrayList();
329 Enumeration<?> children = packageNode.children();
330 while (children.hasMoreElements()) {
331 ClassSelectorClassNode classNode = (ClassSelectorClassNode) children.nextElement();
332 nodes.add(classNode);
333 }
334 return nodes;
335 }
336
337 public void expandPackage(String packageName) {
338 if (packageName == null) {
339 return;
340 }
341 for (ClassSelectorPackageNode packageNode : packageNodes()) {
342 if (packageNode.getPackageName().equals(packageName)) {
343 expandPath(new TreePath(new Object[]{getModel().getRoot(), packageNode}));
344 return;
345 }
346 }
347 }
348
349 public void expandAll() {
350 for (ClassSelectorPackageNode packageNode : packageNodes()) {
351 expandPath(new TreePath(new Object[]{getModel().getRoot(), packageNode}));
352 }
353 }
354
355 public ClassEntry getFirstClass() {
356 ClassSelectorPackageNode packageNode = packageNodes().get(0);
357 if (packageNode != null) {
358 ClassSelectorClassNode classNode = classNodes(packageNode).get(0);
359 if (classNode != null) {
360 return classNode.getClassEntry();
361 }
362 }
363 return null;
364 }
365
366 public ClassSelectorPackageNode getPackageNode(ClassEntry entry) {
367 String packageName = entry.getPackageName();
368 if (packageName == null) {
369 packageName = "(none)";
370 }
371 for (ClassSelectorPackageNode packageNode : packageNodes()) {
372 if (packageNode.getPackageName().equals(packageName)) {
373 return packageNode;
374 }
375 }
376 return null;
377 }
378
379 @Nullable
380 public ClassEntry getDisplayedDeobf(ClassEntry obfEntry) {
381 return displayedObfToDeobf.get(obfEntry);
382 }
383
384 public ClassSelectorPackageNode getPackageNode(ClassSelector selector, ClassEntry entry) {
385 ClassSelectorPackageNode packageNode = getPackageNode(entry);
386
387 if (selector != null && packageNode == null && selector.getPackageNode(entry) != null)
388 return selector.getPackageNode(entry);
389 return packageNode;
390 }
391
392 public ClassEntry getNextClass(ClassEntry entry) {
393 boolean foundIt = false;
394 for (ClassSelectorPackageNode packageNode : packageNodes()) {
395 if (!foundIt) {
396 // skip to the package with our target in it
397 if (packageNode.getPackageName().equals(entry.getPackageName())) {
398 for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
399 if (!foundIt) {
400 if (classNode.getClassEntry().equals(entry)) {
401 foundIt = true;
402 }
403 } else {
404 // return the next class
405 return classNode.getClassEntry();
406 }
407 }
408 }
409 } else {
410 // return the next class
411 ClassSelectorClassNode classNode = classNodes(packageNode).get(0);
412 if (classNode != null) {
413 return classNode.getClassEntry();
414 }
415 }
416 }
417 return null;
418 }
419
420 public void setSelectionClass(ClassEntry classEntry) {
421 expandPackage(classEntry.getPackageName());
422 for (ClassSelectorPackageNode packageNode : packageNodes()) {
423 for (ClassSelectorClassNode classNode : classNodes(packageNode)) {
424 if (classNode.getClassEntry().equals(classEntry)) {
425 TreePath path = new TreePath(new Object[]{getModel().getRoot(), packageNode, classNode});
426 setSelectionPath(path);
427 scrollPathToVisible(path);
428 }
429 }
430 }
431 }
432
433 public void removeNode(ClassSelectorPackageNode packageNode, ClassEntry entry) {
434 DefaultTreeModel model = (DefaultTreeModel) getModel();
435
436 if (packageNode == null)
437 return;
438
439 for (int i = 0; i < packageNode.getChildCount(); i++) {
440 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) packageNode.getChildAt(i);
441 if (childNode.getUserObject() instanceof ClassEntry && childNode.getUserObject().equals(entry)) {
442 model.removeNodeFromParent(childNode);
443 if (childNode instanceof ClassSelectorClassNode) {
444 displayedObfToDeobf.remove(((ClassSelectorClassNode) childNode).getObfEntry());
445 }
446 break;
447 }
448 }
449 }
450
451 public void removeNodeIfEmpty(ClassSelectorPackageNode packageNode) {
452 if (packageNode != null && packageNode.getChildCount() == 0)
453 ((DefaultTreeModel) getModel()).removeNodeFromParent(packageNode);
454 }
455
456 public void moveClassIn(ClassEntry classEntry) {
457 removeEntry(classEntry);
458 insertNode(classEntry);
459 }
460
461 public void moveClassOut(ClassEntry classEntry) {
462 removeEntry(classEntry);
463 }
464
465 private void removeEntry(ClassEntry classEntry) {
466 ClassEntry previousDeobf = displayedObfToDeobf.get(classEntry);
467 if (previousDeobf != null) {
468 ClassSelectorPackageNode packageNode = getPackageNode(previousDeobf);
469 removeNode(packageNode, previousDeobf);
470 removeNodeIfEmpty(packageNode);
471 }
472 }
473
474 public ClassSelectorPackageNode getOrCreatePackage(ClassEntry entry) {
475 DefaultTreeModel model = (DefaultTreeModel) getModel();
476 ClassSelectorPackageNode newPackageNode = getPackageNode(entry);
477 if (newPackageNode == null) {
478 newPackageNode = new ClassSelectorPackageNode(entry.getPackageName());
479 model.insertNodeInto(newPackageNode, (MutableTreeNode) model.getRoot(), getPlacementIndex(newPackageNode));
480 }
481 return newPackageNode;
482 }
483
484 public void insertNode(ClassEntry obfEntry) {
485 ClassEntry deobfEntry = controller.project.getMapper().deobfuscate(obfEntry);
486 ClassSelectorPackageNode packageNode = getOrCreatePackage(deobfEntry);
487
488 DefaultTreeModel model = (DefaultTreeModel) getModel();
489 ClassSelectorClassNode classNode = new ClassSelectorClassNode(obfEntry, deobfEntry);
490 model.insertNodeInto(classNode, packageNode, getPlacementIndex(packageNode, classNode));
491
492 displayedObfToDeobf.put(obfEntry, deobfEntry);
493 }
494
495 public void reload() {
496 DefaultTreeModel model = (DefaultTreeModel) getModel();
497 model.reload(rootNodes);
498 }
499
500 private int getPlacementIndex(ClassSelectorPackageNode newPackageNode, ClassSelectorClassNode classNode) {
501 List<ClassSelectorClassNode> classNodes = classNodes(newPackageNode);
502 classNodes.add(classNode);
503 classNodes.sort((a, b) -> comparator.compare(a.getClassEntry(), b.getClassEntry()));
504 for (int i = 0; i < classNodes.size(); i++)
505 if (classNodes.get(i) == classNode)
506 return i;
507
508 return 0;
509 }
510
511 private int getPlacementIndex(ClassSelectorPackageNode newPackageNode) {
512 List<ClassSelectorPackageNode> packageNodes = packageNodes();
513 if (!packageNodes.contains(newPackageNode)) {
514 packageNodes.add(newPackageNode);
515 packageNodes.sort(Comparator.comparing(ClassSelectorPackageNode::toString));
516 }
517
518 for (int i = 0; i < packageNodes.size(); i++)
519 if (packageNodes.get(i) == newPackageNode)
520 return i;
521
522 return 0;
523 }
524
525 public interface ClassSelectionListener {
526 void onSelectClass(ClassEntry classEntry);
527 }
528
529 public interface RenameSelectionListener {
530 void onSelectionRename(Object prevData, Object data, DefaultMutableTreeNode node);
531 }
532}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java
new file mode 100644
index 00000000..356656b9
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/CodeReader.java
@@ -0,0 +1,73 @@
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/ConnectionState.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java
new file mode 100644
index 00000000..db6590de
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ConnectionState.java
@@ -0,0 +1,7 @@
1package cuchaz.enigma.gui;
2
3public enum ConnectionState {
4 NOT_CONNECTED,
5 HOSTING,
6 CONNECTED,
7}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
new file mode 100644
index 00000000..aca5d724
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
@@ -0,0 +1,160 @@
1package cuchaz.enigma.gui;
2
3import cuchaz.enigma.EnigmaProject;
4import cuchaz.enigma.EnigmaServices;
5import cuchaz.enigma.analysis.EntryReference;
6import cuchaz.enigma.source.Token;
7import 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;
12import cuchaz.enigma.translation.Translator;
13import cuchaz.enigma.translation.mapping.EntryRemapper;
14import cuchaz.enigma.translation.mapping.ResolutionStrategy;
15import cuchaz.enigma.translation.representation.TypeDescriptor;
16import cuchaz.enigma.translation.representation.entry.ClassEntry;
17import cuchaz.enigma.translation.representation.entry.Entry;
18import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry;
19
20import javax.annotation.Nullable;
21import java.util.*;
22
23public class DecompiledClassSource {
24 private final ClassEntry classEntry;
25
26 private final SourceIndex obfuscatedIndex;
27 private SourceIndex remappedIndex;
28
29 private final Map<TokenHighlightType, Collection<Token>> highlightedTokens = new EnumMap<>(TokenHighlightType.class);
30
31 public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) {
32 this.classEntry = classEntry;
33 this.obfuscatedIndex = index;
34 this.remappedIndex = index;
35 }
36
37 public static DecompiledClassSource text(ClassEntry classEntry, String text) {
38 return new DecompiledClassSource(classEntry, new SourceIndex(text));
39 }
40
41 public void remapSource(EnigmaProject project, Translator translator) {
42 highlightedTokens.clear();
43
44 SourceRemapper remapper = new SourceRemapper(obfuscatedIndex.getSource(), obfuscatedIndex.referenceTokens());
45
46 SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> remapToken(project, token, movedToken, translator));
47 remappedIndex = obfuscatedIndex.remapTo(remapResult);
48 }
49
50 private String remapToken(EnigmaProject project, Token token, Token movedToken, Translator translator) {
51 EntryReference<Entry<?>, Entry<?>> reference = obfuscatedIndex.getReference(token);
52
53 Entry<?> entry = reference.getNameableEntry();
54 Entry<?> translatedEntry = translator.translate(entry);
55
56 if (project.isRenamable(reference)) {
57 if (isDeobfuscated(entry, translatedEntry)) {
58 highlightToken(movedToken, TokenHighlightType.DEOBFUSCATED);
59 return translatedEntry.getSourceRemapName();
60 } else {
61 Optional<String> proposedName = proposeName(project, entry);
62 if (proposedName.isPresent()) {
63 highlightToken(movedToken, TokenHighlightType.PROPOSED);
64 return proposedName.get();
65 }
66
67 highlightToken(movedToken, TokenHighlightType.OBFUSCATED);
68 }
69 }
70
71 String defaultName = generateDefaultName(translatedEntry);
72 if (defaultName != null) {
73 return defaultName;
74 }
75
76 return null;
77 }
78
79 private Optional<String> proposeName(EnigmaProject project, Entry<?> entry) {
80 EnigmaServices services = project.getEnigma().getServices();
81
82 return services.get(NameProposalService.TYPE).stream().flatMap(nameProposalService -> {
83 EntryRemapper mapper = project.getMapper();
84 Collection<Entry<?>> resolved = mapper.getObfResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT);
85
86 return resolved.stream()
87 .map(e -> nameProposalService.proposeName(e, mapper))
88 .filter(Optional::isPresent)
89 .map(Optional::get);
90 }).findFirst();
91 }
92
93 @Nullable
94 private String generateDefaultName(Entry<?> entry) {
95 if (entry instanceof LocalVariableDefEntry) {
96 LocalVariableDefEntry localVariable = (LocalVariableDefEntry) entry;
97
98 int index = localVariable.getIndex();
99 if (localVariable.isArgument()) {
100 List<TypeDescriptor> arguments = localVariable.getParent().getDesc().getArgumentDescs();
101 return LocalNameGenerator.generateArgumentName(index, localVariable.getDesc(), arguments);
102 } else {
103 return LocalNameGenerator.generateLocalVariableName(index, localVariable.getDesc());
104 }
105 }
106
107 return null;
108 }
109
110 private boolean isDeobfuscated(Entry<?> entry, Entry<?> translatedEntry) {
111 return !entry.getName().equals(translatedEntry.getName());
112 }
113
114 public ClassEntry getEntry() {
115 return classEntry;
116 }
117
118 public SourceIndex getIndex() {
119 return remappedIndex;
120 }
121
122 public Map<TokenHighlightType, Collection<Token>> getHighlightedTokens() {
123 return highlightedTokens;
124 }
125
126 private void highlightToken(Token token, TokenHighlightType highlightType) {
127 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token);
128 }
129
130 public int getObfuscatedOffset(int deobfOffset) {
131 return getOffset(remappedIndex, obfuscatedIndex, deobfOffset);
132 }
133
134 public int getDeobfuscatedOffset(int obfOffset) {
135 return getOffset(obfuscatedIndex, remappedIndex, obfOffset);
136 }
137
138 private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fromOffset) {
139 int relativeOffset = 0;
140
141 Iterator<Token> fromTokenItr = fromIndex.referenceTokens().iterator();
142 Iterator<Token> toTokenItr = toIndex.referenceTokens().iterator();
143 while (fromTokenItr.hasNext() && toTokenItr.hasNext()) {
144 Token fromToken = fromTokenItr.next();
145 Token toToken = toTokenItr.next();
146 if (fromToken.end > fromOffset) {
147 break;
148 }
149
150 relativeOffset = toToken.end - fromToken.end;
151 }
152
153 return fromOffset + relativeOffset;
154 }
155
156 @Override
157 public String toString() {
158 return remappedIndex.getSource();
159 }
160}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java
new file mode 100644
index 00000000..c912be3a
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaQuickFindDialog.java
@@ -0,0 +1,90 @@
1package cuchaz.enigma.gui;
2
3import de.sciss.syntaxpane.actions.DocumentSearchData;
4import de.sciss.syntaxpane.actions.gui.QuickFindDialog;
5
6import javax.swing.*;
7import javax.swing.text.JTextComponent;
8import java.awt.*;
9import java.awt.event.KeyAdapter;
10import java.awt.event.KeyEvent;
11import java.util.stream.IntStream;
12import java.util.stream.Stream;
13
14public class EnigmaQuickFindDialog extends QuickFindDialog {
15 public EnigmaQuickFindDialog(JTextComponent target) {
16 super(target, DocumentSearchData.getFromEditor(target));
17
18 JToolBar toolBar = getToolBar();
19 JTextField textField = getTextField(toolBar);
20
21 textField.addKeyListener(new KeyAdapter() {
22 @Override
23 public void keyPressed(KeyEvent e) {
24 super.keyPressed(e);
25 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
26 JToolBar toolBar = getToolBar();
27 boolean next = !e.isShiftDown();
28 JButton button = next ? getNextButton(toolBar) : getPrevButton(toolBar);
29 button.doClick();
30 }
31 }
32 });
33 }
34
35 @Override
36 public void showFor(JTextComponent target) {
37 String selectedText = target.getSelectedText();
38
39 try {
40 super.showFor(target);
41 } catch (Exception e) {
42 e.printStackTrace();
43 return;
44 }
45
46 Container view = target.getParent();
47 Point loc = new Point(0, view.getHeight() - getSize().height);
48 setLocationRelativeTo(view);
49 SwingUtilities.convertPointToScreen(loc, view);
50 setLocation(loc);
51
52 JToolBar toolBar = getToolBar();
53 JTextField textField = getTextField(toolBar);
54
55 if (selectedText != null) {
56 textField.setText(selectedText);
57 }
58
59 textField.selectAll();
60 }
61
62 private JToolBar getToolBar() {
63 return components(getContentPane(), JToolBar.class).findFirst().orElse(null);
64 }
65
66 private JTextField getTextField(JToolBar toolBar) {
67 return components(toolBar, JTextField.class).findFirst().orElse(null);
68 }
69
70 private JButton getNextButton(JToolBar toolBar) {
71 Stream<JButton> buttons = components(toolBar, JButton.class);
72 return buttons.skip(1).findFirst().orElse(null);
73 }
74
75 private JButton getPrevButton(JToolBar toolBar) {
76 Stream<JButton> buttons = components(toolBar, JButton.class);
77 return buttons.findFirst().orElse(null);
78 }
79
80 private static Stream<Component> components(Container container) {
81 return IntStream.range(0, container.getComponentCount())
82 .mapToObj(container::getComponent);
83 }
84
85 private static <T extends Component> Stream<T> components(Container container, Class<T> type) {
86 return components(container)
87 .filter(type::isInstance)
88 .map(type::cast);
89 }
90}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java
new file mode 100644
index 00000000..2f08a269
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/EnigmaSyntaxKit.java
@@ -0,0 +1,44 @@
1package cuchaz.enigma.gui;
2
3import cuchaz.enigma.gui.config.Config;
4import de.sciss.syntaxpane.components.LineNumbersRuler;
5import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit;
6import de.sciss.syntaxpane.util.Configuration;
7
8public class EnigmaSyntaxKit extends JavaSyntaxKit {
9 private static Configuration configuration = null;
10
11 @Override
12 public Configuration getConfig() {
13 if(configuration == null){
14 initConfig(super.getConfig(JavaSyntaxKit.class));
15 }
16 return configuration;
17 }
18
19 public void initConfig(Configuration baseConfig){
20 configuration = baseConfig;
21 //See de.sciss.syntaxpane.TokenType
22 configuration.put("Style.KEYWORD", Config.getInstance().highlightColor + ", 0");
23 configuration.put("Style.KEYWORD2", Config.getInstance().highlightColor + ", 3");
24 configuration.put("Style.STRING", Config.getInstance().stringColor + ", 0");
25 configuration.put("Style.STRING2", Config.getInstance().stringColor + ", 1");
26 configuration.put("Style.NUMBER", Config.getInstance().numberColor + ", 1");
27 configuration.put("Style.OPERATOR", Config.getInstance().operatorColor + ", 0");
28 configuration.put("Style.DELIMITER", Config.getInstance().delimiterColor + ", 1");
29 configuration.put("Style.TYPE", Config.getInstance().typeColor + ", 2");
30 configuration.put("Style.TYPE2", Config.getInstance().typeColor + ", 1");
31 configuration.put("Style.IDENTIFIER", Config.getInstance().identifierColor + ", 0");
32 configuration.put("Style.DEFAULT", Config.getInstance().defaultTextColor + ", 0");
33 configuration.put(LineNumbersRuler.PROPERTY_BACKGROUND, Config.getInstance().lineNumbersBackground + "");
34 configuration.put(LineNumbersRuler.PROPERTY_FOREGROUND, Config.getInstance().lineNumbersForeground + "");
35 configuration.put(LineNumbersRuler.PROPERTY_CURRENT_BACK, Config.getInstance().lineNumbersSelected + "");
36 configuration.put("RightMarginColumn", "999"); //No need to have a right margin, if someone wants it add a config
37
38 configuration.put("Action.quick-find", "cuchaz.enigma.gui.QuickFindAction, menu F");
39 }
40
41 public static void invalidate(){
42 configuration = null;
43 }
44}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java
new file mode 100644
index 00000000..6246192c
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ExceptionIgnorer.java
@@ -0,0 +1,35 @@
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
14public class ExceptionIgnorer {
15
16 public static boolean shouldIgnore(Throwable t) {
17
18 // is this that pesky concurrent access bug in the highlight painter system?
19 // (ancient ui code is ancient)
20 if (t instanceof ArrayIndexOutOfBoundsException) {
21 StackTraceElement[] stackTrace = t.getStackTrace();
22 if (stackTrace.length > 1) {
23
24 // does this stack frame match javax.swing.text.DefaultHighlighter.paint*() ?
25 StackTraceElement frame = stackTrace[1];
26 if (frame.getClassName().equals("javax.swing.text.DefaultHighlighter") && frame.getMethodName().startsWith("paint")) {
27 return true;
28 }
29 }
30 }
31
32 return false;
33 }
34
35}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java
new file mode 100644
index 00000000..2ed1010f
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Gui.java
@@ -0,0 +1,1058 @@
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 java.awt.*;
15import java.awt.event.*;
16import java.nio.file.Path;
17import java.util.List;
18import java.util.*;
19import java.util.function.Function;
20
21import javax.swing.*;
22import javax.swing.text.BadLocationException;
23import javax.swing.text.Highlighter;
24import javax.swing.tree.*;
25
26import com.google.common.base.Strings;
27import com.google.common.collect.Lists;
28import cuchaz.enigma.Enigma;
29import cuchaz.enigma.EnigmaProfile;
30import cuchaz.enigma.analysis.*;
31import cuchaz.enigma.gui.config.Config;
32import cuchaz.enigma.gui.config.Themes;
33import cuchaz.enigma.gui.dialog.CrashDialog;
34import cuchaz.enigma.gui.dialog.JavadocDialog;
35import cuchaz.enigma.gui.dialog.SearchDialog;
36import cuchaz.enigma.gui.elements.CollapsibleTabbedPane;
37import cuchaz.enigma.gui.elements.MenuBar;
38import cuchaz.enigma.gui.elements.PopupMenuBar;
39import cuchaz.enigma.gui.filechooser.FileChooserAny;
40import cuchaz.enigma.gui.filechooser.FileChooserFolder;
41import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
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;
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;
57import cuchaz.enigma.utils.I18n;
58import de.sciss.syntaxpane.DefaultSyntaxKit;
59
60public class Gui {
61
62 public final PopupMenuBar popupMenu;
63 private final PanelObf obfPanel;
64 private final PanelDeobf deobfPanel;
65
66 private final MenuBar menuBar;
67 // state
68 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;
73 private boolean isJarOpen;
74
75 public FileDialog jarFileChooser;
76 public FileDialog tinyMappingsFileChooser;
77 public SearchDialog searchDialog;
78 public JFileChooser enigmaMappingsFileChooser;
79 public JFileChooser exportSourceFileChooser;
80 public FileDialog exportJarFileChooser;
81 private GuiController controller;
82 private JFrame frame;
83 public Config.LookAndFeel editorFeel;
84 public PanelEditor editor;
85 public JScrollPane sourceScroller;
86 private JPanel classesPanel;
87 private JSplitPane splitClasses;
88 private PanelIdentifier infoPanel;
89 public Map<TokenHighlightType, BoxHighlightPainter> boxHighlightPainters;
90 private SelectionHighlightPainter selectionHighlightPainter;
91 private JTree inheritanceTree;
92 private JTree implementationsTree;
93 private JTree callsTree;
94 private JList<Token> tokens;
95 private JTabbedPane tabs;
96
97 private JSplitPane splitRight;
98 private JSplitPane logSplit;
99 private CollapsibleTabbedPane logTabs;
100 private JList<String> users;
101 private DefaultListModel<String> userModel;
102 private JScrollPane messageScrollPane;
103 private JList<Message> messages;
104 private DefaultListModel<Message> messageModel;
105 private JTextField chatBox;
106
107 private JPanel statusBar;
108 private JLabel connectionStatusLabel;
109 private JLabel statusLabel;
110
111 public JTextField renameTextField;
112 public JTextArea javadocTextArea;
113
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
126 public Gui(EnigmaProfile profile) {
127 Config.getInstance().lookAndFeel.setGlobalLAF();
128
129 // init frame
130 this.frame = new JFrame(Enigma.NAME);
131 final Container pane = this.frame.getContentPane();
132 pane.setLayout(new BorderLayout());
133
134 if (Boolean.parseBoolean(System.getProperty("enigma.catchExceptions", "true"))) {
135 // install a global exception handler to the event thread
136 CrashDialog.init(this.frame);
137 Thread.setDefaultUncaughtExceptionHandler((thread, t) -> {
138 t.printStackTrace(System.err);
139 if (!ExceptionIgnorer.shouldIgnore(t)) {
140 CrashDialog.show(t);
141 }
142 });
143 }
144
145 this.controller = new GuiController(this, profile);
146
147 Themes.updateTheme(this);
148
149 // init file choosers
150 this.jarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.jar.open"), FileDialog.LOAD);
151
152 this.tinyMappingsFileChooser = new FileDialog(getFrame(), "Open tiny Mappings", FileDialog.LOAD);
153 this.enigmaMappingsFileChooser = new FileChooserAny();
154 this.exportSourceFileChooser = new FileChooserFolder();
155 this.exportJarFileChooser = new FileDialog(getFrame(), I18n.translate("menu.file.export.jar"), FileDialog.SAVE);
156
157 this.obfPanel = new PanelObf(this);
158 this.deobfPanel = new PanelDeobf(this);
159
160 // set up classes panel (don't add the splitter yet)
161 splitClasses = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, this.obfPanel, this.deobfPanel);
162 splitClasses.setResizeWeight(0.3);
163 this.classesPanel = new JPanel();
164 this.classesPanel.setLayout(new BorderLayout());
165 this.classesPanel.setPreferredSize(ScaleUtil.getDimension(250, 0));
166
167 // init info panel
168 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
184 // init inheritance panel
185 inheritanceTree = new JTree();
186 inheritanceTree.setModel(null);
187 inheritanceTree.addMouseListener(new MouseAdapter() {
188 @Override
189 public void mouseClicked(MouseEvent event) {
190 if (event.getClickCount() >= 2) {
191 // get the selected node
192 TreePath path = inheritanceTree.getSelectionPath();
193 if (path == null) {
194 return;
195 }
196
197 Object node = path.getLastPathComponent();
198 if (node instanceof ClassInheritanceTreeNode) {
199 ClassInheritanceTreeNode classNode = (ClassInheritanceTreeNode) node;
200 controller.navigateTo(new ClassEntry(classNode.getObfClassName()));
201 } else if (node instanceof MethodInheritanceTreeNode) {
202 MethodInheritanceTreeNode methodNode = (MethodInheritanceTreeNode) node;
203 if (methodNode.isImplemented()) {
204 controller.navigateTo(methodNode.getMethodEntry());
205 }
206 }
207 }
208 }
209 });
210 TreeCellRenderer cellRenderer = inheritanceTree.getCellRenderer();
211 inheritanceTree.setCellRenderer(new MethodTreeCellRenderer(cellRenderer));
212
213 JPanel inheritancePanel = new JPanel();
214 inheritancePanel.setLayout(new BorderLayout());
215 inheritancePanel.add(new JScrollPane(inheritanceTree));
216
217 // init implementations panel
218 implementationsTree = new JTree();
219 implementationsTree.setModel(null);
220 implementationsTree.addMouseListener(new MouseAdapter() {
221 @Override
222 public void mouseClicked(MouseEvent event) {
223 if (event.getClickCount() >= 2) {
224 // get the selected node
225 TreePath path = implementationsTree.getSelectionPath();
226 if (path == null) {
227 return;
228 }
229
230 Object node = path.getLastPathComponent();
231 if (node instanceof ClassImplementationsTreeNode) {
232 ClassImplementationsTreeNode classNode = (ClassImplementationsTreeNode) node;
233 controller.navigateTo(classNode.getClassEntry());
234 } else if (node instanceof MethodImplementationsTreeNode) {
235 MethodImplementationsTreeNode methodNode = (MethodImplementationsTreeNode) node;
236 controller.navigateTo(methodNode.getMethodEntry());
237 }
238 }
239 }
240 });
241 JPanel implementationsPanel = new JPanel();
242 implementationsPanel.setLayout(new BorderLayout());
243 implementationsPanel.add(new JScrollPane(implementationsTree));
244
245 // init call panel
246 callsTree = new JTree();
247 callsTree.setModel(null);
248 callsTree.addMouseListener(new MouseAdapter() {
249 @SuppressWarnings("unchecked")
250 @Override
251 public void mouseClicked(MouseEvent event) {
252 if (event.getClickCount() >= 2) {
253 // get the selected node
254 TreePath path = callsTree.getSelectionPath();
255 if (path == null) {
256 return;
257 }
258
259 Object node = path.getLastPathComponent();
260 if (node instanceof ReferenceTreeNode) {
261 ReferenceTreeNode<Entry<?>, Entry<?>> referenceNode = ((ReferenceTreeNode<Entry<?>, Entry<?>>) node);
262 if (referenceNode.getReference() != null) {
263 controller.navigateTo(referenceNode.getReference());
264 } else {
265 controller.navigateTo(referenceNode.getEntry());
266 }
267 }
268 }
269 }
270 });
271 tokens = new JList<>();
272 tokens.setCellRenderer(new TokenListCellRenderer(this.controller));
273 tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
274 tokens.setLayoutOrientation(JList.VERTICAL);
275 tokens.addMouseListener(new MouseAdapter() {
276 @Override
277 public void mouseClicked(MouseEvent event) {
278 if (event.getClickCount() == 2) {
279 Token selected = tokens.getSelectedValue();
280 if (selected != null) {
281 showToken(selected);
282 }
283 }
284 }
285 });
286 tokens.setPreferredSize(ScaleUtil.getDimension(0, 200));
287 tokens.setMinimumSize(ScaleUtil.getDimension(0, 200));
288 JSplitPane callPanel = new JSplitPane(
289 JSplitPane.VERTICAL_SPLIT,
290 true,
291 new JScrollPane(callsTree),
292 new JScrollPane(tokens)
293 );
294 callPanel.setResizeWeight(1); // let the top side take all the slack
295 callPanel.resetToPreferredSizes();
296
297 // layout controls
298 JPanel centerPanel = new JPanel();
299 centerPanel.setLayout(new BorderLayout());
300 centerPanel.add(infoPanel, BorderLayout.NORTH);
301 centerPanel.add(sourceScroller, BorderLayout.CENTER);
302 tabs = new JTabbedPane();
303 tabs.setPreferredSize(ScaleUtil.getDimension(250, 0));
304 tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel);
305 tabs.addTab(I18n.translate("info_panel.tree.implementations"), implementationsPanel);
306 tabs.addTab(I18n.translate("info_panel.tree.calls"), callPanel);
307 logTabs = new CollapsibleTabbedPane(JTabbedPane.BOTTOM);
308 userModel = new DefaultListModel<>();
309 users = new JList<>(userModel);
310 messageModel = new DefaultListModel<>();
311 messages = new JList<>(messageModel);
312 messages.setCellRenderer(new MessageListCellRenderer());
313 JPanel messagePanel = new JPanel(new BorderLayout());
314 messageScrollPane = new JScrollPane(this.messages);
315 messagePanel.add(messageScrollPane, BorderLayout.CENTER);
316 JPanel chatPanel = new JPanel(new BorderLayout());
317 chatBox = new JTextField();
318 AbstractAction sendListener = new AbstractAction("Send") {
319 @Override
320 public void actionPerformed(ActionEvent e) {
321 sendMessage();
322 }
323 };
324 chatBox.addActionListener(sendListener);
325 JButton chatSendButton = new JButton(sendListener);
326 chatPanel.add(chatBox, BorderLayout.CENTER);
327 chatPanel.add(chatSendButton, BorderLayout.EAST);
328 messagePanel.add(chatPanel, BorderLayout.SOUTH);
329 logTabs.addTab(I18n.translate("log_panel.users"), new JScrollPane(this.users));
330 logTabs.addTab(I18n.translate("log_panel.messages"), messagePanel);
331 logSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, tabs, logTabs);
332 logSplit.setResizeWeight(0.5);
333 logSplit.resetToPreferredSizes();
334 splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, this.logSplit);
335 splitRight.setResizeWeight(1); // let the left side take all the slack
336 splitRight.resetToPreferredSizes();
337 JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.classesPanel, splitRight);
338 splitCenter.setResizeWeight(0); // let the right side take all the slack
339 pane.add(splitCenter, BorderLayout.CENTER);
340
341 // init menus
342 this.menuBar = new MenuBar(this);
343 this.frame.setJMenuBar(this.menuBar);
344
345 // init status bar
346 statusBar = new JPanel(new BorderLayout());
347 statusBar.setBorder(BorderFactory.createLoweredBevelBorder());
348 connectionStatusLabel = new JLabel();
349 statusLabel = new JLabel();
350 statusBar.add(statusLabel, BorderLayout.CENTER);
351 statusBar.add(connectionStatusLabel, BorderLayout.EAST);
352 pane.add(statusBar, BorderLayout.SOUTH);
353
354 // init state
355 setConnectionState(ConnectionState.NOT_CONNECTED);
356 onCloseJar();
357
358 this.frame.addWindowListener(new WindowAdapter() {
359 @Override
360 public void windowClosing(WindowEvent event) {
361 close();
362 }
363 });
364
365 // show the frame
366 pane.doLayout();
367 this.frame.setSize(ScaleUtil.getDimension(1024, 576));
368 this.frame.setMinimumSize(ScaleUtil.getDimension(640, 480));
369 this.frame.setVisible(true);
370 this.frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
371 this.frame.setLocationRelativeTo(null);
372 }
373
374 public JFrame getFrame() {
375 return this.frame;
376 }
377
378 public GuiController getController() {
379 return this.controller;
380 }
381
382 public void onStartOpenJar() {
383 this.classesPanel.removeAll();
384 redraw();
385 }
386
387 public void onFinishOpenJar(String jarName) {
388 // update gui
389 this.frame.setTitle(Enigma.NAME + " - " + jarName);
390 this.classesPanel.removeAll();
391 this.classesPanel.add(splitClasses);
392 setEditorText(null);
393
394 // update menu
395 isJarOpen = true;
396
397 updateUiState();
398 redraw();
399 }
400
401 public void onCloseJar() {
402
403 // update gui
404 this.frame.setTitle(Enigma.NAME);
405 setObfClasses(null);
406 setDeobfClasses(null);
407 setEditorText(null);
408 this.classesPanel.removeAll();
409
410 // update menu
411 isJarOpen = false;
412 setMappingsFile(null);
413
414 updateUiState();
415 redraw();
416 }
417
418 public void setObfClasses(Collection<ClassEntry> obfClasses) {
419 this.obfPanel.obfClasses.setClasses(obfClasses);
420 }
421
422 public void setDeobfClasses(Collection<ClassEntry> deobfClasses) {
423 this.deobfPanel.deobfClasses.setClasses(deobfClasses);
424 }
425
426 public void setMappingsFile(Path path) {
427 this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null);
428 updateUiState();
429 }
430
431 public void setEditorText(String source) {
432 this.editor.getHighlighter().removeAllHighlights();
433 this.editor.setText(source);
434 }
435
436 public void setSource(DecompiledClassSource source) {
437 editor.setText(source.toString());
438 setHighlightedTokens(source.getHighlightedTokens());
439 }
440
441 public void showToken(final Token token) {
442 if (token == null) {
443 throw new IllegalArgumentException("Token cannot be null!");
444 }
445 CodeReader.navigateToToken(this.editor, token, selectionHighlightPainter);
446 redraw();
447 }
448
449 public void showTokens(Collection<Token> tokens) {
450 Vector<Token> sortedTokens = new Vector<>(tokens);
451 Collections.sort(sortedTokens);
452 if (sortedTokens.size() > 1) {
453 // sort the tokens and update the tokens panel
454 this.tokens.setListData(sortedTokens);
455 this.tokens.setSelectedIndex(0);
456 } else {
457 this.tokens.setListData(new Vector<>());
458 }
459
460 // show the first token
461 showToken(sortedTokens.get(0));
462 }
463
464 public void setHighlightedTokens(Map<TokenHighlightType, Collection<Token>> tokens) {
465 // remove any old highlighters
466 this.editor.getHighlighter().removeAllHighlights();
467
468 if (boxHighlightPainters != null) {
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 }
489
490 private void showCursorReference(EntryReference<Entry<?>, Entry<?>> reference) {
491 if (reference == null) {
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 }
545
546 private void addNameValue(JPanel container, String name, String value) {
547 JPanel panel = new JPanel();
548 panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0));
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 }
558
559 private JComboBox<AccessModifier> addModifierComboBox(JPanel container, String name, Entry<?> entry) {
560 if (!getController().project.isRenamable(entry))
561 return null;
562 JPanel panel = new JPanel();
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 }
586
587 public void onCaretMove(int pos, boolean fromClick) {
588 if (controller.project == null)
589 return;
590 EntryRemapper mapper = controller.project.getMapper();
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 }
686
687 public void startRename() {
688
689 // init the text box
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 }
730
731 private void finishRename(boolean saveName) {
732 String newName = renameTextField.getText();
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
749 // abort the rename
750 showCursorReference(cursorReference);
751
752 this.editor.grabFocus();
753
754 redraw();
755 }
756
757 private boolean isRenaming() {
758 return renameTextField != null;
759 }
760
761 public void showInheritance() {
762
763 if (cursorReference == null) {
764 return;
765 }
766
767 inheritanceTree.setModel(null);
768
769 if (cursorReference.entry instanceof ClassEntry) {
770 // get the class inheritance
771 ClassInheritanceTreeNode classNode = this.controller.getClassInheritance((ClassEntry) cursorReference.entry);
772
773 // show the tree at the root
774 TreePath path = getPathToRoot(classNode);
775 inheritanceTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0)));
776 inheritanceTree.expandPath(path);
777 inheritanceTree.setSelectionRow(inheritanceTree.getRowForPath(path));
778 } else if (cursorReference.entry instanceof MethodEntry) {
779 // get the method inheritance
780 MethodInheritanceTreeNode classNode = this.controller.getMethodInheritance((MethodEntry) cursorReference.entry);
781
782 // show the tree at the root
783 TreePath path = getPathToRoot(classNode);
784 inheritanceTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0)));
785 inheritanceTree.expandPath(path);
786 inheritanceTree.setSelectionRow(inheritanceTree.getRowForPath(path));
787 }
788
789 tabs.setSelectedIndex(0);
790
791 redraw();
792 }
793
794 public void showImplementations() {
795
796 if (cursorReference == null) {
797 return;
798 }
799
800 implementationsTree.setModel(null);
801
802 DefaultMutableTreeNode node = null;
803
804 // get the class implementations
805 if (cursorReference.entry instanceof ClassEntry)
806 node = this.controller.getClassImplementations((ClassEntry) cursorReference.entry);
807 else // get the method implementations
808 if (cursorReference.entry instanceof MethodEntry)
809 node = this.controller.getMethodImplementations((MethodEntry) cursorReference.entry);
810
811 if (node != null) {
812 // show the tree at the root
813 TreePath path = getPathToRoot(node);
814 implementationsTree.setModel(new DefaultTreeModel((TreeNode) path.getPathComponent(0)));
815 implementationsTree.expandPath(path);
816 implementationsTree.setSelectionRow(implementationsTree.getRowForPath(path));
817 }
818
819 tabs.setSelectedIndex(1);
820
821 redraw();
822 }
823
824 public void showCalls(boolean recurse) {
825 if (cursorReference == null) {
826 return;
827 }
828
829 if (cursorReference.entry instanceof ClassEntry) {
830 ClassReferenceTreeNode node = this.controller.getClassReferences((ClassEntry) cursorReference.entry);
831 callsTree.setModel(new DefaultTreeModel(node));
832 } else if (cursorReference.entry instanceof FieldEntry) {
833 FieldReferenceTreeNode node = this.controller.getFieldReferences((FieldEntry) cursorReference.entry);
834 callsTree.setModel(new DefaultTreeModel(node));
835 } else if (cursorReference.entry instanceof MethodEntry) {
836 MethodReferenceTreeNode node = this.controller.getMethodReferences((MethodEntry) cursorReference.entry, recurse);
837 callsTree.setModel(new DefaultTreeModel(node));
838 }
839
840 tabs.setSelectedIndex(2);
841
842 redraw();
843 }
844
845 public void toggleMapping() {
846 Entry<?> obfEntry = cursorReference.entry;
847 Entry<?> deobfEntry = controller.project.getMapper().deobfuscate(obfEntry);
848
849 if (!Objects.equals(obfEntry, deobfEntry)) {
850 this.controller.removeMapping(cursorReference);
851 this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry()));
852 } else {
853 this.controller.markAsDeobfuscated(cursorReference);
854 this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry()));
855 }
856 }
857
858 private TreePath getPathToRoot(TreeNode node) {
859 List<TreeNode> nodes = Lists.newArrayList();
860 TreeNode n = node;
861 do {
862 nodes.add(n);
863 n = n.getParent();
864 } while (n != null);
865 Collections.reverse(nodes);
866 return new TreePath(nodes.toArray());
867 }
868
869 public void showDiscardDiag(Function<Integer, Void> callback, String... options) {
870 int response = JOptionPane.showOptionDialog(this.frame, I18n.translate("prompt.close.summary"), I18n.translate("prompt.close.title"), JOptionPane.YES_NO_CANCEL_OPTION,
871 JOptionPane.QUESTION_MESSAGE, null, options, options[2]);
872 callback.apply(response);
873 }
874
875 public void saveMapping() {
876 if (this.enigmaMappingsFileChooser.getSelectedFile() != null || this.enigmaMappingsFileChooser.showSaveDialog(this.frame) == JFileChooser.APPROVE_OPTION)
877 this.controller.saveMappings(this.enigmaMappingsFileChooser.getSelectedFile().toPath());
878 }
879
880 public void close() {
881 if (!this.controller.isDirty()) {
882 // everything is saved, we can exit safely
883 exit();
884 } else {
885 // ask to save before closing
886 showDiscardDiag((response) -> {
887 if (response == JOptionPane.YES_OPTION) {
888 this.saveMapping();
889 exit();
890 } else if (response == JOptionPane.NO_OPTION) {
891 exit();
892 }
893
894 return null;
895 }, I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.close.cancel"));
896 }
897 }
898
899 private void exit() {
900 if (searchDialog != null) {
901 searchDialog.dispose();
902 }
903 this.frame.dispose();
904 System.exit(0);
905 }
906
907 public void redraw() {
908 this.frame.validate();
909 this.frame.repaint();
910 }
911
912 public void onPanelRename(Object prevData, Object data, DefaultMutableTreeNode node) throws IllegalNameException {
913 // package rename
914 if (data instanceof String) {
915 for (int i = 0; i < node.getChildCount(); i++) {
916 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt(i);
917 ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject();
918 ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName());
919 this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false);
920 this.controller.sendPacket(new RenameC2SPacket(prevDataChild, dataChild.getFullName(), false));
921 childNode.setUserObject(dataChild);
922 }
923 node.setUserObject(data);
924 // Ob package will never be modified, just reload deob view
925 this.deobfPanel.deobfClasses.reload();
926 }
927 // class rename
928 else if (data instanceof ClassEntry) {
929 this.controller.rename(new EntryReference<>((ClassEntry) prevData, ((ClassEntry) prevData).getFullName()), ((ClassEntry) data).getFullName(), false);
930 this.controller.sendPacket(new RenameC2SPacket((ClassEntry) prevData, ((ClassEntry) data).getFullName(), false));
931 }
932 }
933
934 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, String newName) {
935 String oldEntry = obfReference.entry.getContainingClass().getPackageName();
936 String newEntry = new ClassEntry(newName).getPackageName();
937 moveClassTree(obfReference, oldEntry == null, newEntry == null);
938 }
939
940 // TODO: getExpansionState will *not* actually update itself based on name changes!
941 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, boolean isOldOb, boolean isNewOb) {
942 ClassEntry classEntry = obfReference.entry.getContainingClass();
943
944 List<ClassSelector.StateEntry> stateDeobf = this.deobfPanel.deobfClasses.getExpansionState(this.deobfPanel.deobfClasses);
945 List<ClassSelector.StateEntry> stateObf = this.obfPanel.obfClasses.getExpansionState(this.obfPanel.obfClasses);
946
947 // Ob -> deob
948 if (!isNewOb) {
949 this.deobfPanel.deobfClasses.moveClassIn(classEntry);
950 this.obfPanel.obfClasses.moveClassOut(classEntry);
951 this.deobfPanel.deobfClasses.reload();
952 this.obfPanel.obfClasses.reload();
953 }
954 // Deob -> ob
955 else if (!isOldOb) {
956 this.obfPanel.obfClasses.moveClassIn(classEntry);
957 this.deobfPanel.deobfClasses.moveClassOut(classEntry);
958 this.deobfPanel.deobfClasses.reload();
959 this.obfPanel.obfClasses.reload();
960 }
961 // Local move
962 else if (isOldOb) {
963 this.obfPanel.obfClasses.moveClassIn(classEntry);
964 this.obfPanel.obfClasses.reload();
965 } else {
966 this.deobfPanel.deobfClasses.moveClassIn(classEntry);
967 this.deobfPanel.deobfClasses.reload();
968 }
969
970 this.deobfPanel.deobfClasses.restoreExpansionState(this.deobfPanel.deobfClasses, stateDeobf);
971 this.obfPanel.obfClasses.restoreExpansionState(this.obfPanel.obfClasses, stateObf);
972 }
973
974 public PanelObf getObfPanel() {
975 return obfPanel;
976 }
977
978 public PanelDeobf getDeobfPanel() {
979 return deobfPanel;
980 }
981
982 public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) {
983 this.shouldNavigateOnClick = shouldNavigateOnClick;
984 }
985
986 public SearchDialog getSearchDialog() {
987 if (searchDialog == null) {
988 searchDialog = new SearchDialog(this);
989 }
990 return searchDialog;
991 }
992
993
994 public MenuBar getMenuBar() {
995 return menuBar;
996 }
997
998 public void addMessage(Message message) {
999 JScrollBar verticalScrollBar = messageScrollPane.getVerticalScrollBar();
1000 boolean isAtBottom = verticalScrollBar.getValue() >= verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent();
1001 messageModel.addElement(message);
1002 if (isAtBottom) {
1003 SwingUtilities.invokeLater(() -> verticalScrollBar.setValue(verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent()));
1004 }
1005 statusLabel.setText(message.translate());
1006 }
1007
1008 public void setUserList(List<String> users) {
1009 userModel.clear();
1010 users.forEach(userModel::addElement);
1011 connectionStatusLabel.setText(String.format(I18n.translate("status.connected_user_count"), users.size()));
1012 }
1013
1014 private void sendMessage() {
1015 String text = chatBox.getText().trim();
1016 if (!text.isEmpty()) {
1017 getController().sendPacket(new MessageC2SPacket(text));
1018 }
1019 chatBox.setText("");
1020 }
1021
1022 /**
1023 * Updates the state of the UI elements (button text, enabled state, ...) to reflect the current program state.
1024 * This is a central place to update the UI state to prevent multiple code paths from changing the same state,
1025 * causing inconsistencies.
1026 */
1027 public void updateUiState() {
1028 menuBar.connectToServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.HOSTING);
1029 menuBar.connectToServerMenu.setText(I18n.translate(connectionState != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect"));
1030 menuBar.startServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.CONNECTED);
1031 menuBar.startServerMenu.setText(I18n.translate(connectionState != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop"));
1032
1033 menuBar.closeJarMenu.setEnabled(isJarOpen);
1034 menuBar.openMappingsMenus.forEach(item -> item.setEnabled(isJarOpen));
1035 menuBar.saveMappingsMenu.setEnabled(isJarOpen && enigmaMappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED);
1036 menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(isJarOpen));
1037 menuBar.closeMappingsMenu.setEnabled(isJarOpen);
1038 menuBar.exportSourceMenu.setEnabled(isJarOpen);
1039 menuBar.exportJarMenu.setEnabled(isJarOpen);
1040
1041 connectionStatusLabel.setText(I18n.translate(connectionState == ConnectionState.NOT_CONNECTED ? "status.disconnected" : "status.connected"));
1042
1043 if (connectionState == ConnectionState.NOT_CONNECTED) {
1044 logSplit.setLeftComponent(null);
1045 splitRight.setRightComponent(tabs);
1046 } else {
1047 splitRight.setRightComponent(logSplit);
1048 logSplit.setLeftComponent(tabs);
1049 }
1050 }
1051
1052 public void setConnectionState(ConnectionState state) {
1053 connectionState = state;
1054 statusLabel.setText(I18n.translate("status.ready"));
1055 updateUiState();
1056 }
1057
1058}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java
new file mode 100644
index 00000000..94979e77
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/GuiController.java
@@ -0,0 +1,719 @@
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 com.google.common.collect.Lists;
15import com.google.common.util.concurrent.ThreadFactoryBuilder;
16import cuchaz.enigma.Enigma;
17import cuchaz.enigma.EnigmaProfile;
18import cuchaz.enigma.EnigmaProject;
19import cuchaz.enigma.analysis.*;
20import cuchaz.enigma.api.service.ObfuscationTestService;
21import cuchaz.enigma.gui.config.Config;
22import cuchaz.enigma.gui.dialog.ProgressDialog;
23import cuchaz.enigma.gui.stats.StatsGenerator;
24import cuchaz.enigma.gui.stats.StatsMember;
25import cuchaz.enigma.gui.util.GuiUtil;
26import cuchaz.enigma.gui.util.History;
27import cuchaz.enigma.network.*;
28import cuchaz.enigma.network.packet.LoginC2SPacket;
29import cuchaz.enigma.network.packet.Packet;
30import cuchaz.enigma.source.*;
31import cuchaz.enigma.translation.mapping.serde.MappingParseException;
32import cuchaz.enigma.translation.Translator;
33import cuchaz.enigma.translation.mapping.*;
34import cuchaz.enigma.translation.mapping.serde.MappingFormat;
35import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters;
36import cuchaz.enigma.translation.mapping.tree.EntryTree;
37import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
38import cuchaz.enigma.translation.representation.entry.ClassEntry;
39import cuchaz.enigma.translation.representation.entry.Entry;
40import cuchaz.enigma.translation.representation.entry.FieldEntry;
41import cuchaz.enigma.translation.representation.entry.MethodEntry;
42import cuchaz.enigma.utils.I18n;
43import cuchaz.enigma.utils.Utils;
44
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
61public 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;
70 public final Enigma enigma;
71
72 public EnigmaProject project;
73 private DecompilerService decompilerService;
74 private Decompiler decompiler;
75 private IndexTreeBuilder indexTreeBuilder;
76
77 private Path loadedMappingPath;
78 private MappingFormat loadedMappingFormat;
79
80 private DecompiledClassSource currentSource;
81 private Source uncommentedSource;
82
83 private EnigmaClient client;
84 private EnigmaServer server;
85
86 public GuiController(Gui gui, EnigmaProfile profile) {
87 this.gui = gui;
88 this.enigma = Enigma.builder()
89 .setProfile(profile)
90 .build();
91
92 decompilerService = Config.getInstance().decompiler.service;
93 }
94
95 public boolean isDirty() {
96 return project != null && project.getMapper().isDirty();
97 }
98
99 public CompletableFuture<Void> openJar(final Path jarPath) {
100 this.gui.onStartOpenJar();
101
102 return ProgressDialog.runOffThread(gui.getFrame(), progress -> {
103 project = enigma.openJar(jarPath, progress);
104 indexTreeBuilder = new IndexTreeBuilder(project.getJarIndex());
105 decompiler = project.createDecompiler(decompilerService);
106 gui.onFinishOpenJar(jarPath.getFileName().toString());
107 refreshClasses();
108 });
109 }
110
111 public void closeJar() {
112 this.project = null;
113 this.gui.onCloseJar();
114 }
115
116 public CompletableFuture<Void> openMappings(MappingFormat format, Path path) {
117 if (project == null) return CompletableFuture.completedFuture(null);
118
119 gui.setMappingsFile(path);
120
121 return ProgressDialog.runOffThread(gui.getFrame(), progress -> {
122 try {
123 MappingSaveParameters saveParameters = enigma.getProfile().getMappingSaveParameters();
124
125 EntryTree<EntryMapping> mappings = format.read(path, progress, saveParameters);
126 project.setMappings(mappings);
127
128 loadedMappingFormat = format;
129 loadedMappingPath = path;
130
131 refreshClasses();
132 refreshCurrentClass();
133 } catch (MappingParseException e) {
134 JOptionPane.showMessageDialog(gui.getFrame(), e.getMessage());
135 }
136 });
137 }
138
139 @Override
140 public void openMappings(EntryTree<EntryMapping> mappings) {
141 if (project == null) return;
142
143 project.setMappings(mappings);
144 refreshClasses();
145 refreshCurrentClass();
146 }
147
148 public CompletableFuture<Void> saveMappings(Path path) {
149 return saveMappings(path, loadedMappingFormat);
150 }
151
152 public CompletableFuture<Void> saveMappings(Path path, MappingFormat format) {
153 if (project == null) return CompletableFuture.completedFuture(null);
154
155 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> {
156 EntryRemapper mapper = project.getMapper();
157 MappingSaveParameters saveParameters = enigma.getProfile().getMappingSaveParameters();
158
159 MappingDelta<EntryMapping> delta = mapper.takeMappingDelta();
160 boolean saveAll = !path.equals(loadedMappingPath);
161
162 loadedMappingFormat = format;
163 loadedMappingPath = path;
164
165 if (saveAll) {
166 format.write(mapper.getObfToDeobf(), path, progress, saveParameters);
167 } else {
168 format.write(mapper.getObfToDeobf(), delta, path, progress, saveParameters);
169 }
170 });
171 }
172
173 public void closeMappings() {
174 if (project == null) return;
175
176 project.setMappings(null);
177
178 this.gui.setMappingsFile(null);
179 refreshClasses();
180 refreshCurrentClass();
181 }
182
183 public CompletableFuture<Void> dropMappings() {
184 if (project == null) return CompletableFuture.completedFuture(null);
185
186 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> project.dropMappings(progress));
187 }
188
189 public CompletableFuture<Void> exportSource(final Path path) {
190 if (project == null) return CompletableFuture.completedFuture(null);
191
192 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> {
193 EnigmaProject.JarExport jar = project.exportRemappedJar(progress);
194 EnigmaProject.SourceExport source = jar.decompile(progress, decompilerService);
195
196 source.write(path, progress);
197 });
198 }
199
200 public CompletableFuture<Void> exportJar(final Path path) {
201 if (project == null) return CompletableFuture.completedFuture(null);
202
203 return ProgressDialog.runOffThread(this.gui.getFrame(), progress -> {
204 EnigmaProject.JarExport jar = project.exportRemappedJar(progress);
205 jar.write(path, progress);
206 });
207 }
208
209 public Token getToken(int pos) {
210 if (this.currentSource == null) {
211 return null;
212 }
213 return this.currentSource.getIndex().getReferenceToken(pos);
214 }
215
216 @Nullable
217 public EntryReference<Entry<?>, Entry<?>> getReference(Token token) {
218 if (this.currentSource == null) {
219 return null;
220 }
221 return this.currentSource.getIndex().getReference(token);
222 }
223
224 public ReadableToken getReadableToken(Token token) {
225 if (this.currentSource == null) {
226 return null;
227 }
228
229 SourceIndex index = this.currentSource.getIndex();
230 return new ReadableToken(
231 index.getLineNumber(token.start),
232 index.getColumnNumber(token.start),
233 index.getColumnNumber(token.end)
234 );
235 }
236
237 /**
238 * Navigates to the declaration with respect to navigation history
239 *
240 * @param entry the entry whose declaration will be navigated to
241 */
242 public void openDeclaration(Entry<?> entry) {
243 if (entry == null) {
244 throw new IllegalArgumentException("Entry cannot be null!");
245 }
246 openReference(new EntryReference<>(entry, entry.getName()));
247 }
248
249 /**
250 * Navigates to the reference with respect to navigation history
251 *
252 * @param reference the reference
253 */
254 public void openReference(EntryReference<Entry<?>, Entry<?>> reference) {
255 if (reference == null) {
256 throw new IllegalArgumentException("Reference cannot be null!");
257 }
258 if (this.gui.referenceHistory == null) {
259 this.gui.referenceHistory = new History<>(reference);
260 } else {
261 if (!reference.equals(this.gui.referenceHistory.getCurrent())) {
262 this.gui.referenceHistory.push(reference);
263 }
264 }
265 setReference(reference);
266 }
267
268 /**
269 * Navigates to the reference without modifying history. If the class is not currently loaded, it will be loaded.
270 *
271 * @param reference the reference
272 */
273 private void setReference(EntryReference<Entry<?>, Entry<?>> reference) {
274 // get the reference target class
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 }
302
303 public Collection<Token> getTokensForReference(EntryReference<Entry<?>, Entry<?>> reference) {
304 EntryRemapper mapper = this.project.getMapper();
305
306 SourceIndex index = this.currentSource.getIndex();
307 return mapper.getObfResolver().resolveReference(reference, ResolutionStrategy.RESOLVE_CLOSEST)
308 .stream()
309 .flatMap(r -> index.getReferenceTokens(r).stream())
310 .collect(Collectors.toList());
311 }
312
313 public void openPreviousReference() {
314 if (hasPreviousReference()) {
315 setReference(gui.referenceHistory.goBack());
316 }
317 }
318
319 public boolean hasPreviousReference() {
320 return gui.referenceHistory != null && gui.referenceHistory.canGoBack();
321 }
322
323 public void openNextReference() {
324 if (hasNextReference()) {
325 setReference(gui.referenceHistory.goForward());
326 }
327 }
328
329 public boolean hasNextReference() {
330 return gui.referenceHistory != null && gui.referenceHistory.canGoForward();
331 }
332
333 public void navigateTo(Entry<?> entry) {
334 if (!project.isRenamable(entry)) {
335 // entry is not in the jar. Ignore it
336 return;
337 }
338 openDeclaration(entry);
339 }
340
341 public void navigateTo(EntryReference<Entry<?>, Entry<?>> reference) {
342 if (!project.isRenamable(reference.getLocationClassEntry())) {
343 return;
344 }
345 openReference(reference);
346 }
347
348 private void refreshClasses() {
349 List<ClassEntry> obfClasses = Lists.newArrayList();
350 List<ClassEntry> deobfClasses = Lists.newArrayList();
351 this.addSeparatedClasses(obfClasses, deobfClasses);
352 this.gui.setObfClasses(obfClasses);
353 this.gui.setDeobfClasses(deobfClasses);
354 }
355
356 public void addSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses) {
357 EntryRemapper mapper = project.getMapper();
358
359 Collection<ClassEntry> classes = project.getJarIndex().getEntryIndex().getClasses();
360 Stream<ClassEntry> visibleClasses = classes.stream()
361 .filter(entry -> !entry.isInnerClass());
362
363 visibleClasses.forEach(entry -> {
364 ClassEntry deobfEntry = mapper.deobfuscate(entry);
365
366 List<ObfuscationTestService> obfService = enigma.getServices().get(ObfuscationTestService.TYPE);
367 boolean obfuscated = deobfEntry.equals(entry);
368
369 if (obfuscated && !obfService.isEmpty()) {
370 if (obfService.stream().anyMatch(service -> service.testDeobfuscated(entry))) {
371 obfuscated = false;
372 }
373 }
374
375 if (obfuscated) {
376 obfClasses.add(entry);
377 } else {
378 deobfClasses.add(entry);
379 }
380 });
381 }
382
383 public void refreshCurrentClass() {
384 refreshCurrentClass(null);
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
482 private void remapSource(Translator translator) {
483 if (currentSource == null) {
484 return;
485 }
486
487 currentSource.remapSource(project, translator);
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 }
509
510 public ClassInheritanceTreeNode getClassInheritance(ClassEntry entry) {
511 Translator translator = project.getMapper().getDeobfuscator();
512 ClassInheritanceTreeNode rootNode = indexTreeBuilder.buildClassInheritance(translator, entry);
513 return ClassInheritanceTreeNode.findNode(rootNode, entry);
514 }
515
516 public ClassImplementationsTreeNode getClassImplementations(ClassEntry entry) {
517 Translator translator = project.getMapper().getDeobfuscator();
518 return this.indexTreeBuilder.buildClassImplementations(translator, entry);
519 }
520
521 public MethodInheritanceTreeNode getMethodInheritance(MethodEntry entry) {
522 Translator translator = project.getMapper().getDeobfuscator();
523 MethodInheritanceTreeNode rootNode = indexTreeBuilder.buildMethodInheritance(translator, entry);
524 return MethodInheritanceTreeNode.findNode(rootNode, entry);
525 }
526
527 public MethodImplementationsTreeNode getMethodImplementations(MethodEntry entry) {
528 Translator translator = project.getMapper().getDeobfuscator();
529 List<MethodImplementationsTreeNode> rootNodes = indexTreeBuilder.buildMethodImplementations(translator, entry);
530 if (rootNodes.isEmpty()) {
531 return null;
532 }
533 if (rootNodes.size() > 1) {
534 System.err.println("WARNING: Method " + entry + " implements multiple interfaces. Only showing first one.");
535 }
536 return MethodImplementationsTreeNode.findNode(rootNodes.get(0), entry);
537 }
538
539 public ClassReferenceTreeNode getClassReferences(ClassEntry entry) {
540 Translator deobfuscator = project.getMapper().getDeobfuscator();
541 ClassReferenceTreeNode rootNode = new ClassReferenceTreeNode(deobfuscator, entry);
542 rootNode.load(project.getJarIndex(), true);
543 return rootNode;
544 }
545
546 public FieldReferenceTreeNode getFieldReferences(FieldEntry entry) {
547 Translator translator = project.getMapper().getDeobfuscator();
548 FieldReferenceTreeNode rootNode = new FieldReferenceTreeNode(translator, entry);
549 rootNode.load(project.getJarIndex(), true);
550 return rootNode;
551 }
552
553 public MethodReferenceTreeNode getMethodReferences(MethodEntry entry, boolean recursive) {
554 Translator translator = project.getMapper().getDeobfuscator();
555 MethodReferenceTreeNode rootNode = new MethodReferenceTreeNode(translator, entry);
556 rootNode.load(project.getJarIndex(), true, recursive);
557 return rootNode;
558 }
559
560 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree) {
561 rename(reference, newName, refreshClassTree, true);
562 }
563
564 @Override
565 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean jumpToReference) {
566 Entry<?> entry = reference.getNameableEntry();
567 project.getMapper().mapFromObf(entry, new EntryMapping(newName));
568
569 if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
570 this.gui.moveClassTree(reference, newName);
571
572 refreshCurrentClass(jumpToReference ? reference : null);
573 }
574
575 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference) {
576 removeMapping(reference, true);
577 }
578
579 @Override
580 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) {
581 project.getMapper().removeByObf(reference.getNameableEntry());
582
583 if (reference.entry instanceof ClassEntry)
584 this.gui.moveClassTree(reference, false, true);
585 refreshCurrentClass(jumpToReference ? reference : null);
586 }
587
588 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs) {
589 changeDocs(reference, updatedDocs, true);
590 }
591
592 @Override
593 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean jumpToReference) {
594 changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs);
595
596 refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS);
597 }
598
599 private void changeDoc(Entry<?> obfEntry, String newDoc) {
600 EntryRemapper mapper = project.getMapper();
601 if (mapper.getDeobfMapping(obfEntry) == null) {
602 markAsDeobfuscated(obfEntry, false); // NPE
603 }
604 mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false);
605 }
606
607 private void markAsDeobfuscated(Entry<?> obfEntry, boolean renaming) {
608 EntryRemapper mapper = project.getMapper();
609 mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming);
610 }
611
612 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference) {
613 markAsDeobfuscated(reference, true);
614 }
615
616 @Override
617 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) {
618 EntryRemapper mapper = project.getMapper();
619 Entry<?> entry = reference.getNameableEntry();
620 mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName()));
621
622 if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
623 this.gui.moveClassTree(reference, true, false);
624
625 refreshCurrentClass(jumpToReference ? reference : null);
626 }
627
628 public void openStats(Set<StatsMember> includedMembers) {
629 ProgressDialog.runOffThread(gui.getFrame(), progress -> {
630 String data = new StatsGenerator(project).generate(progress, includedMembers);
631
632 try {
633 File statsFile = File.createTempFile("stats", ".html");
634
635 try (FileWriter w = new FileWriter(statsFile)) {
636 w.write(
637 Utils.readResourceToString("/stats.html")
638 .replace("/*data*/", data)
639 );
640 }
641
642 Desktop.getDesktop().open(statsFile);
643 } catch (IOException e) {
644 throw new Error(e);
645 }
646 });
647 }
648
649 public void setDecompiler(DecompilerService service) {
650 uncommentedSource = null;
651 decompilerService = service;
652 decompiler = project.createDecompiler(decompilerService);
653 refreshCurrentClass(null, RefreshMode.FULL);
654 }
655
656 public EnigmaClient getClient() {
657 return client;
658 }
659
660 public EnigmaServer getServer() {
661 return server;
662 }
663
664 public void createClient(String username, String ip, int port, char[] password) throws IOException {
665 client = new EnigmaClient(this, ip, port);
666 client.connect();
667 client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, username));
668 gui.setConnectionState(ConnectionState.CONNECTED);
669 }
670
671 public void createServer(int port, char[] password) throws IOException {
672 server = new IntegratedEnigmaServer(project.getJarChecksum(), password, EntryRemapper.mapped(project.getJarIndex(), new HashEntryTree<>(project.getMapper().getObfToDeobf())), port);
673 server.start();
674 client = new EnigmaClient(this, "127.0.0.1", port);
675 client.connect();
676 client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, EnigmaServer.OWNER_USERNAME));
677 gui.setConnectionState(ConnectionState.HOSTING);
678 }
679
680 @Override
681 public synchronized void disconnectIfConnected(String reason) {
682 if (client == null && server == null) {
683 return;
684 }
685
686 if (client != null) {
687 client.disconnect();
688 }
689 if (server != null) {
690 server.stop();
691 }
692 client = null;
693 server = null;
694 SwingUtilities.invokeLater(() -> {
695 if (reason != null) {
696 JOptionPane.showMessageDialog(gui.getFrame(), I18n.translate(reason), I18n.translate("disconnect.disconnected"), JOptionPane.INFORMATION_MESSAGE);
697 }
698 gui.setConnectionState(ConnectionState.NOT_CONNECTED);
699 });
700 }
701
702 @Override
703 public void sendPacket(Packet<ServerPacketHandler> packet) {
704 if (client != null) {
705 client.sendPacket(packet);
706 }
707 }
708
709 @Override
710 public void addMessage(Message message) {
711 gui.addMessage(message);
712 }
713
714 @Override
715 public void updateUserList(List<String> users) {
716 gui.setUserList(users);
717 }
718
719}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java
new file mode 100644
index 00000000..1f3aa2cb
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/Main.java
@@ -0,0 +1,118 @@
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.EnigmaProfile;
15import cuchaz.enigma.gui.config.Config;
16import cuchaz.enigma.translation.mapping.serde.MappingFormat;
17
18import cuchaz.enigma.utils.I18n;
19import joptsimple.*;
20
21import java.io.IOException;
22import java.nio.file.Files;
23import java.nio.file.Path;
24import java.nio.file.Paths;
25
26import com.google.common.io.MoreFiles;
27
28public class Main {
29
30 public static void main(String[] args) throws IOException {
31 OptionParser parser = new OptionParser();
32
33 OptionSpec<Path> jar = parser.accepts("jar", "Jar file to open at startup")
34 .withRequiredArg()
35 .withValuesConvertedBy(PathConverter.INSTANCE);
36
37 OptionSpec<Path> mappings = parser.accepts("mappings", "Mappings file to open at startup")
38 .withRequiredArg()
39 .withValuesConvertedBy(PathConverter.INSTANCE);
40
41 OptionSpec<Path> profile = parser.accepts("profile", "Profile json to apply at startup")
42 .withRequiredArg()
43 .withValuesConvertedBy(PathConverter.INSTANCE);
44
45 parser.accepts("help", "Displays help information");
46
47 try {
48 OptionSet options = parser.parse(args);
49
50 if (options.has("help")) {
51 parser.printHelpOn(System.out);
52 return;
53 }
54
55 EnigmaProfile parsedProfile = EnigmaProfile.read(options.valueOf(profile));
56
57 I18n.setLanguage(Config.getInstance().language);
58 Gui gui = new Gui(parsedProfile);
59 GuiController controller = gui.getController();
60
61 if (options.has(jar)) {
62 Path jarPath = options.valueOf(jar);
63 controller.openJar(jarPath)
64 .whenComplete((v, t) -> {
65 if (options.has(mappings)) {
66 Path mappingsPath = options.valueOf(mappings);
67 if (Files.isDirectory(mappingsPath)) {
68 controller.openMappings(MappingFormat.ENIGMA_DIRECTORY, mappingsPath);
69 } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsPath))) {
70 controller.openMappings(MappingFormat.ENIGMA_ZIP, mappingsPath);
71 } else {
72 controller.openMappings(MappingFormat.ENIGMA_FILE, mappingsPath);
73 }
74 }
75 });
76 }
77 } catch (OptionException e) {
78 System.out.println("Invalid arguments: " + e.getMessage());
79 System.out.println();
80 parser.printHelpOn(System.out);
81 }
82 }
83
84 public static class PathConverter implements ValueConverter<Path> {
85 public static final ValueConverter<Path> INSTANCE = new PathConverter();
86
87 PathConverter() {
88 }
89
90 @Override
91 public Path convert(String path) {
92 // expand ~ to the home dir
93 if (path.startsWith("~")) {
94 // get the home dir
95 Path dirHome = Paths.get(System.getProperty("user.home"));
96
97 // is the path just ~/ or is it ~user/ ?
98 if (path.startsWith("~/")) {
99 return dirHome.resolve(path.substring(2));
100 } else {
101 return dirHome.getParent().resolve(path.substring(1));
102 }
103 }
104
105 return Paths.get(path);
106 }
107
108 @Override
109 public Class<? extends Path> valueType() {
110 return Path.class;
111 }
112
113 @Override
114 public String valuePattern() {
115 return "path";
116 }
117 }
118}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java
new file mode 100644
index 00000000..1d603409
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java
@@ -0,0 +1,24 @@
1package cuchaz.enigma.gui;
2
3import java.awt.Component;
4
5import javax.swing.DefaultListCellRenderer;
6import javax.swing.JList;
7
8import cuchaz.enigma.network.Message;
9
10// For now, just render the translated text.
11// TODO: Icons or something later?
12public class MessageListCellRenderer extends DefaultListCellRenderer {
13
14 @Override
15 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
16 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
17 Message message = (Message) value;
18 if (message != null) {
19 setText(message.translate());
20 }
21 return this;
22 }
23
24}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java
new file mode 100644
index 00000000..1eead6eb
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/MethodTreeCellRenderer.java
@@ -0,0 +1,42 @@
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.analysis.MethodInheritanceTreeNode;
15import cuchaz.enigma.gui.config.Config;
16
17import javax.swing.*;
18import javax.swing.tree.TreeCellRenderer;
19import java.awt.*;
20
21class MethodTreeCellRenderer implements TreeCellRenderer {
22
23 private final TreeCellRenderer parent;
24
25 MethodTreeCellRenderer(TreeCellRenderer parent) {
26 this.parent = parent;
27 }
28
29 @Override
30 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
31 Component ret = parent.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
32 Config config = Config.getInstance();
33 if (!(value instanceof MethodInheritanceTreeNode) || ((MethodInheritanceTreeNode) value).isImplemented()) {
34 ret.setForeground(new Color(config.defaultTextColor));
35 ret.setFont(ret.getFont().deriveFont(Font.PLAIN));
36 } else {
37 ret.setForeground(new Color(config.numberColor));
38 ret.setFont(ret.getFont().deriveFont(Font.ITALIC));
39 }
40 return ret;
41 }
42}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java
new file mode 100644
index 00000000..b7fa2eba
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/QuickFindAction.java
@@ -0,0 +1,45 @@
1package cuchaz.enigma.gui;
2
3import de.sciss.syntaxpane.SyntaxDocument;
4import de.sciss.syntaxpane.actions.DefaultSyntaxAction;
5
6import javax.swing.text.JTextComponent;
7import java.awt.event.ActionEvent;
8
9public final class QuickFindAction extends DefaultSyntaxAction {
10 public QuickFindAction() {
11 super("quick-find");
12 }
13
14 @Override
15 public void actionPerformed(JTextComponent target, SyntaxDocument document, int dot, ActionEvent event) {
16 Data data = Data.get(target);
17 data.showFindDialog(target);
18 }
19
20 private static class Data {
21 private static final String KEY = "enigma-find-data";
22 private EnigmaQuickFindDialog findDialog;
23
24 private Data() {
25 }
26
27 public static Data get(JTextComponent target) {
28 Object o = target.getDocument().getProperty(KEY);
29 if (o instanceof Data) {
30 return (Data) o;
31 }
32
33 Data data = new Data();
34 target.getDocument().putProperty(KEY, data);
35 return data;
36 }
37
38 public void showFindDialog(JTextComponent target) {
39 if (findDialog == null) {
40 findDialog = new EnigmaQuickFindDialog(target);
41 }
42 findDialog.showFor(target);
43 }
44 }
45}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java
new file mode 100644
index 00000000..3e4b30cd
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/ReadableToken.java
@@ -0,0 +1,30 @@
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
14public class ReadableToken {
15
16 public int line;
17 public int startColumn;
18 public int endColumn;
19
20 public ReadableToken(int line, int startColumn, int endColumn) {
21 this.line = line;
22 this.startColumn = startColumn;
23 this.endColumn = endColumn;
24 }
25
26 @Override
27 public String toString() {
28 return "line " + line + " columns " + startColumn + "-" + endColumn;
29 }
30}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java
new file mode 100644
index 00000000..87cb83b2
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/RefreshMode.java
@@ -0,0 +1,7 @@
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
new file mode 100644
index 00000000..10c418c2
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/TokenListCellRenderer.java
@@ -0,0 +1,35 @@
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 java.awt.*;
18
19public class TokenListCellRenderer implements ListCellRenderer<Token> {
20
21 private GuiController controller;
22 private DefaultListCellRenderer defaultRenderer;
23
24 public TokenListCellRenderer(GuiController controller) {
25 this.controller = controller;
26 this.defaultRenderer = new DefaultListCellRenderer();
27 }
28
29 @Override
30 public Component getListCellRendererComponent(JList<? extends Token> list, Token token, int index, boolean isSelected, boolean hasFocus) {
31 JLabel label = (JLabel) this.defaultRenderer.getListCellRendererComponent(list, token, index, isSelected, hasFocus);
32 label.setText(this.controller.getReadableToken(token).toString());
33 return label;
34 }
35}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java
new file mode 100644
index 00000000..373dcf04
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Config.java
@@ -0,0 +1,261 @@
1package cuchaz.enigma.gui.config;
2
3import com.bulenkov.darcula.DarculaLaf;
4import com.google.common.io.Files;
5import com.google.gson.*;
6import cuchaz.enigma.source.DecompilerService;
7import cuchaz.enigma.source.Decompilers;
8
9import cuchaz.enigma.utils.I18n;
10
11import javax.swing.*;
12import javax.swing.plaf.metal.MetalLookAndFeel;
13import java.awt.*;
14import java.awt.image.BufferedImage;
15import java.io.File;
16import java.io.IOException;
17import java.lang.reflect.Type;
18import java.nio.charset.Charset;
19
20public class Config {
21 public static class AlphaColorEntry {
22 public Integer rgb;
23 public float alpha = 1.0f;
24
25 public AlphaColorEntry(Integer rgb, float alpha) {
26 this.rgb = rgb;
27 this.alpha = alpha;
28 }
29
30 public Color get() {
31 if (rgb == null) {
32 return new Color(0, 0, 0, 0);
33 }
34
35 Color baseColor = new Color(rgb);
36 return new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), (int)(255 * alpha));
37 }
38 }
39
40 public enum LookAndFeel {
41 DEFAULT("Default"),
42 DARCULA("Darcula"),
43 SYSTEM("System"),
44 NONE("None (JVM default)");
45
46 // the "JVM default" look and feel, get it at the beginning and store it so we can set it later
47 private static javax.swing.LookAndFeel NONE_LAF = UIManager.getLookAndFeel();
48 private final String name;
49
50 LookAndFeel(String name) {
51 this.name = name;
52 }
53
54 public String getName() {
55 return name;
56 }
57
58 public void setGlobalLAF() {
59 try {
60 switch (this) {
61 case NONE:
62 UIManager.setLookAndFeel(NONE_LAF);
63 break;
64 case DEFAULT:
65 UIManager.setLookAndFeel(new MetalLookAndFeel());
66 break;
67 case DARCULA:
68 UIManager.setLookAndFeel(new DarculaLaf());
69 break;
70 case SYSTEM:
71 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
72 }
73 } catch (Exception e){
74 throw new Error("Failed to set global look and feel", e);
75 }
76 }
77
78 public static boolean isDarkLaf() {
79 // a bit of a hack because swing doesn't give any API for that, and we need colors that aren't defined in look and feel
80 JPanel panel = new JPanel();
81 panel.setSize(new Dimension(10, 10));
82 panel.doLayout();
83
84 BufferedImage image = new BufferedImage(panel.getSize().width, panel.getSize().height, BufferedImage.TYPE_INT_RGB);
85 panel.printAll(image.getGraphics());
86
87 Color c = new Color(image.getRGB(0, 0));
88
89 // convert the color we got to grayscale
90 int b = (int) (0.3 * c.getRed() + 0.59 * c.getGreen() + 0.11 * c.getBlue());
91 return b < 85;
92 }
93
94 public void apply(Config config) {
95 boolean isDark = this == LookAndFeel.DARCULA || isDarkLaf();
96 if (!isDark) {//Defaults found here: https://github.com/Sciss/SyntaxPane/blob/122da367ff7a5d31627a70c62a48a9f0f4f85a0a/src/main/resources/de/sciss/syntaxpane/defaultsyntaxkit/config.properties#L139
97 config.lineNumbersForeground = 0x333300;
98 config.lineNumbersBackground = 0xEEEEFF;
99 config.lineNumbersSelected = 0xCCCCEE;
100 config.obfuscatedColor = new AlphaColorEntry(0xFFDCDC, 1.0f);
101 config.obfuscatedColorOutline = new AlphaColorEntry(0xA05050, 1.0f);
102 config.proposedColor = new AlphaColorEntry(0x000000, 0.075f);
103 config.proposedColorOutline = new AlphaColorEntry(0x000000, 0.15f);
104 config.deobfuscatedColor = new AlphaColorEntry(0xDCFFDC, 1.0f);
105 config.deobfuscatedColorOutline = new AlphaColorEntry(0x50A050, 1.0f);
106 config.editorBackground = 0xFFFFFF;
107 config.highlightColor = 0x3333EE;
108 config.caretColor = 0x000000;
109 config.selectionHighlightColor = 0x000000;
110 config.stringColor = 0xCC6600;
111 config.numberColor = 0x999933;
112 config.operatorColor = 0x000000;
113 config.delimiterColor = 0x000000;
114 config.typeColor = 0x000000;
115 config.identifierColor = 0x000000;
116 config.defaultTextColor = 0x000000;
117 } else {//Based off colors found here: https://github.com/dracula/dracula-theme/
118 config.lineNumbersForeground = 0xA4A4A3;
119 config.lineNumbersBackground = 0x313335;
120 config.lineNumbersSelected = 0x606366;
121 config.obfuscatedColor = new AlphaColorEntry(0xFF5555, 0.3f);
122 config.obfuscatedColorOutline = new AlphaColorEntry(0xFF5555, 0.5f);
123 config.deobfuscatedColor = new AlphaColorEntry(0x50FA7B, 0.3f);
124 config.deobfuscatedColorOutline = new AlphaColorEntry(0x50FA7B, 0.5f);
125 config.proposedColor = new AlphaColorEntry(0x606366, 0.3f);
126 config.proposedColorOutline = new AlphaColorEntry(0x606366, 0.5f);
127 config.editorBackground = 0x282A36;
128 config.highlightColor = 0xFF79C6;
129 config.caretColor = 0xF8F8F2;
130 config.selectionHighlightColor = 0xF8F8F2;
131 config.stringColor = 0xF1FA8C;
132 config.numberColor = 0xBD93F9;
133 config.operatorColor = 0xF8F8F2;
134 config.delimiterColor = 0xF8F8F2;
135 config.typeColor = 0xF8F8F2;
136 config.identifierColor = 0xF8F8F2;
137 config.defaultTextColor = 0xF8F8F2;
138 }
139 }
140 }
141
142 public enum Decompiler {
143 PROCYON("Procyon", Decompilers.PROCYON),
144 CFR("CFR", Decompilers.CFR);
145
146 public final DecompilerService service;
147 public final String name;
148
149 Decompiler(String name, DecompilerService service) {
150 this.name = name;
151 this.service = service;
152 }
153 }
154
155 private static final File DIR_HOME = new File(System.getProperty("user.home"));
156 private static final File ENIGMA_DIR = new File(DIR_HOME, ".enigma");
157 private static final File CONFIG_FILE = new File(ENIGMA_DIR, "config.json");
158 private static final Config INSTANCE = new Config();
159
160 private final transient Gson gson; // transient to exclude it from being exposed
161
162 public AlphaColorEntry obfuscatedColor;
163 public AlphaColorEntry obfuscatedColorOutline;
164 public AlphaColorEntry proposedColor;
165 public AlphaColorEntry proposedColorOutline;
166 public AlphaColorEntry deobfuscatedColor;
167 public AlphaColorEntry deobfuscatedColorOutline;
168
169 public Integer editorBackground;
170 public Integer highlightColor;
171 public Integer caretColor;
172 public Integer selectionHighlightColor;
173
174 public Integer stringColor;
175 public Integer numberColor;
176 public Integer operatorColor;
177 public Integer delimiterColor;
178 public Integer typeColor;
179 public Integer identifierColor;
180 public Integer defaultTextColor;
181
182 public Integer lineNumbersBackground;
183 public Integer lineNumbersSelected;
184 public Integer lineNumbersForeground;
185
186 public String language = I18n.DEFAULT_LANGUAGE;
187
188 public LookAndFeel lookAndFeel = LookAndFeel.DEFAULT;
189
190 public float scaleFactor = 1.0f;
191
192 public Decompiler decompiler = Decompiler.PROCYON;
193
194 private Config() {
195 gson = new GsonBuilder()
196 .registerTypeAdapter(Integer.class, new IntSerializer())
197 .registerTypeAdapter(Integer.class, new IntDeserializer())
198 .registerTypeAdapter(Config.class, (InstanceCreator<Config>) type -> this)
199 .setPrettyPrinting()
200 .create();
201 try {
202 this.loadConfig();
203 } catch (IOException ignored) {
204 try {
205 this.reset();
206 } catch (IOException ignored1) {
207 }
208 }
209 }
210
211 public void loadConfig() throws IOException {
212 if (!ENIGMA_DIR.exists()) ENIGMA_DIR.mkdirs();
213 File configFile = new File(ENIGMA_DIR, "config.json");
214 boolean loaded = false;
215
216 if (configFile.exists()) {
217 try {
218 gson.fromJson(Files.asCharSource(configFile, Charset.defaultCharset()).read(), Config.class);
219 loaded = true;
220 } catch (Exception e) {
221 e.printStackTrace();
222 }
223 }
224
225 if (!loaded) {
226 this.reset();
227 Files.touch(configFile);
228 }
229 saveConfig();
230 }
231
232 public void saveConfig() throws IOException {
233 Files.asCharSink(CONFIG_FILE, Charset.defaultCharset()).write(gson.toJson(this));
234 }
235
236 public void reset() throws IOException {
237 this.lookAndFeel = LookAndFeel.DEFAULT;
238 this.lookAndFeel.apply(this);
239 this.decompiler = Decompiler.PROCYON;
240 this.language = I18n.DEFAULT_LANGUAGE;
241 this.saveConfig();
242 }
243
244 private static class IntSerializer implements JsonSerializer<Integer> {
245 @Override
246 public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) {
247 return new JsonPrimitive("#" + Integer.toHexString(src).toUpperCase());
248 }
249 }
250
251 private static class IntDeserializer implements JsonDeserializer<Integer> {
252 @Override
253 public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
254 return (int) Long.parseLong(json.getAsString().replace("#", ""), 16);
255 }
256 }
257
258 public static Config getInstance() {
259 return INSTANCE;
260 }
261}
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
new file mode 100644
index 00000000..035b2381
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Themes.java
@@ -0,0 +1,45 @@
1package cuchaz.enigma.gui.config;
2
3import java.io.IOException;
4
5import javax.swing.SwingUtilities;
6
7import com.google.common.collect.ImmutableMap;
8import cuchaz.enigma.gui.EnigmaSyntaxKit;
9import cuchaz.enigma.gui.Gui;
10import cuchaz.enigma.gui.highlight.BoxHighlightPainter;
11import cuchaz.enigma.gui.highlight.TokenHighlightType;
12import cuchaz.enigma.gui.util.ScaleUtil;
13import de.sciss.syntaxpane.DefaultSyntaxKit;
14
15public class Themes {
16
17 public static void setLookAndFeel(Gui gui, Config.LookAndFeel lookAndFeel) {
18 Config.getInstance().lookAndFeel = lookAndFeel;
19 updateTheme(gui);
20 }
21
22 public static void updateTheme(Gui gui) {
23 Config config = Config.getInstance();
24 config.lookAndFeel.setGlobalLAF();
25 config.lookAndFeel.apply(config);
26 try {
27 config.saveConfig();
28 } catch (IOException e) {
29 e.printStackTrace();
30 }
31 EnigmaSyntaxKit.invalidate();
32 DefaultSyntaxKit.initKit();
33 DefaultSyntaxKit.registerContentType("text/enigma-sources", EnigmaSyntaxKit.class.getName());
34 gui.boxHighlightPainters = ImmutableMap.of(
35 TokenHighlightType.OBFUSCATED, BoxHighlightPainter.create(config.obfuscatedColor, config.obfuscatedColorOutline),
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();
42 }
43
44
45}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java
new file mode 100644
index 00000000..fff755d3
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/AboutDialog.java
@@ -0,0 +1,70 @@
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.dialog;
13
14import cuchaz.enigma.Enigma;
15import cuchaz.enigma.gui.util.GuiUtil;
16import cuchaz.enigma.utils.I18n;
17import cuchaz.enigma.gui.util.ScaleUtil;
18import cuchaz.enigma.utils.Utils;
19
20import javax.swing.*;
21import java.awt.*;
22import java.io.IOException;
23
24public class AboutDialog {
25
26 public static void show(JFrame parent) {
27 // init frame
28 final JFrame frame = new JFrame(String.format(I18n.translate("menu.help.about.title"), Enigma.NAME));
29 final Container pane = frame.getContentPane();
30 pane.setLayout(new FlowLayout());
31
32 // load the content
33 try {
34 String html = Utils.readResourceToString("/about.html");
35 html = String.format(html, Enigma.NAME, Enigma.VERSION);
36 JLabel label = new JLabel(html);
37 label.setHorizontalAlignment(JLabel.CENTER);
38 pane.add(label);
39 } catch (IOException ex) {
40 throw new Error(ex);
41 }
42
43 // show the link
44 String html = "<html><a href=\"%s\">%s</a></html>";
45 html = String.format(html, Enigma.URL, Enigma.URL);
46 JButton link = new JButton(html);
47 link.addActionListener(event -> GuiUtil.openUrl(Enigma.URL));
48 link.setBorderPainted(false);
49 link.setOpaque(false);
50 link.setBackground(Color.WHITE);
51 link.setCursor(new Cursor(Cursor.HAND_CURSOR));
52 link.setFocusable(false);
53 JPanel linkPanel = new JPanel();
54 linkPanel.add(link);
55 pane.add(linkPanel);
56
57 // show ok button
58 JButton okButton = new JButton(I18n.translate("menu.help.about.ok"));
59 pane.add(okButton);
60 okButton.addActionListener(arg0 -> frame.dispose());
61
62 // show the frame
63 pane.doLayout();
64 frame.setSize(ScaleUtil.getDimension(400, 220));
65 frame.setResizable(false);
66 frame.setLocationRelativeTo(parent);
67 frame.setVisible(true);
68 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
69 }
70}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java
new file mode 100644
index 00000000..64219ab8
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ChangeDialog.java
@@ -0,0 +1,50 @@
1package cuchaz.enigma.gui.dialog;
2
3import java.awt.BorderLayout;
4import java.awt.event.KeyAdapter;
5import java.awt.event.KeyEvent;
6
7import javax.swing.JButton;
8import javax.swing.JFrame;
9import javax.swing.JLabel;
10import javax.swing.JPanel;
11
12import cuchaz.enigma.gui.Gui;
13import cuchaz.enigma.utils.I18n;
14
15public class ChangeDialog {
16
17 public static void show(Gui gui) {
18 // init frame
19 JFrame frame = new JFrame(I18n.translate("menu.view.change.title"));
20 JPanel textPanel = new JPanel();
21 JPanel buttonPanel = new JPanel();
22 frame.setLayout(new BorderLayout());
23 frame.add(BorderLayout.NORTH, textPanel);
24 frame.add(BorderLayout.SOUTH, buttonPanel);
25
26 // show text
27 JLabel text = new JLabel((I18n.translate("menu.view.change.summary")));
28 text.setHorizontalAlignment(JLabel.CENTER);
29 textPanel.add(text);
30
31 // show ok button
32 JButton okButton = new JButton(I18n.translate("menu.view.change.ok"));
33 buttonPanel.add(okButton);
34 okButton.addActionListener(event -> frame.dispose());
35 okButton.addKeyListener(new KeyAdapter() {
36 @Override
37 public void keyPressed(KeyEvent e) {
38 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
39 frame.dispose();
40 }
41 }
42 });
43
44 // show the frame
45 frame.pack();
46 frame.setVisible(true);
47 frame.setResizable(false);
48 frame.setLocationRelativeTo(gui.getFrame());
49 }
50}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java
new file mode 100644
index 00000000..c5f505cf
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java
@@ -0,0 +1,82 @@
1package cuchaz.enigma.gui.dialog;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.utils.I18n;
5
6import javax.swing.*;
7import java.awt.Frame;
8
9public class ConnectToServerDialog {
10
11 public static Result show(Frame parentComponent) {
12 JTextField usernameField = new JTextField(System.getProperty("user.name"), 20);
13 JPanel usernameRow = new JPanel();
14 usernameRow.add(new JLabel(I18n.translate("prompt.connect.username")));
15 usernameRow.add(usernameField);
16 JTextField ipField = new JTextField(20);
17 JPanel ipRow = new JPanel();
18 ipRow.add(new JLabel(I18n.translate("prompt.connect.ip")));
19 ipRow.add(ipField);
20 JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10);
21 JPanel portRow = new JPanel();
22 portRow.add(new JLabel(I18n.translate("prompt.port")));
23 portRow.add(portField);
24 JPasswordField passwordField = new JPasswordField(20);
25 JPanel passwordRow = new JPanel();
26 passwordRow.add(new JLabel(I18n.translate("prompt.password")));
27 passwordRow.add(passwordField);
28
29 int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{usernameRow, ipRow, portRow, passwordRow}, I18n.translate("prompt.connect.title"), JOptionPane.OK_CANCEL_OPTION);
30 if (response != JOptionPane.OK_OPTION) {
31 return null;
32 }
33
34 String username = usernameField.getText();
35 String ip = ipField.getText();
36 int port;
37 try {
38 port = Integer.parseInt(portField.getText());
39 } catch (NumberFormatException e) {
40 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE);
41 return null;
42 }
43 if (port < 0 || port >= 65536) {
44 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE);
45 return null;
46 }
47 char[] password = passwordField.getPassword();
48
49 return new Result(username, ip, port, password);
50 }
51
52 public static class Result {
53 private final String username;
54 private final String ip;
55 private final int port;
56 private final char[] password;
57
58 public Result(String username, String ip, int port, char[] password) {
59 this.username = username;
60 this.ip = ip;
61 this.port = port;
62 this.password = password;
63 }
64
65 public String getUsername() {
66 return username;
67 }
68
69 public String getIp() {
70 return ip;
71 }
72
73 public int getPort() {
74 return port;
75 }
76
77 public char[] getPassword() {
78 return password;
79 }
80 }
81
82}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java
new file mode 100644
index 00000000..c2a93fa5
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CrashDialog.java
@@ -0,0 +1,105 @@
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.dialog;
13
14import cuchaz.enigma.Enigma;
15import cuchaz.enigma.gui.util.GuiUtil;
16import cuchaz.enigma.utils.I18n;
17import cuchaz.enigma.gui.util.ScaleUtil;
18
19import javax.swing.*;
20import java.awt.*;
21import java.io.PrintWriter;
22import java.io.StringWriter;
23import java.io.FileWriter;
24import java.io.File;
25import java.io.IOException;
26
27public class CrashDialog {
28
29 private static CrashDialog instance = null;
30
31 private JFrame frame;
32 private JTextArea text;
33
34 private CrashDialog(JFrame parent) {
35 // init frame
36 frame = new JFrame(String.format(I18n.translate("crash.title"), Enigma.NAME));
37 final Container pane = frame.getContentPane();
38 pane.setLayout(new BorderLayout());
39
40 JLabel label = new JLabel(String.format(I18n.translate("crash.summary"), Enigma.NAME));
41 label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
42 pane.add(label, BorderLayout.NORTH);
43
44 // report panel
45 text = new JTextArea();
46 text.setTabSize(2);
47 pane.add(new JScrollPane(text), BorderLayout.CENTER);
48
49 // buttons panel
50 JPanel buttonsPanel = new JPanel();
51 buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.LINE_AXIS));
52 JButton exportButton = new JButton(I18n.translate("crash.export"));
53 exportButton.addActionListener(event -> {
54 JFileChooser chooser = new JFileChooser();
55 chooser.setSelectedFile(new File("enigma_crash.log"));
56 if (chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
57 try {
58 File file = chooser.getSelectedFile();
59 FileWriter writer = new FileWriter(file);
60 writer.write(instance.text.getText());
61 writer.close();
62 } catch (IOException ex) {
63 ex.printStackTrace();
64 }
65 }
66 });
67 buttonsPanel.add(exportButton);
68 buttonsPanel.add(Box.createHorizontalGlue());
69 buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("crash.exit.warning"))));
70 JButton ignoreButton = new JButton(I18n.translate("crash.ignore"));
71 ignoreButton.addActionListener(event -> {
72 // close (hide) the dialog
73 frame.setVisible(false);
74 });
75 buttonsPanel.add(ignoreButton);
76 JButton exitButton = new JButton(I18n.translate("crash.exit"));
77 exitButton.addActionListener(event -> {
78 // exit enigma
79 System.exit(1);
80 });
81 buttonsPanel.add(exitButton);
82 pane.add(buttonsPanel, BorderLayout.SOUTH);
83
84 // show the frame
85 frame.setSize(ScaleUtil.getDimension(600, 400));
86 frame.setLocationRelativeTo(parent);
87 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
88 }
89
90 public static void init(JFrame parent) {
91 instance = new CrashDialog(parent);
92 }
93
94 public static void show(Throwable ex) {
95 // get the error report
96 StringWriter buf = new StringWriter();
97 ex.printStackTrace(new PrintWriter(buf));
98 String report = buf.toString();
99
100 // show it!
101 instance.text.setText(report);
102 instance.frame.doLayout();
103 instance.frame.setVisible(true);
104 }
105}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java
new file mode 100644
index 00000000..eea1dff1
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java
@@ -0,0 +1,65 @@
1package cuchaz.enigma.gui.dialog;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.utils.I18n;
5
6import javax.swing.*;
7import java.awt.*;
8
9public class CreateServerDialog {
10
11 public static Result show(Frame parentComponent) {
12 JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10);
13 JPanel portRow = new JPanel();
14 portRow.add(new JLabel(I18n.translate("prompt.port")));
15 portRow.add(portField);
16 JPasswordField passwordField = new JPasswordField(20);
17 JPanel passwordRow = new JPanel();
18 passwordRow.add(new JLabel(I18n.translate("prompt.password")));
19 passwordRow.add(passwordField);
20
21 int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{portRow, passwordRow}, I18n.translate("prompt.create_server.title"), JOptionPane.OK_CANCEL_OPTION);
22 if (response != JOptionPane.OK_OPTION) {
23 return null;
24 }
25
26 int port;
27 try {
28 port = Integer.parseInt(portField.getText());
29 } catch (NumberFormatException e) {
30 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
31 return null;
32 }
33 if (port < 0 || port >= 65536) {
34 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
35 return null;
36 }
37
38 char[] password = passwordField.getPassword();
39 if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) {
40 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.password.too_long"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
41 return null;
42 }
43
44 return new Result(port, password);
45 }
46
47 public static class Result {
48 private final int port;
49 private final char[] password;
50
51 public Result(int port, char[] password) {
52 this.port = port;
53 this.password = password;
54 }
55
56 public int getPort() {
57 return port;
58 }
59
60 public char[] getPassword() {
61 return password;
62 }
63 }
64
65}
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
new file mode 100644
index 00000000..d81460ab
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/JavadocDialog.java
@@ -0,0 +1,159 @@
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.dialog;
13
14import cuchaz.enigma.gui.util.GuiUtil;
15import cuchaz.enigma.utils.I18n;
16import cuchaz.enigma.gui.util.ScaleUtil;
17
18import javax.swing.*;
19import javax.swing.text.html.HTML;
20
21import java.awt.*;
22import java.awt.event.KeyAdapter;
23import java.awt.event.KeyEvent;
24
25public class JavadocDialog {
26
27 private static JavadocDialog instance = null;
28
29 private JFrame frame;
30
31 private JavadocDialog(JFrame parent, JTextArea text, Callback callback) {
32 // init frame
33 frame = new JFrame(I18n.translate("javadocs.edit"));
34 final Container pane = frame.getContentPane();
35 pane.setLayout(new BorderLayout());
36
37 // editor panel
38 text.setTabSize(2);
39 pane.add(new JScrollPane(text), BorderLayout.CENTER);
40 text.addKeyListener(new KeyAdapter() {
41 @Override
42 public void keyPressed(KeyEvent event) {
43 switch (event.getKeyCode()) {
44 case KeyEvent.VK_ENTER:
45 if (event.isControlDown())
46 callback.closeUi(frame, true);
47 break;
48 case KeyEvent.VK_ESCAPE:
49 callback.closeUi(frame, false);
50 break;
51 default:
52 break;
53 }
54 }
55 });
56
57 // buttons panel
58 JPanel buttonsPanel = new JPanel();
59 FlowLayout buttonsLayout = new FlowLayout();
60 buttonsLayout.setAlignment(FlowLayout.RIGHT);
61 buttonsPanel.setLayout(buttonsLayout);
62 buttonsPanel.add(GuiUtil.unboldLabel(new JLabel(I18n.translate("javadocs.instruction"))));
63 JButton cancelButton = new JButton(I18n.translate("javadocs.cancel"));
64 cancelButton.addActionListener(event -> {
65 // close (hide) the dialog
66 callback.closeUi(frame, false);
67 });
68 buttonsPanel.add(cancelButton);
69 JButton saveButton = new JButton(I18n.translate("javadocs.save"));
70 saveButton.addActionListener(event -> {
71 // exit enigma
72 callback.closeUi(frame, true);
73 });
74 buttonsPanel.add(saveButton);
75 pane.add(buttonsPanel, BorderLayout.SOUTH);
76
77 // tags panel
78 JMenuBar tagsMenu = new JMenuBar();
79
80 // add javadoc tags
81 for (JavadocTag tag : JavadocTag.values()) {
82 JButton tagButton = new JButton(tag.getText());
83 tagButton.addActionListener(action -> {
84 boolean textSelected = text.getSelectedText() != null;
85 String tagText = tag.isInline() ? "{" + tag.getText() + " }" : tag.getText() + " ";
86
87 if (textSelected) {
88 if (tag.isInline()) {
89 tagText = "{" + tag.getText() + " " + text.getSelectedText() + "}";
90 } else {
91 tagText = tag.getText() + " " + text.getSelectedText();
92 }
93 text.replaceSelection(tagText);
94 } else {
95 text.insert(tagText, text.getCaretPosition());
96 }
97
98 if (tag.isInline()) {
99 text.setCaretPosition(text.getCaretPosition() - 1);
100 }
101 text.grabFocus();
102 });
103 tagsMenu.add(tagButton);
104 }
105
106 // add html tags
107 JComboBox<String> htmlList = new JComboBox<String>();
108 htmlList.setPreferredSize(new Dimension());
109 for (HTML.Tag htmlTag : HTML.getAllTags()) {
110 htmlList.addItem(htmlTag.toString());
111 }
112 htmlList.addActionListener(action -> {
113 String tagText = "<" + htmlList.getSelectedItem().toString() + ">";
114 text.insert(tagText, text.getCaretPosition());
115 text.grabFocus();
116 });
117 tagsMenu.add(htmlList);
118
119 pane.add(tagsMenu, BorderLayout.NORTH);
120
121 // show the frame
122 frame.setSize(ScaleUtil.getDimension(600, 400));
123 frame.setLocationRelativeTo(parent);
124 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
125 }
126
127 public static void init(JFrame parent, JTextArea area, Callback callback) {
128 instance = new JavadocDialog(parent, area, callback);
129 instance.frame.doLayout();
130 instance.frame.setVisible(true);
131 }
132
133 public interface Callback {
134 void closeUi(JFrame frame, boolean save);
135 }
136
137 private enum JavadocTag {
138 CODE(true),
139 LINK(true),
140 LINKPLAIN(true),
141 RETURN(false),
142 SEE(false),
143 THROWS(false);
144
145 private boolean inline;
146
147 private JavadocTag(boolean inline) {
148 this.inline = inline;
149 }
150
151 public String getText() {
152 return "@" + this.name().toLowerCase();
153 }
154
155 public boolean isInline() {
156 return this.inline;
157 }
158 }
159}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java
new file mode 100644
index 00000000..fa40af75
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/ProgressDialog.java
@@ -0,0 +1,109 @@
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.dialog;
13
14import cuchaz.enigma.Enigma;
15import cuchaz.enigma.ProgressListener;
16import cuchaz.enigma.gui.util.GuiUtil;
17import cuchaz.enigma.utils.I18n;
18import cuchaz.enigma.gui.util.ScaleUtil;
19
20import javax.swing.*;
21import java.awt.*;
22import java.util.concurrent.CompletableFuture;
23
24public class ProgressDialog implements ProgressListener, AutoCloseable {
25
26 private JFrame frame;
27 private JLabel labelTitle;
28 private JLabel labelText;
29 private JProgressBar progress;
30
31 public ProgressDialog(JFrame parent) {
32
33 // init frame
34 this.frame = new JFrame(String.format(I18n.translate("progress.operation"), Enigma.NAME));
35 final Container pane = this.frame.getContentPane();
36 FlowLayout layout = new FlowLayout();
37 layout.setAlignment(FlowLayout.LEFT);
38 pane.setLayout(layout);
39
40 this.labelTitle = new JLabel();
41 pane.add(this.labelTitle);
42
43 // set up the progress bar
44 JPanel panel = new JPanel();
45 pane.add(panel);
46 panel.setLayout(new BorderLayout());
47 this.labelText = GuiUtil.unboldLabel(new JLabel());
48 this.progress = new JProgressBar();
49 this.labelText.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
50 panel.add(this.labelText, BorderLayout.NORTH);
51 panel.add(this.progress, BorderLayout.CENTER);
52 panel.setPreferredSize(ScaleUtil.getDimension(360, 50));
53
54 // show the frame
55 pane.doLayout();
56 this.frame.setSize(ScaleUtil.getDimension(400, 120));
57 this.frame.setResizable(false);
58 this.frame.setLocationRelativeTo(parent);
59 this.frame.setVisible(true);
60 this.frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
61 }
62
63 public static CompletableFuture<Void> runOffThread(final JFrame parent, final ProgressRunnable runnable) {
64 CompletableFuture<Void> future = new CompletableFuture<>();
65 new Thread(() ->
66 {
67 try (ProgressDialog progress = new ProgressDialog(parent)) {
68 runnable.run(progress);
69 future.complete(null);
70 } catch (Exception ex) {
71 future.completeExceptionally(ex);
72 throw new Error(ex);
73 }
74 }).start();
75 return future;
76 }
77
78 @Override
79 public void close() {
80 this.frame.dispose();
81 }
82
83 @Override
84 public void init(int totalWork, String title) {
85 this.labelTitle.setText(title);
86 this.progress.setMinimum(0);
87 this.progress.setMaximum(totalWork);
88 this.progress.setValue(0);
89 }
90
91 @Override
92 public void step(int numDone, String message) {
93 this.labelText.setText(message);
94 if (numDone != -1) {
95 this.progress.setValue(numDone);
96 this.progress.setIndeterminate(false);
97 } else {
98 this.progress.setIndeterminate(true);
99 }
100
101 // update the frame
102 this.frame.validate();
103 this.frame.repaint();
104 }
105
106 public interface ProgressRunnable {
107 void run(ProgressListener listener) throws Exception;
108 }
109}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java
new file mode 100644
index 00000000..2d396c36
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java
@@ -0,0 +1,261 @@
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.dialog;
13
14import java.awt.BorderLayout;
15import java.awt.Color;
16import java.awt.FlowLayout;
17import java.awt.Font;
18import java.awt.event.*;
19import java.util.Arrays;
20import java.util.Collections;
21import java.util.List;
22
23import javax.swing.*;
24import javax.swing.event.DocumentEvent;
25import javax.swing.event.DocumentListener;
26
27import cuchaz.enigma.gui.Gui;
28import cuchaz.enigma.gui.GuiController;
29import cuchaz.enigma.gui.util.AbstractListCellRenderer;
30import cuchaz.enigma.gui.util.ScaleUtil;
31import cuchaz.enigma.translation.representation.entry.ClassEntry;
32import cuchaz.enigma.utils.I18n;
33import cuchaz.enigma.gui.search.SearchEntry;
34import cuchaz.enigma.gui.search.SearchUtil;
35
36public class SearchDialog {
37
38 private final JTextField searchField;
39 private DefaultListModel<SearchEntryImpl> classListModel;
40 private final JList<SearchEntryImpl> classList;
41 private final JDialog dialog;
42
43 private final Gui parent;
44 private final SearchUtil<SearchEntryImpl> su;
45 private SearchUtil.SearchControl currentSearch;
46
47 public SearchDialog(Gui parent) {
48 this.parent = parent;
49
50 su = new SearchUtil<>();
51
52 dialog = new JDialog(parent.getFrame(), I18n.translate("menu.view.search"), true);
53 JPanel contentPane = new JPanel();
54 contentPane.setBorder(ScaleUtil.createEmptyBorder(4, 4, 4, 4));
55 contentPane.setLayout(new BorderLayout(ScaleUtil.scale(4), ScaleUtil.scale(4)));
56
57 searchField = new JTextField();
58 searchField.getDocument().addDocumentListener(new DocumentListener() {
59
60 @Override
61 public void insertUpdate(DocumentEvent e) {
62 updateList();
63 }
64
65 @Override
66 public void removeUpdate(DocumentEvent e) {
67 updateList();
68 }
69
70 @Override
71 public void changedUpdate(DocumentEvent e) {
72 updateList();
73 }
74
75 });
76 searchField.addKeyListener(new KeyAdapter() {
77 @Override
78 public void keyPressed(KeyEvent e) {
79 if (e.getKeyCode() == KeyEvent.VK_DOWN) {
80 int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1;
81 classList.setSelectedIndex(next);
82 classList.ensureIndexIsVisible(next);
83 } else if (e.getKeyCode() == KeyEvent.VK_UP) {
84 int prev = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1;
85 classList.setSelectedIndex(prev);
86 classList.ensureIndexIsVisible(prev);
87 } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
88 close();
89 }
90 }
91 });
92 searchField.addActionListener(e -> openSelected());
93 contentPane.add(searchField, BorderLayout.NORTH);
94
95 classListModel = new DefaultListModel<>();
96 classList = new JList<>();
97 classList.setModel(classListModel);
98 classList.setCellRenderer(new ListCellRendererImpl());
99 classList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
100 classList.addMouseListener(new MouseAdapter() {
101 @Override
102 public void mouseClicked(MouseEvent mouseEvent) {
103 if (mouseEvent.getClickCount() >= 2) {
104 int idx = classList.locationToIndex(mouseEvent.getPoint());
105 SearchEntryImpl entry = classList.getModel().getElementAt(idx);
106 openEntry(entry);
107 }
108 }
109 });
110 contentPane.add(new JScrollPane(classList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
111
112 JPanel buttonBar = new JPanel();
113 buttonBar.setLayout(new FlowLayout(FlowLayout.RIGHT));
114 JButton open = new JButton(I18n.translate("prompt.open"));
115 open.addActionListener(event -> openSelected());
116 buttonBar.add(open);
117 JButton cancel = new JButton(I18n.translate("prompt.cancel"));
118 cancel.addActionListener(event -> close());
119 buttonBar.add(cancel);
120 contentPane.add(buttonBar, BorderLayout.SOUTH);
121
122 // apparently the class list doesn't update by itself when the list
123 // state changes and the dialog is hidden
124 dialog.addComponentListener(new ComponentAdapter() {
125 @Override
126 public void componentShown(ComponentEvent e) {
127 classList.updateUI();
128 }
129 });
130
131 dialog.setContentPane(contentPane);
132 dialog.setSize(ScaleUtil.getDimension(400, 500));
133 dialog.setLocationRelativeTo(parent.getFrame());
134 }
135
136 public void show() {
137 su.clear();
138 parent.getController().project.getJarIndex().getEntryIndex().getClasses().parallelStream()
139 .filter(e -> !e.isInnerClass())
140 .map(e -> SearchEntryImpl.from(e, parent.getController()))
141 .map(SearchUtil.Entry::from)
142 .sequential()
143 .forEach(su::add);
144
145 updateList();
146
147 searchField.requestFocus();
148 searchField.selectAll();
149
150 dialog.setVisible(true);
151 }
152
153 private void openSelected() {
154 SearchEntryImpl selectedValue = classList.getSelectedValue();
155 if (selectedValue != null) {
156 openEntry(selectedValue);
157 }
158 }
159
160 private void openEntry(SearchEntryImpl e) {
161 close();
162 su.hit(e);
163 parent.getController().navigateTo(e.obf);
164 if (e.deobf != null) {
165 parent.getDeobfPanel().deobfClasses.setSelectionClass(e.deobf);
166 } else {
167 parent.getObfPanel().obfClasses.setSelectionClass(e.obf);
168 }
169 }
170
171 private void close() {
172 dialog.setVisible(false);
173 }
174
175 // Updates the list of class names
176 private void updateList() {
177 if (currentSearch != null) currentSearch.stop();
178
179 DefaultListModel<SearchEntryImpl> classListModel = new DefaultListModel<>();
180 this.classListModel = classListModel;
181 classList.setModel(classListModel);
182
183 currentSearch = su.asyncSearch(searchField.getText(), (idx, e) -> SwingUtilities.invokeLater(() -> classListModel.insertElementAt(e, idx)));
184 }
185
186 public void dispose() {
187 dialog.dispose();
188 }
189
190 private static final class SearchEntryImpl implements SearchEntry {
191
192 public final ClassEntry obf;
193 public final ClassEntry deobf;
194
195 private SearchEntryImpl(ClassEntry obf, ClassEntry deobf) {
196 this.obf = obf;
197 this.deobf = deobf;
198 }
199
200 @Override
201 public List<String> getSearchableNames() {
202 if (deobf != null) {
203 return Arrays.asList(obf.getSimpleName(), deobf.getSimpleName());
204 } else {
205 return Collections.singletonList(obf.getSimpleName());
206 }
207 }
208
209 @Override
210 public String getIdentifier() {
211 return obf.getFullName();
212 }
213
214 @Override
215 public String toString() {
216 return String.format("SearchEntryImpl { obf: %s, deobf: %s }", obf, deobf);
217 }
218
219 public static SearchEntryImpl from(ClassEntry e, GuiController controller) {
220 ClassEntry deobf = controller.project.getMapper().deobfuscate(e);
221 if (deobf.equals(e)) deobf = null;
222 return new SearchEntryImpl(e, deobf);
223 }
224
225 }
226
227 private static final class ListCellRendererImpl extends AbstractListCellRenderer<SearchEntryImpl> {
228
229 private final JLabel mainName;
230 private final JLabel secondaryName;
231
232 public ListCellRendererImpl() {
233 this.setLayout(new BorderLayout());
234
235 mainName = new JLabel();
236 this.add(mainName, BorderLayout.WEST);
237
238 secondaryName = new JLabel();
239 secondaryName.setFont(secondaryName.getFont().deriveFont(Font.ITALIC));
240 secondaryName.setForeground(Color.GRAY);
241 this.add(secondaryName, BorderLayout.EAST);
242 }
243
244 @Override
245 public void updateUiForEntry(JList<? extends SearchEntryImpl> list, SearchEntryImpl value, int index, boolean isSelected, boolean cellHasFocus) {
246 if (value.deobf == null) {
247 mainName.setText(value.obf.getSimpleName());
248 mainName.setToolTipText(value.obf.getFullName());
249 secondaryName.setText("");
250 secondaryName.setToolTipText("");
251 } else {
252 mainName.setText(value.deobf.getSimpleName());
253 mainName.setToolTipText(value.deobf.getFullName());
254 secondaryName.setText(value.obf.getSimpleName());
255 secondaryName.setToolTipText(value.obf.getFullName());
256 }
257 }
258
259 }
260
261}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java
new file mode 100644
index 00000000..868eba79
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/dialog/StatsDialog.java
@@ -0,0 +1,82 @@
1package cuchaz.enigma.gui.dialog;
2
3import java.awt.BorderLayout;
4import java.util.Arrays;
5import java.util.Locale;
6import java.util.Map;
7import java.util.Set;
8import java.util.stream.Collectors;
9
10import javax.swing.JButton;
11import javax.swing.JCheckBox;
12import javax.swing.JFrame;
13import javax.swing.JPanel;
14
15import cuchaz.enigma.gui.Gui;
16import cuchaz.enigma.gui.stats.StatsMember;
17import cuchaz.enigma.gui.util.ScaleUtil;
18import cuchaz.enigma.utils.I18n;
19
20public class StatsDialog {
21
22 public static void show(Gui gui) {
23 // init frame
24 JFrame frame = new JFrame(I18n.translate("menu.file.stats.title"));
25 JPanel checkboxesPanel = new JPanel();
26 JPanel buttonPanel = new JPanel();
27 frame.setLayout(new BorderLayout());
28 frame.add(BorderLayout.NORTH, checkboxesPanel);
29 frame.add(BorderLayout.SOUTH, buttonPanel);
30
31 // show checkboxes
32 Map<StatsMember, JCheckBox> checkboxes = Arrays
33 .stream(StatsMember.values())
34 .collect(Collectors.toMap(m -> m, m -> {
35 JCheckBox checkbox = new JCheckBox(I18n.translate("type." + m.name().toLowerCase(Locale.ROOT)));
36 checkboxesPanel.add(checkbox);
37 return checkbox;
38 }));
39
40 // show generate button
41 JButton button = new JButton(I18n.translate("menu.file.stats.generate"));
42 buttonPanel.add(button);
43 button.setEnabled(false);
44 button.addActionListener(action -> {
45 frame.dispose();
46 generateStats(gui, checkboxes);
47 });
48
49 // add action listener to each checkbox
50 checkboxes.entrySet().forEach(checkbox -> {
51 checkbox.getValue().addActionListener(action -> {
52 if (!button.isEnabled()) {
53 button.setEnabled(true);
54 } else if (checkboxes.entrySet().stream().allMatch(entry -> !entry.getValue().isSelected())) {
55 button.setEnabled(false);
56 }
57 });
58 });
59
60 // show the frame
61 frame.pack();
62 frame.setVisible(true);
63 frame.setSize(ScaleUtil.getDimension(500, 120));
64 frame.setResizable(false);
65 frame.setLocationRelativeTo(gui.getFrame());
66 }
67
68 private static void generateStats(Gui gui, Map<StatsMember, JCheckBox> checkboxes) {
69 // get members from selected checkboxes
70 Set<StatsMember> includedMembers = checkboxes
71 .entrySet()
72 .stream()
73 .filter(entry -> entry.getValue().isSelected())
74 .map(Map.Entry::getKey)
75 .collect(Collectors.toSet());
76
77 // checks if a projet is open
78 if (gui.getController().project != null) {
79 gui.getController().openStats(includedMembers);
80 }
81 }
82}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java
new file mode 100644
index 00000000..fb497b11
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.event.MouseEvent;
4
5import javax.swing.JTabbedPane;
6
7public class CollapsibleTabbedPane extends JTabbedPane {
8
9 public CollapsibleTabbedPane() {
10 }
11
12 public CollapsibleTabbedPane(int tabPlacement) {
13 super(tabPlacement);
14 }
15
16 public CollapsibleTabbedPane(int tabPlacement, int tabLayoutPolicy) {
17 super(tabPlacement, tabLayoutPolicy);
18 }
19
20 @Override
21 protected void processMouseEvent(MouseEvent e) {
22 int id = e.getID();
23 if (id == MouseEvent.MOUSE_PRESSED) {
24 if (!isEnabled()) return;
25 int tabIndex = getUI().tabForCoordinate(this, e.getX(), e.getY());
26 if (tabIndex >= 0 && isEnabledAt(tabIndex)) {
27 if (tabIndex == getSelectedIndex()) {
28 if (isFocusOwner() && isRequestFocusEnabled()) {
29 requestFocus();
30 } else {
31 setSelectedIndex(-1);
32 }
33 return;
34 }
35 }
36 }
37 super.processMouseEvent(e);
38 }
39
40}
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
new file mode 100644
index 00000000..24f42ff0
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java
@@ -0,0 +1,386 @@
1package cuchaz.enigma.gui.elements;
2
3import cuchaz.enigma.gui.config.Config;
4import cuchaz.enigma.gui.config.Themes;
5import cuchaz.enigma.gui.Gui;
6import cuchaz.enigma.gui.dialog.AboutDialog;
7import cuchaz.enigma.gui.dialog.ChangeDialog;
8import cuchaz.enigma.gui.dialog.ConnectToServerDialog;
9import cuchaz.enigma.gui.dialog.CreateServerDialog;
10import cuchaz.enigma.gui.dialog.StatsDialog;
11import cuchaz.enigma.gui.util.ScaleUtil;
12import cuchaz.enigma.translation.mapping.serde.MappingFormat;
13import cuchaz.enigma.utils.I18n;
14import cuchaz.enigma.utils.Pair;
15
16import java.awt.Desktop;
17import java.awt.event.InputEvent;
18import java.awt.event.KeyEvent;
19import java.io.File;
20import java.io.IOException;
21import java.net.URISyntaxException;
22import java.net.URL;
23import java.nio.file.Files;
24import java.nio.file.Path;
25import java.nio.file.Paths;
26import java.util.*;
27import java.util.List;
28import java.util.stream.Collectors;
29import java.util.stream.IntStream;
30import javax.swing.*;
31
32public class MenuBar extends JMenuBar {
33
34 public final JMenuItem closeJarMenu;
35 public final List<JMenuItem> openMappingsMenus;
36 public final JMenuItem saveMappingsMenu;
37 public final List<JMenuItem> saveMappingsMenus;
38 public final JMenuItem closeMappingsMenu;
39 public final JMenuItem dropMappingsMenu;
40 public final JMenuItem exportSourceMenu;
41 public final JMenuItem exportJarMenu;
42 public final JMenuItem connectToServerMenu;
43 public final JMenuItem startServerMenu;
44 private final Gui gui;
45
46 public MenuBar(Gui gui) {
47 this.gui = gui;
48
49 /*
50 * File menu
51 */
52 {
53 JMenu menu = new JMenu(I18n.translate("menu.file"));
54 this.add(menu);
55 {
56 JMenuItem item = new JMenuItem(I18n.translate("menu.file.jar.open"));
57 menu.add(item);
58 item.addActionListener(event -> {
59 this.gui.jarFileChooser.setVisible(true);
60 String file = this.gui.jarFileChooser.getFile();
61 // checks if the file name is not empty
62 if (file != null) {
63 Path path = Paths.get(this.gui.jarFileChooser.getDirectory()).resolve(file);
64 // checks if the file name corresponds to an existing file
65 if (Files.exists(path)) {
66 gui.getController().openJar(path);
67 }
68 }
69 });
70 }
71 {
72 JMenuItem item = new JMenuItem(I18n.translate("menu.file.jar.close"));
73 menu.add(item);
74 item.addActionListener(event -> this.gui.getController().closeJar());
75 this.closeJarMenu = item;
76 }
77 menu.addSeparator();
78 JMenu openMenu = new JMenu(I18n.translate("menu.file.mappings.open"));
79 menu.add(openMenu);
80 {
81 openMappingsMenus = new ArrayList<>();
82 for (MappingFormat format : MappingFormat.values()) {
83 if (format.getReader() != null) {
84 JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT)));
85 openMenu.add(item);
86 item.addActionListener(event -> {
87 if (this.gui.enigmaMappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) {
88 File selectedFile = this.gui.enigmaMappingsFileChooser.getSelectedFile();
89 this.gui.getController().openMappings(format, selectedFile.toPath());
90 }
91 });
92 openMappingsMenus.add(item);
93 }
94 }
95 }
96 {
97 JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.save"));
98 menu.add(item);
99 item.addActionListener(event -> {
100 this.gui.getController().saveMappings(this.gui.enigmaMappingsFileChooser.getSelectedFile().toPath());
101 });
102 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));
103 this.saveMappingsMenu = item;
104 }
105 JMenu saveMenu = new JMenu(I18n.translate("menu.file.mappings.save_as"));
106 menu.add(saveMenu);
107 {
108 saveMappingsMenus = new ArrayList<>();
109 for (MappingFormat format : MappingFormat.values()) {
110 if (format.getWriter() != null) {
111 JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT)));
112 saveMenu.add(item);
113 item.addActionListener(event -> {
114 // TODO: Use a specific file chooser for it
115 if (this.gui.enigmaMappingsFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) {
116 this.gui.getController().saveMappings(this.gui.enigmaMappingsFileChooser.getSelectedFile().toPath(), format);
117 this.saveMappingsMenu.setEnabled(true);
118 }
119 });
120 saveMappingsMenus.add(item);
121 }
122 }
123 }
124 {
125 JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.close"));
126 menu.add(item);
127 item.addActionListener(event -> {
128 if (this.gui.getController().isDirty()) {
129 this.gui.showDiscardDiag((response -> {
130 if (response == JOptionPane.YES_OPTION) {
131 gui.saveMapping();
132 this.gui.getController().closeMappings();
133 } else if (response == JOptionPane.NO_OPTION)
134 this.gui.getController().closeMappings();
135 return null;
136 }), I18n.translate("prompt.close.save"), I18n.translate("prompt.close.discard"), I18n.translate("prompt.close.cancel"));
137 } else
138 this.gui.getController().closeMappings();
139
140 });
141 this.closeMappingsMenu = item;
142 }
143 {
144 JMenuItem item = new JMenuItem(I18n.translate("menu.file.mappings.drop"));
145 menu.add(item);
146 item.addActionListener(event -> this.gui.getController().dropMappings());
147 this.dropMappingsMenu = item;
148 }
149 menu.addSeparator();
150 {
151 JMenuItem item = new JMenuItem(I18n.translate("menu.file.export.source"));
152 menu.add(item);
153 item.addActionListener(event -> {
154 if (this.gui.exportSourceFileChooser.showSaveDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) {
155 this.gui.getController().exportSource(this.gui.exportSourceFileChooser.getSelectedFile().toPath());
156 }
157 });
158 this.exportSourceMenu = item;
159 }
160 {
161 JMenuItem item = new JMenuItem(I18n.translate("menu.file.export.jar"));
162 menu.add(item);
163 item.addActionListener(event -> {
164 this.gui.exportJarFileChooser.setVisible(true);
165 if (this.gui.exportJarFileChooser.getFile() != null) {
166 Path path = Paths.get(this.gui.exportJarFileChooser.getDirectory(), this.gui.exportJarFileChooser.getFile());
167 this.gui.getController().exportJar(path);
168 }
169 });
170 this.exportJarMenu = item;
171 }
172 menu.addSeparator();
173 {
174 JMenuItem stats = new JMenuItem(I18n.translate("menu.file.stats"));
175 menu.add(stats);
176 stats.addActionListener(event -> StatsDialog.show(this.gui));
177 }
178 menu.addSeparator();
179 {
180 JMenuItem item = new JMenuItem(I18n.translate("menu.file.exit"));
181 menu.add(item);
182 item.addActionListener(event -> this.gui.close());
183 }
184 }
185
186 /*
187 * Decompiler menu
188 */
189 {
190 JMenu menu = new JMenu(I18n.translate("menu.decompiler"));
191 this.add(menu);
192
193 ButtonGroup decompilerGroup = new ButtonGroup();
194
195 for (Config.Decompiler decompiler : Config.Decompiler.values()) {
196 JRadioButtonMenuItem decompilerButton = new JRadioButtonMenuItem(decompiler.name);
197 decompilerGroup.add(decompilerButton);
198 if (decompiler.equals(Config.getInstance().decompiler)) {
199 decompilerButton.setSelected(true);
200 }
201 menu.add(decompilerButton);
202 decompilerButton.addActionListener(event -> {
203 gui.getController().setDecompiler(decompiler.service);
204
205 try {
206 Config.getInstance().decompiler = decompiler;
207 Config.getInstance().saveConfig();
208 } catch (IOException e) {
209 throw new RuntimeException(e);
210 }
211 });
212 }
213 }
214
215 /*
216 * View menu
217 */
218 {
219 JMenu menu = new JMenu(I18n.translate("menu.view"));
220 this.add(menu);
221 {
222 JMenu themes = new JMenu(I18n.translate("menu.view.themes"));
223 menu.add(themes);
224 ButtonGroup themeGroup = new ButtonGroup();
225 for (Config.LookAndFeel lookAndFeel : Config.LookAndFeel.values()) {
226 JRadioButtonMenuItem themeButton = new JRadioButtonMenuItem(I18n.translate("menu.view.themes." + lookAndFeel.name().toLowerCase(Locale.ROOT)));
227 themeGroup.add(themeButton);
228 if (lookAndFeel.equals(Config.getInstance().lookAndFeel)) {
229 themeButton.setSelected(true);
230 }
231 themes.add(themeButton);
232 themeButton.addActionListener(event -> Themes.setLookAndFeel(gui, lookAndFeel));
233 }
234 }
235 {
236 JMenu languages = new JMenu(I18n.translate("menu.view.languages"));
237 menu.add(languages);
238 ButtonGroup languageGroup = new ButtonGroup();
239 for (String lang : I18n.getAvailableLanguages()) {
240 JRadioButtonMenuItem languageButton = new JRadioButtonMenuItem(I18n.getLanguageName(lang));
241 languageGroup.add(languageButton);
242 if (lang.equals(Config.getInstance().language)) {
243 languageButton.setSelected(true);
244 }
245 languages.add(languageButton);
246 languageButton.addActionListener(event -> {
247 I18n.setLanguage(lang);
248 ChangeDialog.show(this.gui);
249 });
250 }
251 }
252 {
253 JMenu scale = new JMenu(I18n.translate("menu.view.scale"));
254 {
255 ButtonGroup scaleGroup = new ButtonGroup();
256 Map<Float, JRadioButtonMenuItem> map = IntStream.of(100, 125, 150, 175, 200)
257 .mapToObj(scaleFactor -> {
258 float realScaleFactor = scaleFactor / 100f;
259 JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(String.format("%d%%", scaleFactor));
260 menuItem.addActionListener(event -> ScaleUtil.setScaleFactor(realScaleFactor));
261 menuItem.addActionListener(event -> ChangeDialog.show(this.gui));
262 scaleGroup.add(menuItem);
263 scale.add(menuItem);
264 return new Pair<>(realScaleFactor, menuItem);
265 })
266 .collect(Collectors.toMap(x -> x.a, x -> x.b));
267
268 JMenuItem customScale = new JMenuItem(I18n.translate("menu.view.scale.custom"));
269 customScale.addActionListener(event -> {
270 String answer = (String) JOptionPane.showInputDialog(gui.getFrame(), I18n.translate("menu.view.scale.custom.title"), I18n.translate("menu.view.scale.custom.title"),
271 JOptionPane.QUESTION_MESSAGE, null, null, Float.toString(ScaleUtil.getScaleFactor() * 100));
272 if (answer == null) return;
273 float newScale = 1.0f;
274 try {
275 newScale = Float.parseFloat(answer) / 100f;
276 } catch (NumberFormatException ignored) {
277 }
278 ScaleUtil.setScaleFactor(newScale);
279 ChangeDialog.show(this.gui);
280 });
281 scale.add(customScale);
282 ScaleUtil.addListener((newScale, _oldScale) -> {
283 JRadioButtonMenuItem mi = map.get(newScale);
284 if (mi != null) {
285 mi.setSelected(true);
286 } else {
287 scaleGroup.clearSelection();
288 }
289 });
290 JRadioButtonMenuItem mi = map.get(ScaleUtil.getScaleFactor());
291 if (mi != null) {
292 mi.setSelected(true);
293 }
294 }
295 menu.add(scale);
296 }
297 menu.addSeparator();
298 {
299 JMenuItem search = new JMenuItem(I18n.translate("menu.view.search"));
300 search.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.SHIFT_MASK));
301 menu.add(search);
302 search.addActionListener(event -> {
303 if (this.gui.getController().project != null) {
304 this.gui.getSearchDialog().show();
305 }
306 });
307 }
308 }
309
310 /*
311 * Collab menu
312 */
313 {
314 JMenu menu = new JMenu(I18n.translate("menu.collab"));
315 this.add(menu);
316 {
317 JMenuItem item = new JMenuItem(I18n.translate("menu.collab.connect"));
318 menu.add(item);
319 item.addActionListener(event -> {
320 if (this.gui.getController().getClient() != null) {
321 this.gui.getController().disconnectIfConnected(null);
322 return;
323 }
324 ConnectToServerDialog.Result result = ConnectToServerDialog.show(this.gui.getFrame());
325 if (result == null) {
326 return;
327 }
328 this.gui.getController().disconnectIfConnected(null);
329 try {
330 this.gui.getController().createClient(result.getUsername(), result.getIp(), result.getPort(), result.getPassword());
331 } catch (IOException e) {
332 JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.connect.error"), JOptionPane.ERROR_MESSAGE);
333 this.gui.getController().disconnectIfConnected(null);
334 }
335 Arrays.fill(result.getPassword(), (char)0);
336 });
337 this.connectToServerMenu = item;
338 }
339 {
340 JMenuItem item = new JMenuItem(I18n.translate("menu.collab.server.start"));
341 menu.add(item);
342 item.addActionListener(event -> {
343 if (this.gui.getController().getServer() != null) {
344 this.gui.getController().disconnectIfConnected(null);
345 return;
346 }
347 CreateServerDialog.Result result = CreateServerDialog.show(this.gui.getFrame());
348 if (result == null) {
349 return;
350 }
351 this.gui.getController().disconnectIfConnected(null);
352 try {
353 this.gui.getController().createServer(result.getPort(), result.getPassword());
354 } catch (IOException e) {
355 JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.server.start.error"), JOptionPane.ERROR_MESSAGE);
356 this.gui.getController().disconnectIfConnected(null);
357 }
358 });
359 this.startServerMenu = item;
360 }
361 }
362
363 /*
364 * Help menu
365 */
366 {
367 JMenu menu = new JMenu(I18n.translate("menu.help"));
368 this.add(menu);
369 {
370 JMenuItem item = new JMenuItem(I18n.translate("menu.help.about"));
371 menu.add(item);
372 item.addActionListener(event -> AboutDialog.show(this.gui.getFrame()));
373 }
374 {
375 JMenuItem item = new JMenuItem(I18n.translate("menu.help.github"));
376 menu.add(item);
377 item.addActionListener(event -> {
378 try {
379 Desktop.getDesktop().browse(new URL("https://github.com/FabricMC/Enigma").toURI());
380 } catch (URISyntaxException | IOException ignored) {
381 }
382 });
383 }
384 }
385 }
386}
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
new file mode 100644
index 00000000..b92041c3
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/elements/PopupMenuBar.java
@@ -0,0 +1,125 @@
1package cuchaz.enigma.gui.elements;
2
3import cuchaz.enigma.gui.Gui;
4import cuchaz.enigma.utils.I18n;
5
6import javax.swing.*;
7import java.awt.event.InputEvent;
8import java.awt.event.KeyEvent;
9
10public class PopupMenuBar extends JPopupMenu {
11
12 public final JMenuItem renameMenu;
13 public final JMenuItem editJavadocMenu;
14 public final JMenuItem showInheritanceMenu;
15 public final JMenuItem showImplementationsMenu;
16 public final JMenuItem showCallsMenu;
17 public final JMenuItem showCallsSpecificMenu;
18 public final JMenuItem openEntryMenu;
19 public final JMenuItem openPreviousMenu;
20 public final JMenuItem openNextMenu;
21 public final JMenuItem toggleMappingMenu;
22
23 public PopupMenuBar(Gui gui) {
24 {
25 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.rename"));
26 menu.addActionListener(event -> gui.startRename());
27 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK));
28 menu.setEnabled(false);
29 this.add(menu);
30 this.renameMenu = menu;
31 }
32 {
33 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.javadoc"));
34 menu.addActionListener(event -> gui.startDocChange());
35 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK));
36 menu.setEnabled(false);
37 this.add(menu);
38 this.editJavadocMenu = menu;
39 }
40 {
41 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.inheritance"));
42 menu.addActionListener(event -> gui.showInheritance());
43 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK));
44 menu.setEnabled(false);
45 this.add(menu);
46 this.showInheritanceMenu = menu;
47 }
48 {
49 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.implementations"));
50 menu.addActionListener(event -> gui.showImplementations());
51 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK));
52 menu.setEnabled(false);
53 this.add(menu);
54 this.showImplementationsMenu = menu;
55 }
56 {
57 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls"));
58 menu.addActionListener(event -> gui.showCalls(true));
59 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
60 menu.setEnabled(false);
61 this.add(menu);
62 this.showCallsMenu = menu;
63 }
64 {
65 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.calls.specific"));
66 menu.addActionListener(event -> gui.showCalls(false));
67 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK + InputEvent.SHIFT_DOWN_MASK));
68 menu.setEnabled(false);
69 this.add(menu);
70 this.showCallsSpecificMenu = menu;
71 }
72 {
73 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.declaration"));
74 menu.addActionListener(event -> gui.getController().navigateTo(gui.cursorReference.entry));
75 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
76 menu.setEnabled(false);
77 this.add(menu);
78 this.openEntryMenu = menu;
79 }
80 {
81 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.back"));
82 menu.addActionListener(event -> gui.getController().openPreviousReference());
83 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK));
84 menu.setEnabled(false);
85 this.add(menu);
86 this.openPreviousMenu = menu;
87 }
88 {
89 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.forward"));
90 menu.addActionListener(event -> gui.getController().openNextReference());
91 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK));
92 menu.setEnabled(false);
93 this.add(menu);
94 this.openNextMenu = menu;
95 }
96 {
97 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.mark_deobfuscated"));
98 menu.addActionListener(event -> gui.toggleMapping());
99 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
100 menu.setEnabled(false);
101 this.add(menu);
102 this.toggleMappingMenu = menu;
103 }
104 {
105 this.add(new JSeparator());
106 }
107 {
108 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.in"));
109 menu.addActionListener(event -> gui.editor.offsetEditorZoom(2));
110 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.CTRL_DOWN_MASK));
111 this.add(menu);
112 }
113 {
114 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.out"));
115 menu.addActionListener(event -> gui.editor.offsetEditorZoom(-2));
116 menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK));
117 this.add(menu);
118 }
119 {
120 JMenuItem menu = new JMenuItem(I18n.translate("popup_menu.zoom.reset"));
121 menu.addActionListener(event -> gui.editor.resetEditorZoom());
122 this.add(menu);
123 }
124 }
125}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java
new file mode 100644
index 00000000..f5f66287
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserAny.java
@@ -0,0 +1,10 @@
1package cuchaz.enigma.gui.filechooser;
2
3import javax.swing.*;
4
5public class FileChooserAny extends JFileChooser {
6 public FileChooserAny() {
7 this.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
8 this.setAcceptAllFileFilterUsed(false);
9 }
10} \ No newline at end of file
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java
new file mode 100644
index 00000000..cea11a68
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFile.java
@@ -0,0 +1,8 @@
1package cuchaz.enigma.gui.filechooser;
2
3import javax.swing.*;
4
5public class FileChooserFile extends JFileChooser {
6 public FileChooserFile() {
7 }
8}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java
new file mode 100644
index 00000000..c16e0afc
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/filechooser/FileChooserFolder.java
@@ -0,0 +1,11 @@
1package cuchaz.enigma.gui.filechooser;
2
3import javax.swing.*;
4
5public class FileChooserFolder extends JFileChooser {
6
7 public FileChooserFolder() {
8 this.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
9 this.setAcceptAllFileFilterUsed(false);
10 }
11}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java
new file mode 100644
index 00000000..3ae4380f
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/BoxHighlightPainter.java
@@ -0,0 +1,69 @@
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.highlight;
13
14import cuchaz.enigma.gui.config.Config;
15
16import javax.swing.text.BadLocationException;
17import javax.swing.text.Highlighter;
18import javax.swing.text.JTextComponent;
19import java.awt.*;
20
21public class BoxHighlightPainter implements Highlighter.HighlightPainter {
22 private Color fillColor;
23 private Color borderColor;
24
25 protected BoxHighlightPainter(Color fillColor, Color borderColor) {
26 this.fillColor = fillColor;
27 this.borderColor = borderColor;
28 }
29
30 public static BoxHighlightPainter create(Config.AlphaColorEntry entry, Config.AlphaColorEntry entryOutline) {
31 return new BoxHighlightPainter(entry != null ? entry.get() : null, entryOutline != null ? entryOutline.get() : null);
32 }
33
34 public static Rectangle getBounds(JTextComponent text, int start, int end) {
35 try {
36 // determine the bounds of the text
37 Rectangle startRect = text.modelToView(start);
38 Rectangle endRect = text.modelToView(end);
39 Rectangle bounds = startRect.union(endRect);
40
41 // adjust the box so it looks nice
42 bounds.x -= 2;
43 bounds.width += 2;
44 bounds.y += 1;
45 bounds.height -= 2;
46
47 return bounds;
48 } catch (BadLocationException ex) {
49 // don't care... just return something
50 return new Rectangle(0, 0, 0, 0);
51 }
52 }
53
54 @Override
55 public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) {
56 Rectangle bounds = getBounds(text, start, end);
57
58 // fill the area
59 if (this.fillColor != null) {
60 g.setColor(this.fillColor);
61 g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
62 }
63
64 // draw a box around the area
65 g.setColor(this.borderColor);
66 g.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
67 }
68
69}
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
new file mode 100644
index 00000000..2e4e462a
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/SelectionHighlightPainter.java
@@ -0,0 +1,31 @@
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.highlight;
13
14import cuchaz.enigma.gui.config.Config;
15
16import javax.swing.text.Highlighter;
17import javax.swing.text.JTextComponent;
18import java.awt.*;
19
20public class SelectionHighlightPainter implements Highlighter.HighlightPainter {
21
22 @Override
23 public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) {
24 // draw a thick border
25 Graphics2D g2d = (Graphics2D) g;
26 Rectangle bounds = BoxHighlightPainter.getBounds(text, start, end);
27 g2d.setColor(new Color(Config.getInstance().selectionHighlightColor));
28 g2d.setStroke(new BasicStroke(2.0f));
29 g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4);
30 }
31}
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
new file mode 100644
index 00000000..ae23f324
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/highlight/TokenHighlightType.java
@@ -0,0 +1,7 @@
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/node/ClassSelectorClassNode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java
new file mode 100644
index 00000000..922f8f24
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorClassNode.java
@@ -0,0 +1,72 @@
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.node;
13
14import cuchaz.enigma.translation.representation.entry.ClassEntry;
15
16import javax.swing.tree.DefaultMutableTreeNode;
17
18public class ClassSelectorClassNode extends DefaultMutableTreeNode {
19
20 private final ClassEntry obfEntry;
21 private ClassEntry classEntry;
22
23 public ClassSelectorClassNode(ClassEntry obfEntry, ClassEntry classEntry) {
24 this.obfEntry = obfEntry;
25 this.classEntry = classEntry;
26 this.setUserObject(classEntry);
27 }
28
29 public ClassEntry getObfEntry() {
30 return obfEntry;
31 }
32
33 public ClassEntry getClassEntry() {
34 return this.classEntry;
35 }
36
37 @Override
38 public String toString() {
39 return this.classEntry.getSimpleName();
40 }
41
42 @Override
43 public boolean equals(Object other) {
44 return other instanceof ClassSelectorClassNode && equals((ClassSelectorClassNode) other);
45 }
46
47 @Override
48 public int hashCode() {
49 return 17 + (classEntry != null ? classEntry.hashCode() : 0);
50 }
51
52 @Override
53 public Object getUserObject() {
54 return classEntry;
55 }
56
57 @Override
58 public void setUserObject(Object userObject) {
59 String packageName = "";
60 if (classEntry.getPackageName() != null)
61 packageName = classEntry.getPackageName() + "/";
62 if (userObject instanceof String)
63 this.classEntry = new ClassEntry(packageName + userObject);
64 else if (userObject instanceof ClassEntry)
65 this.classEntry = (ClassEntry) userObject;
66 super.setUserObject(classEntry);
67 }
68
69 public boolean equals(ClassSelectorClassNode other) {
70 return this.classEntry.equals(other.classEntry);
71 }
72}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java
new file mode 100644
index 00000000..caa985c9
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/node/ClassSelectorPackageNode.java
@@ -0,0 +1,58 @@
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.node;
13
14import javax.swing.tree.DefaultMutableTreeNode;
15
16public class ClassSelectorPackageNode extends DefaultMutableTreeNode {
17
18 private String packageName;
19
20 public ClassSelectorPackageNode(String packageName) {
21 this.packageName = packageName != null ? packageName : "(none)";
22 }
23
24 public String getPackageName() {
25 return packageName;
26 }
27
28 @Override
29 public Object getUserObject() {
30 return packageName;
31 }
32
33 @Override
34 public void setUserObject(Object userObject) {
35 if (userObject instanceof String)
36 this.packageName = (String) userObject;
37 super.setUserObject(userObject);
38 }
39
40 @Override
41 public String toString() {
42 return !packageName.equals("(none)") ? this.packageName : "(none)";
43 }
44
45 @Override
46 public boolean equals(Object other) {
47 return other instanceof ClassSelectorPackageNode && equals((ClassSelectorPackageNode) other);
48 }
49
50 @Override
51 public int hashCode() {
52 return packageName.hashCode();
53 }
54
55 public boolean equals(ClassSelectorPackageNode other) {
56 return other != null && this.packageName.equals(other.packageName);
57 }
58}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java
new file mode 100644
index 00000000..c24226b3
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelDeobf.java
@@ -0,0 +1,26 @@
1package cuchaz.enigma.gui.panels;
2
3import cuchaz.enigma.gui.ClassSelector;
4import cuchaz.enigma.gui.Gui;
5import cuchaz.enigma.utils.I18n;
6
7import javax.swing.*;
8import java.awt.*;
9
10public class PanelDeobf extends JPanel {
11
12 public final ClassSelector deobfClasses;
13 private final Gui gui;
14
15 public PanelDeobf(Gui gui) {
16 this.gui = gui;
17
18 this.deobfClasses = new ClassSelector(gui, ClassSelector.DEOBF_CLASS_COMPARATOR, true);
19 this.deobfClasses.setSelectionListener(gui.getController()::navigateTo);
20 this.deobfClasses.setRenameSelectionListener(gui::onPanelRename);
21
22 this.setLayout(new BorderLayout());
23 this.add(new JLabel(I18n.translate("info_panel.classes.deobfuscated")), BorderLayout.NORTH);
24 this.add(new JScrollPane(this.deobfClasses), BorderLayout.CENTER);
25 }
26}
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
new file mode 100644
index 00000000..346d6655
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java
@@ -0,0 +1,171 @@
1package cuchaz.enigma.gui.panels;
2
3import cuchaz.enigma.EnigmaProject;
4import cuchaz.enigma.analysis.EntryReference;
5import cuchaz.enigma.gui.config.Config;
6import cuchaz.enigma.gui.BrowserCaret;
7import cuchaz.enigma.gui.Gui;
8import cuchaz.enigma.translation.representation.entry.ClassEntry;
9import cuchaz.enigma.translation.representation.entry.Entry;
10import cuchaz.enigma.gui.util.ScaleUtil;
11
12import javax.swing.*;
13import java.awt.*;
14import java.awt.event.KeyAdapter;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18
19public class PanelEditor extends JEditorPane {
20 private boolean mouseIsPressed = false;
21 public int fontSize = 12;
22
23 public PanelEditor(Gui gui) {
24 this.setEditable(false);
25 this.setSelectionColor(new Color(31, 46, 90));
26 this.setCaret(new BrowserCaret());
27 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize));
28 this.addCaretListener(event -> gui.onCaretMove(event.getDot(), mouseIsPressed));
29 final PanelEditor self = this;
30 this.addMouseListener(new MouseAdapter() {
31 @Override
32 public void mousePressed(MouseEvent mouseEvent) {
33 mouseIsPressed = true;
34 }
35
36 @Override
37 public void mouseReleased(MouseEvent e) {
38 switch (e.getButton()) {
39 case MouseEvent.BUTTON3: // Right click
40 self.setCaretPosition(self.viewToModel(e.getPoint()));
41 break;
42
43 case 4: // Back navigation
44 gui.getController().openPreviousReference();
45 break;
46
47 case 5: // Forward navigation
48 gui.getController().openNextReference();
49 break;
50 }
51 mouseIsPressed = false;
52 }
53 });
54 this.addKeyListener(new KeyAdapter() {
55 @Override
56 public void keyPressed(KeyEvent event) {
57 if (event.isControlDown()) {
58 gui.setShouldNavigateOnClick(false);
59 switch (event.getKeyCode()) {
60 case KeyEvent.VK_I:
61 gui.popupMenu.showInheritanceMenu.doClick();
62 break;
63
64 case KeyEvent.VK_M:
65 gui.popupMenu.showImplementationsMenu.doClick();
66 break;
67
68 case KeyEvent.VK_N:
69 gui.popupMenu.openEntryMenu.doClick();
70 break;
71
72 case KeyEvent.VK_P:
73 gui.popupMenu.openPreviousMenu.doClick();
74 break;
75
76 case KeyEvent.VK_E:
77 gui.popupMenu.openNextMenu.doClick();
78 break;
79
80 case KeyEvent.VK_C:
81 if (event.isShiftDown()) {
82 gui.popupMenu.showCallsSpecificMenu.doClick();
83 } else {
84 gui.popupMenu.showCallsMenu.doClick();
85 }
86 break;
87
88 case KeyEvent.VK_O:
89 gui.popupMenu.toggleMappingMenu.doClick();
90 break;
91
92 case KeyEvent.VK_R:
93 gui.popupMenu.renameMenu.doClick();
94 break;
95
96 case KeyEvent.VK_D:
97 gui.popupMenu.editJavadocMenu.doClick();
98 break;
99
100 case KeyEvent.VK_F5:
101 gui.getController().refreshCurrentClass();
102 break;
103
104 case KeyEvent.VK_F:
105 // prevent navigating on click when quick find activated
106 break;
107
108 case KeyEvent.VK_ADD:
109 case KeyEvent.VK_EQUALS:
110 case KeyEvent.VK_PLUS:
111 self.offsetEditorZoom(2);
112 break;
113 case KeyEvent.VK_SUBTRACT:
114 case KeyEvent.VK_MINUS:
115 self.offsetEditorZoom(-2);
116 break;
117
118 default:
119 gui.setShouldNavigateOnClick(true); // CTRL
120 break;
121 }
122 }
123 }
124
125 @Override
126 public void keyTyped(KeyEvent event) {
127 if (!gui.popupMenu.renameMenu.isEnabled()) return;
128
129 if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) {
130 EnigmaProject project = gui.getController().project;
131 EntryReference<Entry<?>, Entry<?>> reference = project.getMapper().deobfuscate(gui.cursorReference);
132 Entry<?> entry = reference.getNameableEntry();
133
134 String name = String.valueOf(event.getKeyChar());
135 if (entry instanceof ClassEntry && ((ClassEntry) entry).getParent() == null) {
136 String packageName = ((ClassEntry) entry).getPackageName();
137 if (packageName != null) {
138 name = packageName + "/" + name;
139 }
140 }
141
142 gui.popupMenu.renameMenu.doClick();
143 gui.renameTextField.setText(name);
144 }
145 }
146
147 @Override
148 public void keyReleased(KeyEvent event) {
149 gui.setShouldNavigateOnClick(event.isControlDown());
150 }
151 });
152 }
153
154 public void offsetEditorZoom(int zoomAmount) {
155 int newResult = this.fontSize + zoomAmount;
156 if (newResult > 8 && newResult < 72) {
157 this.fontSize = newResult;
158 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize));
159 }
160 }
161
162 public void resetEditorZoom() {
163 this.fontSize = 12;
164 this.setFont(ScaleUtil.getFont(this.getFont().getFontName(), Font.PLAIN, this.fontSize));
165 }
166
167 @Override
168 public Color getCaretColor() {
169 return new Color(Config.getInstance().caretColor);
170 }
171}
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
new file mode 100644
index 00000000..8c19efb5
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelIdentifier.java
@@ -0,0 +1,32 @@
1package cuchaz.enigma.gui.panels;
2
3import cuchaz.enigma.gui.Gui;
4import cuchaz.enigma.gui.util.GuiUtil;
5import cuchaz.enigma.utils.I18n;
6import cuchaz.enigma.gui.util.ScaleUtil;
7
8import javax.swing.*;
9import java.awt.*;
10
11public class PanelIdentifier extends JPanel {
12
13 private final Gui gui;
14
15 public PanelIdentifier(Gui gui) {
16 this.gui = gui;
17
18 this.setLayout(new GridLayout(4, 1, 0, 0));
19 this.setPreferredSize(ScaleUtil.getDimension(0, 100));
20 this.setBorder(BorderFactory.createTitledBorder(I18n.translate("info_panel.identifier")));
21 }
22
23 public void clearReference() {
24 this.removeAll();
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
30 gui.redraw();
31 }
32}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java
new file mode 100644
index 00000000..dd7f9f97
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/panels/PanelObf.java
@@ -0,0 +1,37 @@
1package cuchaz.enigma.gui.panels;
2
3import cuchaz.enigma.gui.ClassSelector;
4import cuchaz.enigma.gui.Gui;
5import cuchaz.enigma.translation.representation.entry.ClassEntry;
6import cuchaz.enigma.utils.I18n;
7
8import javax.swing.*;
9import java.awt.*;
10import java.util.Comparator;
11
12public class PanelObf extends JPanel {
13
14 public final ClassSelector obfClasses;
15 private final Gui gui;
16
17 public PanelObf(Gui gui) {
18 this.gui = gui;
19
20 Comparator<ClassEntry> obfClassComparator = (a, b) -> {
21 String aname = a.getFullName();
22 String bname = b.getFullName();
23 if (aname.length() != bname.length()) {
24 return aname.length() - bname.length();
25 }
26 return aname.compareTo(bname);
27 };
28
29 this.obfClasses = new ClassSelector(gui, obfClassComparator, false);
30 this.obfClasses.setSelectionListener(gui.getController()::navigateTo);
31 this.obfClasses.setRenameSelectionListener(gui::onPanelRename);
32
33 this.setLayout(new BorderLayout());
34 this.add(new JLabel(I18n.translate("info_panel.classes.obfuscated")), BorderLayout.NORTH);
35 this.add(new JScrollPane(this.obfClasses), BorderLayout.CENTER);
36 }
37}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java
new file mode 100644
index 00000000..91727c38
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchEntry.java
@@ -0,0 +1,17 @@
1package cuchaz.enigma.gui.search;
2
3import java.util.List;
4
5public interface SearchEntry {
6
7 List<String> getSearchableNames();
8
9 /**
10 * Returns a type that uniquely identifies this search entry across possible changes.
11 * This is used for tracking the amount of times this entry has been selected.
12 *
13 * @return a unique identifier for this search entry
14 */
15 String getIdentifier();
16
17}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java
new file mode 100644
index 00000000..a3b35faa
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/search/SearchUtil.java
@@ -0,0 +1,268 @@
1package cuchaz.enigma.gui.search;
2
3import java.util.*;
4import java.util.concurrent.Executor;
5import java.util.concurrent.Executors;
6import java.util.concurrent.atomic.AtomicBoolean;
7import java.util.concurrent.atomic.AtomicInteger;
8import java.util.concurrent.locks.Lock;
9import java.util.concurrent.locks.ReentrantLock;
10import java.util.function.BiFunction;
11import java.util.stream.Collectors;
12import java.util.stream.Stream;
13
14import cuchaz.enigma.utils.Pair;
15
16public class SearchUtil<T extends SearchEntry> {
17
18 private final Map<T, Entry<T>> entries = new HashMap<>();
19 private final Map<String, Integer> hitCount = new HashMap<>();
20 private final Executor searchExecutor = Executors.newWorkStealingPool();
21
22 public void add(T entry) {
23 Entry<T> e = Entry.from(entry);
24 entries.put(entry, e);
25 }
26
27 public void add(Entry<T> entry) {
28 entries.put(entry.searchEntry, entry);
29 }
30
31 public void addAll(Collection<T> entries) {
32 this.entries.putAll(entries.parallelStream().collect(Collectors.toMap(e -> e, Entry::from)));
33 }
34
35 public void remove(T entry) {
36 entries.remove(entry);
37 }
38
39 public void clear() {
40 entries.clear();
41 }
42
43 public void clearHits() {
44 hitCount.clear();
45 }
46
47 public Stream<T> search(String term) {
48 return entries.values().parallelStream()
49 .map(e -> new Pair<>(e, e.getScore(term, hitCount.getOrDefault(e.searchEntry.getIdentifier(), 0))))
50 .filter(e -> e.b > 0)
51 .sorted(Comparator.comparingDouble(o -> -o.b))
52 .map(e -> e.a.searchEntry)
53 .sequential();
54 }
55
56 public SearchControl asyncSearch(String term, SearchResultConsumer<T> consumer) {
57 Map<String, Integer> hitCount = new HashMap<>(this.hitCount);
58 Map<T, Entry<T>> entries = new HashMap<>(this.entries);
59 float[] scores = new float[entries.size()];
60 Lock scoresLock = new ReentrantLock();
61 AtomicInteger size = new AtomicInteger();
62 AtomicBoolean control = new AtomicBoolean(false);
63 AtomicInteger elapsed = new AtomicInteger();
64 for (Entry<T> value : entries.values()) {
65 searchExecutor.execute(() -> {
66 try {
67 if (control.get()) return;
68 float score = value.getScore(term, hitCount.getOrDefault(value.searchEntry.getIdentifier(), 0));
69 if (score <= 0) return;
70 score = -score; // sort descending
71 try {
72 scoresLock.lock();
73 if (control.get()) return;
74 int dataSize = size.getAndIncrement();
75 int index = Arrays.binarySearch(scores, 0, dataSize, score);
76 if (index < 0) {
77 index = ~index;
78 }
79 System.arraycopy(scores, index, scores, index + 1, dataSize - index);
80 scores[index] = score;
81 consumer.add(index, value.searchEntry);
82 } finally {
83 scoresLock.unlock();
84 }
85 } finally {
86 elapsed.incrementAndGet();
87 }
88 });
89 }
90
91 return new SearchControl() {
92 @Override
93 public void stop() {
94 control.set(true);
95 }
96
97 @Override
98 public boolean isFinished() {
99 return entries.size() == elapsed.get();
100 }
101
102 @Override
103 public float getProgress() {
104 return (float) elapsed.get() / entries.size();
105 }
106 };
107 }
108
109 public void hit(T entry) {
110 if (entries.containsKey(entry)) {
111 hitCount.compute(entry.getIdentifier(), (_id, i) -> i == null ? 1 : i + 1);
112 }
113 }
114
115 public static final class Entry<T extends SearchEntry> {
116
117 public final T searchEntry;
118 private final String[][] components;
119
120 private Entry(T searchEntry, String[][] components) {
121 this.searchEntry = searchEntry;
122 this.components = components;
123 }
124
125 public float getScore(String term, int hits) {
126 String ucTerm = term.toUpperCase(Locale.ROOT);
127 float maxScore = (float) Arrays.stream(components)
128 .mapToDouble(name -> getScoreFor(ucTerm, name))
129 .max().orElse(0.0);
130 return maxScore * (hits + 1);
131 }
132
133 /**
134 * Computes the score for the given <code>name</code> against the given search term.
135 *
136 * @param term the search term (expected to be upper-case)
137 * @param name the entry name, split at word boundaries (see {@link Entry#wordwiseSplit(String)})
138 * @return the computed score for the entry
139 */
140 private static float getScoreFor(String term, String[] name) {
141 int totalLength = Arrays.stream(name).mapToInt(String::length).sum();
142 float scorePerChar = 1f / totalLength;
143
144 // This map contains a snapshot of all the states the search has
145 // been in. The keys are the remaining characters of the search
146 // term, the values are the maximum scores for that remaining
147 // search term part.
148 Map<String, Float> snapshots = new HashMap<>();
149 snapshots.put(term, 0f);
150
151 // For each component, start at each existing snapshot, searching
152 // for the next longest match, and calculate the new score for each
153 // match length until the maximum. Then the new scores are put back
154 // into the snapshot map.
155 for (int componentIndex = 0; componentIndex < name.length; componentIndex++) {
156 String component = name[componentIndex];
157 float posMultiplier = (name.length - componentIndex) * 0.3f;
158 Map<String, Float> newSnapshots = new HashMap<>();
159 for (Map.Entry<String, Float> snapshot : snapshots.entrySet()) {
160 String remaining = snapshot.getKey();
161 float score = snapshot.getValue();
162 component = component.toUpperCase(Locale.ROOT);
163 int l = compareEqualLength(remaining, component);
164 for (int i = 1; i <= l; i++) {
165 float baseScore = scorePerChar * i;
166 float chainBonus = (i - 1) * 0.5f;
167 merge(newSnapshots, Collections.singletonMap(remaining.substring(i), score + baseScore * posMultiplier + chainBonus), Math::max);
168 }
169 }
170 merge(snapshots, newSnapshots, Math::max);
171 }
172
173 // Only return the score for when the search term was completely
174 // consumed.
175 return snapshots.getOrDefault("", 0f);
176 }
177
178 private static <K, V> void merge(Map<K, V> self, Map<K, V> source, BiFunction<V, V, V> combiner) {
179 source.forEach((k, v) -> self.compute(k, (_k, v1) -> v1 == null ? v : v == null ? v1 : combiner.apply(v, v1)));
180 }
181
182 public static <T extends SearchEntry> Entry<T> from(T e) {
183 String[][] components = e.getSearchableNames().parallelStream()
184 .map(Entry::wordwiseSplit)
185 .toArray(String[][]::new);
186 return new Entry<>(e, components);
187 }
188
189 private static int compareEqualLength(String s1, String s2) {
190 int len = 0;
191 while (len < s1.length() && len < s2.length() && s1.charAt(len) == s2.charAt(len)) {
192 len += 1;
193 }
194 return len;
195 }
196
197 /**
198 * Splits the given input into components, trying to detect word parts.
199 * <p>
200 * Example of how words get split (using <code>|</code> as seperator):
201 * <p><code>MinecraftClientGame -> Minecraft|Client|Game</code></p>
202 * <p><code>HTTPInputStream -> HTTP|Input|Stream</code></p>
203 * <p><code>class_932 -> class|_|932</code></p>
204 * <p><code>X11FontManager -> X|11|Font|Manager</code></p>
205 * <p><code>openHTTPConnection -> open|HTTP|Connection</code></p>
206 * <p><code>open_http_connection -> open|_|http|_|connection</code></p>
207 *
208 * @param input the input to split
209 * @return the resulting components
210 */
211 private static String[] wordwiseSplit(String input) {
212 List<String> list = new ArrayList<>();
213 while (!input.isEmpty()) {
214 int take;
215 if (Character.isLetter(input.charAt(0))) {
216 if (input.length() == 1) {
217 take = 1;
218 } else {
219 boolean nextSegmentIsUppercase = Character.isUpperCase(input.charAt(0)) && Character.isUpperCase(input.charAt(1));
220 if (nextSegmentIsUppercase) {
221 int nextLowercase = 1;
222 while (Character.isUpperCase(input.charAt(nextLowercase))) {
223 nextLowercase += 1;
224 if (nextLowercase == input.length()) {
225 nextLowercase += 1;
226 break;
227 }
228 }
229 take = nextLowercase - 1;
230 } else {
231 int nextUppercase = 1;
232 while (nextUppercase < input.length() && Character.isLowerCase(input.charAt(nextUppercase))) {
233 nextUppercase += 1;
234 }
235 take = nextUppercase;
236 }
237 }
238 } else if (Character.isDigit(input.charAt(0))) {
239 int nextNonNum = 1;
240 while (nextNonNum < input.length() && Character.isLetter(input.charAt(nextNonNum)) && !Character.isLowerCase(input.charAt(nextNonNum))) {
241 nextNonNum += 1;
242 }
243 take = nextNonNum;
244 } else {
245 take = 1;
246 }
247 list.add(input.substring(0, take));
248 input = input.substring(take);
249 }
250 return list.toArray(new String[0]);
251 }
252
253 }
254
255 @FunctionalInterface
256 public interface SearchResultConsumer<T extends SearchEntry> {
257 void add(int index, T entry);
258 }
259
260 public interface SearchControl {
261 void stop();
262
263 boolean isFinished();
264
265 float getProgress();
266 }
267
268}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java
new file mode 100644
index 00000000..d7f7ec0a
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsGenerator.java
@@ -0,0 +1,197 @@
1package cuchaz.enigma.gui.stats;
2
3import com.google.gson.GsonBuilder;
4import cuchaz.enigma.EnigmaProject;
5import cuchaz.enigma.ProgressListener;
6import cuchaz.enigma.analysis.index.EntryIndex;
7import cuchaz.enigma.api.service.NameProposalService;
8import cuchaz.enigma.api.service.ObfuscationTestService;
9import cuchaz.enigma.translation.mapping.EntryRemapper;
10import cuchaz.enigma.translation.mapping.EntryResolver;
11import cuchaz.enigma.translation.mapping.ResolutionStrategy;
12import cuchaz.enigma.translation.representation.TypeDescriptor;
13import cuchaz.enigma.translation.representation.entry.*;
14import cuchaz.enigma.utils.I18n;
15
16import java.util.*;
17
18public class StatsGenerator {
19 private final EntryIndex entryIndex;
20 private final EntryRemapper mapper;
21 private final EntryResolver entryResolver;
22 private final List<ObfuscationTestService> obfuscationTestServices;
23 private final List<NameProposalService> nameProposalServices;
24
25 public StatsGenerator(EnigmaProject project) {
26 entryIndex = project.getJarIndex().getEntryIndex();
27 mapper = project.getMapper();
28 entryResolver = project.getJarIndex().getEntryResolver();
29 obfuscationTestServices = project.getEnigma().getServices().get(ObfuscationTestService.TYPE);
30 nameProposalServices = project.getEnigma().getServices().get(NameProposalService.TYPE);
31 }
32
33 public String generate(ProgressListener progress, Set<StatsMember> includedMembers) {
34 includedMembers = EnumSet.copyOf(includedMembers);
35 int totalWork = 0;
36
37 if (includedMembers.contains(StatsMember.METHODS) || includedMembers.contains(StatsMember.PARAMETERS)) {
38 totalWork += entryIndex.getMethods().size();
39 }
40
41 if (includedMembers.contains(StatsMember.FIELDS)) {
42 totalWork += entryIndex.getFields().size();
43 }
44
45 if (includedMembers.contains(StatsMember.CLASSES)) {
46 totalWork += entryIndex.getClasses().size();
47 }
48
49 progress.init(totalWork, "progress.stats");
50
51 Map<String, Integer> counts = new HashMap<>();
52
53 int numDone = 0;
54 if (includedMembers.contains(StatsMember.METHODS) || includedMembers.contains(StatsMember.PARAMETERS)) {
55 for (MethodEntry method : entryIndex.getMethods()) {
56 progress.step(numDone++, I18n.translate("type.methods"));
57 MethodEntry root = entryResolver
58 .resolveEntry(method, ResolutionStrategy.RESOLVE_ROOT)
59 .stream()
60 .findFirst()
61 .orElseThrow(AssertionError::new);
62
63 if (root == method && !((MethodDefEntry) method).getAccess().isSynthetic()) {
64 if (includedMembers.contains(StatsMember.METHODS)) {
65 update(counts, method);
66 }
67
68 if (includedMembers.contains(StatsMember.PARAMETERS)) {
69 int index = ((MethodDefEntry) method).getAccess().isStatic() ? 0 : 1;
70 for (TypeDescriptor argument : method.getDesc().getArgumentDescs()) {
71 update(counts, new LocalVariableEntry(method, index, "", true,null));
72 index += argument.getSize();
73 }
74 }
75 }
76 }
77 }
78
79 if (includedMembers.contains(StatsMember.FIELDS)) {
80 for (FieldEntry field : entryIndex.getFields()) {
81 progress.step(numDone++, I18n.translate("type.fields"));
82 update(counts, field);
83 }
84 }
85
86 if (includedMembers.contains(StatsMember.CLASSES)) {
87 for (ClassEntry clazz : entryIndex.getClasses()) {
88 progress.step(numDone++, I18n.translate("type.classes"));
89 update(counts, clazz);
90 }
91 }
92
93 progress.step(-1, I18n.translate("progress.stats.data"));
94
95 Tree<Integer> tree = new Tree<>();
96
97 for (Map.Entry<String, Integer> entry : counts.entrySet()) {
98 if (entry.getKey().startsWith("com.mojang")) continue; // just a few unmapped names, no point in having a subsection
99 tree.getNode(entry.getKey()).value = entry.getValue();
100 }
101
102 tree.collapse(tree.root);
103 return new GsonBuilder().setPrettyPrinting().create().toJson(tree.root);
104 }
105
106 private void update(Map<String, Integer> counts, Entry<?> entry) {
107 if (isObfuscated(entry)) {
108 String parent = mapper.deobfuscate(entry.getAncestry().get(0)).getName().replace('/', '.');
109 counts.put(parent, counts.getOrDefault(parent, 0) + 1);
110 }
111 }
112
113 private boolean isObfuscated(Entry<?> entry) {
114 String name = entry.getName();
115
116 if (!obfuscationTestServices.isEmpty()) {
117 for (ObfuscationTestService service : obfuscationTestServices) {
118 if (service.testDeobfuscated(entry)) {
119 return false;
120 }
121 }
122 }
123
124 if (!nameProposalServices.isEmpty()) {
125 for (NameProposalService service : nameProposalServices) {
126 if (service.proposeName(entry, mapper).isPresent()) {
127 return false;
128 }
129 }
130 }
131
132 String mappedName = mapper.deobfuscate(entry).getName();
133 if (mappedName != null && !mappedName.isEmpty() && !mappedName.equals(name)) {
134 return false;
135 }
136
137 return true;
138 }
139
140 private static class Tree<T> {
141 public final Node<T> root;
142 private final Map<String, Node<T>> nodes = new HashMap<>();
143
144 public static class Node<T> {
145 public String name;
146 public T value;
147 public List<Node<T>> children = new ArrayList<>();
148 private final transient Map<String, Node<T>> namedChildren = new HashMap<>();
149
150 public Node(String name, T value) {
151 this.name = name;
152 this.value = value;
153 }
154 }
155
156 public Tree() {
157 root = new Node<>("", null);
158 }
159
160 public Node<T> getNode(String name) {
161 Node<T> node = nodes.get(name);
162
163 if (node == null) {
164 node = root;
165
166 for (String part : name.split("\\.")) {
167 Node<T> child = node.namedChildren.get(part);
168
169 if (child == null) {
170 child = new Node<>(part, null);
171 node.namedChildren.put(part, child);
172 node.children.add(child);
173 }
174
175 node = child;
176 }
177
178 nodes.put(name, node);
179 }
180
181 return node;
182 }
183
184 public void collapse(Node<T> node) {
185 while (node.children.size() == 1) {
186 Node<T> child = node.children.get(0);
187 node.name = node.name.isEmpty() ? child.name : node.name + "." + child.name;
188 node.children = child.children;
189 node.value = child.value;
190 }
191
192 for (Node<T> child : node.children) {
193 collapse(child);
194 }
195 }
196 }
197}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java
new file mode 100644
index 00000000..70b4f40d
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/stats/StatsMember.java
@@ -0,0 +1,8 @@
1package cuchaz.enigma.gui.stats;
2
3public enum StatsMember {
4 METHODS,
5 FIELDS,
6 PARAMETERS,
7 CLASSES
8}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java
new file mode 100644
index 00000000..612e3e92
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java
@@ -0,0 +1,77 @@
1package cuchaz.enigma.gui.util;
2
3import java.awt.Component;
4import java.awt.event.MouseEvent;
5
6import javax.swing.*;
7import javax.swing.border.Border;
8
9public abstract class AbstractListCellRenderer<E> extends JPanel implements ListCellRenderer<E> {
10
11 private static final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1);
12
13 private Border noFocusBorder;
14
15 public AbstractListCellRenderer() {
16 setBorder(getNoFocusBorder());
17 }
18
19 protected Border getNoFocusBorder() {
20 if (noFocusBorder == null) {
21 Border border = UIManager.getLookAndFeel().getDefaults().getBorder("List.List.cellNoFocusBorder");
22 noFocusBorder = border != null ? border : NO_FOCUS_BORDER;
23 }
24 return noFocusBorder;
25 }
26
27 protected Border getBorder(boolean isSelected, boolean cellHasFocus) {
28 Border b = null;
29 if (cellHasFocus) {
30 UIDefaults defaults = UIManager.getLookAndFeel().getDefaults();
31 if (isSelected) {
32 b = defaults.getBorder("List.focusSelectedCellHighlightBorder");
33 }
34 if (b == null) {
35 b = defaults.getBorder("List.focusCellHighlightBorder");
36 }
37 } else {
38 b = getNoFocusBorder();
39 }
40 return b;
41 }
42
43 public abstract void updateUiForEntry(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus);
44
45 @Override
46 public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus) {
47 updateUiForEntry(list, value, index, isSelected, cellHasFocus);
48
49 if (isSelected) {
50 setBackground(list.getSelectionBackground());
51 setForeground(list.getSelectionForeground());
52 } else {
53 setBackground(list.getBackground());
54 setForeground(list.getForeground());
55 }
56
57 setEnabled(list.isEnabled());
58 setFont(list.getFont());
59
60 setBorder(getBorder(isSelected, cellHasFocus));
61
62 // This isn't the width of the cell, but it's close enough for where it's needed (getComponentAt in getToolTipText)
63 setSize(list.getWidth(), getPreferredSize().height);
64
65 return this;
66 }
67
68 @Override
69 public String getToolTipText(MouseEvent event) {
70 Component c = getComponentAt(event.getPoint());
71 if (c instanceof JComponent) {
72 return ((JComponent) c).getToolTipText();
73 }
74 return getToolTipText();
75 }
76
77}
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
new file mode 100644
index 00000000..70172fe7
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/GuiUtil.java
@@ -0,0 +1,56 @@
1package cuchaz.enigma.gui.util;
2
3import javax.swing.*;
4import javax.swing.text.BadLocationException;
5import javax.swing.text.JTextComponent;
6import java.awt.*;
7import java.awt.event.MouseEvent;
8import java.io.IOException;
9import java.net.URI;
10import java.net.URISyntaxException;
11import java.util.Locale;
12import java.util.StringJoiner;
13
14public class GuiUtil {
15 public static void openUrl(String url) {
16 if (Desktop.isDesktopSupported()) {
17 Desktop desktop = Desktop.getDesktop();
18 try {
19 desktop.browse(new URI(url));
20 } catch (IOException ex) {
21 throw new Error(ex);
22 } catch (URISyntaxException ex) {
23 throw new IllegalArgumentException(ex);
24 }
25 }
26 }
27
28 public static JLabel unboldLabel(JLabel label) {
29 Font font = label.getFont();
30 label.setFont(font.deriveFont(font.getStyle() & ~Font.BOLD));
31 return label;
32 }
33
34 public static void showToolTipNow(JComponent component) {
35 // HACKHACK: trick the tooltip manager into showing the tooltip right now
36 ToolTipManager manager = ToolTipManager.sharedInstance();
37 int oldDelay = manager.getInitialDelay();
38 manager.setInitialDelay(0);
39 manager.mouseMoved(new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 0, 0, 0, false));
40 manager.setInitialDelay(oldDelay);
41 }
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}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java
new file mode 100644
index 00000000..b1286998
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/History.java
@@ -0,0 +1,49 @@
1package cuchaz.enigma.gui.util;
2
3import com.google.common.collect.Queues;
4
5import java.util.Deque;
6
7public class History<T> {
8 private final Deque<T> previous = Queues.newArrayDeque();
9 private final Deque<T> next = Queues.newArrayDeque();
10 private T current;
11
12 public History(T initial) {
13 current = initial;
14 }
15
16 public T getCurrent() {
17 return current;
18 }
19
20 public void push(T value) {
21 previous.addLast(current);
22 current = value;
23 next.clear();
24 }
25
26 public void replace(T value) {
27 current = value;
28 }
29
30 public boolean canGoBack() {
31 return !previous.isEmpty();
32 }
33
34 public T goBack() {
35 next.addFirst(current);
36 current = previous.removeLast();
37 return current;
38 }
39
40 public boolean canGoForward() {
41 return !next.isEmpty();
42 }
43
44 public T goForward() {
45 previous.addLast(current);
46 current = next.removeFirst();
47 return current;
48 }
49}
diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java
new file mode 100644
index 00000000..d045c6d5
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleChangeListener.java
@@ -0,0 +1,8 @@
1package cuchaz.enigma.gui.util;
2
3@FunctionalInterface
4public interface ScaleChangeListener {
5
6 void onScaleChanged(float scale, float oldScale);
7
8}
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
new file mode 100644
index 00000000..e7ee5657
--- /dev/null
+++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java
@@ -0,0 +1,110 @@
1package cuchaz.enigma.gui.util;
2
3import java.awt.Dimension;
4import java.awt.Font;
5import java.io.IOException;
6import java.lang.reflect.Field;
7import java.util.ArrayList;
8import java.util.List;
9
10import javax.swing.BorderFactory;
11import javax.swing.UIManager;
12import javax.swing.border.Border;
13
14import com.github.swingdpi.UiDefaultsScaler;
15import com.github.swingdpi.plaf.BasicTweaker;
16import com.github.swingdpi.plaf.MetalTweaker;
17import com.github.swingdpi.plaf.NimbusTweaker;
18import com.github.swingdpi.plaf.WindowsTweaker;
19import cuchaz.enigma.gui.config.Config;
20import de.sciss.syntaxpane.DefaultSyntaxKit;
21
22public class ScaleUtil {
23
24 private static List<ScaleChangeListener> listeners = new ArrayList<>();
25
26 public static float getScaleFactor() {
27 return Config.getInstance().scaleFactor;
28 }
29
30 public static void setScaleFactor(float scaleFactor) {
31 float oldScale = getScaleFactor();
32 float clamped = Math.min(Math.max(0.25f, scaleFactor), 10.0f);
33 Config.getInstance().scaleFactor = clamped;
34 try {
35 Config.getInstance().saveConfig();
36 } catch (IOException e) {
37 e.printStackTrace();
38 }
39 listeners.forEach(l -> l.onScaleChanged(clamped, oldScale));
40 }
41
42 public static void addListener(ScaleChangeListener listener) {
43 listeners.add(listener);
44 }
45
46 public static void removeListener(ScaleChangeListener listener) {
47 listeners.remove(listener);
48 }
49
50 public static Dimension getDimension(int width, int height) {
51 return new Dimension(scale(width), scale(height));
52 }
53
54 public static Font getFont(String fontName, int plain, int fontSize) {
55 return scaleFont(new Font(fontName, plain, fontSize));
56 }
57
58 public static Font scaleFont(Font font) {
59 return createTweakerForCurrentLook(getScaleFactor()).modifyFont("", font);
60 }
61
62 public static float scale(float f) {
63 return f * getScaleFactor();
64 }
65
66 public static float invert(float f) {
67 return f / getScaleFactor();
68 }
69
70 public static int scale(int i) {
71 return (int) (i * getScaleFactor());
72 }
73
74 public static Border createEmptyBorder(int top, int left, int bottom, int right) {
75 return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right));
76 }
77
78 public static int invert(int i) {
79 return (int) (i / getScaleFactor());
80 }
81
82 public static void applyScaling() {
83 float scale = getScaleFactor();
84 UiDefaultsScaler.updateAndApplyGlobalScaling((int) (100 * scale), true);
85 try {
86 Field defaultFontField = DefaultSyntaxKit.class.getDeclaredField("DEFAULT_FONT");
87 defaultFontField.setAccessible(true);
88 Font font = (Font) defaultFontField.get(null);
89 font = font.deriveFont(12 * scale);
90 defaultFontField.set(null, font);
91 } catch (NoSuchFieldException | IllegalAccessException e) {
92 e.printStackTrace();
93 }
94 }
95
96 private static BasicTweaker createTweakerForCurrentLook(float dpiScaling) {
97 String testString = UIManager.getLookAndFeel().getName().toLowerCase();
98 if (testString.contains("windows")) {
99 return new WindowsTweaker(dpiScaling, testString.contains("classic"));
100 }
101 if (testString.contains("metal")) {
102 return new MetalTweaker(dpiScaling);
103 }
104 if (testString.contains("nimbus")) {
105 return new NimbusTweaker(dpiScaling);
106 }
107 return new BasicTweaker(dpiScaling);
108 }
109
110}
diff --git a/enigma-swing/src/main/resources/about.html b/enigma-swing/src/main/resources/about.html
new file mode 100644
index 00000000..b75c1bf0
--- /dev/null
+++ b/enigma-swing/src/main/resources/about.html
@@ -0,0 +1,6 @@
1<html>
2 <h1>%s</h1>
3 <p>A tool for debofuscation of Java code</p>
4 <p>
5 <p>Version: %s</p>
6</html> \ No newline at end of file
diff --git a/enigma-swing/src/main/resources/stats.html b/enigma-swing/src/main/resources/stats.html
new file mode 100644
index 00000000..fcff7c0f
--- /dev/null
+++ b/enigma-swing/src/main/resources/stats.html
@@ -0,0 +1,34 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta content="width=device-width, initial-scale=1" name="viewport">
6 <title>Stats</title>
7 <link href="https://cdn.anychart.com/releases/v8/css/anychart-ui.min.css" rel="stylesheet" type="text/css">
8 <style>
9 html, body, #container {
10 width: 100%;
11 height: 100%;
12 margin: 0;
13 padding: 0;
14 }
15 </style>
16</head>
17
18<body>
19<div id="container"></div>
20<script src="https://cdn.anychart.com/releases/v8/js/anychart-base.min.js"></script>
21<script src="https://cdn.anychart.com/releases/v8/js/anychart-sunburst.min.js"></script>
22<script src="https://cdn.anychart.com/releases/v8/js/anychart-exports.min.js"></script>
23<script src="https://cdn.anychart.com/releases/v8/js/anychart-ui.min.js"></script>
24<script>
25 anychart.onDocumentReady(function () {
26 var chart = anychart.sunburst([/*data*/], "as-tree");
27 chart.sort("desc");
28 chart.calculationMode("parent-independent");
29 chart.container("container");
30 chart.draw();
31 });
32</script>
33</body>
34</html>