package net.sf.jabref.gui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Insets; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; 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.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.event.UndoableEditListener; import net.sf.jabref.*; import net.sf.jabref.external.*; import net.sf.jabref.groups.EntryTableTransferHandler; import net.sf.jabref.undo.NamedCompound; import net.sf.jabref.undo.UndoableFieldChange; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.layout.FormLayout; /** * Created by Morten O. Alver 2007.02.22 */ public class FileListEditor extends JTable implements FieldEditor, DownloadExternalFile.DownloadCallback { FieldNameLabel label; FileListEntryEditor editor = null; private JabRefFrame frame; private MetaData metaData; private String fieldName; private EntryEditor entryEditor; private JPanel panel; private FileListTableModel tableModel; private JScrollPane sPane; private JButton add, remove, up, down, auto, download; private JPopupMenu menu = new JPopupMenu(); private JMenuItem openLink = new JMenuItem(Globals.lang("Open")); private JMenuItem rename = new JMenuItem(Globals.lang("Move/rename file")); private JMenuItem moveToFileDir = new JMenuItem(Globals.lang("Move to file directory")); public FileListEditor(JabRefFrame frame, MetaData metaData, String fieldName, String content, EntryEditor entryEditor) { this.frame = frame; this.metaData = metaData; this.fieldName = fieldName; this.entryEditor = entryEditor; label = new FieldNameLabel(" " + Util.nCase(fieldName) + " "); tableModel = new FileListTableModel(); setText(content); setModel(tableModel); sPane = new JScrollPane(this); setTableHeader(null); addMouseListener(new TableClickListener()); add = new JButton(GUIGlobals.getImage("add")); add.setToolTipText(Globals.lang("New file link (INSERT)")); remove = new JButton(GUIGlobals.getImage("remove")); remove.setToolTipText(Globals.lang("Remove file link (DELETE)")); up = new JButton(GUIGlobals.getImage("up")); down = new JButton(GUIGlobals.getImage("down")); auto = new JButton(Globals.lang("Auto")); download = new JButton(Globals.lang("Download")); add.setMargin(new Insets(0,0,0,0)); remove.setMargin(new Insets(0,0,0,0)); up.setMargin(new Insets(0,0,0,0)); down.setMargin(new Insets(0,0,0,0)); add.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addEntry(); } }); remove.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { removeEntries(); } }); up.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { moveEntry(-1); } }); down.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { moveEntry(1); } }); auto.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { autoSetLinks(); } }); download.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { downloadFile(); } }); DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout ("fill:pref,1dlu,fill:pref,1dlu,fill:pref", "fill:pref,fill:pref")); builder.append(up); builder.append(add); builder.append(auto); builder.append(down); builder.append(remove); builder.append(download); panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(sPane, BorderLayout.CENTER); panel.add(builder.getPanel(), BorderLayout.EAST); TransferHandler th = new FileListEditorTransferHandler(); setTransferHandler(th); panel.setTransferHandler(th); // Add an input/action pair for deleting entries: getInputMap().put(KeyStroke.getKeyStroke("DELETE"), "delete"); getActionMap().put("delete", new AbstractAction() { public void actionPerformed(ActionEvent actionEvent) { int row = getSelectedRow(); removeEntries(); row = Math.min(row, getRowCount()-1); if (row >= 0) setRowSelectionInterval(row, row); } }); // Add an input/action pair for inserting an entry: getInputMap().put(KeyStroke.getKeyStroke("INSERT"), "insert"); getActionMap().put("insert", new AbstractAction() { public void actionPerformed(ActionEvent actionEvent) { addEntry(); } }); menu.add(openLink); openLink.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { openSelectedFile(); } }); menu.add(rename); rename.addActionListener(new MoveFileAction(frame, entryEditor, this, false)); menu.add(moveToFileDir); moveToFileDir.addActionListener(new MoveFileAction(frame, entryEditor, this, true)); } private void openSelectedFile() { int row = getSelectedRow(); if (row >= 0) { FileListEntry entry = tableModel.getEntry(row); try { ExternalFileType type = Globals.prefs.getExternalFileTypeByName(entry.getType().getName()); Util.openExternalFileAnyFormat(metaData, entry.getLink(), type != null ? type : entry.getType()); } catch (IOException e) { e.printStackTrace(); } } } public FileListTableModel getTableModel() { return tableModel; } public String getFieldName() { return fieldName; } /* * Returns the component to be added to a container. Might be a JScrollPane * or the component itself. */ public JComponent getPane() { return panel; } /* * Returns the text component itself. */ public JComponent getTextComponent() { return this; } public JLabel getLabel() { return label; } public void setLabelColor(Color c) { label.setForeground(c); } public String getText() { return tableModel.getStringRepresentation(); } public void setText(String newText) { tableModel.setContent(newText); } public void append(String text) { } public void updateFont() { } public void paste(String textToInsert) { } public String getSelectedText() { return null; } private void addEntry(String initialLink) { int row = getSelectedRow(); if (row == -1) row = 0; FileListEntry entry = new FileListEntry("", initialLink, null); if (editListEntry(entry, true)) tableModel.addEntry(row, entry); entryEditor.updateField(this); } private void addEntry() { addEntry(""); } private void removeEntries() { int[] rows = getSelectedRows(); if (rows != null) for (int i = rows.length-1; i>=0; i--) { tableModel.removeEntry(rows[i]); } entryEditor.updateField(this); } private void moveEntry(int i) { int[] sel = getSelectedRows(); if ((sel.length != 1) || (tableModel.getRowCount() < 2)) return; int toIdx = sel[0]+i; if (toIdx >= tableModel.getRowCount()) toIdx -= tableModel.getRowCount(); if (toIdx < 0) toIdx += tableModel.getRowCount(); FileListEntry entry = tableModel.getEntry(sel[0]); tableModel.removeEntry(sel[0]); tableModel.addEntry(toIdx, entry); entryEditor.updateField(this); setRowSelectionInterval(toIdx, toIdx); } /** * Open an editor for this entry. * @param entry The entry to edit. * @param openBrowse True to indicate that a Browse dialog should be immediately opened. * @return true if the edit was accepted, false if it was cancelled. */ private boolean editListEntry(FileListEntry entry, boolean openBrowse) { if (editor == null) { editor = new FileListEntryEditor(frame, entry, false, true, metaData); } else editor.setEntry(entry); editor.setVisible(true, openBrowse); if (editor.okPressed()) tableModel.fireTableDataChanged(); entryEditor.updateField(this); return editor.okPressed(); } private void autoSetLinks() { auto.setEnabled(false); BibtexEntry entry = entryEditor.getEntry(); JDialog diag = new JDialog(frame.getFrame(), true); autoSetLinks(entry, tableModel, metaData, new ActionListener() { public void actionPerformed(ActionEvent e) { auto.setEnabled(true); if (e.getID() > 0) { entryEditor.updateField(FileListEditor.this); frame.output(Globals.lang("Finished autosetting external links.")); } else frame.output(Globals.lang("Finished autosetting external links.") +" "+Globals.lang("No files found.")); } }, diag); } /** * Automatically add links for this set of entries, based on the globally stored list of * external file types. The entries are modified, and corresponding UndoEdit elements * added to the NamedCompound given as argument. Furthermore, all entries which are modified * are added to the Set of entries given as an argument. * * The entries' bibtex keys must have been set - entries lacking key are ignored. * The operation is done in a new thread, which is returned for the caller to wait for * if needed. * * @param entries A collection of BibtexEntry objects to find links for. * @param ce A NamedCompound to add UndoEdit elements to. * @param changedEntries A Set of BibtexEntry objects to which all modified entries is added. * @return the thread performing the autosetting */ public static Thread autoSetLinks(final Collection<BibtexEntry> entries, final NamedCompound ce, final Set<BibtexEntry> changedEntries, final ArrayList<File> dirs) { Runnable r = new Runnable() { public void run() { ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection(); Collection<String> extensions = new ArrayList<String>(); for (int i = 0; i < types.length; i++) { final ExternalFileType type = types[i]; extensions.add(type.getExtension()); } // Run the search operation: Map<BibtexEntry, java.util.List<File>> result; if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) { String regExp = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY); result = RegExpFileSearch.findFilesForSet(entries, extensions, dirs, regExp); } else result = Util.findAssociatedFiles(entries, extensions, dirs); // Iterate over the entries: for (Iterator<BibtexEntry> i=result.keySet().iterator(); i.hasNext();) { BibtexEntry anEntry = i.next(); FileListTableModel tableModel = new FileListTableModel(); String oldVal = anEntry.getField(GUIGlobals.FILE_FIELD); if (oldVal != null) tableModel.setContent(oldVal); List<File> files = result.get(anEntry); for (File f : files) { f = relativizePath(f, dirs); boolean alreadyHas = false; //System.out.println("File: "+f.getPath()); for (int j = 0; j < tableModel.getRowCount(); j++) { FileListEntry existingEntry = tableModel.getEntry(j); //System.out.println("Comp: "+existingEntry.getLink()); if (new File(existingEntry.getLink()).equals(f)) { alreadyHas = true; break; } } if (!alreadyHas) { int index = f.getPath().lastIndexOf('.'); if ((index >= 0) && (index < f.getPath().length()-1)) { ExternalFileType type = Globals.prefs.getExternalFileTypeByExt (f.getPath().substring(index+1).toLowerCase()); FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), type); tableModel.addEntry(tableModel.getRowCount(), flEntry); } else { FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), new UnknownExternalFileType("")); tableModel.addEntry(tableModel.getRowCount(), flEntry); } String newVal = tableModel.getStringRepresentation(); if (newVal.length() == 0) newVal = null; UndoableFieldChange change = new UndoableFieldChange(anEntry, GUIGlobals.FILE_FIELD, oldVal, newVal); ce.addEdit(change); anEntry.setField(GUIGlobals.FILE_FIELD, newVal); changedEntries.add(anEntry); } } } } }; Thread t = new Thread(r); t.start(); return t; } /** * Automatically add links for this entry to the table model given as an argument, based on * the globally stored list of external file types. The entry itself is not modified. The entry's * bibtex key must have been set. * The operation is done in a new thread, which is returned for the caller to wait for * if needed. * * @param entry The BibtexEntry to find links for. * @param tableModel The table model to insert links into. Already existing links are not duplicated or removed. * @param metaData The MetaData providing the relevant file directory, if any. * @param callback An ActionListener that is notified (on the event dispatch thread) when the search is * finished. The ActionEvent has id=0 if no new links were added, and id=1 if one or more links were added. * This parameter can be null, which means that no callback will be notified. * @param diag An instantiated modal JDialog which will be used to display the progress of the autosetting. * This parameter can be null, which means that no progress update will be shown. * @return the thread performing the autosetting */ public static Thread autoSetLinks(final BibtexEntry entry, final FileListTableModel tableModel, final MetaData metaData, final ActionListener callback, final JDialog diag) { final Collection<BibtexEntry> entries = new ArrayList<BibtexEntry>(); entries.add(entry); final ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection(); final JProgressBar prog = new JProgressBar(JProgressBar.HORIZONTAL, 0, types.length-1); prog.setIndeterminate(true); prog.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); final JLabel label = new JLabel(Globals.lang("Searching for files")); if (diag != null) { diag.setTitle(Globals.lang("Autosetting links")); diag.getContentPane().add(prog, BorderLayout.CENTER); diag.getContentPane().add(label, BorderLayout.SOUTH); diag.pack(); diag.setLocationRelativeTo(diag.getParent()); } Runnable r = new Runnable() { public void run() { boolean foundAny = false; ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection(); ArrayList<File> dirs = new ArrayList<File>(); if (metaData.getFileDirectory(GUIGlobals.FILE_FIELD) != null) dirs.add(new File(metaData.getFileDirectory(GUIGlobals.FILE_FIELD))); Collection<String> extensions = new ArrayList<String>(); for (int i = 0; i < types.length; i++) { final ExternalFileType type = types[i]; extensions.add(type.getExtension()); } // Run the search operation: Map<BibtexEntry, java.util.List<File>> result; if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) { String regExp = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY); result = RegExpFileSearch.findFilesForSet(entries, extensions, dirs, regExp); } else result = Util.findAssociatedFiles(entries, extensions, dirs); // Iterate over the entries: for (Iterator<BibtexEntry> i=result.keySet().iterator(); i.hasNext();) { BibtexEntry anEntry = i.next(); List<File> files = result.get(anEntry); for (File f : files) { f = relativizePath(f, dirs); boolean alreadyHas = false; for (int j = 0; j < tableModel.getRowCount(); j++) { FileListEntry existingEntry = tableModel.getEntry(j); if (new File(existingEntry.getLink()).equals(f)) { alreadyHas = true; break; } } if (!alreadyHas) { int index = f.getPath().lastIndexOf('.'); if ((index >= 0) && (index < f.getPath().length()-1)) { ExternalFileType type = Globals.prefs.getExternalFileTypeByExt (f.getPath().substring(index+1)); FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), type); tableModel.addEntry(tableModel.getRowCount(), flEntry); foundAny = true; } else { FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), new UnknownExternalFileType("")); tableModel.addEntry(tableModel.getRowCount(), flEntry); foundAny = true; } } } } final int id = foundAny ? 1 : 0; SwingUtilities.invokeLater(new Runnable() { public void run() { if (diag != null) diag.dispose(); if (callback != null) callback.actionPerformed(new ActionEvent(this, id, "")); } }); } }; Thread t = new Thread(r); t.start(); if (diag != null) { diag.setVisible(true); } return t; } /** * If the file is below one of the directories in a list, return a File specifying * a path relative to that directory. */ public static File relativizePath(File f, ArrayList<File> dirs) { String pth = f.getPath(); for (File dir : dirs) { if ((dir.getPath().length() > 0) && pth.startsWith(dir.getPath())) { String subs = pth.substring(dir.getPath().length()); if ((subs.length() > 0) && ((subs.charAt(0) == '/') || (subs.charAt(0) == '\\'))) subs = subs.substring(1); return new File(subs); } } return f; } /** * Run a file download operation. */ private void downloadFile() { String bibtexKey = entryEditor.getEntry().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) { ActionListener l = entryEditor.generateKeyAction; l.actionPerformed(null); bibtexKey = entryEditor.getEntry().getCiteKey(); } } DownloadExternalFile def = new DownloadExternalFile(frame, frame.basePanel().metaData(), bibtexKey); try { def.download(this); } catch (IOException ex) { ex.printStackTrace(); } } /** * This is the callback method that the DownloadExternalFile class uses to report the result * of a download operation. This call may never come, if the user cancelled the operation. * @param file The FileListEntry linking to the resulting local file. */ public void downloadComplete(FileListEntry file) { tableModel.addEntry(tableModel.getRowCount(), file); entryEditor.updateField(this); } class TableClickListener extends MouseAdapter { public void mouseClicked(MouseEvent e) { if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2)) { int row = rowAtPoint(e.getPoint()); if (row >= 0) { FileListEntry entry = tableModel.getEntry(row); editListEntry(entry, false); } } else if (e.isPopupTrigger()) processPopupTrigger(e); } public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) processPopupTrigger(e); } public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) processPopupTrigger(e); } private void processPopupTrigger(MouseEvent e) { int row = rowAtPoint(e.getPoint()); if (row >= 0) { setRowSelectionInterval(row, row); menu.show(FileListEditor.this, e.getX(), e.getY()); } } } class FileListEditorTransferHandler extends TransferHandler { protected DataFlavor urlFlavor; protected DataFlavor stringFlavor; public FileListEditorTransferHandler() { stringFlavor = DataFlavor.stringFlavor; try { urlFlavor = new DataFlavor("application/x-java-url; class=java.net.URL"); } catch (ClassNotFoundException e) { Globals.logger("Unable to configure drag and drop for file link table"); e.printStackTrace(); } } /** * Overriden to indicate which types of drags are supported (only LINK). * * @override */ public int getSourceActions(JComponent c) { return DnDConstants.ACTION_LINK; } /*public boolean importData(TransferSupport transferSupport) { return importData(FileListEditor.this, transferSupport.getTransferable()); }*/ @SuppressWarnings("unchecked") public boolean importData(JComponent comp, Transferable t) { // If the drop target is the main table, we want to record which // row the item was dropped on, to identify the entry if needed: try { List<File> files = null; // This flavor is used for dragged file links in Windows: if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { // JOptionPane.showMessageDialog(null, "Received // javaFileListFlavor"); files = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor); } if (t.isDataFlavorSupported(urlFlavor)) { URL dropLink = (URL) t.getTransferData(urlFlavor); System.out.println("URL: "+dropLink); //return handleDropTransfer(dropLink, dropRow); } // This is used when one or more files are pasted from the file manager // under Gnome. The data consists of the file paths, one file per line: if (t.isDataFlavorSupported(stringFlavor)) { String dropStr = (String)t.getTransferData(stringFlavor); files = EntryTableTransferHandler.getFilesFromDraggedFilesString(dropStr); } if (files != null) { final List<File> theFiles = files; SwingUtilities.invokeLater(new Runnable() { public void run() { //addAll(files); for (File f : theFiles){ // Find the file's extension, if any: String name = f.getAbsolutePath(); String extension = ""; ExternalFileType fileType = null; int index = name.lastIndexOf('.'); if ((index >= 0) && (index < name.length())) { extension = name.substring(index + 1).toLowerCase(); fileType = Globals.prefs.getExternalFileTypeByExt(extension); } if (fileType != null) { DroppedFileHandler dfh = new DroppedFileHandler(frame, frame.basePanel()); dfh.handleDroppedfile(name, fileType, true, entryEditor.getEntry()); } } } }); return true; } } catch (IOException ioe) { System.err.println("failed to read dropped data: " + ioe.toString()); } catch (UnsupportedFlavorException ufe) { System.err.println("drop type error: " + ufe.toString()); } // all supported flavors failed System.err.println("can't transfer input: "); DataFlavor inflavs[] = t.getTransferDataFlavors(); for (int i = 0; i < inflavs.length; i++) { System.out.println(" " + inflavs[i].toString()); } return false; } /** * This method is called to query whether the transfer can be imported. * * Will return true for urls, strings, javaFileLists * * @override */ public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { // accept this if any input flavor matches any of our supported flavors for (int i = 0; i < transferFlavors.length; i++) { DataFlavor inflav = transferFlavors[i]; if (inflav.match(urlFlavor) || inflav.match(stringFlavor) || inflav.match(DataFlavor.javaFileListFlavor)) return true; } // nope, never heard of this type return false; } } public boolean hasUndoInformation() { return false; } public void undo() { } public boolean hasRedoInformation() { return false; } public void redo() { } public void addUndoableEditListener(UndoableEditListener listener) { } public void setAutoCompleteListener(AutoCompleteListener listener) { } public void clearAutoCompleteSuggestion() { } public void setActiveBackgroundColor() { } public void setValidBackgroundColor() { } public void setInvalidBackgroundColor() { } public void updateFontColor() { } }