package org.jabref.gui.fieldeditors;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.TransferHandler;
import javax.swing.table.TableCellRenderer;
import org.jabref.Globals;
import org.jabref.gui.IconTheme;
import org.jabref.gui.JabRefFrame;
import org.jabref.gui.autocompleter.AutoCompleteListener;
import org.jabref.gui.desktop.JabRefDesktop;
import org.jabref.gui.entryeditor.EntryEditor;
import org.jabref.gui.externalfiles.DownloadExternalFile;
import org.jabref.gui.externalfiles.MoveFileAction;
import org.jabref.gui.externalfiles.RenameFileAction;
import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.filelist.FileListEntry;
import org.jabref.gui.filelist.FileListEntryEditor;
import org.jabref.gui.filelist.FileListTableModel;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import com.jgoodies.forms.builder.FormBuilder;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class FileListEditor extends JTable implements FieldEditor, DownloadExternalFile.DownloadCallback {
private static final Log LOGGER = LogFactory.getLog(FileListEditor.class);
private final JabRefFrame frame;
private final BibDatabaseContext databaseContext;
private final String fieldName;
private final EntryEditor entryEditor;
private final JPanel panel;
private final FileListTableModel tableModel;
private final JPopupMenu menu = new JPopupMenu();
private FileListEntryEditor editor;
public FileListEditor(JabRefFrame frame, BibDatabaseContext databaseContext, String fieldName, String content,
EntryEditor entryEditor) {
this.frame = frame;
this.databaseContext = databaseContext;
this.fieldName = fieldName;
this.entryEditor = entryEditor;
tableModel = new FileListTableModel();
setText(content);
setModel(tableModel);
JScrollPane sPane = new JScrollPane(this);
setTableHeader(null);
addMouseListener(new TableClickListener());
initKeyBindings();
JButton remove = new JButton(IconTheme.JabRefIcon.REMOVE_NOBOX.getSmallIcon());
remove.setToolTipText(Localization.lang("Remove file link (DELETE)"));
JButton up = new JButton(IconTheme.JabRefIcon.UP.getSmallIcon());
JButton down = new JButton(IconTheme.JabRefIcon.DOWN.getSmallIcon());
remove.setMargin(new Insets(0, 0, 0, 0));
up.setMargin(new Insets(0, 0, 0, 0));
down.setMargin(new Insets(0, 0, 0, 0));
remove.addActionListener(e -> removeEntries());
up.addActionListener(e -> moveEntry(-1));
down.addActionListener(e -> moveEntry(1));
FormBuilder builder = FormBuilder.create()
.layout(new FormLayout("fill:pref,1dlu,fill:pref,1dlu,fill:pref", "fill:pref,fill:pref"));
builder.add(up).xy(1, 1);
builder.add(down).xy(1, 2);
builder.add(remove).xy(3, 2);
panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(sPane, BorderLayout.CENTER);
panel.add(builder.getPanel(), BorderLayout.EAST);
TransferHandler transferHandler = new FileListEditorTransferHandler(frame, entryEditor, null);
setTransferHandler(transferHandler);
panel.setTransferHandler(transferHandler);
JMenuItem openLink = new JMenuItem(Localization.lang("Open"));
menu.add(openLink);
openLink.addActionListener(e -> openSelectedFile());
JMenuItem openFolder = new JMenuItem(Localization.lang("Open folder"));
menu.add(openFolder);
openFolder.addActionListener(e -> {
int row = getSelectedRow();
if (row >= 0) {
FileListEntry entry = tableModel.getEntry(row);
try {
Path path = null;
// absolute path
if (Paths.get(entry.getLink()).isAbsolute()) {
path = Paths.get(entry.getLink());
} else {
// relative to file folder
for (String folder : databaseContext
.getFileDirectories(Globals.prefs.getFileDirectoryPreferences())) {
Path file = Paths.get(folder, entry.getLink());
if (Files.exists(file)) {
path = file;
break;
}
}
}
if (path != null) {
JabRefDesktop.openFolderAndSelectFile(path);
} else {
JOptionPane.showMessageDialog(frame,
Localization.lang("File not found"),
Localization.lang("Error"),
JOptionPane.ERROR_MESSAGE);
}
} catch (IOException ex) {
LOGGER.debug("Cannot open folder", ex);
}
}
});
JMenuItem rename = new JMenuItem(Localization.lang("Rename file"));
menu.add(rename);
rename.addActionListener(new RenameFileAction(frame, entryEditor, this));
JMenuItem moveToFileDir = new JMenuItem(Localization.lang("Move file to file directory"));
menu.add(moveToFileDir);
moveToFileDir.addActionListener(new MoveFileAction(frame, entryEditor, this));
JMenuItem deleteFile = new JMenuItem(Localization.lang("Permanently delete local file"));
menu.add(deleteFile);
deleteFile.addActionListener(e -> {
int row = getSelectedRow();
// no selection
if (row == -1) {
return;
}
FileListEntry entry = tableModel.getEntry(row);
Optional<Path> file = entry.toParsedFileField().findIn(databaseContext, Globals.prefs.getFileDirectoryPreferences());
if (file.isPresent()) {
String[] options = {Localization.lang("Delete"), Localization.lang("Cancel")};
int userConfirm = JOptionPane.showOptionDialog(frame,
Localization.lang("Delete '%0'?", file.get().getFileName().toString()),
Localization.lang("Delete file"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0]);
if (userConfirm == JOptionPane.YES_OPTION) {
try {
Files.delete(file.get());
removeEntries();
} catch (IOException ex) {
JOptionPane.showMessageDialog(frame, Localization.lang("File permission error"),
Localization.lang("Cannot delete file"), JOptionPane.ERROR_MESSAGE);
LOGGER.warn("File permission error while deleting: " + entry.getLink(), ex);
}
}
} else {
JOptionPane.showMessageDialog(frame, Localization.lang("File not found"),
Localization.lang("Cannot delete file"), JOptionPane.ERROR_MESSAGE);
}
});
adjustColumnWidth();
}
private void initKeyBindings() {
// Add an input/action pair for deleting entries:
getInputMap().put(KeyStroke.getKeyStroke("DELETE"), "delete");
getActionMap().put("delete", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
int row = getSelectedRow();
removeEntries();
row = Math.min(row, getRowCount() - 1);
if (row >= 0) {
setRowSelectionInterval(row, row);
}
}
});
// Add input/action pair for moving an entry up:
getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.FILE_LIST_EDITOR_MOVE_ENTRY_UP), "move up");
getActionMap().put("move up", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
moveEntry(-1);
}
});
// Add input/action pair for moving an entry down:
getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.FILE_LIST_EDITOR_MOVE_ENTRY_DOWN), "move down");
getActionMap().put("move down", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
moveEntry(1);
}
});
getInputMap().put(KeyStroke.getKeyStroke("F4"),"open file");
getActionMap().put("open file", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
openSelectedFile();
}
});
}
public void adjustColumnWidth() {
for (int column = 0; column < this.getColumnCount(); column++) {
int width = 0;
for (int row = 0; row < this.getRowCount(); row++) {
TableCellRenderer renderer = this.getCellRenderer(row, column);
Component comp = this.prepareRenderer(renderer, row, column);
width = Math.max(comp.getPreferredSize().width, width);
}
this.columnModel.getColumn(column).setPreferredWidth(width);
}
}
private void openSelectedFile() {
int row = getSelectedRow();
if (row >= 0) {
FileListEntry entry = tableModel.getEntry(row);
try {
Optional<ExternalFileType> type = ExternalFileTypes.getInstance()
.getExternalFileTypeByName(entry.getType().get().getName());
JabRefDesktop.openExternalFileAnyFormat(databaseContext, entry.getLink(),
type.isPresent() ? type : entry.getType());
} catch (IOException e) {
LOGGER.warn("Cannot open selected file.", e);
}
}
}
public FileListTableModel getTableModel() {
return tableModel;
}
@Override
public String getFieldName() {
return fieldName;
}
/*
* Returns the component to be added to a container. Might be a JScrollPane
* or the component itself.
*/
@Override
public JComponent getPane() {
return panel;
}
/*
* Returns the text component itself.
*/
@Override
public JComponent getTextComponent() {
return this;
}
@Override
public String getText() {
return tableModel.getStringRepresentation();
}
@Override
public void setText(String newText) {
tableModel.setContent(newText);
}
@Override
public void append(String text) {
// Do nothing
}
@Override
public void paste(String textToInsert) {
// Do nothing
}
@Override
public String getSelectedText() {
return null;
}
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);
adjustColumnWidth();
}
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);
adjustColumnWidth();
}
/**
* 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 canceled.
*/
private boolean editListEntry(FileListEntry entry, boolean openBrowse) {
if (editor == null) {
editor = new FileListEntryEditor(frame, entry, false, true, databaseContext);
} else {
editor.setEntry(entry);
}
editor.setVisible(true, openBrowse);
if (editor.okPressed()) {
tableModel.fireTableDataChanged();
}
entryEditor.updateField(this);
adjustColumnWidth();
return editor.okPressed();
}
/**
* 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 canceled the operation.
*
* @param file The FileListEntry linking to the resulting local file.
*/
@Override
public void downloadComplete(FileListEntry file) {
tableModel.addEntry(0, file);
entryEditor.updateField(this);
adjustColumnWidth();
}
@Override
public void undo() {
// Do nothing
}
@Override
public void redo() {
// Do nothing
}
@Override
public void setAutoCompleteListener(AutoCompleteListener listener) {
// Do nothing
}
@Override
public void clearAutoCompleteSuggestion() {
// Do nothing
}
@Override
public void setActiveBackgroundColor() {
// Do nothing
}
@Override
public void setValidBackgroundColor() {
// Do nothing
}
@Override
public void setInvalidBackgroundColor() {
// Do nothing
}
class TableClickListener extends MouseAdapter {
@Override
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);
}
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
processPopupTrigger(e);
}
}
@Override
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());
}
}
}
}