1 package net.sf.jabref.gui;
3 import net.sf.jabref.*;
4 import net.sf.jabref.groups.EntryTableTransferHandler;
5 import net.sf.jabref.undo.NamedCompound;
6 import net.sf.jabref.undo.UndoableFieldChange;
7 import net.sf.jabref.external.*;
11 import java.awt.dnd.DnDConstants;
12 import java.awt.datatransfer.Transferable;
13 import java.awt.datatransfer.DataFlavor;
14 import java.awt.datatransfer.UnsupportedFlavorException;
15 import java.awt.event.*;
17 import java.util.List;
19 import java.io.IOException;
22 import com.jgoodies.forms.builder.DefaultFormBuilder;
23 import com.jgoodies.forms.layout.FormLayout;
26 * Created by Morten O. Alver 2007.02.22
28 public class FileListEditor extends JTable implements FieldEditor,
29 DownloadExternalFile.DownloadCallback {
32 FileListEntryEditor editor = null;
33 private JabRefFrame frame;
34 private MetaData metaData;
35 private String fieldName;
36 private EntryEditor entryEditor;
38 private FileListTableModel tableModel;
39 private JScrollPane sPane;
40 private JButton add, remove, up, down, auto, download;
41 private JPopupMenu menu = new JPopupMenu();
42 private JMenuItem item = new JMenuItem(Globals.lang("Open"));
46 public FileListEditor(JabRefFrame frame, MetaData metaData, String fieldName, String content,
47 EntryEditor entryEditor) {
49 this.metaData = metaData;
50 this.fieldName = fieldName;
51 this.entryEditor = entryEditor;
52 label = new FieldNameLabel(" " + Util.nCase(fieldName) + " ");
53 tableModel = new FileListTableModel();
56 sPane = new JScrollPane(this);
58 addMouseListener(new TableClickListener());
60 add = new JButton(GUIGlobals.getImage("add"));
61 add.setToolTipText(Globals.lang("New file link (INSERT)"));
62 remove = new JButton(GUIGlobals.getImage("remove"));
63 remove.setToolTipText(Globals.lang("Remove file link (DELETE)"));
64 up = new JButton(GUIGlobals.getImage("up"));
66 down = new JButton(GUIGlobals.getImage("down"));
67 auto = new JButton(Globals.lang("Auto"));
68 download = new JButton(Globals.lang("Download"));
69 add.setMargin(new Insets(0,0,0,0));
70 remove.setMargin(new Insets(0,0,0,0));
71 up.setMargin(new Insets(0,0,0,0));
72 down.setMargin(new Insets(0,0,0,0));
73 add.addActionListener(new ActionListener() {
74 public void actionPerformed(ActionEvent e) {
78 remove.addActionListener(new ActionListener() {
79 public void actionPerformed(ActionEvent e) {
83 up.addActionListener(new ActionListener() {
84 public void actionPerformed(ActionEvent e) {
88 down.addActionListener(new ActionListener() {
89 public void actionPerformed(ActionEvent e) {
93 auto.addActionListener(new ActionListener() {
94 public void actionPerformed(ActionEvent e) {
98 download.addActionListener(new ActionListener() {
99 public void actionPerformed(ActionEvent e) {
103 DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout
104 ("fill:pref,1dlu,fill:pref,1dlu,fill:pref", "fill:pref,fill:pref"));
107 builder.append(auto);
108 builder.append(down);
109 builder.append(remove);
110 builder.append(download);
111 panel = new JPanel();
112 panel.setLayout(new BorderLayout());
113 panel.add(sPane, BorderLayout.CENTER);
114 panel.add(builder.getPanel(), BorderLayout.EAST);
116 TransferHandler th = new FileListEditorTransferHandler();
117 setTransferHandler(th);
118 panel.setTransferHandler(th);
120 // Add an input/action pair for deleting entries:
121 getInputMap().put(KeyStroke.getKeyStroke("DELETE"), "delete");
122 getActionMap().put("delete", new AbstractAction() {
123 public void actionPerformed(ActionEvent actionEvent) {
124 int row = getSelectedRow();
126 row = Math.min(row, getRowCount()-1);
128 setRowSelectionInterval(row, row);
132 // Add an input/action pair for inserting an entry:
133 getInputMap().put(KeyStroke.getKeyStroke("INSERT"), "insert");
134 getActionMap().put("insert", new AbstractAction() {
136 public void actionPerformed(ActionEvent actionEvent) {
142 item.addActionListener(new ActionListener() {
143 public void actionPerformed(ActionEvent actionEvent) {
149 private void openSelectedFile() {
150 int row = getSelectedRow();
152 FileListEntry entry = tableModel.getEntry(row);
154 Util.openExternalFileAnyFormat(metaData, entry.getLink(), entry.getType());
155 } catch (IOException e) {
162 public String getFieldName() {
167 * Returns the component to be added to a container. Might be a JScrollPane
168 * or the component itself.
170 public JComponent getPane() {
175 * Returns the text component itself.
177 public JComponent getTextComponent() {
181 public JLabel getLabel() {
185 public void setLabelColor(Color c) {
186 label.setForeground(c);
189 public String getText() {
190 return tableModel.getStringRepresentation();
193 public void setText(String newText) {
194 tableModel.setContent(newText);
198 public void append(String text) {
202 public void updateFont() {
206 public void paste(String textToInsert) {
210 public String getSelectedText() {
214 private void addEntry(String initialLink) {
215 int row = getSelectedRow();
218 FileListEntry entry = new FileListEntry("", initialLink, null);
219 if (editListEntry(entry))
220 tableModel.addEntry(row, entry);
221 entryEditor.updateField(this);
224 private void addEntry() {
229 * Add the listed files, prompting the user with the entry editor in each case.
230 * @param files A list of File objects.
232 private void addAll(List files) {
233 for (Iterator i = files.iterator(); i.hasNext();) {
234 File file = (File)i.next();
235 String fileName = file.getAbsolutePath();
236 FileListEntry entry = new FileListEntry("", fileName, null);
237 if (editListEntry(entry))
238 tableModel.addEntry(tableModel.getRowCount(), entry);
240 entryEditor.updateField(this);
243 private void removeEntries() {
244 int[] rows = getSelectedRows();
246 for (int i = rows.length-1; i>=0; i--) {
247 tableModel.removeEntry(rows[i]);
249 entryEditor.updateField(this);
252 private void moveEntry(int i) {
253 int[] sel = getSelectedRows();
254 if ((sel.length != 1) || (tableModel.getRowCount() < 2))
256 int toIdx = sel[0]+i;
257 if (toIdx >= tableModel.getRowCount())
258 toIdx -= tableModel.getRowCount();
260 toIdx += tableModel.getRowCount();
261 FileListEntry entry = tableModel.getEntry(sel[0]);
262 tableModel.removeEntry(sel[0]);
263 tableModel.addEntry(toIdx, entry);
264 entryEditor.updateField(this);
265 setRowSelectionInterval(toIdx, toIdx);
268 private boolean editListEntry(FileListEntry entry) {
269 if (editor == null) {
270 editor = new FileListEntryEditor(frame, entry, false, metaData);
273 editor.setEntry(entry);
274 editor.setVisible(true);
275 if (editor.okPressed())
276 tableModel.fireTableDataChanged();
277 entryEditor.updateField(this);
278 return editor.okPressed();
281 private void autoSetLinks() {
282 auto.setEnabled(false);
283 BibtexEntry entry = entryEditor.getEntry();
284 int tableSize = tableModel.getRowCount();
285 JDialog diag = new JDialog(frame, true);
286 autoSetLinks(entry, tableModel, metaData, new ActionListener() {
287 public void actionPerformed(ActionEvent e) {
288 auto.setEnabled(true);
290 entryEditor.updateField(FileListEditor.this);
291 frame.output(Globals.lang("Finished autosetting external links."));
293 else frame.output(Globals.lang("Finished autosetting external links.")
294 +" "+Globals.lang("No files found."));
301 * Automatically add links for this set of entries, based on the globally stored list of
302 * external file types. The entries are modified, and corresponding UndoEdit elements
303 * added to the NamedCompound given as argument. Furthermore, all entries which are modified
304 * are added to the Set of entries given as an argument.
306 * The entries' bibtex keys must have been set - entries lacking key are ignored.
307 * The operation is done in a new thread, which is returned for the caller to wait for
310 * @param entries A collection of BibtexEntry objects to find links for.
311 * @param ce A NamedCompound to add UndoEdit elements to.
312 * @param changedEntries A Set of BibtexEntry objects to which all modified entries is added.
313 * @return the thread performing the autosetting
315 public static Thread autoSetLinks(final Collection<BibtexEntry> entries, final NamedCompound ce,
316 final Set<BibtexEntry> changedEntries,
317 final ArrayList<File> dirs) {
319 final ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection();
320 final JLabel label = new JLabel(Globals.lang("Searching for files"));
321 Runnable r = new Runnable() {
324 boolean foundAny = false;
325 ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection();
326 Collection<String> extensions = new ArrayList<String>();
327 for (int i = 0; i < types.length; i++) {
328 final ExternalFileType type = types[i];
329 extensions.add(type.getExtension());
331 // Run the search operation:
332 Map<BibtexEntry, java.util.List<File>> result =
333 Util.findAssociatedFiles(entries, extensions, dirs);
335 // Iterate over the entries:
336 for (Iterator<BibtexEntry> i=result.keySet().iterator(); i.hasNext();) {
337 BibtexEntry anEntry = i.next();
338 FileListTableModel tableModel = new FileListTableModel();
339 Object oldVal = anEntry.getField(GUIGlobals.FILE_FIELD);
341 tableModel.setContent((String)oldVal);
342 List<File> files = result.get(anEntry);
343 for (File f : files) {
344 f = relativizePath(f, dirs);
345 boolean alreadyHas = false;
346 //System.out.println("File: "+f.getPath());
347 for (int j = 0; j < tableModel.getRowCount(); j++) {
348 FileListEntry existingEntry = tableModel.getEntry(j);
349 //System.out.println("Comp: "+existingEntry.getLink());
350 if (new File(existingEntry.getLink()).equals(f)) {
356 int index = f.getPath().lastIndexOf('.');
357 if ((index >= 0) && (index < f.getPath().length()-1)) {
358 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt
359 (f.getPath().substring(index+1));
360 FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), type);
361 tableModel.addEntry(tableModel.getRowCount(), flEntry);
363 FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(),
364 new UnknownExternalFileType(""));
365 tableModel.addEntry(tableModel.getRowCount(), flEntry);
367 String newVal = tableModel.getStringRepresentation();
368 if (newVal.length() == 0)
370 UndoableFieldChange change = new UndoableFieldChange(anEntry,
371 GUIGlobals.FILE_FIELD, oldVal, newVal);
373 anEntry.setField(GUIGlobals.FILE_FIELD, newVal);
374 changedEntries.add(anEntry);
380 Thread t = new Thread(r);
387 * Automatically add links for this entry to the table model given as an argument, based on
388 * the globally stored list of external file types. The entry itself is not modified. The entry's
389 * bibtex key must have been set.
390 * The operation is done in a new thread, which is returned for the caller to wait for
393 * @param entry The BibtexEntry to find links for.
394 * @param tableModel The table model to insert links into. Already existing links are not duplicated or removed.
395 * @param metaData The MetaData providing the relevant file directory, if any.
396 * @param callback An ActionListener that is notified (on the event dispatch thread) when the search is
397 * finished. The ActionEvent has id=0 if no new links were added, and id=1 if one or more links were added.
398 * This parameter can be null, which means that no callback will be notified.
399 * @param diag An instantiated modal JDialog which will be used to display the progress of the autosetting.
400 * This parameter can be null, which means that no progress update will be shown.
401 * @return the thread performing the autosetting
403 public static Thread autoSetLinks(final BibtexEntry entry, final FileListTableModel tableModel,
404 final MetaData metaData, final ActionListener callback,
405 final JDialog diag) {
407 final Collection<BibtexEntry> entries = new ArrayList<BibtexEntry>();
409 final ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection();
410 final JProgressBar prog = new JProgressBar(JProgressBar.HORIZONTAL, types.length-1);
411 prog.setIndeterminate(true);
412 prog.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
413 final JLabel label = new JLabel(Globals.lang("Searching for files"));
415 diag.setTitle(Globals.lang("Autosetting links"));
416 diag.getContentPane().add(prog, BorderLayout.CENTER);
417 diag.getContentPane().add(label, BorderLayout.SOUTH);
420 diag.setLocationRelativeTo(diag.getParent());
422 Runnable r = new Runnable() {
425 boolean foundAny = false;
426 ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection();
427 ArrayList<File> dirs = new ArrayList<File>();
428 if (metaData.getFileDirectory(GUIGlobals.FILE_FIELD) != null)
429 dirs.add(new File(metaData.getFileDirectory(GUIGlobals.FILE_FIELD)));
430 Collection<String> extensions = new ArrayList<String>();
431 for (int i = 0; i < types.length; i++) {
432 final ExternalFileType type = types[i];
433 extensions.add(type.getExtension());
435 // Run the search operation:
436 Map<BibtexEntry, java.util.List<File>> result =
437 Util.findAssociatedFiles(entries, extensions, dirs);
439 // Iterate over the entries:
440 for (Iterator<BibtexEntry> i=result.keySet().iterator(); i.hasNext();) {
441 BibtexEntry anEntry = i.next();
442 List<File> files = result.get(anEntry);
443 for (File f : files) {
444 f = relativizePath(f, dirs);
445 boolean alreadyHas = false;
446 for (int j = 0; j < tableModel.getRowCount(); j++) {
447 FileListEntry existingEntry = tableModel.getEntry(j);
448 if (new File(existingEntry.getLink()).equals(f)) {
454 int index = f.getPath().lastIndexOf('.');
455 if ((index >= 0) && (index < f.getPath().length()-1)) {
456 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt
457 (f.getPath().substring(index+1));
458 FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), type);
459 tableModel.addEntry(tableModel.getRowCount(), flEntry);
462 FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(),
463 new UnknownExternalFileType(""));
464 tableModel.addEntry(tableModel.getRowCount(), flEntry);
470 final int id = foundAny ? 1 : 0;
471 SwingUtilities.invokeLater(new Runnable() {
475 if (callback != null)
476 callback.actionPerformed(new ActionEvent(this, id, ""));
482 Thread t = new Thread(r);
485 diag.setVisible(true);
491 * If the file is below one of the directories in a list, return a File specifying
492 * a path relative to that directory.
494 public static File relativizePath(File f, ArrayList<File> dirs) {
495 String pth = f.getPath();
496 for (File dir : dirs) {
497 if (pth.startsWith(dir.getPath())) {
498 String subs = pth.substring(dir.getPath().length());
499 if ((subs.length() > 0) && ((subs.charAt(0) == '/') || (subs.charAt(0) == '\\')))
500 subs = subs.substring(1);
501 return new File(subs);
509 * Run a file download operation.
511 private void downloadFile() {
512 String bibtexKey = entryEditor.getEntry().getCiteKey();
513 if (bibtexKey == null) {
514 int answer = JOptionPane.showConfirmDialog(frame,
515 Globals.lang("This entry has no BibTeX key. Generate key now?"),
516 Globals.lang("Download file"), JOptionPane.OK_CANCEL_OPTION,
517 JOptionPane.QUESTION_MESSAGE);
518 if (answer == JOptionPane.OK_OPTION) {
519 ActionListener l = entryEditor.generateKeyAction;
520 l.actionPerformed(null);
521 bibtexKey = entryEditor.getEntry().getCiteKey();
524 DownloadExternalFile def = new DownloadExternalFile(frame,
525 frame.basePanel().metaData(), bibtexKey);
528 } catch (IOException ex) {
529 ex.printStackTrace();
534 * This is the callback method that the DownloadExternalFile class uses to report the result
535 * of a download operation. This call may never come, if the user cancelled the operation.
536 * @param file The FileListEntry linking to the resulting local file.
538 public void downloadComplete(FileListEntry file) {
539 tableModel.addEntry(tableModel.getRowCount(), file);
540 entryEditor.updateField(this);
543 class TableClickListener extends MouseAdapter {
545 public void mouseClicked(MouseEvent e) {
546 if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2)) {
547 int row = rowAtPoint(e.getPoint());
549 FileListEntry entry = tableModel.getEntry(row);
550 editListEntry(entry);
553 else if (e.isPopupTrigger())
554 processPopupTrigger(e);
558 public void mousePressed(MouseEvent e) {
559 if (e.isPopupTrigger())
560 processPopupTrigger(e);
562 public void mouseReleased(MouseEvent e) {
563 if (e.isPopupTrigger())
564 processPopupTrigger(e);
568 private void processPopupTrigger(MouseEvent e) {
569 int row = rowAtPoint(e.getPoint());
571 setRowSelectionInterval(row, row);
572 menu.show(FileListEditor.this, e.getX(), e.getY());
578 class FileListEditorTransferHandler extends TransferHandler {
580 protected DataFlavor urlFlavor;
581 protected DataFlavor stringFlavor;
583 public FileListEditorTransferHandler() {
584 stringFlavor = DataFlavor.stringFlavor;
586 urlFlavor = new DataFlavor("application/x-java-url; class=java.net.URL");
587 } catch (ClassNotFoundException e) {
588 Globals.logger("Unable to configure drag and drop for file link table");
593 * Overriden to indicate which types of drags are supported (only LINK).
597 public int getSourceActions(JComponent c) {
598 return DnDConstants.ACTION_LINK;
601 /*public boolean importData(TransferSupport transferSupport) {
603 return importData(FileListEditor.this, transferSupport.getTransferable());
606 public boolean importData(JComponent comp, Transferable t) {
607 // If the drop target is the main table, we want to record which
608 // row the item was dropped on, to identify the entry if needed:
610 if (comp instanceof JTable) {
611 dropRow = ((JTable) comp).getSelectedRow();
617 // This flavor is used for dragged file links in Windows:
618 if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
619 // JOptionPane.showMessageDialog(null, "Received
620 // javaFileListFlavor");
621 files = (List) t.getTransferData(DataFlavor.javaFileListFlavor);
624 if (t.isDataFlavorSupported(urlFlavor)) {
625 URL dropLink = (URL) t.getTransferData(urlFlavor);
626 System.out.println("URL: "+dropLink);
627 //return handleDropTransfer(dropLink, dropRow);
630 // This is used when one or more files are pasted from the file manager
631 // under Gnome. The data consists of the file paths, one file per line:
632 if (t.isDataFlavorSupported(stringFlavor)) {
633 String dropStr = (String)t.getTransferData(stringFlavor);
634 files = EntryTableTransferHandler.getFilesFromDraggedFilesString(dropStr);
638 final List theFiles = files;
639 SwingUtilities.invokeLater(new Runnable() {
642 for (Iterator i=theFiles.iterator(); i.hasNext();) {
643 File f = (File)i.next();
644 // Find the file's extension, if any:
645 String name = f.getAbsolutePath();
646 String extension = "";
647 ExternalFileType fileType = null;
648 int index = name.lastIndexOf('.');
649 if ((index >= 0) && (index < name.length())) {
650 extension = name.substring(index + 1).toLowerCase();
651 fileType = Globals.prefs.getExternalFileTypeByExt(extension);
653 if (fileType != null) {
654 DroppedFileHandler dfh = new DroppedFileHandler(frame, frame.basePanel());
655 dfh.handleDroppedfile(name, fileType, true, entryEditor.getEntry());
663 } catch (IOException ioe) {
664 System.err.println("failed to read dropped data: " + ioe.toString());
665 } catch (UnsupportedFlavorException ufe) {
666 System.err.println("drop type error: " + ufe.toString());
669 // all supported flavors failed
670 System.err.println("can't transfer input: ");
671 DataFlavor inflavs[] = t.getTransferDataFlavors();
672 for (int i = 0; i < inflavs.length; i++) {
673 System.out.println(" " + inflavs[i].toString());
680 * This method is called to query whether the transfer can be imported.
682 * Will return true for urls, strings, javaFileLists
686 public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
688 // accept this if any input flavor matches any of our supported flavors
689 for (int i = 0; i < transferFlavors.length; i++) {
690 DataFlavor inflav = transferFlavors[i];
691 if (inflav.match(urlFlavor) || inflav.match(stringFlavor)
692 || inflav.match(DataFlavor.javaFileListFlavor))
696 // nope, never heard of this type