diff options
| author | 2020-06-03 13:39:42 -0400 | |
|---|---|---|
| committer | 2020-06-03 18:39:42 +0100 | |
| commit | 0f47403d0220757fed189b76e2071e25b1025cb8 (patch) | |
| tree | 879bf72c4476f0a5e0d82da99d7ff2b2276bcaca /enigma-swing/src/main | |
| parent | Fix search dialog hanging for a short time sometimes (#250) (diff) | |
| download | enigma-0f47403d0220757fed189b76e2071e25b1025cb8.tar.gz enigma-0f47403d0220757fed189b76e2071e25b1025cb8.tar.xz enigma-0f47403d0220757fed189b76e2071e25b1025cb8.zip | |
Split GUI code to separate module (#242)
* Split into modules
* Post merge compile fixes
Co-authored-by: modmuss50 <modmuss50@gmail.com>
Diffstat (limited to 'enigma-swing/src/main')
54 files changed, 6249 insertions, 0 deletions
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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import javax.swing.text.DefaultCaret; | ||
| 15 | |||
| 16 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import java.awt.event.MouseAdapter; | ||
| 15 | import java.awt.event.MouseEvent; | ||
| 16 | import java.util.*; | ||
| 17 | |||
| 18 | import javax.annotation.Nullable; | ||
| 19 | import javax.swing.JOptionPane; | ||
| 20 | import javax.swing.JTree; | ||
| 21 | import javax.swing.event.CellEditorListener; | ||
| 22 | import javax.swing.event.ChangeEvent; | ||
| 23 | import javax.swing.tree.*; | ||
| 24 | |||
| 25 | import com.google.common.collect.ArrayListMultimap; | ||
| 26 | import com.google.common.collect.Lists; | ||
| 27 | import com.google.common.collect.Maps; | ||
| 28 | import com.google.common.collect.Multimap; | ||
| 29 | import cuchaz.enigma.gui.node.ClassSelectorClassNode; | ||
| 30 | import cuchaz.enigma.gui.node.ClassSelectorPackageNode; | ||
| 31 | import cuchaz.enigma.translation.mapping.IllegalNameException; | ||
| 32 | import cuchaz.enigma.translation.Translator; | ||
| 33 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 34 | |||
| 35 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import cuchaz.enigma.source.Token; | ||
| 15 | |||
| 16 | import javax.swing.*; | ||
| 17 | import javax.swing.text.BadLocationException; | ||
| 18 | import javax.swing.text.Document; | ||
| 19 | import javax.swing.text.Highlighter.HighlightPainter; | ||
| 20 | import java.awt.*; | ||
| 21 | import java.awt.event.ActionEvent; | ||
| 22 | import java.awt.event.ActionListener; | ||
| 23 | |||
| 24 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import cuchaz.enigma.EnigmaProject; | ||
| 4 | import cuchaz.enigma.EnigmaServices; | ||
| 5 | import cuchaz.enigma.analysis.EntryReference; | ||
| 6 | import cuchaz.enigma.source.Token; | ||
| 7 | import cuchaz.enigma.api.service.NameProposalService; | ||
| 8 | import cuchaz.enigma.gui.highlight.TokenHighlightType; | ||
| 9 | import cuchaz.enigma.source.SourceIndex; | ||
| 10 | import cuchaz.enigma.source.SourceRemapper; | ||
| 11 | import cuchaz.enigma.translation.LocalNameGenerator; | ||
| 12 | import cuchaz.enigma.translation.Translator; | ||
| 13 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 14 | import cuchaz.enigma.translation.mapping.ResolutionStrategy; | ||
| 15 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 16 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 17 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 18 | import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry; | ||
| 19 | |||
| 20 | import javax.annotation.Nullable; | ||
| 21 | import java.util.*; | ||
| 22 | |||
| 23 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import de.sciss.syntaxpane.actions.DocumentSearchData; | ||
| 4 | import de.sciss.syntaxpane.actions.gui.QuickFindDialog; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import javax.swing.text.JTextComponent; | ||
| 8 | import java.awt.*; | ||
| 9 | import java.awt.event.KeyAdapter; | ||
| 10 | import java.awt.event.KeyEvent; | ||
| 11 | import java.util.stream.IntStream; | ||
| 12 | import java.util.stream.Stream; | ||
| 13 | |||
| 14 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.config.Config; | ||
| 4 | import de.sciss.syntaxpane.components.LineNumbersRuler; | ||
| 5 | import de.sciss.syntaxpane.syntaxkits.JavaSyntaxKit; | ||
| 6 | import de.sciss.syntaxpane.util.Configuration; | ||
| 7 | |||
| 8 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import java.awt.*; | ||
| 15 | import java.awt.event.*; | ||
| 16 | import java.nio.file.Path; | ||
| 17 | import java.util.List; | ||
| 18 | import java.util.*; | ||
| 19 | import java.util.function.Function; | ||
| 20 | |||
| 21 | import javax.swing.*; | ||
| 22 | import javax.swing.text.BadLocationException; | ||
| 23 | import javax.swing.text.Highlighter; | ||
| 24 | import javax.swing.tree.*; | ||
| 25 | |||
| 26 | import com.google.common.base.Strings; | ||
| 27 | import com.google.common.collect.Lists; | ||
| 28 | import cuchaz.enigma.Enigma; | ||
| 29 | import cuchaz.enigma.EnigmaProfile; | ||
| 30 | import cuchaz.enigma.analysis.*; | ||
| 31 | import cuchaz.enigma.gui.config.Config; | ||
| 32 | import cuchaz.enigma.gui.config.Themes; | ||
| 33 | import cuchaz.enigma.gui.dialog.CrashDialog; | ||
| 34 | import cuchaz.enigma.gui.dialog.JavadocDialog; | ||
| 35 | import cuchaz.enigma.gui.dialog.SearchDialog; | ||
| 36 | import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; | ||
| 37 | import cuchaz.enigma.gui.elements.MenuBar; | ||
| 38 | import cuchaz.enigma.gui.elements.PopupMenuBar; | ||
| 39 | import cuchaz.enigma.gui.filechooser.FileChooserAny; | ||
| 40 | import cuchaz.enigma.gui.filechooser.FileChooserFolder; | ||
| 41 | import cuchaz.enigma.gui.highlight.BoxHighlightPainter; | ||
| 42 | import cuchaz.enigma.gui.highlight.SelectionHighlightPainter; | ||
| 43 | import cuchaz.enigma.gui.highlight.TokenHighlightType; | ||
| 44 | import cuchaz.enigma.gui.panels.PanelDeobf; | ||
| 45 | import cuchaz.enigma.gui.panels.PanelEditor; | ||
| 46 | import cuchaz.enigma.gui.panels.PanelIdentifier; | ||
| 47 | import cuchaz.enigma.gui.panels.PanelObf; | ||
| 48 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 49 | import cuchaz.enigma.gui.util.History; | ||
| 50 | import cuchaz.enigma.network.packet.*; | ||
| 51 | import cuchaz.enigma.source.Token; | ||
| 52 | import cuchaz.enigma.translation.mapping.IllegalNameException; | ||
| 53 | import cuchaz.enigma.translation.mapping.*; | ||
| 54 | import cuchaz.enigma.translation.representation.entry.*; | ||
| 55 | import cuchaz.enigma.network.Message; | ||
| 56 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 57 | import cuchaz.enigma.utils.I18n; | ||
| 58 | import de.sciss.syntaxpane.DefaultSyntaxKit; | ||
| 59 | |||
| 60 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import com.google.common.collect.Lists; | ||
| 15 | import com.google.common.util.concurrent.ThreadFactoryBuilder; | ||
| 16 | import cuchaz.enigma.Enigma; | ||
| 17 | import cuchaz.enigma.EnigmaProfile; | ||
| 18 | import cuchaz.enigma.EnigmaProject; | ||
| 19 | import cuchaz.enigma.analysis.*; | ||
| 20 | import cuchaz.enigma.api.service.ObfuscationTestService; | ||
| 21 | import cuchaz.enigma.gui.config.Config; | ||
| 22 | import cuchaz.enigma.gui.dialog.ProgressDialog; | ||
| 23 | import cuchaz.enigma.gui.stats.StatsGenerator; | ||
| 24 | import cuchaz.enigma.gui.stats.StatsMember; | ||
| 25 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 26 | import cuchaz.enigma.gui.util.History; | ||
| 27 | import cuchaz.enigma.network.*; | ||
| 28 | import cuchaz.enigma.network.packet.LoginC2SPacket; | ||
| 29 | import cuchaz.enigma.network.packet.Packet; | ||
| 30 | import cuchaz.enigma.source.*; | ||
| 31 | import cuchaz.enigma.translation.mapping.serde.MappingParseException; | ||
| 32 | import cuchaz.enigma.translation.Translator; | ||
| 33 | import cuchaz.enigma.translation.mapping.*; | ||
| 34 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 35 | import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; | ||
| 36 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 37 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 38 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 39 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 40 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 41 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 42 | import cuchaz.enigma.utils.I18n; | ||
| 43 | import cuchaz.enigma.utils.Utils; | ||
| 44 | |||
| 45 | import javax.annotation.Nullable; | ||
| 46 | import javax.swing.JOptionPane; | ||
| 47 | import javax.swing.SwingUtilities; | ||
| 48 | import java.awt.*; | ||
| 49 | import java.awt.event.ItemEvent; | ||
| 50 | import java.io.*; | ||
| 51 | import java.nio.file.Path; | ||
| 52 | import java.util.Collection; | ||
| 53 | import java.util.List; | ||
| 54 | import java.util.Set; | ||
| 55 | import java.util.concurrent.CompletableFuture; | ||
| 56 | import java.util.concurrent.ExecutorService; | ||
| 57 | import java.util.concurrent.Executors; | ||
| 58 | import java.util.stream.Collectors; | ||
| 59 | import java.util.stream.Stream; | ||
| 60 | |||
| 61 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import cuchaz.enigma.EnigmaProfile; | ||
| 15 | import cuchaz.enigma.gui.config.Config; | ||
| 16 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 17 | |||
| 18 | import cuchaz.enigma.utils.I18n; | ||
| 19 | import joptsimple.*; | ||
| 20 | |||
| 21 | import java.io.IOException; | ||
| 22 | import java.nio.file.Files; | ||
| 23 | import java.nio.file.Path; | ||
| 24 | import java.nio.file.Paths; | ||
| 25 | |||
| 26 | import com.google.common.io.MoreFiles; | ||
| 27 | |||
| 28 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import java.awt.Component; | ||
| 4 | |||
| 5 | import javax.swing.DefaultListCellRenderer; | ||
| 6 | import javax.swing.JList; | ||
| 7 | |||
| 8 | import cuchaz.enigma.network.Message; | ||
| 9 | |||
| 10 | // For now, just render the translated text. | ||
| 11 | // TODO: Icons or something later? | ||
| 12 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import cuchaz.enigma.analysis.MethodInheritanceTreeNode; | ||
| 15 | import cuchaz.enigma.gui.config.Config; | ||
| 16 | |||
| 17 | import javax.swing.*; | ||
| 18 | import javax.swing.tree.TreeCellRenderer; | ||
| 19 | import java.awt.*; | ||
| 20 | |||
| 21 | class 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import de.sciss.syntaxpane.SyntaxDocument; | ||
| 4 | import de.sciss.syntaxpane.actions.DefaultSyntaxAction; | ||
| 5 | |||
| 6 | import javax.swing.text.JTextComponent; | ||
| 7 | import java.awt.event.ActionEvent; | ||
| 8 | |||
| 9 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | public 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 | |||
| 12 | package cuchaz.enigma.gui; | ||
| 13 | |||
| 14 | import cuchaz.enigma.source.Token; | ||
| 15 | |||
| 16 | import javax.swing.*; | ||
| 17 | import java.awt.*; | ||
| 18 | |||
| 19 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.config; | ||
| 2 | |||
| 3 | import com.bulenkov.darcula.DarculaLaf; | ||
| 4 | import com.google.common.io.Files; | ||
| 5 | import com.google.gson.*; | ||
| 6 | import cuchaz.enigma.source.DecompilerService; | ||
| 7 | import cuchaz.enigma.source.Decompilers; | ||
| 8 | |||
| 9 | import cuchaz.enigma.utils.I18n; | ||
| 10 | |||
| 11 | import javax.swing.*; | ||
| 12 | import javax.swing.plaf.metal.MetalLookAndFeel; | ||
| 13 | import java.awt.*; | ||
| 14 | import java.awt.image.BufferedImage; | ||
| 15 | import java.io.File; | ||
| 16 | import java.io.IOException; | ||
| 17 | import java.lang.reflect.Type; | ||
| 18 | import java.nio.charset.Charset; | ||
| 19 | |||
| 20 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.config; | ||
| 2 | |||
| 3 | import java.io.IOException; | ||
| 4 | |||
| 5 | import javax.swing.SwingUtilities; | ||
| 6 | |||
| 7 | import com.google.common.collect.ImmutableMap; | ||
| 8 | import cuchaz.enigma.gui.EnigmaSyntaxKit; | ||
| 9 | import cuchaz.enigma.gui.Gui; | ||
| 10 | import cuchaz.enigma.gui.highlight.BoxHighlightPainter; | ||
| 11 | import cuchaz.enigma.gui.highlight.TokenHighlightType; | ||
| 12 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 13 | import de.sciss.syntaxpane.DefaultSyntaxKit; | ||
| 14 | |||
| 15 | public 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 | |||
| 12 | package cuchaz.enigma.gui.dialog; | ||
| 13 | |||
| 14 | import cuchaz.enigma.Enigma; | ||
| 15 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 16 | import cuchaz.enigma.utils.I18n; | ||
| 17 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 18 | import cuchaz.enigma.utils.Utils; | ||
| 19 | |||
| 20 | import javax.swing.*; | ||
| 21 | import java.awt.*; | ||
| 22 | import java.io.IOException; | ||
| 23 | |||
| 24 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import java.awt.BorderLayout; | ||
| 4 | import java.awt.event.KeyAdapter; | ||
| 5 | import java.awt.event.KeyEvent; | ||
| 6 | |||
| 7 | import javax.swing.JButton; | ||
| 8 | import javax.swing.JFrame; | ||
| 9 | import javax.swing.JLabel; | ||
| 10 | import javax.swing.JPanel; | ||
| 11 | |||
| 12 | import cuchaz.enigma.gui.Gui; | ||
| 13 | import cuchaz.enigma.utils.I18n; | ||
| 14 | |||
| 15 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.utils.I18n; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import java.awt.Frame; | ||
| 8 | |||
| 9 | public 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 | |||
| 12 | package cuchaz.enigma.gui.dialog; | ||
| 13 | |||
| 14 | import cuchaz.enigma.Enigma; | ||
| 15 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 16 | import cuchaz.enigma.utils.I18n; | ||
| 17 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 18 | |||
| 19 | import javax.swing.*; | ||
| 20 | import java.awt.*; | ||
| 21 | import java.io.PrintWriter; | ||
| 22 | import java.io.StringWriter; | ||
| 23 | import java.io.FileWriter; | ||
| 24 | import java.io.File; | ||
| 25 | import java.io.IOException; | ||
| 26 | |||
| 27 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.utils.I18n; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import java.awt.*; | ||
| 8 | |||
| 9 | public 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 | |||
| 12 | package cuchaz.enigma.gui.dialog; | ||
| 13 | |||
| 14 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 15 | import cuchaz.enigma.utils.I18n; | ||
| 16 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 17 | |||
| 18 | import javax.swing.*; | ||
| 19 | import javax.swing.text.html.HTML; | ||
| 20 | |||
| 21 | import java.awt.*; | ||
| 22 | import java.awt.event.KeyAdapter; | ||
| 23 | import java.awt.event.KeyEvent; | ||
| 24 | |||
| 25 | public 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 | |||
| 12 | package cuchaz.enigma.gui.dialog; | ||
| 13 | |||
| 14 | import cuchaz.enigma.Enigma; | ||
| 15 | import cuchaz.enigma.ProgressListener; | ||
| 16 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 17 | import cuchaz.enigma.utils.I18n; | ||
| 18 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 19 | |||
| 20 | import javax.swing.*; | ||
| 21 | import java.awt.*; | ||
| 22 | import java.util.concurrent.CompletableFuture; | ||
| 23 | |||
| 24 | public 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 | |||
| 12 | package cuchaz.enigma.gui.dialog; | ||
| 13 | |||
| 14 | import java.awt.BorderLayout; | ||
| 15 | import java.awt.Color; | ||
| 16 | import java.awt.FlowLayout; | ||
| 17 | import java.awt.Font; | ||
| 18 | import java.awt.event.*; | ||
| 19 | import java.util.Arrays; | ||
| 20 | import java.util.Collections; | ||
| 21 | import java.util.List; | ||
| 22 | |||
| 23 | import javax.swing.*; | ||
| 24 | import javax.swing.event.DocumentEvent; | ||
| 25 | import javax.swing.event.DocumentListener; | ||
| 26 | |||
| 27 | import cuchaz.enigma.gui.Gui; | ||
| 28 | import cuchaz.enigma.gui.GuiController; | ||
| 29 | import cuchaz.enigma.gui.util.AbstractListCellRenderer; | ||
| 30 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 31 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 32 | import cuchaz.enigma.utils.I18n; | ||
| 33 | import cuchaz.enigma.gui.search.SearchEntry; | ||
| 34 | import cuchaz.enigma.gui.search.SearchUtil; | ||
| 35 | |||
| 36 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import java.awt.BorderLayout; | ||
| 4 | import java.util.Arrays; | ||
| 5 | import java.util.Locale; | ||
| 6 | import java.util.Map; | ||
| 7 | import java.util.Set; | ||
| 8 | import java.util.stream.Collectors; | ||
| 9 | |||
| 10 | import javax.swing.JButton; | ||
| 11 | import javax.swing.JCheckBox; | ||
| 12 | import javax.swing.JFrame; | ||
| 13 | import javax.swing.JPanel; | ||
| 14 | |||
| 15 | import cuchaz.enigma.gui.Gui; | ||
| 16 | import cuchaz.enigma.gui.stats.StatsMember; | ||
| 17 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 18 | import cuchaz.enigma.utils.I18n; | ||
| 19 | |||
| 20 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | ||
| 2 | |||
| 3 | import java.awt.event.MouseEvent; | ||
| 4 | |||
| 5 | import javax.swing.JTabbedPane; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.config.Config; | ||
| 4 | import cuchaz.enigma.gui.config.Themes; | ||
| 5 | import cuchaz.enigma.gui.Gui; | ||
| 6 | import cuchaz.enigma.gui.dialog.AboutDialog; | ||
| 7 | import cuchaz.enigma.gui.dialog.ChangeDialog; | ||
| 8 | import cuchaz.enigma.gui.dialog.ConnectToServerDialog; | ||
| 9 | import cuchaz.enigma.gui.dialog.CreateServerDialog; | ||
| 10 | import cuchaz.enigma.gui.dialog.StatsDialog; | ||
| 11 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 12 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 13 | import cuchaz.enigma.utils.I18n; | ||
| 14 | import cuchaz.enigma.utils.Pair; | ||
| 15 | |||
| 16 | import java.awt.Desktop; | ||
| 17 | import java.awt.event.InputEvent; | ||
| 18 | import java.awt.event.KeyEvent; | ||
| 19 | import java.io.File; | ||
| 20 | import java.io.IOException; | ||
| 21 | import java.net.URISyntaxException; | ||
| 22 | import java.net.URL; | ||
| 23 | import java.nio.file.Files; | ||
| 24 | import java.nio.file.Path; | ||
| 25 | import java.nio.file.Paths; | ||
| 26 | import java.util.*; | ||
| 27 | import java.util.List; | ||
| 28 | import java.util.stream.Collectors; | ||
| 29 | import java.util.stream.IntStream; | ||
| 30 | import javax.swing.*; | ||
| 31 | |||
| 32 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.Gui; | ||
| 4 | import cuchaz.enigma.utils.I18n; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import java.awt.event.InputEvent; | ||
| 8 | import java.awt.event.KeyEvent; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.filechooser; | ||
| 2 | |||
| 3 | import javax.swing.*; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.filechooser; | ||
| 2 | |||
| 3 | import javax.swing.*; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.filechooser; | ||
| 2 | |||
| 3 | import javax.swing.*; | ||
| 4 | |||
| 5 | public 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 | |||
| 12 | package cuchaz.enigma.gui.highlight; | ||
| 13 | |||
| 14 | import cuchaz.enigma.gui.config.Config; | ||
| 15 | |||
| 16 | import javax.swing.text.BadLocationException; | ||
| 17 | import javax.swing.text.Highlighter; | ||
| 18 | import javax.swing.text.JTextComponent; | ||
| 19 | import java.awt.*; | ||
| 20 | |||
| 21 | public 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 | |||
| 12 | package cuchaz.enigma.gui.highlight; | ||
| 13 | |||
| 14 | import cuchaz.enigma.gui.config.Config; | ||
| 15 | |||
| 16 | import javax.swing.text.Highlighter; | ||
| 17 | import javax.swing.text.JTextComponent; | ||
| 18 | import java.awt.*; | ||
| 19 | |||
| 20 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.highlight; | ||
| 2 | |||
| 3 | public 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 | |||
| 12 | package cuchaz.enigma.gui.node; | ||
| 13 | |||
| 14 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 15 | |||
| 16 | import javax.swing.tree.DefaultMutableTreeNode; | ||
| 17 | |||
| 18 | public 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 | |||
| 12 | package cuchaz.enigma.gui.node; | ||
| 13 | |||
| 14 | import javax.swing.tree.DefaultMutableTreeNode; | ||
| 15 | |||
| 16 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.panels; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.ClassSelector; | ||
| 4 | import cuchaz.enigma.gui.Gui; | ||
| 5 | import cuchaz.enigma.utils.I18n; | ||
| 6 | |||
| 7 | import javax.swing.*; | ||
| 8 | import java.awt.*; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.panels; | ||
| 2 | |||
| 3 | import cuchaz.enigma.EnigmaProject; | ||
| 4 | import cuchaz.enigma.analysis.EntryReference; | ||
| 5 | import cuchaz.enigma.gui.config.Config; | ||
| 6 | import cuchaz.enigma.gui.BrowserCaret; | ||
| 7 | import cuchaz.enigma.gui.Gui; | ||
| 8 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 9 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 10 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 11 | |||
| 12 | import javax.swing.*; | ||
| 13 | import java.awt.*; | ||
| 14 | import java.awt.event.KeyAdapter; | ||
| 15 | import java.awt.event.KeyEvent; | ||
| 16 | import java.awt.event.MouseAdapter; | ||
| 17 | import java.awt.event.MouseEvent; | ||
| 18 | |||
| 19 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.panels; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.Gui; | ||
| 4 | import cuchaz.enigma.gui.util.GuiUtil; | ||
| 5 | import cuchaz.enigma.utils.I18n; | ||
| 6 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 7 | |||
| 8 | import javax.swing.*; | ||
| 9 | import java.awt.*; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.panels; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.ClassSelector; | ||
| 4 | import cuchaz.enigma.gui.Gui; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 6 | import cuchaz.enigma.utils.I18n; | ||
| 7 | |||
| 8 | import javax.swing.*; | ||
| 9 | import java.awt.*; | ||
| 10 | import java.util.Comparator; | ||
| 11 | |||
| 12 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.search; | ||
| 2 | |||
| 3 | import java.util.List; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.search; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | import java.util.concurrent.Executor; | ||
| 5 | import java.util.concurrent.Executors; | ||
| 6 | import java.util.concurrent.atomic.AtomicBoolean; | ||
| 7 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 8 | import java.util.concurrent.locks.Lock; | ||
| 9 | import java.util.concurrent.locks.ReentrantLock; | ||
| 10 | import java.util.function.BiFunction; | ||
| 11 | import java.util.stream.Collectors; | ||
| 12 | import java.util.stream.Stream; | ||
| 13 | |||
| 14 | import cuchaz.enigma.utils.Pair; | ||
| 15 | |||
| 16 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.stats; | ||
| 2 | |||
| 3 | import com.google.gson.GsonBuilder; | ||
| 4 | import cuchaz.enigma.EnigmaProject; | ||
| 5 | import cuchaz.enigma.ProgressListener; | ||
| 6 | import cuchaz.enigma.analysis.index.EntryIndex; | ||
| 7 | import cuchaz.enigma.api.service.NameProposalService; | ||
| 8 | import cuchaz.enigma.api.service.ObfuscationTestService; | ||
| 9 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 10 | import cuchaz.enigma.translation.mapping.EntryResolver; | ||
| 11 | import cuchaz.enigma.translation.mapping.ResolutionStrategy; | ||
| 12 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 13 | import cuchaz.enigma.translation.representation.entry.*; | ||
| 14 | import cuchaz.enigma.utils.I18n; | ||
| 15 | |||
| 16 | import java.util.*; | ||
| 17 | |||
| 18 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.stats; | ||
| 2 | |||
| 3 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import java.awt.Component; | ||
| 4 | import java.awt.event.MouseEvent; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import javax.swing.border.Border; | ||
| 8 | |||
| 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import javax.swing.*; | ||
| 4 | import javax.swing.text.BadLocationException; | ||
| 5 | import javax.swing.text.JTextComponent; | ||
| 6 | import java.awt.*; | ||
| 7 | import java.awt.event.MouseEvent; | ||
| 8 | import java.io.IOException; | ||
| 9 | import java.net.URI; | ||
| 10 | import java.net.URISyntaxException; | ||
| 11 | import java.util.Locale; | ||
| 12 | import java.util.StringJoiner; | ||
| 13 | |||
| 14 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import com.google.common.collect.Queues; | ||
| 4 | |||
| 5 | import java.util.Deque; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | @FunctionalInterface | ||
| 4 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.util; | ||
| 2 | |||
| 3 | import java.awt.Dimension; | ||
| 4 | import java.awt.Font; | ||
| 5 | import java.io.IOException; | ||
| 6 | import java.lang.reflect.Field; | ||
| 7 | import java.util.ArrayList; | ||
| 8 | import java.util.List; | ||
| 9 | |||
| 10 | import javax.swing.BorderFactory; | ||
| 11 | import javax.swing.UIManager; | ||
| 12 | import javax.swing.border.Border; | ||
| 13 | |||
| 14 | import com.github.swingdpi.UiDefaultsScaler; | ||
| 15 | import com.github.swingdpi.plaf.BasicTweaker; | ||
| 16 | import com.github.swingdpi.plaf.MetalTweaker; | ||
| 17 | import com.github.swingdpi.plaf.NimbusTweaker; | ||
| 18 | import com.github.swingdpi.plaf.WindowsTweaker; | ||
| 19 | import cuchaz.enigma.gui.config.Config; | ||
| 20 | import de.sciss.syntaxpane.DefaultSyntaxKit; | ||
| 21 | |||
| 22 | public 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> | ||