// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.event.ListDataListener;
import javax.swing.text.JTextComponent;
import org.infinity.NearInfinity;
import org.infinity.icon.Icons;
import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.resource.key.ResourceTreeModel;
import org.infinity.util.MapTree;
import org.infinity.util.Misc;
/**
* Implements a search panel for quickly finding specific resources.
*/
public class QuickSearch extends JPanel implements Runnable
{
// Internally used to control actions in the background task
private enum Command {
IDLE, UPDATE, DESTROY
}
// Defines available search actions
private enum Result {
CANCEL, OPEN, OPEN_NEW,
}
private final ButtonPopupWindow parent;
private final ResourceTree tree;
private final MapTree<Character, List<ResourceEntry>> resourceTree;
private final Object monitor = new Object(); // synchronization object
private final JPanel mainPanel = new JPanel(new GridBagLayout());
private JLabel lSearch;
private JComboBox<ResourceEntry> cbSearch;
private JButton bOk, bOkNew, bCancel;
private String keyword;
private Command command;
public QuickSearch(ButtonPopupWindow parent, ResourceTree tree)
{
super();
if (parent == null || tree == null) {
throw new NullPointerException("parent and tree must not be null!");
}
this.parent = parent;
this.tree = tree;
this.resourceTree = new MapTree<Character, List<ResourceEntry>>(Character.valueOf('\0'), null);
this.command = Command.IDLE;
new Thread(this).start(); // updating list of matching resources is done in the background
init();
}
private void init()
{
// Action for pressing "Enter"
final Action acceptAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e)
{
close(Result.OPEN);
}
};
// Action for pressing "Enter"
final Action acceptNewAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e)
{
close(Result.OPEN_NEW);
}
};
// Action for pressing "Escape"
final Action rejectAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e)
{
close(Result.CANCEL);
}
};
// Action for changing text in search field
final KeyListener keyListener = new KeyListener() {
@Override
public void keyReleased(KeyEvent event)
{
switch (event.getKeyCode()) {
case KeyEvent.VK_ESCAPE:
event.consume();
close(Result.CANCEL);
break;
case KeyEvent.VK_ENTER:
event.consume();
if ((event.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) == InputEvent.SHIFT_DOWN_MASK) {
close(Result.OPEN_NEW);
} else {
close(Result.OPEN);
}
break;
default:
if (event.getKeyChar() != KeyEvent.CHAR_UNDEFINED) {
updateSuggestions(getSearchString());
}
}
}
@Override public void keyTyped(KeyEvent e) {}
@Override public void keyPressed(KeyEvent e) {}
};
final PopupWindowListener popupListener = new PopupWindowListener() {
@Override public void popupWindowWillBecomeVisible(PopupWindowEvent event) {}
@Override
public void popupWindowWillBecomeInvisible(PopupWindowEvent event)
{
cbSearch.hidePopup();
synchronized (monitor) {
command = Command.DESTROY;
monitor.notify();
}
}
};
parent.addGlobalKeyStroke("ENTER", KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), acceptAction);
parent.addGlobalKeyStroke("ESCAPE", KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), rejectAction);
parent.addPopupWindowListener(popupListener);
setLayout(new GridBagLayout());
lSearch = new JLabel("Search:", SwingConstants.LEFT);
cbSearch = new JComboBox<>();
cbSearch.setPreferredSize(Misc.getPrototypeSize(cbSearch, "WWWWWWWW.WWWW")); // space for at least 8.4 characters
cbSearch.setEditable(true);
cbSearch.getEditor().getEditorComponent().addKeyListener(keyListener);
bOk = new JButton(Icons.getIcon(Icons.ICON_CHECK_16));
bOk.addActionListener(acceptAction);
bOk.setMargin(new Insets(1, 4, 1, 4));
bOk.setToolTipText("Open (Shortcut: Enter)");
bOkNew = new JButton(Icons.getIcon(Icons.ICON_OPEN_16));
bOkNew.addActionListener(acceptNewAction);
bOkNew.setMargin(new Insets(1, 5, 1, 4));
bOkNew.setToolTipText("Open in new window (Shortcut: Shift+Enter)");
bCancel = new JButton(Icons.getIcon(Icons.ICON_CHECK_NOT_16));
bCancel.setMargin(new Insets(1, 2, 1, 2));
bCancel.setToolTipText("Cancel search (Shortcut: Esc)");
bCancel.addActionListener(rejectAction);
GridBagConstraints gbc = new GridBagConstraints();
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
mainPanel.add(lSearch, gbc);
gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0);
mainPanel.add(cbSearch, gbc);
gbc = ViewerUtil.setGBC(gbc, 2, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.VERTICAL, new Insets(0, 4, 0, 0), 0, 0);
mainPanel.add(bOk, gbc);
gbc = ViewerUtil.setGBC(gbc, 3, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.VERTICAL, new Insets(0, 4, 0, 0), 0, 0);
mainPanel.add(bOkNew, gbc);
gbc = ViewerUtil.setGBC(gbc, 4, 0, 1, 1, 0.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.VERTICAL, new Insets(0, 4, 0, 0), 0, 0);
mainPanel.add(bCancel, gbc);
gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.BOTH, new Insets(4, 4, 4, 4), 0, 0);
add(mainPanel, gbc);
}
@Override
public boolean requestFocusInWindow()
{
return cbSearch.requestFocusInWindow();
}
// Updates the list of resources matching the specified text
private void updateSuggestions(String text)
{
synchronized (monitor) {
keyword = (text != null) ? text : "";
command = Command.UPDATE;
monitor.notify();
}
}
// Returns the text field content of the combobox
private String getSearchString()
{
return ((JTextComponent)cbSearch.getEditor().getEditorComponent()).getText();
}
// Executed when accepting current input
private void close(Result result)
{
if (result != Result.CANCEL) {
Object item = cbSearch.getSelectedItem();
if (!(item instanceof ResourceEntry)) {
item = cbSearch.getItemAt(0);
}
if (item instanceof ResourceEntry) {
if (result == Result.OPEN) {
tree.select((ResourceEntry)item);
} else if (result == Result.OPEN_NEW) {
Resource res = ResourceFactory.getResource((ResourceEntry)item);
if (res != null) {
new ViewFrame(NearInfinity.getInstance(), res);
}
}
}
}
parent.hidePopupWindow();
}
// Generates root list of resource entries
private void generateRootNode()
{
// removing old list (if any)
List<ResourceEntry> list = resourceTree.getValue();
if (list != null) {
list.clear();
} else {
list = new Vector<ResourceEntry>();
resourceTree.setValue(list);
}
// populating list with new entries
ResourceTreeModel model = tree.getModel();
if (model != null) {
Collection<ResourceEntry> entries = model.getResourceEntries();
if (entries != null) {
for (Iterator<ResourceEntry> iter = entries.iterator(); iter.hasNext();) {
list.add(iter.next());
}
}
Collections.sort(list);
}
}
// Creates a new node with a list of matching resources based on the specified node and the new character
private MapTree<Character, List<ResourceEntry>> generateNode(MapTree<Character, List<ResourceEntry>> node, char ch)
{
// determining node level (0 = first level)
int index = 0;
for (MapTree<Character, List<ResourceEntry>> curNode = node;
curNode.getParent() != null;
curNode = curNode.getParent()) {
index++;
}
// generating filtered list of resource entries based on list of previous node
ch = Character.toUpperCase(ch);
// preparing child node
MapTree<Character, List<ResourceEntry>> retVal = node.getChild(Character.valueOf(ch));
if (retVal != null) {
if (retVal.getValue() != null) {
retVal.getValue().clear();
} else {
retVal.setValue(new Vector<ResourceEntry>());
}
} else {
retVal = new MapTree<Character, List<ResourceEntry>>(Character.valueOf(ch),
new Vector<ResourceEntry>());
}
node.addChild(retVal);
// generating filtered list of resource entries
List<ResourceEntry> parentList = node.getValue();
List<ResourceEntry> curList = retVal.getValue();
for (Iterator<ResourceEntry> iter = parentList.iterator(); iter.hasNext();) {
final ResourceEntry entry = iter.next();
final String resName = entry.getResourceName();
if (resName.length() > index && Character.toUpperCase(resName.charAt(index)) == ch) {
curList.add(entry);
}
}
return retVal;
}
// Removes all child nodes and their values recursively
// private void clearResourceTree(MapTree<Character, List<ResourceEntry>> node)
// {
// if (node != null) {
// for (Iterator<MapTree<Character, List<ResourceEntry>>> iter = node.getChildren().iterator();
// iter.hasNext();) {
// MapTree<Character, List<ResourceEntry>> curNode = iter.next();
// clearResourceTree(curNode);
// }
// node.removeAllChildren();
// List<ResourceEntry> list = node.setValue(null);
// if (list != null) {
// list.clear();
// list = null;
// }
// }
// }
// --------------------- Begin Interface Runnable ---------------------
@Override
public void run()
{
// main loop
while (true) {
if (command == Command.DESTROY) {
synchronized (monitor) {
command = Command.IDLE;
}
break;
} else if (command == Command.UPDATE) {
synchronized (monitor) {
command = Command.IDLE;
// populating root node
if (resourceTree.getValue() == null || resourceTree.getValue().isEmpty()) {
generateRootNode();
}
// processing new keyword
if (keyword != null) {
keyword = keyword.toUpperCase(Locale.ENGLISH);
MapTree<Character, List<ResourceEntry>> node = resourceTree;
for (int i = 0, size = keyword.length(); i < size; i++) {
MapTree<Character, List<ResourceEntry>> newNode = node.getChild(Character.valueOf(keyword.charAt(i)));
if (newNode == null) {
node = generateNode(node, keyword.charAt(i));
} else {
node = newNode;
}
}
// setting matching resource entries
DefaultComboBoxModel<ResourceEntry> cbModel = (DefaultComboBoxModel<ResourceEntry>)cbSearch.getModel();
// Deactivating listeners to prevent autoselecting items
ListDataListener[] listeners = cbModel.getListDataListeners();
for (int i = listeners.length - 1; i >= 0; i--) {
cbModel.removeListDataListener(listeners[i]);
}
cbSearch.hidePopup(); // XXX: work-around to force visual update of file list
cbModel.removeAllElements();
if (!keyword.isEmpty() && node != null && node.getValue() != null) {
List<ResourceEntry> list = node.getValue();
for (Iterator<ResourceEntry> iter = list.iterator(); iter.hasNext();) {
cbModel.addElement(iter.next());
}
}
// Reactivating listeners
for (int i = 0; i < listeners.length; i++) {
cbModel.addListDataListener(listeners[i]);
}
cbSearch.setMaximumRowCount(Math.min(8, cbModel.getSize()));
if (cbModel.getSize() > 0 && !cbSearch.isPopupVisible()) {
cbSearch.showPopup();
} else if (cbModel.getSize() == 0 && cbSearch.isPopupVisible()) {
cbSearch.hidePopup();
}
}
// } else if (command == Command.Clear) {
// // reset data
// synchronized(monitor) {
// command = Command.Idle;
// clearResourceTree(resourceTree);
// ((DefaultComboBoxModel)cbSearch.getModel()).removeAllElements();
// ((JTextComponent)cbSearch.getEditor().getEditorComponent()).setText("");
}
} else {
// nothing else to do?
synchronized(monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
}
}
}
}
}
// --------------------- End Interface Runnable ---------------------
}