summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--build.gradle1
-rw-r--r--src/main/java/cuchaz/enigma/gui/ClassSelector.java24
-rw-r--r--src/main/java/cuchaz/enigma/gui/Gui.java29
-rw-r--r--src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java310
-rw-r--r--src/main/java/cuchaz/enigma/gui/elements/MenuBar.java12
-rw-r--r--src/main/java/cuchaz/enigma/gui/panels/PanelEditor.java2
-rw-r--r--src/main/java/cuchaz/enigma/gui/util/AbstractListCellRenderer.java75
-rw-r--r--src/main/java/cuchaz/enigma/gui/util/ScaleUtil.java6
-rw-r--r--src/main/java/cuchaz/enigma/utils/search/SearchEntry.java17
-rw-r--r--src/main/java/cuchaz/enigma/utils/search/SearchUtil.java195
-rw-r--r--src/main/resources/lang/en_us.json2
12 files changed, 550 insertions, 124 deletions
diff --git a/README.md b/README.md
index 24fc9c5f..7d01dacf 100644
--- a/README.md
+++ b/README.md
@@ -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
12package cuchaz.enigma.gui; 12package cuchaz.enigma.gui;
13 13
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.util.*;
17
18import javax.annotation.Nullable;
19import javax.swing.JOptionPane;
20import javax.swing.JTree;
21import javax.swing.event.CellEditorListener;
22import javax.swing.event.ChangeEvent;
23import javax.swing.tree.*;
24
14import com.google.common.collect.ArrayListMultimap; 25import com.google.common.collect.ArrayListMultimap;
15import com.google.common.collect.Lists; 26import com.google.common.collect.Lists;
16import com.google.common.collect.Maps; 27import com.google.common.collect.Maps;
@@ -21,15 +32,6 @@ import cuchaz.enigma.throwables.IllegalNameException;
21import cuchaz.enigma.translation.Translator; 32import cuchaz.enigma.translation.Translator;
22import cuchaz.enigma.translation.representation.entry.ClassEntry; 33import cuchaz.enigma.translation.representation.entry.ClassEntry;
23 34
24import javax.annotation.Nullable;
25import javax.swing.*;
26import javax.swing.event.CellEditorListener;
27import javax.swing.event.ChangeEvent;
28import javax.swing.tree.*;
29import java.awt.event.MouseAdapter;
30import java.awt.event.MouseEvent;
31import java.util.*;
32
33public class ClassSelector extends JTree { 35public 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;
33import cuchaz.enigma.config.Themes; 33import cuchaz.enigma.config.Themes;
34import cuchaz.enigma.gui.dialog.CrashDialog; 34import cuchaz.enigma.gui.dialog.CrashDialog;
35import cuchaz.enigma.gui.dialog.JavadocDialog; 35import cuchaz.enigma.gui.dialog.JavadocDialog;
36import cuchaz.enigma.gui.dialog.SearchDialog;
36import cuchaz.enigma.gui.elements.MenuBar; 37import cuchaz.enigma.gui.elements.MenuBar;
37import cuchaz.enigma.gui.elements.PopupMenuBar; 38import cuchaz.enigma.gui.elements.PopupMenuBar;
38import cuchaz.enigma.gui.filechooser.FileChooserAny; 39import 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
12package cuchaz.enigma.gui.dialog; 12package cuchaz.enigma.gui.dialog;
13 13
14import com.google.common.collect.Lists; 14import java.awt.BorderLayout;
15import java.awt.Color;
16import java.awt.FlowLayout;
17import java.awt.Font;
18import java.awt.event.*;
19import java.util.Arrays;
20import java.util.Collections;
21import java.util.List;
22
23import javax.swing.*;
24import javax.swing.event.DocumentEvent;
25import javax.swing.event.DocumentListener;
26
15import cuchaz.enigma.gui.Gui; 27import cuchaz.enigma.gui.Gui;
28import cuchaz.enigma.gui.GuiController;
29import cuchaz.enigma.gui.util.AbstractListCellRenderer;
30import cuchaz.enigma.gui.util.ScaleUtil;
16import cuchaz.enigma.translation.representation.entry.ClassEntry; 31import cuchaz.enigma.translation.representation.entry.ClassEntry;
17import cuchaz.enigma.utils.I18n; 32import cuchaz.enigma.utils.I18n;
18import cuchaz.enigma.gui.util.ScaleUtil; 33import cuchaz.enigma.utils.search.SearchEntry;
19import me.xdrop.fuzzywuzzy.FuzzySearch; 34import cuchaz.enigma.utils.search.SearchUtil;
20import me.xdrop.fuzzywuzzy.model.ExtractedResult;
21
22import javax.swing.*;
23import javax.swing.border.EmptyBorder;
24import java.awt.*;
25import java.awt.event.*;
26import java.util.List;
27import java.util.function.Consumer;
28import java.util.stream.Collectors;
29 35
30public class SearchDialog { 36public 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;
29import cuchaz.enigma.utils.I18n; 29import cuchaz.enigma.utils.I18n;
30import cuchaz.enigma.utils.Pair; 30import cuchaz.enigma.utils.Pair;
31 31
32import javax.swing.*;
33
34import cuchaz.enigma.config.Config;
35import cuchaz.enigma.config.Themes;
36import cuchaz.enigma.gui.Gui;
37import cuchaz.enigma.gui.dialog.AboutDialog;
38import cuchaz.enigma.gui.stats.StatsMember;
39import cuchaz.enigma.translation.mapping.serde.MappingFormat;
40import cuchaz.enigma.utils.I18n;
41
32public class MenuBar extends JMenuBar { 42public 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 @@
1package cuchaz.enigma.gui.util;
2
3import java.awt.Component;
4import java.awt.event.MouseEvent;
5
6import javax.swing.*;
7import javax.swing.border.Border;
8
9public abstract class AbstractListCellRenderer<E> extends JPanel implements ListCellRenderer<E> {
10
11 private static final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1);
12
13 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;
7import java.util.ArrayList; 7import java.util.ArrayList;
8import java.util.List; 8import java.util.List;
9 9
10import javax.swing.BorderFactory;
10import javax.swing.UIManager; 11import javax.swing.UIManager;
12import javax.swing.border.Border;
11 13
12import com.github.swingdpi.UiDefaultsScaler; 14import com.github.swingdpi.UiDefaultsScaler;
13import com.github.swingdpi.plaf.BasicTweaker; 15import 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 @@
1package cuchaz.enigma.utils.search;
2
3import java.util.List;
4
5public interface SearchEntry {
6
7 List<String> getSearchableNames();
8
9 /**
10 * Returns a type that uniquely identifies this search entry across possible changes.
11 * This is used for tracking the amount of times this entry has been selected.
12 *
13 * @return a unique identifier for this search entry
14 */
15 String getIdentifier();
16
17}
diff --git a/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 @@
1package cuchaz.enigma.utils.search;
2
3import java.util.*;
4import java.util.function.BiFunction;
5import java.util.stream.Collectors;
6import java.util.stream.Stream;
7
8import cuchaz.enigma.utils.Pair;
9
10public 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! =(",