package net.sf.jabref.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import javax.swing.undo.AbstractUndoableEdit;
import net.sf.jabref.AuthorList;
import net.sf.jabref.BasePanel;
import net.sf.jabref.BibtexDatabase;
import net.sf.jabref.BibtexEntry;
import net.sf.jabref.BibtexFields;
import net.sf.jabref.CheckBoxMessage;
import net.sf.jabref.DuplicateCheck;
import net.sf.jabref.DuplicateResolverDialog;
import net.sf.jabref.FieldComparator;
import net.sf.jabref.GUIGlobals;
import net.sf.jabref.GeneralRenderer;
import net.sf.jabref.Globals;
import net.sf.jabref.HelpAction;
import net.sf.jabref.JabRefFrame;
import net.sf.jabref.KeyCollisionException;
import net.sf.jabref.MetaData;
import net.sf.jabref.PreviewPanel;
import net.sf.jabref.Util;
import net.sf.jabref.external.DownloadExternalFile;
import net.sf.jabref.external.ExternalFileMenuItem;
import net.sf.jabref.groups.AbstractGroup;
import net.sf.jabref.groups.AllEntriesGroup;
import net.sf.jabref.groups.GroupTreeNode;
import net.sf.jabref.groups.UndoableChangeAssignment;
import net.sf.jabref.imports.ImportInspector;
import net.sf.jabref.labelPattern.LabelPatternUtil;
import net.sf.jabref.undo.NamedCompound;
import net.sf.jabref.undo.UndoableInsertEntry;
import net.sf.jabref.undo.UndoableRemoveEntry;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.EventSelectionModel;
import ca.odell.glazedlists.swing.EventTableModel;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.ButtonStackBuilder;
import com.jgoodies.uif_lite.component.UIFSplitPane;
/**
* Dialog to allow the selection of entries as part of an Import.
*
* The usual way to use this class is to pass it to an Importer which will do
* the following:
* <ul>
* <li>Register itself as a callback to get notified if the user wants to stop
* the import.</li>
* <li>Call setVisible(true) to display the dialog</li>
* <li>For each entry that has been found call addEntry(...)</li>
* <li>Call entryListComplete() after all entries have been fetched</li>
* </ul>
*
* If the importer wants to cancel the import, it should call the dispose()
* method.
*
* If the importer receives the stopFetching-call, it should stop fetching as
* soon as possible (it is not really critical, but good style to not contribute
* any more results via addEntry, call entryListComplete() or dispose(), after
* receiving this call).
*
* @author alver
* @author $Author: mortenalver $
* @version $Revision: 3047 $ ($Date: 2007-11-14 01:25:31 +0100 (Mi, 14 Nov
* 2007) $)
*
*/
public class ImportInspectionDialog extends JDialog implements ImportInspector {
public static interface CallBack {
/**
* This method is called by the dialog when the user has cancelled or
* signalled a stop. It is expected that any long-running fetch
* operations will stop after this method is called.
*/
public void stopFetching();
}
protected ImportInspectionDialog ths = this;
protected BasePanel panel;
protected JabRefFrame frame;
protected MetaData metaData;
protected UIFSplitPane contentPane = new UIFSplitPane(UIFSplitPane.VERTICAL_SPLIT);
protected JTable glTable;
protected TableComparatorChooser<BibtexEntry> comparatorChooser;
protected EventSelectionModel<BibtexEntry> selectionModel;
protected String[] fields;
protected JProgressBar progressBar = new JProgressBar(JProgressBar.HORIZONTAL);
protected JButton ok = new JButton(Globals.lang("Ok")), cancel = new JButton(Globals
.lang("Cancel")), generate = new JButton(Globals.lang("Generate now"));
protected EventList<BibtexEntry> entries = new BasicEventList<BibtexEntry>();
protected SortedList<BibtexEntry> sortedList;
/**
* Duplicate resolving may require deletion of old entries.
*/
protected List<BibtexEntry> entriesToDelete = new ArrayList<BibtexEntry>();
protected String undoName;
protected ArrayList<CallBack> callBacks = new ArrayList<CallBack>();
protected boolean newDatabase;
protected JMenu groupsAdd = new JMenu(Globals.lang("Add to group"));
protected JPopupMenu popup = new JPopupMenu();
protected JButton selectAll = new JButton(Globals.lang("Select all"));
protected JButton deselectAll = new JButton(Globals.lang("Deselect all"));
protected JButton deselectAllDuplicates = new JButton(Globals.lang("Deselect all duplicates"));
protected JButton stop = new JButton(Globals.lang("Stop"));
protected JButton delete = new JButton(Globals.lang("Delete"));
protected JButton help = new JButton(Globals.lang("Help"));
protected PreviewPanel preview;
protected boolean generatedKeys = false; // Set to true after keys have
// been
// generated.
protected boolean defaultSelected = true;
protected Rectangle toRect = new Rectangle(0, 0, 1, 1);
protected Map<BibtexEntry, Set<GroupTreeNode>> groupAdditions = new HashMap<BibtexEntry, Set<GroupTreeNode>>();
protected JCheckBox autoGenerate = new JCheckBox(Globals.lang("Generate keys"), Globals.prefs
.getBoolean("generateKeysAfterInspection"));
protected JLabel duplLabel = new JLabel(GUIGlobals.getImage("duplicate")),
fileLabel = new JLabel(GUIGlobals.getImage("psSmall")), pdfLabel = new JLabel(GUIGlobals
.getImage("pdfSmall")), psLabel = new JLabel(GUIGlobals.getImage("psSmall")),
urlLabel = new JLabel(GUIGlobals.getImage("wwwSmall"));
protected final int DUPL_COL = 1, FILE_COL = 2, PDF_COL = -1,// 3,
PS_COL = -2,// 4,
URL_COL = 3,// 5,
PAD = 4; // 6;
/**
* The "defaultSelected" boolean value determines if new entries added are
* selected for import or not. This value is true by default.
*
* @param defaultSelected
* The desired value.
*/
public void setDefaultSelected(boolean defaultSelected) {
this.defaultSelected = defaultSelected;
}
/**
* Creates a dialog that displays the given list of fields in the table. The
* dialog allows another process to add entries dynamically while the dialog
* is shown.
*
* @param frame
* @param panel
* @param fields
*/
public ImportInspectionDialog(JabRefFrame frame, BasePanel panel, String[] fields,
String undoName, boolean newDatabase) {
this.frame = frame;
this.panel = panel;
this.metaData = (panel != null) ? panel.metaData() : new MetaData();
this.fields = fields;
this.undoName = undoName;
this.newDatabase = newDatabase;
preview = new PreviewPanel(null, metaData, Globals.prefs.get("preview1"));
duplLabel.setToolTipText(Globals
.lang("Possible duplicate of existing entry. Click to resolve."));
sortedList = new SortedList<BibtexEntry>(entries);
EventTableModel<BibtexEntry> tableModelGl = new EventTableModel<BibtexEntry>(sortedList,
new EntryTableFormat());
glTable = new EntryTable(tableModelGl);
GeneralRenderer renderer = new GeneralRenderer(Color.white);
glTable.setDefaultRenderer(JLabel.class, renderer);
glTable.setDefaultRenderer(String.class, renderer);
glTable.getInputMap().put(Globals.prefs.getKey("Delete"), "delete");
DeleteListener deleteListener = new DeleteListener();
glTable.getActionMap().put("delete", deleteListener);
selectionModel = new EventSelectionModel<BibtexEntry>(sortedList);
glTable.setSelectionModel(selectionModel);
selectionModel.getSelected().addListEventListener(new EntrySelectionListener());
comparatorChooser = new TableComparatorChooser<BibtexEntry>(glTable, sortedList,
TableComparatorChooser.MULTIPLE_COLUMN_KEYBOARD);
setupComparatorChooser();
glTable.addMouseListener(new TableClickListener());
setWidths();
getContentPane().setLayout(new BorderLayout());
progressBar.setIndeterminate(true);
JPanel centerPan = new JPanel();
centerPan.setLayout(new BorderLayout());
contentPane.setTopComponent(new JScrollPane(glTable));
contentPane.setBottomComponent(preview);
centerPan.add(contentPane, BorderLayout.CENTER);
centerPan.add(progressBar, BorderLayout.SOUTH);
popup.add(deleteListener);
popup.addSeparator();
if (!newDatabase) {
GroupTreeNode node = metaData.getGroups();
groupsAdd.setEnabled(false); // Will get enabled if there are
// groups that can be added to.
insertNodes(groupsAdd, node);
popup.add(groupsAdd);
}
// Add "Attach file" menu choices to right click menu:
popup.add(new LinkLocalFile());
popup.add(new DownloadFile());
popup.add(new AutoSetLinks());
// popup.add(new AttachFile("pdf"));
// popup.add(new AttachFile("ps"));
popup.add(new AttachUrl());
getContentPane().add(centerPan, BorderLayout.CENTER);
ButtonBarBuilder bb = new ButtonBarBuilder();
bb.addGlue();
bb.addGridded(ok);
bb.addGridded(stop);
bb.addGridded(cancel);
bb.addRelatedGap();
bb.addGridded(help);
bb.addGlue();
bb.getPanel().setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
ButtonStackBuilder builder = new ButtonStackBuilder();
builder.addGridded(selectAll);
builder.addGridded(deselectAll);
builder.addGridded(deselectAllDuplicates);
builder.addRelatedGap();
builder.addGridded(delete);
builder.addRelatedGap();
builder.addGridded(autoGenerate);
builder.addGridded(generate);
builder.getPanel().setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
centerPan.add(builder.getPanel(), BorderLayout.WEST);
ok.setEnabled(false);
generate.setEnabled(false);
ok.addActionListener(new OkListener());
cancel.addActionListener(new CancelListener());
generate.addActionListener(new GenerateListener());
stop.addActionListener(new StopListener());
selectAll.addActionListener(new SelectionButton(true));
deselectAll.addActionListener(new SelectionButton(false));
deselectAllDuplicates.addActionListener(new DeselectDuplicatesButtonListener());
deselectAllDuplicates.setEnabled(false);
delete.addActionListener(deleteListener);
help.addActionListener(new HelpAction(frame.helpDiag, GUIGlobals.importInspectionHelp));
getContentPane().add(bb.getPanel(), BorderLayout.SOUTH);
// Remember and default to last size:
setSize(new Dimension(Globals.prefs.getInt("importInspectionDialogWidth"), Globals.prefs
.getInt("importInspectionDialogHeight")));
addWindowListener(new WindowAdapter() {
public void windowOpened(WindowEvent e) {
contentPane.setDividerLocation(0.5f);
}
public void windowClosed(WindowEvent e) {
Globals.prefs.putInt("importInspectionDialogWidth", getSize().width);
Globals.prefs.putInt("importInspectionDialogHeight", getSize().height);
}
});
// Key bindings:
AbstractAction closeAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
dispose();
}
};
ActionMap am = contentPane.getActionMap();
InputMap im = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
im.put(Globals.prefs.getKey("Close dialog"), "close");
am.put("close", closeAction);
}
/* (non-Javadoc)
* @see net.sf.jabref.gui.ImportInspection#setProgress(int, int)
*/
public void setProgress(int current, int max) {
progressBar.setIndeterminate(false);
progressBar.setMinimum(0);
progressBar.setMaximum(max);
progressBar.setValue(current);
}
/* (non-Javadoc)
* @see net.sf.jabref.gui.ImportInspection#addEntry(net.sf.jabref.BibtexEntry)
*/
public void addEntry(BibtexEntry entry) {
List<BibtexEntry> list = new ArrayList<BibtexEntry>();
list.add(entry);
addEntries(list);
}
/* (non-Javadoc)
* @see net.sf.jabref.gui.ImportInspection#addEntries(java.util.Collection)
*/
public void addEntries(Collection<BibtexEntry> entries) {
for (BibtexEntry entry : entries) {
// We exploit the entry's search status for indicating "Keep"
// status:
entry.setSearchHit(defaultSelected);
// We exploit the entry's group status for indicating duplicate
// status.
// Checking duplicates means both checking against the background
// database (if
// applicable) and against entries already in the table.
if (((panel != null) && (DuplicateCheck.containsDuplicate(panel.database(), entry) != null)) ||
(internalDuplicate(this.entries, entry) != null)) {
entry.setGroupHit(true);
deselectAllDuplicates.setEnabled(true);
}
this.entries.getReadWriteLock().writeLock().lock();
this.entries.add(entry);
this.entries.getReadWriteLock().writeLock().unlock();
}
}
/**
* Checks if there are duplicates to the given entry in the Collection. Does
* not report the entry as duplicate of itself if it is in the Collection.
*
* @param entries
* A Collection of BibtexEntry instances.
* @param entry
* The entry to search for duplicates of.
* @return A possible duplicate, if any, or null if none were found.
*/
protected BibtexEntry internalDuplicate(Collection<BibtexEntry> entries, BibtexEntry entry) {
for (BibtexEntry othEntry : entries) {
if (othEntry == entry)
continue; // Don't compare the entry to itself
if (DuplicateCheck.isDuplicate(entry, othEntry))
return othEntry;
}
return null;
}
/**
* Removes all selected entries from the table. Synchronizes on this.entries
* to prevent conflict with addition of new entries.
*/
public void removeSelectedEntries() {
int row = glTable.getSelectedRow();
List<Object> toRemove = new ArrayList<Object>();
toRemove.addAll(selectionModel.getSelected());
entries.getReadWriteLock().writeLock().lock();
for (Object o : toRemove) {
entries.remove(o);
}
entries.getReadWriteLock().writeLock().unlock();
glTable.clearSelection();
if ((row >= 0) && (entries.size() > 0)) {
row = Math.min(entries.size() - 1, row);
glTable.addRowSelectionInterval(row, row);
}
}
/* (non-Javadoc)
* @see net.sf.jabref.gui.ImportInspection#entryListComplete()
*/
public void entryListComplete() {
progressBar.setIndeterminate(false);
progressBar.setVisible(false);
ok.setEnabled(true);
if (!generatedKeys)
generate.setEnabled(true);
stop.setEnabled(false);
}
/**
* This method returns a List containing all entries that are selected
* (checkbox checked).
*
* @return a List containing the selected entries.
*/
public List<BibtexEntry> getSelectedEntries() {
List<BibtexEntry> selected = new ArrayList<BibtexEntry>();
for (Iterator<BibtexEntry> i = entries.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
if (entry.isSearchHit())
selected.add(entry);
}
/*
* for (int i = 0; i < table.getRowCount(); i++) { Boolean sel =
* (Boolean) table.getValueAt(i, 0); if (sel.booleanValue()) {
* selected.add(entries.get(i)); } }
*/
return selected;
}
/**
* Generate key for the selected entry only.
*/
public void generateKeySelectedEntry() {
if (selectionModel.getSelected().size() != 1)
return;
BibtexEntry entry = selectionModel.getSelected().get(0);
entries.getReadWriteLock().writeLock().lock();
BibtexDatabase database = null;
// Relate to the existing database, if any:
if (panel != null)
database = panel.database();
// ... or create a temporary one:
else
database = new BibtexDatabase();
try {
entry.setId(Util.createNeutralId());
// Add the entry to the database we are working with:
database.insertEntry(entry);
} catch (KeyCollisionException ex) {
ex.printStackTrace();
}
// Generate a unique key:
LabelPatternUtil.makeLabel(Globals.prefs.getKeyPattern(), database, entry);
// Remove the entry from the database again, since we only added it in
// order to
// make sure the key was unique:
database.removeEntry(entry.getId());
entries.getReadWriteLock().writeLock().lock();
glTable.repaint();
}
/**
* Generate keys for all entries. All keys will be unique with respect to
* one another, and, if they are destined for an existing database, with
* respect to existing keys in the database.
*/
public void generateKeys(boolean addColumn) {
entries.getReadWriteLock().writeLock().lock();
BibtexDatabase database = null;
// Relate to the existing database, if any:
if (panel != null)
database = panel.database();
// ... or create a temporary one:
else
database = new BibtexDatabase();
List<String> keys = new ArrayList<String>(entries.size());
// Iterate over the entries, add them to the database we are working
// with,
// and generate unique keys:
for (Iterator<BibtexEntry> i = entries.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
// if (newDatabase) {
try {
entry.setId(Util.createNeutralId());
database.insertEntry(entry);
} catch (KeyCollisionException ex) {
ex.printStackTrace();
}
// }
LabelPatternUtil.makeLabel(Globals.prefs.getKeyPattern(), database, entry);
// Add the generated key to our list:
keys.add(entry.getCiteKey());
}
// Remove the entries from the database again, since they are not
// supposed to
// added yet. They only needed to be in it while we generated the keys,
// to keep
// control over key uniqueness.
for (Iterator<BibtexEntry> i = entries.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
database.removeEntry(entry.getId());
}
entries.getReadWriteLock().writeLock().lock();
glTable.repaint();
}
public void insertNodes(JMenu menu, GroupTreeNode node) {
final AbstractAction action = getAction(node);
if (node.getChildCount() == 0) {
menu.add(action);
if (action.isEnabled())
menu.setEnabled(true);
return;
}
JMenu submenu = null;
if (node.getGroup() instanceof AllEntriesGroup) {
for (int i = 0; i < node.getChildCount(); ++i) {
insertNodes(menu, (GroupTreeNode) node.getChildAt(i));
}
} else {
submenu = new JMenu("[" + node.getGroup().getName() + "]");
// setEnabled(true) is done above/below if at least one menu
// entry (item or submenu) is enabled
submenu.setEnabled(action.isEnabled());
submenu.add(action);
submenu.add(new JPopupMenu.Separator());
for (int i = 0; i < node.getChildCount(); ++i)
insertNodes(submenu, (GroupTreeNode) node.getChildAt(i));
menu.add(submenu);
if (submenu.isEnabled())
menu.setEnabled(true);
}
}
protected AbstractAction getAction(GroupTreeNode node) {
AbstractAction action = new AddToGroupAction(node);
AbstractGroup group = node.getGroup();
action.setEnabled(group.supportsAdd());
return action;
}
/**
* Stores the information about the selected entries being scheduled for
* addition to this group. The entries are *not* added to the group at this
* time. <p/> Synchronizes on this.entries to prevent conflict with threads
* that modify the entry list.
*/
class AddToGroupAction extends AbstractAction {
protected GroupTreeNode node;
public AddToGroupAction(GroupTreeNode node) {
super(node.getGroup().getName());
this.node = node;
}
public void actionPerformed(ActionEvent event) {
selectionModel.getSelected().getReadWriteLock().writeLock().lock();
for (Iterator<BibtexEntry> i = selectionModel.getSelected().iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
// We store the groups this entry should be added to in a Set in
// the Map:
Set<GroupTreeNode> groups = groupAdditions.get(entry);
if (groups == null) {
// No previous definitions, so we create the Set now:
groups = new HashSet<GroupTreeNode>();
groupAdditions.put(entry, groups);
}
// Add the group:
groups.add(node);
}
selectionModel.getSelected().getReadWriteLock().writeLock().unlock();
}
}
public void addCallBack(CallBack cb) {
callBacks.add(cb);
}
class OkListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
// First check if we are supposed to warn about duplicates. If so,
// see if there
// are unresolved duplicates, and warn if yes.
if (Globals.prefs.getBoolean("warnAboutDuplicatesInInspection")) {
for (Iterator<BibtexEntry> i = entries.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
// Only check entries that are to be imported. Keep status
// is indicated
// through the search hit status of the entry:
if (!entry.isSearchHit())
continue;
// Check if the entry is a suspected, unresolved, duplicate.
// This status
// is indicated by the entry's group hit status:
if (entry.isGroupHit()) {
CheckBoxMessage cbm = new CheckBoxMessage(
Globals
.lang("There are possible duplicates (marked with a 'D' icon) that haven't been resolved. Continue?"),
Globals.lang("Disable this confirmation dialog"), false);
int answer = JOptionPane.showConfirmDialog(ImportInspectionDialog.this,
cbm, Globals.lang("Duplicates found"), JOptionPane.YES_NO_OPTION);
if (cbm.isSelected())
Globals.prefs.putBoolean("warnAboutDuplicatesInInspection", false);
if (answer == JOptionPane.NO_OPTION)
return;
break;
}
}
}
// The compund undo action used to contain all changes made by this
// dialog.
NamedCompound ce = new NamedCompound(undoName);
// See if we should remove any old entries for duplicate resolving:
if (entriesToDelete.size() > 0) {
for (Iterator<BibtexEntry> i = entriesToDelete.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
ce.addEdit(new UndoableRemoveEntry(panel.database(), entry, panel));
panel.database().removeEntry(entry.getId());
}
}
// If "Generate keys" is checked, generate keys unless it's already
// been done:
if (autoGenerate.isSelected() && !generatedKeys) {
generateKeys(false);
}
// Remember the choice until next time:
Globals.prefs.putBoolean("generateKeysAfterInspection", autoGenerate.isSelected());
final List<BibtexEntry> selected = getSelectedEntries();
if (selected.size() > 0) {
if (newDatabase) {
// Create a new BasePanel for the entries:
BibtexDatabase base = new BibtexDatabase();
panel = new BasePanel(frame, base, null, new HashMap<String, String>(),
Globals.prefs.get("defaultEncoding"));
}
boolean groupingCanceled = false;
// Set owner/timestamp if options are enabled:
Util.setAutomaticFields(selected, Globals.prefs.getBoolean("overwriteOwner"),
Globals.prefs.getBoolean("overwriteTimeStamp"), Globals.prefs.getBoolean("markImportedEntries"));
// Check if we should unmark entries before adding the new ones:
if (Globals.prefs.getBoolean("unmarkAllEntriesBeforeImporting"))
for (BibtexEntry entry : panel.database().getEntries()) {
Util.unmarkEntry(entry, panel.database(), ce);
}
for (Iterator<BibtexEntry> i = selected.iterator(); i.hasNext();) {
BibtexEntry entry = i.next();
// entry.clone();
// Remove settings to group/search hit status:
entry.setSearchHit(false);
entry.setGroupHit(false);
// If this entry should be added to any groups, do it now:
Set<GroupTreeNode> groups = groupAdditions.get(entry);
if (!groupingCanceled && (groups != null)) {
if (entry.getField(BibtexFields.KEY_FIELD) == null) {
// The entry has no key, so it can't be added to the
// group.
// The best course of ation is probably to ask the
// user if a key should be generated
// immediately.
int answer = JOptionPane
.showConfirmDialog(
ImportInspectionDialog.this,
Globals
.lang("Cannot add entries to group without generating keys. Generate keys now?"),
Globals.lang("Add to group"), JOptionPane.YES_NO_OPTION);
if (answer == JOptionPane.YES_OPTION) {
generateKeys(false);
} else
groupingCanceled = true;
}
// If the key was list, or has been list now, go ahead:
if (entry.getField(BibtexFields.KEY_FIELD) != null) {
for (Iterator<GroupTreeNode> i2 = groups.iterator(); i2.hasNext();) {
GroupTreeNode node = i2.next();
if (node.getGroup().supportsAdd()) {
// Add the entry:
AbstractUndoableEdit undo = node.getGroup().add(
new BibtexEntry[] { entry });
if (undo instanceof UndoableChangeAssignment)
((UndoableChangeAssignment) undo).setEditedNode(node);
ce.addEdit(undo);
} else {
// Shouldn't happen...
}
}
}
}
try {
entry.setId(Util.createNeutralId());
panel.database().insertEntry(entry);
// Let the autocompleters, if any, harvest words from
// the entry:
Util.updateCompletersForEntry(panel.getAutoCompleters(), entry);
ce.addEdit(new UndoableInsertEntry(panel.database(), entry, panel));
} catch (KeyCollisionException e) {
e.printStackTrace();
}
}
ce.end();
panel.undoManager.addEdit(ce);
}
dispose();
SwingUtilities.invokeLater(new Thread() {
public void run() {
if (newDatabase) {
frame.addTab(panel, null, true);
}
panel.markBaseChanged();
if (selected.size() > 0) {
frame.output(Globals.lang("Number of entries successfully imported") +
": " + selected.size());
} else {
frame.output(Globals.lang("No entries imported."));
}
}
});
}
}
protected void signalStopFetching() {
for (CallBack c : callBacks) {
c.stopFetching();
}
}
protected void setWidths() {
TableColumnModel cm = glTable.getColumnModel();
cm.getColumn(0).setPreferredWidth(55);
cm.getColumn(0).setMinWidth(55);
cm.getColumn(0).setMaxWidth(55);
for (int i = 1; i < PAD; i++) {
// Lock the width of icon columns.
cm.getColumn(i).setPreferredWidth(GUIGlobals.WIDTH_ICON_COL);
cm.getColumn(i).setMinWidth(GUIGlobals.WIDTH_ICON_COL);
cm.getColumn(i).setMaxWidth(GUIGlobals.WIDTH_ICON_COL);
}
for (int i = 0; i < fields.length; i++) {
int width = BibtexFields.getFieldLength(fields[i]);
glTable.getColumnModel().getColumn(i + PAD).setPreferredWidth(width);
}
}
class StopListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
signalStopFetching();
entryListComplete();
}
}
class CancelListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
signalStopFetching();
dispose();
frame.output(Globals.lang("Import canceled by user"));
}
}
class GenerateListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
generate.setEnabled(false);
generatedKeys = true; // To prevent the button from getting
// enabled again.
generateKeys(true); // Generate the keys.
}
}
class DeleteListener extends AbstractAction implements ActionListener {
public DeleteListener() {
super(Globals.lang("Delete"), GUIGlobals.getImage("delete"));
}
public void actionPerformed(ActionEvent event) {
removeSelectedEntries();
}
}
class MyTable extends JTable {
public MyTable(TableModel model) {
super(model);
// setDefaultRenderer(Boolean.class, );
}
public boolean isCellEditable(int row, int col) {
return col == 0;
}
}
class MyTableModel extends DefaultTableModel {
public Class<?> getColumnClass(int i) {
if (i == 0)
return Boolean.class;
else
return String.class;
}
}
class SelectionButton implements ActionListener {
protected Boolean enable;
public SelectionButton(boolean enable) {
this.enable = Boolean.valueOf(enable);
}
public void actionPerformed(ActionEvent event) {
for (int i = 0; i < glTable.getRowCount(); i++) {
glTable.setValueAt(enable, i, 0);
}
glTable.repaint();
}
}
class DeselectDuplicatesButtonListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
for (int i = 0; i < glTable.getRowCount(); i++) {
if (glTable.getValueAt(i, DUPL_COL) != null) {
glTable.setValueAt(Boolean.valueOf(false), i, 0);
}
}
glTable.repaint();
}
}
class EntrySelectionListener implements ListEventListener<BibtexEntry> {
public void listChanged(ListEvent<BibtexEntry> listEvent) {
if (listEvent.getSourceList().size() == 1) {
preview.setEntry(listEvent.getSourceList().get(0));
contentPane.setDividerLocation(0.5f);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
preview.scrollRectToVisible(toRect);
}
});
}
}
}
/**
* This class handles clicks on the table that should trigger specific
* events, like opening the popup menu.
*/
class TableClickListener implements MouseListener {
public boolean isIconColumn(int col) {
return (col == FILE_COL) || (col == PDF_COL) || (col == PS_COL) || (col == URL_COL);
}
public void mouseClicked(MouseEvent e) {
final int col = glTable.columnAtPoint(e.getPoint()), row = glTable.rowAtPoint(e
.getPoint());
if (isIconColumn(col)) {
BibtexEntry entry = sortedList.get(row);
switch (col) {
case FILE_COL:
Object o = entry.getField(GUIGlobals.FILE_FIELD);
if (o != null) {
FileListTableModel tableModel = new FileListTableModel();
tableModel.setContent((String) o);
if (tableModel.getRowCount() == 0)
return;
FileListEntry fl = tableModel.getEntry(0);
(new ExternalFileMenuItem(frame, entry, "", fl.getLink(), null, panel
.metaData(), fl.getType())).actionPerformed(null);
}
break;
case URL_COL:
openExternalLink("url", e);
break;
case PDF_COL:
openExternalLink("pdf", e);
break;
case PS_COL:
openExternalLink("ps", e);
break;
}
}
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
/**
* Show right-click menu. If the click happened in an icon column that
* presents its own popup menu, show that. Otherwise, show the ordinary
* popup menu.
*
* @param e
* The mouse event that triggered the popup.
*/
public void showPopup(MouseEvent e) {
final int col = glTable.columnAtPoint(e.getPoint());
switch (col) {
case FILE_COL:
showFileFieldMenu(e);
break;
default:
showOrdinaryRightClickMenu(e);
break;
}
}
public void showOrdinaryRightClickMenu(MouseEvent e) {
popup.show(glTable, e.getX(), e.getY());
}
/**
* Show the popup menu for the FILE field.
*
* @param e
* The mouse event that triggered the popup.
*/
public void showFileFieldMenu(MouseEvent e) {
final int row = glTable.rowAtPoint(e.getPoint());
BibtexEntry entry = sortedList.get(row);
JPopupMenu menu = new JPopupMenu();
int count = 0;
Object o = entry.getField(GUIGlobals.FILE_FIELD);
FileListTableModel fileList = new FileListTableModel();
fileList.setContent((String) o);
// If there are one or more links, open the first one:
for (int i = 0; i < fileList.getRowCount(); i++) {
FileListEntry flEntry = fileList.getEntry(i);
String description = flEntry.getDescription();
if ((description == null) || (description.trim().length() == 0))
description = flEntry.getLink();
menu.add(new ExternalFileMenuItem(panel.frame(), entry, description, flEntry
.getLink(), flEntry.getType().getIcon(), panel.metaData(), flEntry.getType()));
count++;
}
if (count == 0) {
showOrdinaryRightClickMenu(e);
} else
menu.show(glTable, e.getX(), e.getY());
}
/**
* Open old-style external links after user clicks icon.
*
* @param fieldName
* The name of the BibTeX field this icon is used for.
* @param e
* The MouseEvent that triggered this operation.
*/
public void openExternalLink(String fieldName, MouseEvent e) {
final int row = glTable.rowAtPoint(e.getPoint());
BibtexEntry entry = sortedList.get(row);
Object link = entry.getField(fieldName);
try {
if (link != null)
Util.openExternalViewer(panel.metaData(), (String) link, fieldName);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void mouseReleased(MouseEvent e) {
// Check if the user has right-clicked. If so, open the right-click
// menu.
if (e.isPopupTrigger()) {
showPopup(e);
return;
}
}
public void mousePressed(MouseEvent e) {
// Check if the user has right-clicked. If so, open the right-click
// menu.
if (e.isPopupTrigger()) {
showPopup(e);
return;
}
// Check if any other action should be taken:
final int col = glTable.columnAtPoint(e.getPoint()), row = glTable.rowAtPoint(e
.getPoint());
// Is this the duplicate icon column, and is there an icon?
if ((col == DUPL_COL) && (glTable.getValueAt(row, col) != null)) {
BibtexEntry first = sortedList.get(row);
BibtexEntry other = DuplicateCheck.containsDuplicate(panel.database(), first);
if (other != null) {
// This will be true if the duplicate is in the existing
// database.
DuplicateResolverDialog diag = new DuplicateResolverDialog(
ImportInspectionDialog.this, other, first,
DuplicateResolverDialog.INSPECTION);
Util.placeDialog(diag, ImportInspectionDialog.this);
diag.setVisible(true);
ImportInspectionDialog.this.toFront();
if (diag.getSelected() == DuplicateResolverDialog.KEEP_UPPER) {
// Remove old entry. Or... add it to a list of entries
// to be deleted. We only delete
// it after Ok is clicked.
entriesToDelete.add(other);
// Clear duplicate icon, which is controlled by the
// group hit
// field of the entry:
entries.getReadWriteLock().writeLock().lock();
first.setGroupHit(false);
entries.getReadWriteLock().writeLock().unlock();
} else if (diag.getSelected() == DuplicateResolverDialog.KEEP_LOWER) {
// Remove the entry from the import inspection dialog.
entries.getReadWriteLock().writeLock().lock();
entries.remove(first);
entries.getReadWriteLock().writeLock().unlock();
} else if (diag.getSelected() == DuplicateResolverDialog.KEEP_BOTH) {
// Do nothing.
entries.getReadWriteLock().writeLock().lock();
first.setGroupHit(false);
entries.getReadWriteLock().writeLock().unlock();
}
}
// Check if the duplicate is of another entry in the import:
other = internalDuplicate(entries, first);
if (other != null) {
int answer = DuplicateResolverDialog.resolveDuplicate(
ImportInspectionDialog.this, first, other);
if (answer == DuplicateResolverDialog.KEEP_UPPER) {
entries.remove(other);
first.setGroupHit(false);
} else if (answer == DuplicateResolverDialog.KEEP_LOWER) {
entries.remove(first);
} else if (answer == DuplicateResolverDialog.KEEP_BOTH) {
first.setGroupHit(false);
}
}
}
}
}
class AttachUrl extends JMenuItem implements ActionListener {
public AttachUrl() {
super(Globals.lang("Attach URL"));
addActionListener(this);
}
public void actionPerformed(ActionEvent event) {
if (selectionModel.getSelected().size() != 1)
return;
BibtexEntry entry = selectionModel.getSelected().get(0);
String result = JOptionPane.showInputDialog(ImportInspectionDialog.this, Globals
.lang("Enter URL"), entry.getField("url"));
entries.getReadWriteLock().writeLock().lock();
if (result != null) {
if (result.equals("")) {
entry.clearField("url");
} else {
entry.setField("url", result);
}
}
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
class DownloadFile extends JMenuItem implements ActionListener,
DownloadExternalFile.DownloadCallback {
BibtexEntry entry = null;
public DownloadFile() {
super(Globals.lang("Download file"));
addActionListener(this);
}
public void actionPerformed(ActionEvent actionEvent) {
if (selectionModel.getSelected().size() != 1)
return;
entry = selectionModel.getSelected().get(0);
String bibtexKey = entry.getCiteKey();
if (bibtexKey == null) {
int answer = JOptionPane.showConfirmDialog(frame, Globals
.lang("This entry has no BibTeX key. Generate key now?"), Globals
.lang("Download file"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (answer == JOptionPane.OK_OPTION) {
generateKeySelectedEntry();
bibtexKey = entry.getCiteKey();
}
}
DownloadExternalFile def = new DownloadExternalFile(frame, metaData, bibtexKey);
try {
def.download(this);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void downloadComplete(FileListEntry file) {
ImportInspectionDialog.this.toFront(); // Hack
FileListTableModel model = new FileListTableModel();
String oldVal = entry.getField(GUIGlobals.FILE_FIELD);
if (oldVal != null)
model.setContent(oldVal);
model.addEntry(model.getRowCount(), file);
entries.getReadWriteLock().writeLock().lock();
entry.setField(GUIGlobals.FILE_FIELD, model.getStringRepresentation());
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
class AutoSetLinks extends JMenuItem implements ActionListener {
public AutoSetLinks() {
super(Globals.lang("Autoset external links"));
addActionListener(this);
}
public void actionPerformed(ActionEvent actionEvent) {
if (selectionModel.getSelected().size() != 1)
return;
final BibtexEntry entry = selectionModel.getSelected().get(0);
String bibtexKey = entry.getCiteKey();
if (bibtexKey == null) {
int answer = JOptionPane.showConfirmDialog(frame, Globals
.lang("This entry has no BibTeX key. Generate key now?"), Globals
.lang("Download file"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (answer == JOptionPane.OK_OPTION) {
generateKeySelectedEntry();
bibtexKey = entry.getCiteKey();
} else
return; // Can't go on without the bibtex key.
}
final FileListTableModel model = new FileListTableModel();
String oldVal = entry.getField(GUIGlobals.FILE_FIELD);
if (oldVal != null)
model.setContent(oldVal);
// We have a static utility method for searching for all relevant
// links:
JDialog diag = new JDialog(ImportInspectionDialog.this, true);
FileListEditor.autoSetLinks(entry, model, metaData, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (e.getID() > 0) {
entries.getReadWriteLock().writeLock().lock();
entry.setField(GUIGlobals.FILE_FIELD, model.getStringRepresentation());
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
}, diag);
}
}
class LinkLocalFile extends JMenuItem implements ActionListener,
DownloadExternalFile.DownloadCallback {
BibtexEntry entry = null;
public LinkLocalFile() {
super(Globals.lang("Link local file"));
addActionListener(this);
}
public void actionPerformed(ActionEvent actionEvent) {
if (selectionModel.getSelected().size() != 1)
return;
entry = selectionModel.getSelected().get(0);
FileListEntry flEntry = new FileListEntry("", "", null);
FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, true,
metaData);
editor.setVisible(true, true);
if (editor.okPressed()) {
FileListTableModel model = new FileListTableModel();
String oldVal = entry.getField(GUIGlobals.FILE_FIELD);
if (oldVal != null)
model.setContent(oldVal);
model.addEntry(model.getRowCount(), flEntry);
entries.getReadWriteLock().writeLock().lock();
entry.setField(GUIGlobals.FILE_FIELD, model.getStringRepresentation());
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
public void downloadComplete(FileListEntry file) {
ImportInspectionDialog.this.toFront(); // Hack
FileListTableModel model = new FileListTableModel();
String oldVal = entry.getField(GUIGlobals.FILE_FIELD);
if (oldVal != null)
model.setContent(oldVal);
model.addEntry(model.getRowCount(), file);
entries.getReadWriteLock().writeLock().lock();
entry.setField(GUIGlobals.FILE_FIELD, model.getStringRepresentation());
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
class AttachFile extends JMenuItem implements ActionListener {
String fileType;
public AttachFile(String fileType) {
super(Globals.lang("Attach %0 file", new String[] { fileType.toUpperCase() }));
this.fileType = fileType;
addActionListener(this);
}
public void actionPerformed(ActionEvent event) {
if (selectionModel.getSelected().size() != 1)
return;
BibtexEntry entry = selectionModel.getSelected().get(0);
// Call up a dialog box that provides Browse, Download and auto
// buttons:
AttachFileDialog diag = new AttachFileDialog(ImportInspectionDialog.this, metaData,
entry, fileType);
Util.placeDialog(diag, ImportInspectionDialog.this);
diag.setVisible(true);
// After the dialog has closed, if it wasn't cancelled, list the
// field:
if (!diag.cancelled()) {
entries.getReadWriteLock().writeLock().lock();
entry.setField(fileType, diag.getValue());
entries.getReadWriteLock().writeLock().unlock();
glTable.repaint();
}
}
}
@SuppressWarnings("unchecked")
protected void setupComparatorChooser() {
// First column:
java.util.List<Comparator<BibtexEntry>> comparators = comparatorChooser
.getComparatorsForColumn(0);
comparators.clear();
comparators = comparatorChooser.getComparatorsForColumn(1);
comparators.clear();
// Icon columns:
for (int i = 2; i < PAD; i++) {
comparators = comparatorChooser.getComparatorsForColumn(i);
comparators.clear();
if (i == FILE_COL)
comparators.add(new IconComparator(new String[] { GUIGlobals.FILE_FIELD }));
else if (i == PDF_COL)
comparators.add(new IconComparator(new String[] { "pdf" }));
else if (i == PS_COL)
comparators.add(new IconComparator(new String[] { "ps" }));
else if (i == URL_COL)
comparators.add(new IconComparator(new String[] { "url" }));
}
// Remaining columns:
for (int i = PAD; i < PAD + fields.length; i++) {
comparators = comparatorChooser.getComparatorsForColumn(i);
comparators.clear();
comparators.add(new FieldComparator(fields[i - PAD]));
}
// Set initial sort columns:
/*
* // Default sort order: String[] sortFields = new String[]
* {Globals.prefs.get("priSort"), Globals.prefs.get("secSort"),
* Globals.prefs.get("terSort")}; boolean[] sortDirections = new
* boolean[] {Globals.prefs.getBoolean("priDescending"),
* Globals.prefs.getBoolean("secDescending"),
* Globals.prefs.getBoolean("terDescending")}; // descending
*/
sortedList.getReadWriteLock().writeLock().lock();
comparatorChooser.appendComparator(PAD, 0, false);
sortedList.getReadWriteLock().writeLock().unlock();
}
class EntryTable extends JTable {
GeneralRenderer renderer = new GeneralRenderer(Color.white);
public EntryTable(TableModel model) {
super(model);
getTableHeader().setReorderingAllowed(false);
}
public TableCellRenderer getCellRenderer(int row, int column) {
return column == 0 ? getDefaultRenderer(Boolean.class) : renderer;
}
/*
* public TableCellEditor getCellEditor() { return
* getDefaultEditor(Boolean.class); }
*/
public Class<?> getColumnClass(int col) {
if (col == 0)
return Boolean.class;
else if (col < PAD)
return JLabel.class;
else
return String.class;
}
public boolean isCellEditable(int row, int column) {
return column == 0;
}
public void setValueAt(Object value, int row, int column) {
// Only column 0, which is controlled by BibtexEntry.searchHit, is
// editable:
entries.getReadWriteLock().writeLock().lock();
BibtexEntry entry = sortedList.get(row);
entry.setSearchHit(((Boolean) value).booleanValue());
entries.getReadWriteLock().writeLock().unlock();
}
}
class EntryTableFormat implements TableFormat<BibtexEntry> {
public int getColumnCount() {
return PAD + fields.length;
}
public String getColumnName(int i) {
if (i == 0)
return Globals.lang("Keep");
if (i >= PAD) {
return Util.nCase(fields[i - PAD]);
}
return "";
}
public Object getColumnValue(BibtexEntry entry, int i) {
if (i == 0)
return entry.isSearchHit() ? Boolean.TRUE : Boolean.FALSE;
else if (i < PAD) {
Object o;
switch (i) {
case DUPL_COL:
return entry.isGroupHit() ? duplLabel : null;
case FILE_COL:
o = entry.getField(GUIGlobals.FILE_FIELD);
if (o != null) {
FileListTableModel model = new FileListTableModel();
model.setContent((String) o);
fileLabel.setToolTipText(model.getToolTipHTMLRepresentation());
if (model.getRowCount() > 0)
fileLabel.setIcon(model.getEntry(0).getType().getIcon());
return fileLabel;
} else
return null;
case PDF_COL:
o = entry.getField("pdf");
if (o != null) {
pdfLabel.setToolTipText((String) o);
return pdfLabel;
} else
return null;
case PS_COL:
o = entry.getField("ps");
if (o != null) {
psLabel.setToolTipText((String) o);
return psLabel;
} else
return null;
case URL_COL:
o = entry.getField("url");
if (o != null) {
urlLabel.setToolTipText((String) o);
return urlLabel;
} else
return null;
default:
return null;
}
} else {
String field = fields[i - PAD];
if (field.equals("author") || field.equals("editor")) {
String contents = entry.getField(field);
return (contents != null) ? AuthorList.fixAuthor_Natbib(contents) : "";
} else
return entry.getField(field);
}
}
}
public void toFront() {
super.toFront();
}
}