diff options
| author | 2020-04-29 18:24:29 +0200 | |
|---|---|---|
| committer | 2020-04-29 12:24:29 -0400 | |
| commit | 0eff06096bc4852f2580f20a0c5bf970ecf66987 (patch) | |
| tree | fc6c996c66abde850aa8598a24da134c37d1d531 /src/main/java/cuchaz/enigma/gui/dialog | |
| parent | This doesn't need to be scaled, potentially fixes circular class loading cras... (diff) | |
| download | enigma-fork-0eff06096bc4852f2580f20a0c5bf970ecf66987.tar.gz enigma-fork-0eff06096bc4852f2580f20a0c5bf970ecf66987.tar.xz enigma-fork-0eff06096bc4852f2580f20a0c5bf970ecf66987.zip | |
Rewrite search dialog (#233)
* Fix searching
* Make buttons use localization
* Fix rename field opening when pressing shift+space
* Tweak search algorithm
* Add a bit of documentation
* Remove duplicate example line
* Use max() when building the inner map instead of overwriting the old value
* Keep search dialog state
* Formatting
* Fix cursor key selection not scrolling to selected item
* Don't set font size
* Rename close0 to exit
* Fix wrong scrolling when selecting search dialog entry
Diffstat (limited to 'src/main/java/cuchaz/enigma/gui/dialog')
| -rw-r--r-- | src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java | 310 |
1 files changed, 204 insertions, 106 deletions
diff --git a/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/SearchDialog.java index 56ce751..b36ebfb 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 | } |