diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | build.gradle | 1 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/ClassSelector.java | 24 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/Gui.java | 29 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java | 310 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/elements/MenuBar.java | 12 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java | 2 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java | 75 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java | 6 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/utils/search/SearchEntry.java | 17 | ||||
| -rw-r--r-- | src/main/java/cuchaz/enigma/utils/search/SearchUtil.java | 195 | ||||
| -rw-r--r-- | src/main/resources/lang/en_us.json | 2 |
12 files changed, 550 insertions, 124 deletions
| @@ -12,7 +12,6 @@ Enigma includes the following open-source libraries: | |||
| 12 | - [Guava](https://github.com/google/guava) (Apache-2.0) | 12 | - [Guava](https://github.com/google/guava) (Apache-2.0) |
| 13 | - [SyntaxPane](https://github.com/Sciss/SyntaxPane) (Apache-2.0) | 13 | - [SyntaxPane](https://github.com/Sciss/SyntaxPane) (Apache-2.0) |
| 14 | - [Darcula](https://github.com/bulenkov/Darcula) (Apache-2.0) | 14 | - [Darcula](https://github.com/bulenkov/Darcula) (Apache-2.0) |
| 15 | - [fuzzywuzzy](https://github.com/xdrop/fuzzywuzzy/) (GPL-3.0) | ||
| 16 | - [jopt-simple](https://github.com/jopt-simple/jopt-simple) (MIT) | 15 | - [jopt-simple](https://github.com/jopt-simple/jopt-simple) (MIT) |
| 17 | - [ASM](https://asm.ow2.io/) (BSD-3-Clause) | 16 | - [ASM](https://asm.ow2.io/) (BSD-3-Clause) |
| 18 | 17 | ||
diff --git a/build.gradle b/build.gradle index f771ec7c..a42b2257 100644 --- a/build.gradle +++ b/build.gradle | |||
| @@ -53,7 +53,6 @@ dependencies { | |||
| 53 | implementation 'net.fabricmc:cfr:0.0.1' | 53 | implementation 'net.fabricmc:cfr:0.0.1' |
| 54 | implementation 'com.bulenkov:darcula:1.0.0-bobbylight' | 54 | implementation 'com.bulenkov:darcula:1.0.0-bobbylight' |
| 55 | implementation 'de.sciss:syntaxpane:1.2.0' | 55 | implementation 'de.sciss:syntaxpane:1.2.0' |
| 56 | implementation 'me.xdrop:fuzzywuzzy:1.2.0' | ||
| 57 | implementation 'com.github.lukeu:swing-dpi:0.6' | 56 | implementation 'com.github.lukeu:swing-dpi:0.6' |
| 58 | 57 | ||
| 59 | testImplementation 'junit:junit:4.+' | 58 | testImplementation 'junit:junit:4.+' |
diff --git a/src/main/java/cuchaz/enigma/gui/ClassSelector.java b/src/main/java/cuchaz/enigma/gui/ClassSelector.java index 5051032d..a23e24c2 100644 --- a/src/main/java/cuchaz/enigma/gui/ClassSelector.java +++ b/src/main/java/cuchaz/enigma/gui/ClassSelector.java | |||
| @@ -11,6 +11,17 @@ | |||
| 11 | 11 | ||
| 12 | package cuchaz.enigma.gui; | 12 | package cuchaz.enigma.gui; |
| 13 | 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 | |||
| 14 | import com.google.common.collect.ArrayListMultimap; | 25 | import com.google.common.collect.ArrayListMultimap; |
| 15 | import com.google.common.collect.Lists; | 26 | import com.google.common.collect.Lists; |
| 16 | import com.google.common.collect.Maps; | 27 | import com.google.common.collect.Maps; |
| @@ -21,15 +32,6 @@ import cuchaz.enigma.throwables.IllegalNameException; | |||
| 21 | import cuchaz.enigma.translation.Translator; | 32 | import cuchaz.enigma.translation.Translator; |
| 22 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 33 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 23 | 34 | ||
| 24 | import javax.annotation.Nullable; | ||
| 25 | import javax.swing.*; | ||
| 26 | import javax.swing.event.CellEditorListener; | ||
| 27 | import javax.swing.event.ChangeEvent; | ||
| 28 | import javax.swing.tree.*; | ||
| 29 | import java.awt.event.MouseAdapter; | ||
| 30 | import java.awt.event.MouseEvent; | ||
| 31 | import java.util.*; | ||
| 32 | |||
| 33 | public class ClassSelector extends JTree { | 35 | public class ClassSelector extends JTree { |
| 34 | 36 | ||
| 35 | public static final Comparator<ClassEntry> DEOBF_CLASS_COMPARATOR = Comparator.comparing(ClassEntry::getFullName); | 37 | public static final Comparator<ClassEntry> DEOBF_CLASS_COMPARATOR = Comparator.comparing(ClassEntry::getFullName); |
| @@ -420,7 +422,9 @@ public class ClassSelector extends JTree { | |||
| 420 | for (ClassSelectorPackageNode packageNode : packageNodes()) { | 422 | for (ClassSelectorPackageNode packageNode : packageNodes()) { |
| 421 | for (ClassSelectorClassNode classNode : classNodes(packageNode)) { | 423 | for (ClassSelectorClassNode classNode : classNodes(packageNode)) { |
| 422 | if (classNode.getClassEntry().equals(classEntry)) { | 424 | if (classNode.getClassEntry().equals(classEntry)) { |
| 423 | setSelectionPath(new TreePath(new Object[]{getModel().getRoot(), packageNode, classNode})); | 425 | TreePath path = new TreePath(new Object[]{getModel().getRoot(), packageNode, classNode}); |
| 426 | setSelectionPath(path); | ||
| 427 | scrollPathToVisible(path); | ||
| 424 | } | 428 | } |
| 425 | } | 429 | } |
| 426 | } | 430 | } |
diff --git a/src/main/java/cuchaz/enigma/gui/Gui.java b/src/main/java/cuchaz/enigma/gui/Gui.java index 8f0d6fac..3412cd51 100644 --- a/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/src/main/java/cuchaz/enigma/gui/Gui.java | |||
| @@ -33,6 +33,7 @@ import cuchaz.enigma.config.Config; | |||
| 33 | import cuchaz.enigma.config.Themes; | 33 | import cuchaz.enigma.config.Themes; |
| 34 | import cuchaz.enigma.gui.dialog.CrashDialog; | 34 | import cuchaz.enigma.gui.dialog.CrashDialog; |
| 35 | import cuchaz.enigma.gui.dialog.JavadocDialog; | 35 | import cuchaz.enigma.gui.dialog.JavadocDialog; |
| 36 | import cuchaz.enigma.gui.dialog.SearchDialog; | ||
| 36 | import cuchaz.enigma.gui.elements.MenuBar; | 37 | import cuchaz.enigma.gui.elements.MenuBar; |
| 37 | import cuchaz.enigma.gui.elements.PopupMenuBar; | 38 | import cuchaz.enigma.gui.elements.PopupMenuBar; |
| 38 | import cuchaz.enigma.gui.filechooser.FileChooserAny; | 39 | import cuchaz.enigma.gui.filechooser.FileChooserAny; |
| @@ -67,6 +68,7 @@ public class Gui { | |||
| 67 | 68 | ||
| 68 | public FileDialog jarFileChooser; | 69 | public FileDialog jarFileChooser; |
| 69 | public FileDialog tinyMappingsFileChooser; | 70 | public FileDialog tinyMappingsFileChooser; |
| 71 | public SearchDialog searchDialog; | ||
| 70 | public JFileChooser enigmaMappingsFileChooser; | 72 | public JFileChooser enigmaMappingsFileChooser; |
| 71 | public JFileChooser exportSourceFileChooser; | 73 | public JFileChooser exportSourceFileChooser; |
| 72 | public FileDialog exportJarFileChooser; | 74 | public FileDialog exportJarFileChooser; |
| @@ -811,16 +813,15 @@ public class Gui { | |||
| 811 | public void close() { | 813 | public void close() { |
| 812 | if (!this.controller.isDirty()) { | 814 | if (!this.controller.isDirty()) { |
| 813 | // everything is saved, we can exit safely | 815 | // everything is saved, we can exit safely |
| 814 | this.frame.dispose(); | 816 | exit(); |
| 815 | System.exit(0); | ||
| 816 | } else { | 817 | } else { |
| 817 | // ask to save before closing | 818 | // ask to save before closing |
| 818 | showDiscardDiag((response) -> { | 819 | showDiscardDiag((response) -> { |
| 819 | if (response == JOptionPane.YES_OPTION) { | 820 | if (response == JOptionPane.YES_OPTION) { |
| 820 | this.saveMapping(); | 821 | this.saveMapping(); |
| 821 | this.frame.dispose(); | 822 | exit(); |
| 822 | } else if (response == JOptionPane.NO_OPTION) { | 823 | } else if (response == JOptionPane.NO_OPTION) { |
| 823 | this.frame.dispose(); | 824 | exit(); |
| 824 | } | 825 | } |
| 825 | 826 | ||
| 826 | return null; | 827 | return null; |
| @@ -828,6 +829,14 @@ public class Gui { | |||
| 828 | } | 829 | } |
| 829 | } | 830 | } |
| 830 | 831 | ||
| 832 | private void exit() { | ||
| 833 | if (searchDialog != null) { | ||
| 834 | searchDialog.dispose(); | ||
| 835 | } | ||
| 836 | this.frame.dispose(); | ||
| 837 | System.exit(0); | ||
| 838 | } | ||
| 839 | |||
| 831 | public void redraw() { | 840 | public void redraw() { |
| 832 | this.frame.validate(); | 841 | this.frame.validate(); |
| 833 | this.frame.repaint(); | 842 | this.frame.repaint(); |
| @@ -892,6 +901,10 @@ public class Gui { | |||
| 892 | this.obfPanel.obfClasses.restoreExpansionState(this.obfPanel.obfClasses, stateObf); | 901 | this.obfPanel.obfClasses.restoreExpansionState(this.obfPanel.obfClasses, stateObf); |
| 893 | } | 902 | } |
| 894 | 903 | ||
| 904 | public PanelObf getObfPanel() { | ||
| 905 | return obfPanel; | ||
| 906 | } | ||
| 907 | |||
| 895 | public PanelDeobf getDeobfPanel() { | 908 | public PanelDeobf getDeobfPanel() { |
| 896 | return deobfPanel; | 909 | return deobfPanel; |
| 897 | } | 910 | } |
| @@ -899,4 +912,12 @@ public class Gui { | |||
| 899 | public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) { | 912 | public void setShouldNavigateOnClick(boolean shouldNavigateOnClick) { |
| 900 | this.shouldNavigateOnClick = shouldNavigateOnClick; | 913 | this.shouldNavigateOnClick = shouldNavigateOnClick; |
| 901 | } | 914 | } |
| 915 | |||
| 916 | public SearchDialog getSearchDialog() { | ||
| 917 | if (searchDialog == null) { | ||
| 918 | searchDialog = new SearchDialog(this); | ||
| 919 | } | ||
| 920 | return searchDialog; | ||
| 921 | } | ||
| 922 | |||
| 902 | } | 923 | } |
diff --git a/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java index 56ce7515..b36ebfb4 100644 --- a/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java +++ b/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java | |||
| @@ -11,150 +11,248 @@ | |||
| 11 | 11 | ||
| 12 | package cuchaz.enigma.gui.dialog; | 12 | package cuchaz.enigma.gui.dialog; |
| 13 | 13 | ||
| 14 | import com.google.common.collect.Lists; | 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 | |||
| 15 | import cuchaz.enigma.gui.Gui; | 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; | ||
| 16 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 31 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 17 | import cuchaz.enigma.utils.I18n; | 32 | import cuchaz.enigma.utils.I18n; |
| 18 | import cuchaz.enigma.gui.util.ScaleUtil; | 33 | import cuchaz.enigma.utils.search.SearchEntry; |
| 19 | import me.xdrop.fuzzywuzzy.FuzzySearch; | 34 | import cuchaz.enigma.utils.search.SearchUtil; |
| 20 | import me.xdrop.fuzzywuzzy.model.ExtractedResult; | ||
| 21 | |||
| 22 | import javax.swing.*; | ||
| 23 | import javax.swing.border.EmptyBorder; | ||
| 24 | import java.awt.*; | ||
| 25 | import java.awt.event.*; | ||
| 26 | import java.util.List; | ||
| 27 | import java.util.function.Consumer; | ||
| 28 | import java.util.stream.Collectors; | ||
| 29 | 35 | ||
| 30 | public class SearchDialog { | 36 | public class SearchDialog { |
| 31 | 37 | ||
| 32 | private JTextField searchField; | 38 | private final JTextField searchField; |
| 33 | private JList<String> classList; | 39 | private final DefaultListModel<SearchEntryImpl> classListModel; |
| 34 | private JFrame frame; | 40 | private final JList<SearchEntryImpl> classList; |
| 35 | 41 | private final JDialog dialog; | |
| 36 | private Gui parent; | ||
| 37 | private List<ClassEntry> deobfClasses; | ||
| 38 | 42 | ||
| 39 | private KeyEventDispatcher keyEventDispatcher; | 43 | private final Gui parent; |
| 44 | private final SearchUtil<SearchEntryImpl> su; | ||
| 40 | 45 | ||
| 41 | public SearchDialog(Gui parent) { | 46 | public SearchDialog(Gui parent) { |
| 42 | this.parent = parent; | 47 | this.parent = parent; |
| 43 | 48 | ||
| 44 | deobfClasses = Lists.newArrayList(); | 49 | su = new SearchUtil<>(); |
| 45 | this.parent.getController().addSeparatedClasses(Lists.newArrayList(), deobfClasses); | ||
| 46 | deobfClasses.removeIf(ClassEntry::isInnerClass); | ||
| 47 | } | ||
| 48 | 50 | ||
| 49 | public void show() { | 51 | dialog = new JDialog(parent.getFrame(), I18n.translate("menu.view.search"), true); |
| 50 | frame = new JFrame(I18n.translate("menu.view.search")); | 52 | JPanel contentPane = new JPanel(); |
| 51 | frame.setVisible(false); | 53 | contentPane.setBorder(ScaleUtil.createEmptyBorder(4, 4, 4, 4)); |
| 52 | JPanel pane = new JPanel(); | 54 | contentPane.setLayout(new BorderLayout(ScaleUtil.scale(4), ScaleUtil.scale(4))); |
| 53 | pane.setBorder(new EmptyBorder(5, 10, 5, 10)); | ||
| 54 | |||
| 55 | addRow(pane, jPanel -> { | ||
| 56 | searchField = new JTextField("", 20); | ||
| 57 | |||
| 58 | searchField.addKeyListener(new KeyAdapter() { | ||
| 59 | @Override | ||
| 60 | public void keyTyped(KeyEvent keyEvent) { | ||
| 61 | updateList(); | ||
| 62 | } | ||
| 63 | }); | ||
| 64 | 55 | ||
| 65 | jPanel.add(searchField); | 56 | searchField = new JTextField(); |
| 66 | }); | 57 | searchField.getDocument().addDocumentListener(new DocumentListener() { |
| 67 | |||
| 68 | addRow(pane, jPanel -> { | ||
| 69 | classList = new JList<>(); | ||
| 70 | classList.setLayoutOrientation(JList.VERTICAL); | ||
| 71 | classList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); | ||
| 72 | |||
| 73 | classList.addMouseListener(new MouseAdapter() { | ||
| 74 | @Override | ||
| 75 | public void mouseClicked(MouseEvent mouseEvent) { | ||
| 76 | if(mouseEvent.getClickCount() >= 2){ | ||
| 77 | openSelected(); | ||
| 78 | } | ||
| 79 | } | ||
| 80 | }); | ||
| 81 | jPanel.add(classList); | ||
| 82 | }); | ||
| 83 | 58 | ||
| 59 | @Override | ||
| 60 | public void insertUpdate(DocumentEvent e) { | ||
| 61 | updateList(); | ||
| 62 | } | ||
| 84 | 63 | ||
| 85 | keyEventDispatcher = keyEvent -> { | 64 | @Override |
| 86 | if(!frame.isVisible()){ | 65 | public void removeUpdate(DocumentEvent e) { |
| 87 | return false; | 66 | updateList(); |
| 88 | } | 67 | } |
| 89 | if(keyEvent.getKeyCode() == KeyEvent.VK_DOWN){ | 68 | |
| 90 | int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1; | 69 | @Override |
| 91 | classList.setSelectedIndex(next); | 70 | public void changedUpdate(DocumentEvent e) { |
| 71 | updateList(); | ||
| 92 | } | 72 | } |
| 93 | if(keyEvent.getKeyCode() == KeyEvent.VK_UP){ | 73 | |
| 94 | int next = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1; | 74 | }); |
| 95 | classList.setSelectedIndex(next); | 75 | searchField.addKeyListener(new KeyAdapter() { |
| 76 | @Override | ||
| 77 | public void keyPressed(KeyEvent e) { | ||
| 78 | if (e.getKeyCode() == KeyEvent.VK_DOWN) { | ||
| 79 | int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1; | ||
| 80 | classList.setSelectedIndex(next); | ||
| 81 | classList.ensureIndexIsVisible(next); | ||
| 82 | } else if (e.getKeyCode() == KeyEvent.VK_UP) { | ||
| 83 | int prev = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1; | ||
| 84 | classList.setSelectedIndex(prev); | ||
| 85 | classList.ensureIndexIsVisible(prev); | ||
| 86 | } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { | ||
| 87 | close(); | ||
| 88 | } | ||
| 96 | } | 89 | } |
| 97 | if(keyEvent.getKeyCode() == KeyEvent.VK_ENTER){ | 90 | }); |
| 98 | openSelected(); | 91 | searchField.addActionListener(e -> openSelected()); |
| 92 | contentPane.add(searchField, BorderLayout.NORTH); | ||
| 93 | |||
| 94 | classListModel = new DefaultListModel<>(); | ||
| 95 | classList = new JList<>(); | ||
| 96 | classList.setModel(classListModel); | ||
| 97 | classList.setCellRenderer(new ListCellRendererImpl()); | ||
| 98 | classList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); | ||
| 99 | classList.addMouseListener(new MouseAdapter() { | ||
| 100 | @Override | ||
| 101 | public void mouseClicked(MouseEvent mouseEvent) { | ||
| 102 | if (mouseEvent.getClickCount() >= 2) { | ||
| 103 | int idx = classList.locationToIndex(mouseEvent.getPoint()); | ||
| 104 | SearchEntryImpl entry = classList.getModel().getElementAt(idx); | ||
| 105 | openEntry(entry); | ||
| 106 | } | ||
| 99 | } | 107 | } |
| 100 | if(keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE){ | 108 | }); |
| 101 | close(); | 109 | contentPane.add(new JScrollPane(classList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER); |
| 110 | |||
| 111 | JPanel buttonBar = new JPanel(); | ||
| 112 | buttonBar.setLayout(new FlowLayout(FlowLayout.RIGHT)); | ||
| 113 | JButton open = new JButton(I18n.translate("prompt.open")); | ||
| 114 | open.addActionListener(event -> openSelected()); | ||
| 115 | buttonBar.add(open); | ||
| 116 | JButton cancel = new JButton(I18n.translate("prompt.cancel")); | ||
| 117 | cancel.addActionListener(event -> close()); | ||
| 118 | buttonBar.add(cancel); | ||
| 119 | contentPane.add(buttonBar, BorderLayout.SOUTH); | ||
| 120 | |||
| 121 | // apparently the class list doesn't update by itself when the list | ||
| 122 | // state changes and the dialog is hidden | ||
| 123 | dialog.addComponentListener(new ComponentAdapter() { | ||
| 124 | @Override | ||
| 125 | public void componentShown(ComponentEvent e) { | ||
| 126 | classList.updateUI(); | ||
| 102 | } | 127 | } |
| 103 | return false; | 128 | }); |
| 104 | }; | ||
| 105 | 129 | ||
| 106 | KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEventDispatcher); | 130 | dialog.setContentPane(contentPane); |
| 131 | dialog.setSize(ScaleUtil.getDimension(400, 500)); | ||
| 132 | dialog.setLocationRelativeTo(parent.getFrame()); | ||
| 133 | } | ||
| 134 | |||
| 135 | public void show() { | ||
| 136 | su.clear(); | ||
| 137 | parent.getController().project.getJarIndex().getEntryIndex().getClasses().parallelStream() | ||
| 138 | .filter(e -> !e.isInnerClass()) | ||
| 139 | .map(e -> SearchEntryImpl.from(e, parent.getController())) | ||
| 140 | .map(SearchUtil.Entry::from) | ||
| 141 | .sequential() | ||
| 142 | .forEach(su::add); | ||
| 107 | 143 | ||
| 108 | frame.setContentPane(pane); | 144 | updateList(); |
| 109 | frame.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS)); | ||
| 110 | 145 | ||
| 111 | frame.setSize(ScaleUtil.getDimension(360, 500)); | 146 | searchField.requestFocus(); |
| 112 | frame.setAlwaysOnTop(true); | 147 | searchField.selectAll(); |
| 113 | frame.setResizable(false); | ||
| 114 | frame.setLocationRelativeTo(parent.getFrame()); | ||
| 115 | frame.setVisible(true); | ||
| 116 | frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); | ||
| 117 | 148 | ||
| 118 | searchField.requestFocusInWindow(); | 149 | dialog.setVisible(true); |
| 119 | } | 150 | } |
| 120 | 151 | ||
| 121 | private void openSelected(){ | 152 | private void openSelected() { |
| 122 | close(); | 153 | SearchEntryImpl selectedValue = classList.getSelectedValue(); |
| 123 | if(classList.isSelectionEmpty()){ | 154 | if (selectedValue != null) { |
| 124 | return; | 155 | openEntry(selectedValue); |
| 125 | } | 156 | } |
| 126 | deobfClasses.stream() | ||
| 127 | .filter(classEntry -> classEntry.getSimpleName().equals(classList.getSelectedValue())). | ||
| 128 | findFirst() | ||
| 129 | .ifPresent(classEntry -> { | ||
| 130 | parent.getController().navigateTo(classEntry); | ||
| 131 | parent.getDeobfPanel().deobfClasses.setSelectionClass(classEntry); | ||
| 132 | }); | ||
| 133 | } | 157 | } |
| 134 | 158 | ||
| 135 | private void close(){ | 159 | private void openEntry(SearchEntryImpl e) { |
| 136 | frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING)); | 160 | close(); |
| 137 | KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(keyEventDispatcher); | 161 | su.hit(e); |
| 162 | parent.getController().navigateTo(e.obf); | ||
| 163 | if (e.deobf != null) { | ||
| 164 | parent.getDeobfPanel().deobfClasses.setSelectionClass(e.deobf); | ||
| 165 | } else { | ||
| 166 | parent.getObfPanel().obfClasses.setSelectionClass(e.obf); | ||
| 167 | } | ||
| 138 | } | 168 | } |
| 139 | 169 | ||
| 140 | private void addRow(JPanel pane, Consumer<JPanel> consumer) { | 170 | private void close() { |
| 141 | JPanel panel = new JPanel(new FlowLayout()); | 171 | dialog.setVisible(false); |
| 142 | consumer.accept(panel); | ||
| 143 | pane.add(panel, BorderLayout.CENTER); | ||
| 144 | } | 172 | } |
| 145 | 173 | ||
| 146 | //Updates the list of class names | 174 | // Updates the list of class names |
| 147 | private void updateList() { | 175 | private void updateList() { |
| 148 | DefaultListModel<String> listModel = new DefaultListModel<>(); | 176 | classListModel.clear(); |
| 149 | 177 | ||
| 150 | //Basic search using the Fuzzy libary | 178 | su.search(searchField.getText()) |
| 151 | //TODO improve on this, to not just work from string and to keep the ClassEntry | 179 | .limit(100) |
| 152 | List<ExtractedResult> results = FuzzySearch.extractTop(searchField.getText(), deobfClasses.stream().map(ClassEntry::getSimpleName).collect(Collectors.toList()), 25); | 180 | .forEach(classListModel::addElement); |
| 153 | results.forEach(extractedResult -> listModel.addElement(extractedResult.getString())); | 181 | } |
| 154 | 182 | ||
| 155 | classList.setModel(listModel); | 183 | public void dispose() { |
| 184 | dialog.dispose(); | ||
| 156 | } | 185 | } |
| 157 | 186 | ||
| 187 | private static final class SearchEntryImpl implements SearchEntry { | ||
| 188 | |||
| 189 | public final ClassEntry obf; | ||
| 190 | public final ClassEntry deobf; | ||
| 158 | 191 | ||
| 192 | private SearchEntryImpl(ClassEntry obf, ClassEntry deobf) { | ||
| 193 | this.obf = obf; | ||
| 194 | this.deobf = deobf; | ||
| 195 | } | ||
| 196 | |||
| 197 | @Override | ||
| 198 | public List<String> getSearchableNames() { | ||
| 199 | if (deobf != null) { | ||
| 200 | return Arrays.asList(obf.getSimpleName(), deobf.getSimpleName()); | ||
| 201 | } else { | ||
| 202 | return Collections.singletonList(obf.getSimpleName()); | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | @Override | ||
| 207 | public String getIdentifier() { | ||
| 208 | return obf.getFullName(); | ||
| 209 | } | ||
| 210 | |||
| 211 | @Override | ||
| 212 | public String toString() { | ||
| 213 | return String.format("SearchEntryImpl { obf: %s, deobf: %s }", obf, deobf); | ||
| 214 | } | ||
| 215 | |||
| 216 | public static SearchEntryImpl from(ClassEntry e, GuiController controller) { | ||
| 217 | ClassEntry deobf = controller.project.getMapper().deobfuscate(e); | ||
| 218 | if (deobf.equals(e)) deobf = null; | ||
| 219 | return new SearchEntryImpl(e, deobf); | ||
| 220 | } | ||
| 221 | |||
| 222 | } | ||
| 223 | |||
| 224 | private static final class ListCellRendererImpl extends AbstractListCellRenderer<SearchEntryImpl> { | ||
| 225 | |||
| 226 | private final JLabel mainName; | ||
| 227 | private final JLabel secondaryName; | ||
| 228 | |||
| 229 | public ListCellRendererImpl() { | ||
| 230 | this.setLayout(new BorderLayout()); | ||
| 231 | |||
| 232 | mainName = new JLabel(); | ||
| 233 | this.add(mainName, BorderLayout.WEST); | ||
| 234 | |||
| 235 | secondaryName = new JLabel(); | ||
| 236 | secondaryName.setFont(secondaryName.getFont().deriveFont(Font.ITALIC)); | ||
| 237 | secondaryName.setForeground(Color.GRAY); | ||
| 238 | this.add(secondaryName, BorderLayout.EAST); | ||
| 239 | } | ||
| 240 | |||
| 241 | @Override | ||
| 242 | public void updateUiForEntry(JList<? extends SearchEntryImpl> list, SearchEntryImpl value, int index, boolean isSelected, boolean cellHasFocus) { | ||
| 243 | if (value.deobf == null) { | ||
| 244 | mainName.setText(value.obf.getSimpleName()); | ||
| 245 | mainName.setToolTipText(value.obf.getFullName()); | ||
| 246 | secondaryName.setText(""); | ||
| 247 | secondaryName.setToolTipText(""); | ||
| 248 | } else { | ||
| 249 | mainName.setText(value.deobf.getSimpleName()); | ||
| 250 | mainName.setToolTipText(value.deobf.getFullName()); | ||
| 251 | secondaryName.setText(value.obf.getSimpleName()); | ||
| 252 | secondaryName.setToolTipText(value.obf.getFullName()); | ||
| 253 | } | ||
| 254 | } | ||
| 255 | |||
| 256 | } | ||
| 159 | 257 | ||
| 160 | } | 258 | } |
diff --git a/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java index fd521aba..8098178b 100644 --- a/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java +++ b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java | |||
| @@ -29,6 +29,16 @@ import cuchaz.enigma.translation.mapping.serde.MappingFormat; | |||
| 29 | import cuchaz.enigma.utils.I18n; | 29 | import cuchaz.enigma.utils.I18n; |
| 30 | import cuchaz.enigma.utils.Pair; | 30 | import cuchaz.enigma.utils.Pair; |
| 31 | 31 | ||
| 32 | import javax.swing.*; | ||
| 33 | |||
| 34 | import cuchaz.enigma.config.Config; | ||
| 35 | import cuchaz.enigma.config.Themes; | ||
| 36 | import cuchaz.enigma.gui.Gui; | ||
| 37 | import cuchaz.enigma.gui.dialog.AboutDialog; | ||
| 38 | import cuchaz.enigma.gui.stats.StatsMember; | ||
| 39 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 40 | import cuchaz.enigma.utils.I18n; | ||
| 41 | |||
| 32 | public class MenuBar extends JMenuBar { | 42 | public class MenuBar extends JMenuBar { |
| 33 | 43 | ||
| 34 | public final JMenuItem closeJarMenu; | 44 | public final JMenuItem closeJarMenu; |
| @@ -325,7 +335,7 @@ public class MenuBar extends JMenuBar { | |||
| 325 | menu.add(search); | 335 | menu.add(search); |
| 326 | search.addActionListener(event -> { | 336 | search.addActionListener(event -> { |
| 327 | if (this.gui.getController().project != null) { | 337 | if (this.gui.getController().project != null) { |
| 328 | new SearchDialog(this.gui).show(); | 338 | this.gui.getSearchDialog().show(); |
| 329 | } | 339 | } |
| 330 | }); | 340 | }); |
| 331 | 341 | ||
diff --git a/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java b/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java index 8296842c..8637afd9 100644 --- a/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java +++ b/src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java | |||
| @@ -126,7 +126,7 @@ public class PanelEditor extends JEditorPane { | |||
| 126 | public void keyTyped(KeyEvent event) { | 126 | public void keyTyped(KeyEvent event) { |
| 127 | if (!gui.popupMenu.renameMenu.isEnabled()) return; | 127 | if (!gui.popupMenu.renameMenu.isEnabled()) return; |
| 128 | 128 | ||
| 129 | if (!event.isControlDown() && !event.isAltDown()) { | 129 | if (!event.isControlDown() && !event.isAltDown() && Character.isJavaIdentifierPart(event.getKeyChar())) { |
| 130 | EnigmaProject project = gui.getController().project; | 130 | EnigmaProject project = gui.getController().project; |
| 131 | EntryReference<Entry<?>, Entry<?>> reference = project.getMapper().deobfuscate(gui.cursorReference); | 131 | EntryReference<Entry<?>, Entry<?>> reference = project.getMapper().deobfuscate(gui.cursorReference); |
| 132 | Entry<?> entry = reference.getNameableEntry(); | 132 | Entry<?> entry = reference.getNameableEntry(); |
diff --git a/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java b/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java new file mode 100644 index 00000000..e071fe1c --- /dev/null +++ b/src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java | |||
| @@ -0,0 +1,75 @@ | |||
| 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 | public AbstractListCellRenderer() { | ||
| 14 | setBorder(getNoFocusBorder()); | ||
| 15 | } | ||
| 16 | |||
| 17 | protected Border getNoFocusBorder() { | ||
| 18 | Border border = UIManager.getLookAndFeel().getDefaults().getBorder("List.List.cellNoFocusBorder"); | ||
| 19 | if (border == null) { | ||
| 20 | return NO_FOCUS_BORDER; | ||
| 21 | } | ||
| 22 | return border; | ||
| 23 | } | ||
| 24 | |||
| 25 | protected Border getBorder(boolean isSelected, boolean cellHasFocus) { | ||
| 26 | Border b = null; | ||
| 27 | if (cellHasFocus) { | ||
| 28 | UIDefaults defaults = UIManager.getLookAndFeel().getDefaults(); | ||
| 29 | if (isSelected) { | ||
| 30 | b = defaults.getBorder("List.focusSelectedCellHighlightBorder"); | ||
| 31 | } | ||
| 32 | if (b == null) { | ||
| 33 | b = defaults.getBorder("List.focusCellHighlightBorder"); | ||
| 34 | } | ||
| 35 | } else { | ||
| 36 | b = getNoFocusBorder(); | ||
| 37 | } | ||
| 38 | return b; | ||
| 39 | } | ||
| 40 | |||
| 41 | public abstract void updateUiForEntry(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus); | ||
| 42 | |||
| 43 | @Override | ||
| 44 | public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus) { | ||
| 45 | updateUiForEntry(list, value, index, isSelected, cellHasFocus); | ||
| 46 | |||
| 47 | if (isSelected) { | ||
| 48 | setBackground(list.getSelectionBackground()); | ||
| 49 | setForeground(list.getSelectionForeground()); | ||
| 50 | } else { | ||
| 51 | setBackground(list.getBackground()); | ||
| 52 | setForeground(list.getForeground()); | ||
| 53 | } | ||
| 54 | |||
| 55 | setEnabled(list.isEnabled()); | ||
| 56 | setFont(list.getFont()); | ||
| 57 | |||
| 58 | setBorder(getBorder(isSelected, cellHasFocus)); | ||
| 59 | |||
| 60 | // This isn't the width of the cell, but it's close enough for where it's needed (getComponentAt in getToolTipText) | ||
| 61 | setSize(list.getWidth(), getPreferredSize().height); | ||
| 62 | |||
| 63 | return this; | ||
| 64 | } | ||
| 65 | |||
| 66 | @Override | ||
| 67 | public String getToolTipText(MouseEvent event) { | ||
| 68 | Component c = getComponentAt(event.getPoint()); | ||
| 69 | if (c instanceof JComponent) { | ||
| 70 | return ((JComponent) c).getToolTipText(); | ||
| 71 | } | ||
| 72 | return getToolTipText(); | ||
| 73 | } | ||
| 74 | |||
| 75 | } | ||
diff --git a/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java b/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java index 8bc826fc..9f722e9f 100644 --- a/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java +++ b/src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java | |||
| @@ -7,7 +7,9 @@ import java.lang.reflect.Field; | |||
| 7 | import java.util.ArrayList; | 7 | import java.util.ArrayList; |
| 8 | import java.util.List; | 8 | import java.util.List; |
| 9 | 9 | ||
| 10 | import javax.swing.BorderFactory; | ||
| 10 | import javax.swing.UIManager; | 11 | import javax.swing.UIManager; |
| 12 | import javax.swing.border.Border; | ||
| 11 | 13 | ||
| 12 | import com.github.swingdpi.UiDefaultsScaler; | 14 | import com.github.swingdpi.UiDefaultsScaler; |
| 13 | import com.github.swingdpi.plaf.BasicTweaker; | 15 | import com.github.swingdpi.plaf.BasicTweaker; |
| @@ -69,6 +71,10 @@ public class ScaleUtil { | |||
| 69 | return (int) (i * getScaleFactor()); | 71 | return (int) (i * getScaleFactor()); |
| 70 | } | 72 | } |
| 71 | 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 | |||
| 72 | public static int invert(int i) { | 78 | public static int invert(int i) { |
| 73 | return (int) (i / getScaleFactor()); | 79 | return (int) (i / getScaleFactor()); |
| 74 | } | 80 | } |
diff --git a/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java b/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java new file mode 100644 index 00000000..48b255f8 --- /dev/null +++ b/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | package cuchaz.enigma.utils.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/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java b/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java new file mode 100644 index 00000000..e5ed35fd --- /dev/null +++ b/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java | |||
| @@ -0,0 +1,195 @@ | |||
| 1 | package cuchaz.enigma.utils.search; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | import java.util.function.BiFunction; | ||
| 5 | import java.util.stream.Collectors; | ||
| 6 | import java.util.stream.Stream; | ||
| 7 | |||
| 8 | import cuchaz.enigma.utils.Pair; | ||
| 9 | |||
| 10 | public class SearchUtil<T extends SearchEntry> { | ||
| 11 | |||
| 12 | private final Map<T, Entry<T>> entries = new HashMap<>(); | ||
| 13 | private final Map<String, Integer> hitCount = new HashMap<>(); | ||
| 14 | |||
| 15 | public void add(T entry) { | ||
| 16 | Entry<T> e = Entry.from(entry); | ||
| 17 | entries.put(entry, e); | ||
| 18 | } | ||
| 19 | |||
| 20 | public void add(Entry<T> entry) { | ||
| 21 | entries.put(entry.searchEntry, entry); | ||
| 22 | } | ||
| 23 | |||
| 24 | public void addAll(Collection<T> entries) { | ||
| 25 | this.entries.putAll(entries.parallelStream().collect(Collectors.toMap(e -> e, Entry::from))); | ||
| 26 | } | ||
| 27 | |||
| 28 | public void remove(T entry) { | ||
| 29 | entries.remove(entry); | ||
| 30 | } | ||
| 31 | |||
| 32 | public void clear() { | ||
| 33 | entries.clear(); | ||
| 34 | } | ||
| 35 | |||
| 36 | public void clearHits() { | ||
| 37 | hitCount.clear(); | ||
| 38 | } | ||
| 39 | |||
| 40 | public Stream<T> search(String term) { | ||
| 41 | return entries.values().parallelStream() | ||
| 42 | .map(e -> new Pair<>(e, e.getScore(term, hitCount.getOrDefault(e.searchEntry.getIdentifier(), 0)))) | ||
| 43 | .filter(e -> e.b > 0) | ||
| 44 | .sorted(Comparator.comparingDouble(o -> -o.b)) | ||
| 45 | .map(e -> e.a.searchEntry) | ||
| 46 | .sequential(); | ||
| 47 | } | ||
| 48 | |||
| 49 | public void hit(T entry) { | ||
| 50 | if (entries.containsKey(entry)) { | ||
| 51 | hitCount.compute(entry.getIdentifier(), (_id, i) -> i == null ? 1 : i + 1); | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | public static final class Entry<T extends SearchEntry> { | ||
| 56 | |||
| 57 | public final T searchEntry; | ||
| 58 | private final String[][] components; | ||
| 59 | |||
| 60 | private Entry(T searchEntry, String[][] components) { | ||
| 61 | this.searchEntry = searchEntry; | ||
| 62 | this.components = components; | ||
| 63 | } | ||
| 64 | |||
| 65 | public float getScore(String term, int hits) { | ||
| 66 | String ucTerm = term.toUpperCase(Locale.ROOT); | ||
| 67 | float maxScore = (float) Arrays.stream(components) | ||
| 68 | .mapToDouble(name -> getScoreFor(ucTerm, name)) | ||
| 69 | .max().orElse(0.0); | ||
| 70 | return maxScore * (hits + 1); | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Computes the score for the given <code>name</code> against the given search term. | ||
| 75 | * | ||
| 76 | * @param term the search term (expected to be upper-case) | ||
| 77 | * @param name the entry name, split at word boundaries (see {@link Entry#wordwiseSplit(String)}) | ||
| 78 | * @return the computed score for the entry | ||
| 79 | */ | ||
| 80 | private static float getScoreFor(String term, String[] name) { | ||
| 81 | int totalLength = Arrays.stream(name).mapToInt(String::length).sum(); | ||
| 82 | float scorePerChar = 1f / totalLength; | ||
| 83 | |||
| 84 | // This map contains a snapshot of all the states the search has | ||
| 85 | // been in. The keys are the remaining characters of the search | ||
| 86 | // term, the values are the maximum scores for that remaining | ||
| 87 | // search term part. | ||
| 88 | Map<String, Float> snapshots = new HashMap<>(); | ||
| 89 | snapshots.put(term, 0f); | ||
| 90 | |||
| 91 | // For each component, start at each existing snapshot, searching | ||
| 92 | // for the next longest match, and calculate the new score for each | ||
| 93 | // match length until the maximum. Then the new scores are put back | ||
| 94 | // into the snapshot map. | ||
| 95 | for (int componentIndex = 0; componentIndex < name.length; componentIndex++) { | ||
| 96 | String component = name[componentIndex]; | ||
| 97 | float posMultiplier = (name.length - componentIndex) * 0.3f; | ||
| 98 | Map<String, Float> newSnapshots = new HashMap<>(); | ||
| 99 | for (Map.Entry<String, Float> snapshot : snapshots.entrySet()) { | ||
| 100 | String remaining = snapshot.getKey(); | ||
| 101 | float score = snapshot.getValue(); | ||
| 102 | component = component.toUpperCase(Locale.ROOT); | ||
| 103 | int l = compareEqualLength(remaining, component); | ||
| 104 | for (int i = 1; i <= l; i++) { | ||
| 105 | float baseScore = scorePerChar * i; | ||
| 106 | float chainBonus = (i - 1) * 0.5f; | ||
| 107 | merge(newSnapshots, Collections.singletonMap(remaining.substring(i), score + baseScore * posMultiplier + chainBonus), Math::max); | ||
| 108 | } | ||
| 109 | } | ||
| 110 | merge(snapshots, newSnapshots, Math::max); | ||
| 111 | } | ||
| 112 | |||
| 113 | // Only return the score for when the search term was completely | ||
| 114 | // consumed. | ||
| 115 | return snapshots.getOrDefault("", 0f); | ||
| 116 | } | ||
| 117 | |||
| 118 | private static <K, V> void merge(Map<K, V> self, Map<K, V> source, BiFunction<V, V, V> combiner) { | ||
| 119 | source.forEach((k, v) -> self.compute(k, (_k, v1) -> v1 == null ? v : v == null ? v1 : combiner.apply(v, v1))); | ||
| 120 | } | ||
| 121 | |||
| 122 | public static <T extends SearchEntry> Entry<T> from(T e) { | ||
| 123 | String[][] components = e.getSearchableNames().parallelStream() | ||
| 124 | .map(Entry::wordwiseSplit) | ||
| 125 | .toArray(String[][]::new); | ||
| 126 | return new Entry<>(e, components); | ||
| 127 | } | ||
| 128 | |||
| 129 | private static int compareEqualLength(String s1, String s2) { | ||
| 130 | int len = 0; | ||
| 131 | while (len < s1.length() && len < s2.length() && s1.charAt(len) == s2.charAt(len)) { | ||
| 132 | len += 1; | ||
| 133 | } | ||
| 134 | return len; | ||
| 135 | } | ||
| 136 | |||
| 137 | /** | ||
| 138 | * Splits the given input into components, trying to detect word parts. | ||
| 139 | * <p> | ||
| 140 | * Example of how words get split (using <code>|</code> as seperator): | ||
| 141 | * <p><code>MinecraftClientGame -> Minecraft|Client|Game</code></p> | ||
| 142 | * <p><code>HTTPInputStream -> HTTP|Input|Stream</code></p> | ||
| 143 | * <p><code>class_932 -> class|_|932</code></p> | ||
| 144 | * <p><code>X11FontManager -> X|11|Font|Manager</code></p> | ||
| 145 | * <p><code>openHTTPConnection -> open|HTTP|Connection</code></p> | ||
| 146 | * <p><code>open_http_connection -> open|_|http|_|connection</code></p> | ||
| 147 | * | ||
| 148 | * @param input the input to split | ||
| 149 | * @return the resulting components | ||
| 150 | */ | ||
| 151 | private static String[] wordwiseSplit(String input) { | ||
| 152 | List<String> list = new ArrayList<>(); | ||
| 153 | while (!input.isEmpty()) { | ||
| 154 | int take; | ||
| 155 | if (Character.isLetter(input.charAt(0))) { | ||
| 156 | if (input.length() == 1) { | ||
| 157 | take = 1; | ||
| 158 | } else { | ||
| 159 | boolean nextSegmentIsUppercase = Character.isUpperCase(input.charAt(0)) && Character.isUpperCase(input.charAt(1)); | ||
| 160 | if (nextSegmentIsUppercase) { | ||
| 161 | int nextLowercase = 1; | ||
| 162 | while (Character.isUpperCase(input.charAt(nextLowercase))) { | ||
| 163 | nextLowercase += 1; | ||
| 164 | if (nextLowercase == input.length()) { | ||
| 165 | nextLowercase += 1; | ||
| 166 | break; | ||
| 167 | } | ||
| 168 | } | ||
| 169 | take = nextLowercase - 1; | ||
| 170 | } else { | ||
| 171 | int nextUppercase = 1; | ||
| 172 | while (nextUppercase < input.length() && Character.isLowerCase(input.charAt(nextUppercase))) { | ||
| 173 | nextUppercase += 1; | ||
| 174 | } | ||
| 175 | take = nextUppercase; | ||
| 176 | } | ||
| 177 | } | ||
| 178 | } else if (Character.isDigit(input.charAt(0))) { | ||
| 179 | int nextNonNum = 1; | ||
| 180 | while (nextNonNum < input.length() && Character.isLetter(input.charAt(nextNonNum)) && !Character.isLowerCase(input.charAt(nextNonNum))) { | ||
| 181 | nextNonNum += 1; | ||
| 182 | } | ||
| 183 | take = nextNonNum; | ||
| 184 | } else { | ||
| 185 | take = 1; | ||
| 186 | } | ||
| 187 | list.add(input.substring(0, take)); | ||
| 188 | input = input.substring(take); | ||
| 189 | } | ||
| 190 | return list.toArray(new String[0]); | ||
| 191 | } | ||
| 192 | |||
| 193 | } | ||
| 194 | |||
| 195 | } | ||
diff --git a/src/main/resources/lang/en_us.json b/src/main/resources/lang/en_us.json index fe1ac621..a8b33064 100644 --- a/src/main/resources/lang/en_us.json +++ b/src/main/resources/lang/en_us.json | |||
| @@ -113,6 +113,8 @@ | |||
| 113 | "prompt.close.save": "Save and close", | 113 | "prompt.close.save": "Save and close", |
| 114 | "prompt.close.discard": "Discard changes", | 114 | "prompt.close.discard": "Discard changes", |
| 115 | "prompt.close.cancel": "Cancel", | 115 | "prompt.close.cancel": "Cancel", |
| 116 | "prompt.open": "Open", | ||
| 117 | "prompt.cancel": "Cancel", | ||
| 116 | 118 | ||
| 117 | "crash.title": "%s - Crash Report", | 119 | "crash.title": "%s - Crash Report", |
| 118 | "crash.summary": "%s has crashed! =(", | 120 | "crash.summary": "%s has crashed! =(", |