package org.jabref.gui.entryeditor; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.StringJoiner; import javax.swing.BoxLayout; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.jabref.gui.ClipBoardManager; import org.jabref.gui.GUIGlobals; import org.jabref.gui.IconTheme; import org.jabref.logic.l10n.Localization; import org.jabref.logic.pdf.FileAnnotationCache; import org.jabref.model.entry.FieldName; import org.jabref.model.pdf.FileAnnotation; import com.jgoodies.forms.builder.FormBuilder; import com.jgoodies.forms.factories.Paddings; import static org.jabref.model.pdf.FileAnnotationType.NONE; class FileAnnotationTab extends JPanel { private final JList<FileAnnotation> annotationList = new JList<>(); private final JScrollPane annotationScrollPane = new JScrollPane(); private final JLabel fileNameLabel = new JLabel(Localization.lang("Filename"), JLabel.CENTER); private final JComboBox<String> fileNameComboBox = new JComboBox<>(); private final JScrollPane fileNameScrollPane = new JScrollPane(); private final JLabel authorLabel = new JLabel(Localization.lang("Author"), JLabel.CENTER); private final JTextArea authorArea = new JTextArea("author"); private final JScrollPane authorScrollPane = new JScrollPane(); private final JLabel dateLabel = new JLabel(Localization.lang("Date"), JLabel.CENTER); private final JTextArea dateArea = new JTextArea("date"); private final JScrollPane dateScrollPane = new JScrollPane(); private final JLabel pageLabel = new JLabel(Localization.lang("Page"), JLabel.CENTER); private final JTextArea pageArea = new JTextArea("page"); private final JScrollPane pageScrollPane = new JScrollPane(); private final JLabel annotationTextLabel = new JLabel(Localization.lang("Content"), JLabel.CENTER); private final JTextArea contentTxtArea = new JTextArea(); private final JLabel markedTextLabel = new JLabel(Localization.lang("Marking"), JLabel.CENTER); private final JTextArea markedTxtArea = new JTextArea(); private final JScrollPane annotationTextScrollPane = new JScrollPane(); private final JScrollPane markedTextScrollPane = new JScrollPane(); private final JButton copyToClipboardButton = new JButton(); private final JButton reloadAnnotationsButton = new JButton(); private final FileAnnotationCache fileAnnotationCache; private DefaultListModel<FileAnnotation> listModel; private final EntryEditor parent; private boolean isInitialized; FileAnnotationTab(EntryEditor parent, FileAnnotationCache cache) { this.fileAnnotationCache = cache; this.parent = parent; setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); listModel = new DefaultListModel<>(); this.isInitialized = false; } public FileAnnotationTab initializeTab(FileAnnotationTab tab) { if (tab.isInitialized) { return tab; } tab.addAnnotations(); tab.setUpGui(); tab.isInitialized = true; tab.parent.repaint(); return tab; } /** * Adds pdf annotations from all attached pdf files belonging to the entry selected in the main table and * shows those from the first file in the file annotations tab */ private void addAnnotations() { if (parent.getEntry().getField(FieldName.FILE).isPresent()) { if (!annotationList.getModel().equals(listModel)) { annotationList.setModel(listModel); annotationList.addListSelectionListener(new AnnotationListSelectionListener()); annotationList.setCellRenderer(new AnnotationListCellRenderer()); } //set up the comboBox for representing the selected file fileNameComboBox.removeAllItems(); final Map<String, List<FileAnnotation>> fileAnnotations = fileAnnotationCache.getFromCache(parent.getEntry()); fileAnnotations.keySet().forEach(fileNameComboBox::addItem); //show the annotationsOfFiles attached to the selected file updateShownAnnotations(fileAnnotations.get(fileNameComboBox.getSelectedItem() == null ? fileNameComboBox.getItemAt(0) : fileNameComboBox.getSelectedItem().toString())); //select the first annotation if (annotationList.isSelectionEmpty()) { annotationList.setSelectedIndex(0); } } } /** * Updates the list model to show the given notes without those with no content * * @param annotations value is the annotation name and the value is a pdfAnnotation object to add to the list model */ private void updateShownAnnotations(List<FileAnnotation> annotations) { listModel.clear(); if (annotations == null || annotations.isEmpty()) { listModel.addElement(new FileAnnotation("", LocalDateTime.now(), 0, Localization.lang("File has no attached annotations"), NONE, Optional.empty())); } else { Comparator<FileAnnotation> byPage = Comparator.comparingInt(FileAnnotation::getPage); annotations.stream() .filter(annotation -> (null != annotation.getContent())) .sorted(byPage) .forEach(annotation -> listModel.addElement(new FileAnnotationViewModel(annotation))); } } /** * Updates the text fields showing meta data and the content from the selected annotation * * @param annotation pdf annotation which data should be shown in the text fields */ private void updateTextFields(FileAnnotation annotation) { authorArea.setText(annotation.getAuthor()); dateArea.setText(annotation.getTimeModified().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))); pageArea.setText(String.valueOf(annotation.getPage())); updateContentAndMarkedTextAreas(annotation); } /** * Updates the selection of files that are attached to the pdf file */ private void updateFileNameComboBox() { int indexSelectedByComboBox; if (fileNameComboBox.getItemCount() == 0) { indexSelectedByComboBox = 0; } else { indexSelectedByComboBox = fileNameComboBox.getSelectedIndex(); } fileNameComboBox.removeAllItems(); final Map<String, List<FileAnnotation>> fileAnnotations = fileAnnotationCache.getFromCache(parent.getEntry()); fileAnnotations.keySet().stream().filter(filename -> filename.toLowerCase(Locale.ROOT).endsWith(".pdf")). forEach((fileNameComboBox::addItem)); fileNameComboBox.setSelectedIndex(indexSelectedByComboBox); updateShownAnnotations(fileAnnotations.get(fileNameComboBox.getSelectedItem().toString())); } private void setUpGui() { JPanel annotationPanel = FormBuilder.create() .columns("pref, $lcgap, pref:grow") .rows("pref, $lg, fill:pref:grow, $lg, pref") .padding(Paddings.DIALOG) .add(fileNameLabel).xy(1, 1, "left, top") .add(fileNameScrollPane).xyw(2, 1, 2) .add(annotationScrollPane).xyw(1, 3, 3) .build(); annotationScrollPane.setViewportView(annotationList); JPanel informationPanel = FormBuilder.create() .columns("pref, $lcgap, pref:grow") .rows("pref, $lg, pref, $lg, pref, $lg, pref, $lg, pref:grow, $lg, pref:grow, $lg, fill:pref") .padding(Paddings.DIALOG) .add(authorLabel).xy(1, 3, "left, top") .add(authorScrollPane).xy(3, 3) .add(dateLabel).xy(1, 5, "left, top") .add(dateScrollPane).xy(3, 5) .add(pageLabel).xy(1, 7, "left, top") .add(pageScrollPane).xy(3, 7) .add(annotationTextLabel).xy(1, 9, "left, top") .add(annotationTextScrollPane).xywh(3, 9, 1, 2) .add(markedTextLabel).xy(1, 11, "left, top") .add(markedTextScrollPane).xywh(3, 11, 1, 2) .add(this.setUpButtons()).xyw(1, 13, 3) .build(); fileNameScrollPane.setViewportView(fileNameComboBox); fileNameLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); authorLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); dateLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); pageLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); annotationTextLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); markedTextLabel.setForeground(GUIGlobals.ENTRY_EDITOR_LABEL_COLOR); fileNameScrollPane.setBorder(null); authorScrollPane.setViewportView(authorArea); authorScrollPane.setBorder(null); dateScrollPane.setViewportView(dateArea); dateScrollPane.setBorder(null); pageScrollPane.setViewportView(pageArea); pageScrollPane.setBorder(null); annotationTextScrollPane.setViewportView(contentTxtArea); markedTextScrollPane.setViewportView(markedTxtArea); authorArea.setEditable(false); dateArea.setEditable(false); pageArea.setEditable(false); contentTxtArea.setEditable(false); contentTxtArea.setLineWrap(true); markedTxtArea.setEditable(false); markedTxtArea.setLineWrap(true); fileNameComboBox.setEditable(false); fileNameComboBox.addActionListener(e -> updateFileNameComboBox()); this.add(FormBuilder.create() .columns("0:grow, $lcgap, 0:grow") .rows("fill:pref:grow") .add(annotationPanel).xy(1, 1) .add(informationPanel).xy(3, 1) .build()); } private JPanel setUpButtons() { JPanel buttonPanel = new JPanel(new GridBagLayout()); GridBagConstraints buttonConstraints = new GridBagConstraints(); copyToClipboardButton.setText(Localization.lang("Copy to clipboard")); copyToClipboardButton.addActionListener(e -> copyToClipboard()); reloadAnnotationsButton.setText(Localization.lang("Reload annotations")); reloadAnnotationsButton.addActionListener(e -> reloadAnnotations()); buttonConstraints.gridy = 10; buttonConstraints.gridx = 3; buttonPanel.add(copyToClipboardButton, buttonConstraints); buttonConstraints.gridx = 2; buttonConstraints.gridx = 1; buttonPanel.add(reloadAnnotationsButton, buttonConstraints); return buttonPanel; } /** * Copies the meta and content information of the pdf annotation to the clipboard */ private void copyToClipboard() { StringJoiner sj = new StringJoiner(System.getProperty("line.separator")); sj.add(Localization.lang("Author") + ": " + authorArea.getText()); sj.add(Localization.lang("Date") + ": " + dateArea.getText()); sj.add(Localization.lang("Page") + ": " + pageArea.getText()); sj.add(Localization.lang("Content") + ": " + contentTxtArea.getText()); sj.add(Localization.lang("Marking") + ": " + markedTxtArea.getText()); new ClipBoardManager().setClipboardContents(sj.toString()); } private void reloadAnnotations() { isInitialized = false; Arrays.stream(this.getComponents()).forEach(this::remove); fileAnnotationCache.remove(parent.getEntry()); initializeTab(this); this.repaint(); } /** * Fills the TextAreas of the content and the highlighted or underlined text with the corresponding text and also * changes the label accordingly. * * @param annotation either a text annotation or a marking from a PDF */ private void updateContentAndMarkedTextAreas(final FileAnnotation annotation) { updateMarkingType(annotation); if (annotation.hasLinkedAnnotation()) { // isPresent() of the optional is already checked in annotation.hasLinkedAnnotation() if (!annotation.getLinkedFileAnnotation().getContent().isEmpty()) { contentTxtArea.setText(annotation.getLinkedFileAnnotation().getContent()); contentTxtArea.setEnabled(true); } else { contentTxtArea.setText("N/A"); contentTxtArea.setEnabled(false); } if (annotation.getContent().isEmpty()) { markedTxtArea.setEnabled(false); markedTxtArea.setText(Localization.lang("The marked area does not contain any legible text!")); } else { markedTxtArea.setEnabled(true); markedTxtArea.setText(annotation.getContent()); } } else { contentTxtArea.setEnabled(true); if ("File has no attached annotations.".equals(annotation.getContent())) { authorArea.setText("N/A"); authorArea.setEnabled(false); dateArea.setText("N/A"); dateArea.setEnabled(false); pageArea.setText("N/A"); pageArea.setEnabled(false); contentTxtArea.setEnabled(false); } contentTxtArea.setText(annotation.getContent()); markedTxtArea.setText("N/A"); markedTxtArea.setEnabled(false); } } private void updateMarkingType(FileAnnotation annotation) { switch (annotation.getAnnotationType()) { case UNDERLINE: markedTextLabel.setText(Localization.lang("Underline")); break; case HIGHLIGHT: markedTextLabel.setText(Localization.lang("Highlight")); break; default: markedTextLabel.setText(Localization.lang("Marking")); break; } } private class AnnotationListSelectionListener implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { int index; int annotationListSelectedIndex = 0; if (annotationList.getSelectedIndex() >= 0) { index = annotationList.getSelectedIndex(); updateTextFields(listModel.get(index)); annotationListSelectedIndex = index; } annotationList.setSelectedIndex(annotationListSelectedIndex); //repaint the list to refresh the linked annotation annotationList.repaint(); } } /** * Cell renderer that shows different icons dependent on the annotation subtype */ class AnnotationListCellRenderer extends DefaultListCellRenderer { JLabel label; AnnotationListCellRenderer() { this.label = new JLabel(); } @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { FileAnnotation annotation = (FileAnnotation) value; //call the super method so that the cell selection is done as usual label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); //If more different annotation types should be reflected by icons in the list, add them here switch (annotation.getAnnotationType()) { case HIGHLIGHT: label.setIcon(IconTheme.JabRefIcon.MARKER.getSmallIcon()); break; case UNDERLINE: label.setIcon(IconTheme.JabRefIcon.MARKER.getSmallIcon()); break; default: label.setIcon(IconTheme.JabRefIcon.OPTIONAL.getSmallIcon()); break; } label.setToolTipText(annotation.getAnnotationType().toString()); label.setText(annotation.toString()); return label; } } public boolean isInitialized() { return isInitialized; } }