// 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.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
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.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.util.ObjectString;
import org.infinity.util.SimpleListModel;
/**
* Provides a modal dialog for selecting a single or multiple game resources of one or more
* given resource types.
*/
public class OpenResourceDialog extends JDialog
implements ItemListener, ListSelectionListener, DocumentListener
{
private final List<List<ResourceEntry>> resources = new ArrayList<List<ResourceEntry>>();
private ResourceEntry[] result;
private ObjectString[] extensions;
private JList<ResourceEntry> list;
private SimpleListModel<ResourceEntry> listModel;
private JComboBox<ObjectString> cbType;
private JTextField tfSearch;
private PlainDocument searchDoc;
private JButton bOpen, bCancel;
private boolean searchLock;
/**
* Opens a modal dialog where the user can select one or more internal game resources.
* @param owner The parent window of this dialog.
* @param title The dialog title.
* @param extensions A list of file extensions which is to limit the list of internal files.
* Specify {@code null} to show all available resources.
* @param multiSelection Specify {@code true} to allow selecting more than one resource.
* @return An array of selected ResourceEntry objects.
* Returns {@code null} if the user cancelled the operation.
*/
public static ResourceEntry[] showOpenDialog(Window owner, String title, String[] extensions,
boolean multiSelection)
{
ResourceEntry[] retVal = null;
if (title == null) {
title = "Select resource";
}
OpenResourceDialog dlg = new OpenResourceDialog(owner, title, extensions);
dlg.setMultiSelection(multiSelection);
dlg.setVisible(true);
retVal = dlg.getResult();
return retVal;
}
//--------------------- Begin Interface ItemListener ---------------------
@Override
public void itemStateChanged(ItemEvent e)
{
if (e.getSource() == cbType) {
try {
WindowBlocker.blockWindow(this, true);
if (cbType.getSelectedIndex() >= 0 && cbType.getSelectedIndex() < resources.size()) {
updateList(resources.get(cbType.getSelectedIndex()));
}
} finally {
WindowBlocker.blockWindow(this, false);
}
}
}
//--------------------- End Interface ItemListener ---------------------
//--------------------- Begin Interface ListSelectionListener ---------------------
@Override
public void valueChanged(ListSelectionEvent e)
{
if (e.getSource() == list && !isSearchLock()) {
try {
setSearchLock(true);
bOpen.setEnabled(!list.isSelectionEmpty());
updateSearchField();
} finally {
setSearchLock(false);
}
}
}
//--------------------- End Interface ListSelectionListener ---------------------
//--------------------- Begin Interface DocumentListener ---------------------
@Override
public void insertUpdate(DocumentEvent e)
{
if (e.getDocument() == searchDoc && !isSearchLock()) {
try {
setSearchLock(true);
updateListSelection(searchDoc.getText(0, searchDoc.getLength()));
} catch (BadLocationException ble) {
} finally {
setSearchLock(false);
}
}
}
@Override
public void removeUpdate(DocumentEvent e)
{
if (e.getDocument() == searchDoc && !isSearchLock()) {
try {
setSearchLock(true);
updateListSelection(searchDoc.getText(0, searchDoc.getLength()));
} catch (BadLocationException ble) {
} finally {
setSearchLock(false);
}
}
}
@Override
public void changedUpdate(DocumentEvent e)
{
if (e.getDocument() == searchDoc && !isSearchLock()) {
try {
setSearchLock(true);
updateListSelection(searchDoc.getText(0, searchDoc.getLength()));
} catch (BadLocationException ble) {
} finally {
setSearchLock(false);
}
}
}
//--------------------- End Interface DocumentListener ---------------------
protected OpenResourceDialog(Window owner, String title, String[] extensions)
{
super(owner, title, ModalityType.APPLICATION_MODAL);
init();
setExtensions(extensions);
}
/** Specifies a list of supported resource types for this dialog. */
protected void setExtensions(String[] extList)
{
if (extList != null && extList.length > 0) {
int extra = (extList.length > 1) ? 1 : 0;
extensions = new ObjectString[extList.length + extra];
for (int i = 0; i < extList.length; i++) {
final String s = extList[i].trim().toUpperCase(Locale.ENGLISH);
extensions[i + extra] = new ObjectString(s + " resources", s, ObjectString.FMT_STRING_ONLY);
}
// adding an extra entry which combines all listed extensions
if (extra > 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < extList.length; i++) {
if (i > 0) {
sb.append(';');
}
final String s = extList[i].trim().toUpperCase(Locale.ENGLISH);
sb.append(s);
}
extensions[0] = new ObjectString("Supported resources", sb.toString(), ObjectString.FMT_STRING_ONLY);
}
} else {
extensions = new ObjectString[]{new ObjectString("All resources", "", ObjectString.FMT_STRING_ONLY)};
}
updateResources();
}
/** Returns a list of resource types defined for this dialog. */
protected String[] getExtensions()
{
if (!extensions[0].getObject().toString().isEmpty()) {
return extensions[0].getObject().toString().split(";");
} else {
return new String[]{""};
}
}
/** Returns {@code true} if multiple list items can be selected. */
protected boolean isMultiSelection()
{
return (list.getSelectionMode() == ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
}
/** Specify whether multiple list items can be selected. */
protected void setMultiSelection(boolean multi)
{
if (multi) {
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
} else {
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
}
/**
* Returns the result of the last dialog operation.
* Returns a list of ResourceEntry objects if dialog operation was successful.
* Returns {@code null} if operation has been cancelled.
*/
protected ResourceEntry[] getResult()
{
return result;
}
private void accept()
{
setVisible(false);
List<ResourceEntry> entries = list.getSelectedValuesList();
if (entries != null) {
result = entries.toArray(new ResourceEntry[entries.size()]);
} else {
result = new ResourceEntry[0];
}
}
private void cancel()
{
setVisible(false);
result = null;
}
// Generates a list of available resources for all supported types
private void updateResources()
{
resources.clear();
for (int i = 0; i < extensions.length; i++) {
List<ResourceEntry> list = new ArrayList<ResourceEntry>();
String data = extensions[i].getObject();
String[] ext = null;
if (data.isEmpty()) {
ext = Profile.getAvailableResourceTypes();
} else {
ext = data.split(";");
}
for (int j = 0; j < ext.length; j++) {
list.addAll(ResourceFactory.getResources(ext[j]));
}
Collections.sort(list);
resources.add(list);
}
updateGui();
}
// Initializes type combobox
private void updateGui()
{
DefaultComboBoxModel<ObjectString> model = (DefaultComboBoxModel<ObjectString>)cbType.getModel();
model.removeAllElements();
if (extensions != null) {
for (final ObjectString os: extensions) {
model.addElement(os);
}
}
if (model.getSize() > 0) {
cbType.setSelectedIndex(0);
}
}
// Initializes resource list
private void updateList(List<ResourceEntry> entries)
{
listModel.clear();
if (entries != null) {
listModel.addAll(entries);
if (listModel.size() > 0) {
list.setSelectedIndex(0);
list.ensureIndexIsVisible(0);
list.requestFocusInWindow();
}
}
}
// Select one or more list items based on search text
private void updateListSelection(String search)
{
if (search == null) {
search = "";
} else {
search = search.trim();
}
// preparing search entries
String[] entries;
if (search.isEmpty()) {
entries = new String[0];
} else {
entries = search.split("[; ]+");
for (int i = 0; i < entries.length; i++) {
entries[i] = entries[i].trim();
}
}
// selecting entries
if (isMultiSelection()) {
// multi-selection mode
List<Integer> indexList = new ArrayList<Integer>();
for (int i = 0; i < entries.length; i++) {
String entry = entries[i];
Integer idx = getClosestIndex(entry);
if (idx >= 0 && !indexList.contains(idx)) {
indexList.add(idx);
}
}
int[] indices;
if (indexList.size() > 0) {
indices = new int[indexList.size()];
for (int i = 0; i < indexList.size(); i++) {
indices[i] = indexList.get(i);
}
} else {
indices = new int[0];
}
list.setSelectedIndices(indices);
if (indices.length > 0) {
list.ensureIndexIsVisible(indices[0]);
} else {
list.ensureIndexIsVisible(0);
}
} else {
// single selection mode
String entry = (entries.length > 0) ? entries[0] : "";
int idx = getClosestIndex(entry);
list.setSelectedIndex(idx);
if (idx >= 0) {
list.ensureIndexIsVisible(idx);
} else {
list.ensureIndexIsVisible(0);
}
}
}
// Returns index of closest list item match for given string
private int getClosestIndex(String text)
{
int retVal = -1;
if (text != null) {
text = text.toUpperCase(Locale.ENGLISH);
int selected = 0;
for (int size = listModel.getSize(); selected < size; selected++) {
String s = listModel.get(selected).toString().toUpperCase(Locale.ENGLISH);
if (s.startsWith(text)) {
break;
}
}
if (selected < listModel.getSize()) {
retVal = selected;
}
}
return retVal;
}
// Synchronizes search field with list selections
private void updateSearchField()
{
int[] indices = list.getSelectedIndices();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indices.length; i++) {
if (i > 0) {
sb.append(' ');
}
String s = listModel.get(indices[i]).toString();
sb.append(s);
}
tfSearch.setText(sb.toString());
tfSearch.setCaretPosition(0);
}
private boolean isSearchLock()
{
return searchLock;
}
private synchronized void setSearchLock(boolean set)
{
if (searchLock != set) {
searchLock = set;
if (searchLock) {
searchDoc.removeDocumentListener(this);
list.removeListSelectionListener(this);
} else {
list.addListSelectionListener(this);
searchDoc.addDocumentListener(this);
}
}
}
// Constructs the dialog elements
private void init()
{
AbstractAction actOpen = new AbstractAction("Open") {
@Override
public void actionPerformed(ActionEvent e)
{
accept();
}
};
AbstractAction actCancel = new AbstractAction("Cancel") {
@Override
public void actionPerformed(ActionEvent e)
{
cancel();
}
};
bOpen = new JButton(actOpen);
bCancel = new JButton(actCancel);
Dimension d = new Dimension(Math.max(bOpen.getPreferredSize().width, bCancel.getPreferredSize().width),
Math.max(bOpen.getPreferredSize().height, bCancel.getPreferredSize().height));
bOpen.setPreferredSize(d);
bCancel.setPreferredSize(d);
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), bOpen);
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), bCancel);
getRootPane().getActionMap().put(bOpen, actOpen);
getRootPane().getActionMap().put(bCancel, actCancel);
cbType = new JComboBox<>(new DefaultComboBoxModel<ObjectString>());
cbType.setEditable(false);
cbType.addItemListener(this);
JLabel lType = new JLabel("Type:");
lType.setDisplayedMnemonic('T');
lType.setLabelFor(cbType);
searchDoc = new PlainDocument();
searchDoc.addDocumentListener(this);
tfSearch = new JTextField(searchDoc, null, 0);
JLabel lSearch = new JLabel("Search:");
lSearch.setDisplayedMnemonic('S');
lSearch.setLabelFor(tfSearch);
listModel = new SimpleListModel<ResourceEntry>();
list = new JList<>(listModel);
list.setLayoutOrientation(JList.VERTICAL_WRAP);
list.setVisibleRowCount(0); // no limit
list.addListSelectionListener(this);
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event)
{
if (event.getClickCount() == 2 && !list.isSelectionEmpty()) {
accept();
}
}
});
JScrollPane scroll = new JScrollPane(list);
scroll.setPreferredSize(new Dimension(400, 200));
GridBagConstraints c = new GridBagConstraints();
JPanel pType = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pType.add(lType, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0);
pType.add(cbType, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(12, 0, 0, 0), 0, 0);
pType.add(lSearch, c);
c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(12, 8, 0, 0), 0, 0);
pType.add(tfSearch, c);
JPanel pList = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
pList.add(scroll, c);
JPanel pButtons = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pButtons.add(new JPanel(), c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pButtons.add(bOpen, c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 0, 0);
pButtons.add(bCancel, c);
JPanel pMain = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 0, 8), 0, 0);
pMain.add(pType, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.BOTH, new Insets(8, 8, 0, 8), 0, 0);
pMain.add(pList, c);
c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 8, 8), 0, 0);
pMain.add(pButtons, c);
setLayout(new BorderLayout());
add(pMain, BorderLayout.CENTER);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e)
{
if (e.getSource() == this) {
cancel();
}
}
});
pack();
setResizable(true);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
setLocationRelativeTo(getOwner());
list.requestFocusInWindow();
}
}