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;
}
}