package org.jabref.gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.print.PrinterException; import java.io.IOException; import java.io.StringReader; import java.util.Optional; import java.util.regex.Pattern; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; import javax.print.attribute.standard.JobName; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import org.jabref.Globals; import org.jabref.JabRefExecutorService; import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.fieldeditors.PreviewPanelTransferHandler; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.worker.CitationStyleWorker; import org.jabref.logic.citationstyle.CitationStyle; import org.jabref.logic.exporter.ExportFormats; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.Layout; import org.jabref.logic.layout.LayoutHelper; import org.jabref.logic.search.SearchQueryHighlightListener; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.preferences.PreviewPreferences; import com.google.common.eventbus.Subscribe; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Displays an BibEntry using the given layout format. */ public class PreviewPanel extends JPanel implements SearchQueryHighlightListener, EntryContainer { private static final Log LOGGER = LogFactory.getLog(PreviewPanel.class); /** * The bibtex entry currently shown */ private Optional<BibEntry> bibEntry = Optional.empty(); /** * If a database is set, the preview will attempt to resolve strings in the * previewed entry using that database. */ private Optional<BibDatabaseContext> databaseContext = Optional.empty(); private Optional<BasePanel> basePanel = Optional.empty(); private boolean fixedLayout; private Optional<Layout> layout = Optional.empty(); private JEditorPaneWithHighlighting previewPane; private final JScrollPane scrollPane; private final PrintAction printAction = new PrintAction(); private final CloseAction closeAction = new CloseAction(); private final CopyPreviewAction copyPreviewAction = new CopyPreviewAction(); private Optional<Pattern> highlightPattern = Optional.empty(); private Optional<CitationStyleWorker> citationStyleWorker = Optional.empty(); /** * @param databaseContext * (may be null) Optionally used to resolve strings and for resolving pdf directories for links. * @param entry * (may be null) If given this entry is shown otherwise you have * to call setEntry to make something visible. * @param panel * (may be null) If not given no toolbar is shown on the right * hand side. */ public PreviewPanel(BibDatabaseContext databaseContext, BibEntry entry, BasePanel panel) { this(panel, databaseContext); setEntry(entry); } /** * * @param panel * (may be null) If not given no toolbar is shown on the right * hand side. * @param databaseContext * (may be null) Used for resolving pdf directories for links. */ public PreviewPanel(BasePanel panel, BibDatabaseContext databaseContext) { super(new BorderLayout(), true); this.databaseContext = Optional.ofNullable(databaseContext); this.basePanel = Optional.ofNullable(panel); createPreviewPane(); if (this.basePanel.isPresent()) { // dropped files handler only created for main window // not for Windows as like the search results window this.previewPane.setTransferHandler(new PreviewPanelTransferHandler(panel.frame(), this, this.previewPane.getTransferHandler())); } // Set up scroll pane for preview pane scrollPane = new JScrollPane(previewPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setBorder(null); add(scrollPane, BorderLayout.CENTER); this.createKeyBindings(); updateLayout(); } private void createKeyBindings() { ActionMap actionMap = this.getActionMap(); InputMap inputMap = this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); final String close = "close"; inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), close); actionMap.put(close, this.closeAction); final String copy = "copy"; getInputMap(JComponent.WHEN_FOCUSED).put(Globals.getKeyPrefs().getKey(KeyBinding.COPY_PREVIEW), copy); actionMap.put(copy, this.copyPreviewAction); } private JPopupMenu createPopupMenu() { JPopupMenu menu = new JPopupMenu(); menu.add(this.printAction); menu.add(this.copyPreviewAction); this.basePanel.ifPresent(p -> menu.add(p.frame().getNextPreviewStyleAction())); this.basePanel.ifPresent(p -> menu.add(p.frame().getPreviousPreviewStyleAction())); return menu; } private void createPreviewPane() { previewPane = new JEditorPaneWithHighlighting() { @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } }; previewPane.setMargin(new Insets(3, 3, 3, 3)); previewPane.setComponentPopupMenu(createPopupMenu()); previewPane.setEditable(false); previewPane.setDragEnabled(true); // this has an effect only, if no custom transfer handler is registered. We keep the statement if the transfer handler is removed. previewPane.setContentType("text/html"); previewPane.addHyperlinkListener(hyperlinkEvent -> { if ((hyperlinkEvent.getEventType() == HyperlinkEvent.EventType.ACTIVATED) && PreviewPanel.this.databaseContext .isPresent()) { try { String address = hyperlinkEvent.getURL().toString(); JabRefDesktop.openExternalViewer(PreviewPanel.this.databaseContext.get(), address, FieldName.URL); } catch (IOException e) { LOGGER.warn("Could not open external viewer", e); } } }); } public void setDatabaseContext(BibDatabaseContext databaseContext) { this.databaseContext = Optional.ofNullable(databaseContext); } public Optional<BasePanel> getBasePanel() { return this.basePanel; } public void setBasePanel(BasePanel basePanel) { this.basePanel = Optional.ofNullable(basePanel); } public void updateLayout() { if (fixedLayout) { LOGGER.debug("cannot change the layout because the layout is fixed"); return; } PreviewPreferences previewPreferences = Globals.prefs.getPreviewPreferences(); String style = previewPreferences.getPreviewCycle().get(previewPreferences.getPreviewCyclePosition()); if (CitationStyle.isCitationStyleFile(style)) { if (basePanel.isPresent()) { layout = Optional.empty(); CitationStyle citationStyle = CitationStyle.createCitationStyleFromFile(style); if (citationStyle != null) { basePanel.get().getCitationStyleCache().setCitationStyle(citationStyle); basePanel.get().output(Localization.lang("Preview style changed to: %0", citationStyle.getTitle())); } } } else { updatePreviewLayout(previewPreferences.getPreviewStyle()); if (basePanel.isPresent()) { basePanel.get().output(Localization.lang("Preview style changed to: %0", Localization.lang("Preview"))); } } update(); } private void updatePreviewLayout(String layoutFile) { StringReader sr = new StringReader(layoutFile.replace("__NEWLINE__", "\n")); try { layout = Optional.of( new LayoutHelper(sr, Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader)) .getLayoutFromText()); } catch (IOException e) { layout = Optional.empty(); LOGGER.debug("no layout could be set", e); } } public void setLayout(Layout layout) { this.layout = Optional.ofNullable(layout); } public void setEntry(BibEntry newEntry) { bibEntry.filter(e -> e != newEntry).ifPresent(e -> e.unregisterListener(this)); bibEntry = Optional.ofNullable(newEntry); bibEntry.ifPresent(e -> e.registerListener(this)); update(); } /** * Listener for ChangedFieldEvent. */ @SuppressWarnings("unused") @Subscribe public void listen(FieldChangedEvent fieldChangedEvent) { update(); } @Override public BibEntry getEntry() { return this.bibEntry.orElse(null); } public void update() { ExportFormats.entryNumber = 1; // Set entry number in case that is included in the preview layout. if (citationStyleWorker.isPresent()) { citationStyleWorker.get().cancel(true); citationStyleWorker = Optional.empty(); } if (layout.isPresent()) { StringBuilder sb = new StringBuilder(); bibEntry.ifPresent(entry -> sb.append(layout.get() .doLayout(entry, databaseContext.map(BibDatabaseContext::getDatabase).orElse(null)))); setPreviewLabel(sb.toString()); markHighlights(); } else if (basePanel.isPresent()) { citationStyleWorker = Optional.of(new CitationStyleWorker(this, previewPane)); citationStyleWorker.get().execute(); } } public void markHighlights() { previewPane.highlightPattern(highlightPattern); } public void setPreviewLabel(String text) { if (SwingUtilities.isEventDispatchThread()) { previewPane.setText(text); previewPane.revalidate(); } else { SwingUtilities.invokeLater(() -> { previewPane.setText(text); previewPane.revalidate(); }); } this.scrollToTop(); } private void scrollToTop() { SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar().setValue(0)); } @Override public void highlightPattern(Optional<Pattern> newPattern) { this.highlightPattern = newPattern; update(); } public Optional<Pattern> getHighlightPattern() { return highlightPattern; } /** * this fixes the Layout, the user cannot change it anymore. Useful for testing the styles in the settings * @param parameter should be either a {@link String} (for the old PreviewStyle) or a {@link CitationStyle}. */ public PreviewPanel setFixedLayout(Object parameter) { this.fixedLayout = true; if (parameter instanceof String) { updatePreviewLayout((String) parameter); } else if (parameter instanceof CitationStyle) { layout = Optional.empty(); if (basePanel.isPresent()) { basePanel.get().getCitationStyleCache().setCitationStyle((CitationStyle) parameter); } } else { LOGGER.error("unknown style type"); } update(); return this; } class PrintAction extends AbstractAction { public PrintAction() { super(Localization.lang("Print entry preview"), IconTheme.JabRefIcon.PRINTED.getIcon()); putValue(Action.SHORT_DESCRIPTION, Localization.lang("Print entry preview")); } @Override public void actionPerformed(ActionEvent arg0) { // Background this, as it takes a while. JabRefExecutorService.INSTANCE.execute(() -> { try { PrintRequestAttributeSet pras = new HashPrintRequestAttributeSet(); pras.add(new JobName(bibEntry.flatMap(BibEntry::getCiteKeyOptional).orElse("NO ENTRY"), null)); previewPane.print(null, null, true, null, pras, false); } catch (PrinterException e) { // Inform the user... we don't know what to do. JOptionPane.showMessageDialog(PreviewPanel.this, Localization.lang("Could not print preview") + ".\n" + e.getMessage(), Localization.lang("Print entry preview"), JOptionPane.ERROR_MESSAGE); LOGGER.info("Could not print preview", e); } }); } } public void close() { basePanel.ifPresent(BasePanel::hideBottomComponent); } class CloseAction extends AbstractAction { public CloseAction() { super(Localization.lang("Close window"), IconTheme.JabRefIcon.CLOSE.getSmallIcon()); putValue(Action.SHORT_DESCRIPTION, Localization.lang("Close window")); } @Override public void actionPerformed(ActionEvent e) { close(); } } class CopyPreviewAction extends AbstractAction { public CopyPreviewAction() { super(Localization.lang("Copy preview"), IconTheme.JabRefIcon.COPY.getSmallIcon()); putValue(Action.SHORT_DESCRIPTION, Localization.lang("Copy preview")); putValue(Action.ACCELERATOR_KEY, Globals.getKeyPrefs().getKey(KeyBinding.COPY_PREVIEW)); } @Override public void actionPerformed(ActionEvent e) { previewPane.selectAll(); previewPane.copy(); previewPane.select(0, -1); } } public PrintAction getPrintAction() { return printAction; } }