package org.jabref.gui.fieldeditors; import java.awt.BorderLayout; import java.awt.Component; import java.awt.FontMetrics; 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.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.table.DefaultTableModel; 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.entryeditor.EntryEditor; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.Layout; import org.jabref.logic.layout.LayoutHelper; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.EntryLinkList; import org.jabref.model.entry.ParsedEntryLink; 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 EntryLinkListEditor extends JTable implements FieldEditor { private static final String layoutFormat = "\\begin{author}\\format[Authors(2,1),LatexToUnicode]{\\author}\\end{author}\\begin{title}, \"\\format[LatexToUnicode]{\\title}\"\\end{title}\\begin{year}, \\year\\end{year}"; private static final Log LOGGER = LogFactory.getLog(EntryLinkListEditor.class); private final JabRefFrame frame; private final BibDatabaseContext databaseContext; private final String fieldName; private final EntryEditor entryEditor; private final JPanel panel; private final EntryLinkListTableModel tableModel; private final JPopupMenu menu = new JPopupMenu(); private final boolean singleEntry; private final JButton add = new JButton(IconTheme.JabRefIcon.ADD_NOBOX.getSmallIcon()); private final JButton remove = new JButton(IconTheme.JabRefIcon.REMOVE_NOBOX.getSmallIcon()); public EntryLinkListEditor(JabRefFrame frame, BibDatabaseContext databaseContext, String fieldName, String content, EntryEditor entryEditor, boolean singleEntry) { this.frame = frame; this.databaseContext = databaseContext; this.fieldName = fieldName; this.entryEditor = entryEditor; this.singleEntry = singleEntry; tableModel = new EntryLinkListTableModel(EntryLinkList.parse(content, databaseContext.getDatabase())); setText(content); setModel(tableModel); JScrollPane sPane = new JScrollPane(this); setTableHeader(null); addMouseListener(new TableClickListener()); add.setToolTipText(("New entry link (INSERT)")); remove.setToolTipText(("Remove entry link (DELETE)")); add.setMargin(new Insets(0, 0, 0, 0)); remove.setMargin(new Insets(0, 0, 0, 0)); add.addActionListener(e -> addEntry()); remove.addActionListener(e -> removeEntries()); FormLayout layout = new FormLayout( "fill:pref:grow,1dlu,fill:pref:grow", "fill:pref,fill:pref,1dlu,fill:pref" ); FormBuilder builder = FormBuilder.create().layout(layout); if (!singleEntry) { JButton up = new JButton(IconTheme.JabRefIcon.UP.getSmallIcon()); JButton down = new JButton(IconTheme.JabRefIcon.DOWN.getSmallIcon()); up.setMargin(new Insets(0, 0, 0, 0)); down.setMargin(new Insets(0, 0, 0, 0)); up.addActionListener(e -> moveEntry(-1)); down.addActionListener(e -> moveEntry(1)); builder.add(up).xy(1, 1); builder.add(down).xy(1, 2); } builder.add(add).xy(3, 1); builder.add(remove).xy(3, 2); JButton button = new JButton(Localization.lang("Jump to entry")); button.addActionListener(e -> jumpToEntry()); builder.add(button).xyw(1, 4, 3); panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(sPane, BorderLayout.CENTER); panel.add(builder.getPanel(), BorderLayout.EAST); // 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 an input/action pair for inserting an entry: getInputMap().put(KeyStroke.getKeyStroke("INSERT"), "insert"); getActionMap().put("insert", new AbstractAction() { @Override public void actionPerformed(ActionEvent actionEvent) { int row = getSelectedRow(); addEntry(); 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); } }); JMenuItem openLink = new JMenuItem(Localization.lang("Jump to entry")); menu.add(openLink); openLink.addActionListener(e -> jumpToEntry()); // Set table row height FontMetrics metrics = getFontMetrics(getFont()); setRowHeight(Math.max(getRowHeight(), metrics.getHeight())); updateButtonStates(); } private static String formatEntry(BibEntry entry, BibDatabase database) { StringReader sr = new StringReader(layoutFormat); try { Layout layout = new LayoutHelper(sr, Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader)) .getLayoutFromText(); return layout.doLayout(entry, database); } catch (IOException e) { LOGGER.warn("Problem generating entry layout", e); } return ""; } private void jumpToEntry() { String entryKey = null; if (singleEntry) { ParsedEntryLink firstEntry = tableModel.getEntry(0); if (firstEntry != null) { entryKey = firstEntry.getKey(); } } else { int selectedRow = getSelectedRow(); if (selectedRow != -1) { entryKey = tableModel.getEntry(selectedRow).getKey(); } } if (entryKey != null) { frame.getCurrentBasePanel().getDatabase().getEntryByKey(entryKey).ifPresent( e -> frame.getCurrentBasePanel().highlightEntry(e) ); } } 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); } } @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.getText(); } @Override public void setText(String newText) { tableModel.setContent(EntryLinkList.parse(newText, databaseContext.getDatabase())); adjustColumnWidth(); updateButtonStates(); } @Override public void append(String text) { // Do nothing } @Override public void paste(String textToInsert) { // Do nothing } @Override public String getSelectedText() { return null; } private void addEntry() { int row = getSelectedRow(); if (row == -1) { row = 0; } ParsedEntryLink entry = new ParsedEntryLink("", databaseContext.getDatabase()); tableModel.addEntry(row, entry); entryEditor.updateField(this); adjustColumnWidth(); updateButtonStates(); } 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(); updateButtonStates(); } private void updateButtonStates() { if (singleEntry) { if (tableModel.isEmpty()) { add.setEnabled(true); remove.setEnabled(false); } else { add.setEnabled(false); remove.setEnabled(true); } } } 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(); } ParsedEntryLink entry = tableModel.getEntry(sel[0]); tableModel.removeEntry(sel[0]); tableModel.addEntry(toIdx, entry); entryEditor.updateField(this); setRowSelectionInterval(toIdx, toIdx); 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) { Optional<BibEntry> entry = tableModel.getEntry(row).getLinkedEntry(); if (entry.isPresent()) { // Select entry in main table frame.getCurrentBasePanel().highlightEntry(entry.get()); } else { // Focus BibTeX key field } } } 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(EntryLinkListEditor.this, e.getX(), e.getY()); } } } private class EntryLinkListTableModel extends DefaultTableModel { private final List<ParsedEntryLink> internalList = Collections.synchronizedList(new ArrayList<>()); public EntryLinkListTableModel(List<ParsedEntryLink> originalList) { addEntries(originalList); } public String getText() { synchronized (internalList) { String result = EntryLinkList.serialize(internalList); return result; } } public void addEntries(List<ParsedEntryLink> newList) { internalList.addAll(newList); if (SwingUtilities.isEventDispatchThread()) { fireTableDataChanged(); } else { SwingUtilities.invokeLater(() -> fireTableDataChanged()); } } public void setContent(List<ParsedEntryLink> newList) { internalList.clear(); internalList.addAll(newList); if (SwingUtilities.isEventDispatchThread()) { fireTableDataChanged(); } else { SwingUtilities.invokeLater(() -> fireTableDataChanged()); } } @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { if (internalList == null) { return 0; } synchronized (internalList) { return internalList.size(); } } @Override public Class<String> getColumnClass(int columnIndex) { return String.class; } @Override public Object getValueAt(int rowIndex, int columnIndex) { synchronized (internalList) { ParsedEntryLink entry = internalList.get(rowIndex); switch (columnIndex) { case 0: return entry.getKey(); case 1: return entry.getLinkedEntry() .map(bibEntry -> formatEntry(bibEntry, entry.getDataBase())) .orElse("Unknown entry"); default: return null; } } } public ParsedEntryLink getEntry(int index) { synchronized (internalList) { return internalList.get(index); } } public void removeEntry(int index) { internalList.remove(index); if (SwingUtilities.isEventDispatchThread()) { fireTableRowsDeleted(index, index); } else { SwingUtilities.invokeLater(() -> fireTableRowsDeleted(index, index)); } } public boolean isEmpty() { return internalList.isEmpty(); } /** * Add an entry to the table model, and fire a change event. The change event * is fired on the event dispatch thread. * @param index The row index to insert the entry at. * @param entry The entry to insert. */ public void addEntry(final int index, final ParsedEntryLink entry) { synchronized (internalList) { internalList.add(index, entry); if (SwingUtilities.isEventDispatchThread()) { fireTableDataChanged(); } else { SwingUtilities.invokeLater(() -> fireTableDataChanged()); } } } @Override public boolean isCellEditable(int row, int column) { return (column == 0); } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { synchronized (internalList) { if (columnIndex == 0) { internalList.get(rowIndex).setKey((String) aValue); if (SwingUtilities.isEventDispatchThread()) { fireTableRowsUpdated(rowIndex, rowIndex); } else { SwingUtilities.invokeLater(() -> fireTableRowsUpdated(rowIndex, rowIndex)); } } } } } }