/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
*
Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.gui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import cuchaz.enigma.analysis.index.EntryIndex;
import cuchaz.enigma.gui.Gui;
import cuchaz.enigma.gui.GuiController;
import cuchaz.enigma.gui.search.SearchEntry;
import cuchaz.enigma.gui.search.SearchUtil;
import cuchaz.enigma.gui.util.AbstractListCellRenderer;
import cuchaz.enigma.gui.util.GuiUtil;
import cuchaz.enigma.gui.util.ScaleUtil;
import cuchaz.enigma.translation.representation.entry.ClassEntry;
import cuchaz.enigma.translation.representation.entry.FieldEntry;
import cuchaz.enigma.translation.representation.entry.MethodEntry;
import cuchaz.enigma.translation.representation.entry.ParentedEntry;
import cuchaz.enigma.utils.I18n;
public class SearchDialog {
private final JPanel inputPanel;
private final JTextField searchField;
private final JCheckBox onlyExactMatchesCheckbox;
private DefaultListModel classListModel;
private final JList classList;
private final JDialog dialog;
private final Gui parent;
private final SearchUtil su;
private SearchUtil.SearchControl currentSearch;
public SearchDialog(Gui parent) {
this.parent = parent;
su = new SearchUtil<>();
dialog = new JDialog(parent.getFrame(), true);
JPanel contentPane = new JPanel();
contentPane.setBorder(ScaleUtil.createEmptyBorder(4, 4, 4, 4));
contentPane.setLayout(new BorderLayout(ScaleUtil.scale(4), ScaleUtil.scale(4)));
inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout(ScaleUtil.scale(4), ScaleUtil.scale(4)));
contentPane.add(inputPanel, BorderLayout.NORTH);
searchField = new JTextField();
searchField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
updateList();
}
@Override
public void removeUpdate(DocumentEvent e) {
updateList();
}
@Override
public void changedUpdate(DocumentEvent e) {
updateList();
}
});
searchField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
int next = classList.isSelectionEmpty() ? 0 : classList.getSelectedIndex() + 1;
classList.setSelectedIndex(next);
classList.ensureIndexIsVisible(next);
} else if (e.getKeyCode() == KeyEvent.VK_UP) {
int prev = classList.isSelectionEmpty() ? classList.getModel().getSize() : classList.getSelectedIndex() - 1;
classList.setSelectedIndex(prev);
classList.ensureIndexIsVisible(prev);
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
close();
}
}
});
searchField.addActionListener(e -> openSelected());
inputPanel.add(searchField, BorderLayout.NORTH);
onlyExactMatchesCheckbox = new JCheckBox(I18n.translate("menu.search.only_exact_matches"));
onlyExactMatchesCheckbox.addActionListener(e -> updateList());
inputPanel.add(onlyExactMatchesCheckbox, BorderLayout.SOUTH);
classListModel = new DefaultListModel<>();
classList = new JList<>();
classList.setModel(classListModel);
classList.setCellRenderer(new ListCellRendererImpl(parent));
classList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
classList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() >= 2) {
int idx = classList.locationToIndex(mouseEvent.getPoint());
SearchEntryImpl entry = classList.getModel().getElementAt(idx);
openEntry(entry);
}
}
});
contentPane.add(new JScrollPane(classList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER);
JPanel buttonBar = new JPanel();
buttonBar.setLayout(new FlowLayout(FlowLayout.RIGHT));
JButton open = new JButton(I18n.translate("prompt.open"));
open.addActionListener(event -> openSelected());
buttonBar.add(open);
JButton cancel = new JButton(I18n.translate("prompt.cancel"));
cancel.addActionListener(event -> close());
buttonBar.add(cancel);
contentPane.add(buttonBar, BorderLayout.SOUTH);
// apparently the class list doesn't update by itself when the list
// state changes and the dialog is hidden
dialog.addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
classList.updateUI();
}
});
dialog.setContentPane(contentPane);
dialog.setSize(ScaleUtil.getDimension(400, 500));
dialog.setLocationRelativeTo(parent.getFrame());
}
public void show(Type type) {
su.clear();
final EntryIndex entryIndex = parent.getController().project.getJarIndex().getEntryIndex();
switch (type) {
case CLASS -> entryIndex.getClasses().parallelStream().filter(e -> !e.isInnerClass()).map(e -> SearchEntryImpl.from(e, parent.getController())).map(SearchUtil.Entry::from).sequential().forEach(su::add);
case METHOD -> entryIndex.getMethods().parallelStream().filter(e -> !e.isConstructor() && !entryIndex.getMethodAccess(e).isSynthetic()).map(e -> SearchEntryImpl.from(e, parent.getController())).map(SearchUtil.Entry::from).sequential().forEach(su::add);
case FIELD -> entryIndex.getFields().parallelStream().map(e -> SearchEntryImpl.from(e, parent.getController())).map(SearchUtil.Entry::from).sequential().forEach(su::add);
}
updateList();
searchField.requestFocus();
searchField.selectAll();
dialog.setTitle(I18n.translate(type.getTranslationKey()));
dialog.setVisible(true);
}
private void openSelected() {
SearchEntryImpl selectedValue = classList.getSelectedValue();
if (selectedValue != null) {
openEntry(selectedValue);
}
}
private void openEntry(SearchEntryImpl e) {
close();
su.hit(e);
parent.getController().navigateTo(e.obf);
if (e.obf instanceof ClassEntry) {
if (e.deobf != null) {
parent.getDeobfPanel().deobfClasses.setSelectionClass((ClassEntry) e.deobf);
} else {
parent.getObfPanel().obfClasses.setSelectionClass((ClassEntry) e.obf);
}
} else {
if (e.deobf != null) {
parent.getDeobfPanel().deobfClasses.setSelectionClass((ClassEntry) e.deobf.getParent());
} else {
parent.getObfPanel().obfClasses.setSelectionClass((ClassEntry) e.obf.getParent());
}
}
}
private void close() {
dialog.setVisible(false);
}
// Updates the list of class names
private void updateList() {
if (currentSearch != null) {
currentSearch.stop();
}
DefaultListModel classListModel = new DefaultListModel<>();
this.classListModel = classListModel;
classList.setModel(classListModel);
// handle these search result like minecraft scheduled tasks to prevent
// flooding swing buttons inputs etc with tons of (possibly outdated) invocations
record Order(int idx, SearchEntryImpl e) {
}
Queue queue = new ConcurrentLinkedQueue<>();
Runnable updater = new Runnable() {
@Override
public void run() {
if (SearchDialog.this.classListModel != classListModel || !SearchDialog.this.dialog.isVisible()) {
return;
}
// too large count may increase delay for key and input handling, etc.
int count = 100;
while (count > 0 && !queue.isEmpty()) {
Order o = queue.remove();
classListModel.insertElementAt(o.e, o.idx);
count--;
}
SwingUtilities.invokeLater(this);
}
};
currentSearch = su.asyncSearch(searchField.getText(), (idx, e) -> queue.add(new Order(idx, e)), onlyExactMatchesCheckbox.isSelected());
SwingUtilities.invokeLater(updater);
}
public void dispose() {
dialog.dispose();
}
private static final class SearchEntryImpl implements SearchEntry {
public final ParentedEntry> obf;
public final ParentedEntry> deobf;
private SearchEntryImpl(ParentedEntry> obf, ParentedEntry> deobf) {
this.obf = obf;
this.deobf = deobf;
}
@Override
public List getSearchableNames() {
if (deobf != null) {
return Arrays.asList(obf.getSimpleName(), deobf.getSimpleName());
} else {
return Collections.singletonList(obf.getSimpleName());
}
}
@Override
public String getIdentifier() {
return obf.getFullName();
}
@Override
public String toString() {
return String.format("SearchEntryImpl { obf: %s, deobf: %s }", obf, deobf);
}
public static SearchEntryImpl from(ParentedEntry> e, GuiController controller) {
ParentedEntry> deobf = controller.project.getMapper().deobfuscate(e);
if (deobf.equals(e)) {
deobf = null;
}
return new SearchEntryImpl(e, deobf);
}
}
private static final class ListCellRendererImpl extends AbstractListCellRenderer {
private final Gui gui;
private final JLabel mainName;
private final JLabel secondaryName;
ListCellRendererImpl(Gui gui) {
this.setLayout(new BorderLayout());
this.gui = gui;
mainName = new JLabel();
this.add(mainName, BorderLayout.WEST);
secondaryName = new JLabel();
secondaryName.setFont(secondaryName.getFont().deriveFont(Font.ITALIC));
secondaryName.setForeground(Color.GRAY);
this.add(secondaryName, BorderLayout.EAST);
}
@Override
public void updateUiForEntry(JList extends SearchEntryImpl> list, SearchEntryImpl value, int index, boolean isSelected, boolean cellHasFocus) {
if (value.deobf == null) {
mainName.setText(value.obf.getContextualName());
mainName.setToolTipText(value.obf.getFullName());
secondaryName.setText("");
secondaryName.setToolTipText("");
} else {
mainName.setText(value.deobf.getContextualName());
mainName.setToolTipText(value.deobf.getFullName());
secondaryName.setText(value.obf.getSimpleName());
secondaryName.setToolTipText(value.obf.getFullName());
}
if (value.obf instanceof ClassEntry classEntry) {
mainName.setIcon(GuiUtil.getClassIcon(gui, classEntry));
} else if (value.obf instanceof MethodEntry methodEntry) {
mainName.setIcon(GuiUtil.getMethodIcon(methodEntry));
} else if (value.obf instanceof FieldEntry) {
mainName.setIcon(GuiUtil.FIELD_ICON);
}
}
}
public enum Type {
CLASS("menu.search.class"),
METHOD("menu.search.method"),
FIELD("menu.search.field");
private final String translationKey;
Type(String translationKey) {
this.translationKey = translationKey;
}
public String getTranslationKey() {
return translationKey;
}
}
}