package org.jabref.gui.plaintextimport; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.DefaultListCellRenderer; import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JTextPane; import javax.swing.JToolBar; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.border.TitledBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.text.BadLocationException; import javax.swing.text.EditorKit; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import javax.swing.text.StyledDocument; import org.jabref.Globals; import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; import org.jabref.gui.EntryMarker; import org.jabref.gui.FXDialogService; import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefDialog; import org.jabref.gui.JabRefFrame; import org.jabref.gui.OSXCompatibleToolbar; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.component.OverlayPanel; import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.LatexFieldFormatter; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.fileformat.FreeCiteImporter; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.FileExtensions; import org.jabref.logic.util.OS; import org.jabref.logic.util.UpdateField; import org.jabref.model.EntryTypes; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.EntryType; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.FieldProperty; import org.jabref.model.entry.InternalBibtexFields; import org.jabref.preferences.JabRefPreferences; import com.jgoodies.forms.builder.ButtonBarBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * import from plain text => simple mark/copy/paste into bibtex entry * <p> * TODO * - change colors and fonts * - delete selected text * - make textarea editable * - create several bibtex entries in dialog * - if the dialog works with an existing entry (right click menu item), the cancel option doesn't work well */ public class TextInputDialog extends JabRefDialog { private static final Log LOGGER = LogFactory.getLog(TextInputDialog.class); private final JButton okButton = new JButton(Localization.lang("Accept")); private final JButton cancelButton = new JButton(Localization.lang("Cancel")); private final JButton insertButton = new JButton(Localization.lang("Insert")); private final JButton parseWithFreeCiteButton = new JButton(Localization.lang("Parse with FreeCite")); private final JPanel panel1 = new JPanel(); private final JPanel buttons = new JPanel(); private final JPanel rawPanel = new JPanel(); private final JPanel sourcePanel = new JPanel(); private JList<String> fieldList; private final JRadioButton override = new JRadioButton(Localization.lang("Override")); private final JRadioButton append = new JRadioButton(Localization.lang("Append")); private final JToolBar toolBar = new OSXCompatibleToolbar(); private final List<String> allFields = new ArrayList<>(); private final List<String> requiredFields = new ArrayList<>(); private final List<String> optionalFields = new ArrayList<>(); private final BibEntry entry; private final JPopupMenu inputMenu = new JPopupMenu(); private StyledDocument document; // content from inputPane private final JTextPane textPane = new JTextPane(); private final JTextArea sourcePreview = new JTextArea(); private final TagToMarkedTextStore markedTextStore; private final JabRefFrame frame; private boolean okPressed; public TextInputDialog(JabRefFrame frame, BibEntry bibEntry) { super(frame, true, TextInputDialog.class); this.frame = Objects.requireNonNull(frame); entry = Objects.requireNonNull(bibEntry); markedTextStore = new TagToMarkedTextStore(); jbInit(); pack(); updateSourceView(); } private void jbInit() { getContentPane().setLayout(new BorderLayout()); StringBuilder typeStr = new StringBuilder("Plain text import"); if (entry.getType() != null) { typeStr.append(' ').append(Localization.lang("for")).append(' ').append(entry.getType()); } this.setTitle(typeStr.toString()); getContentPane().add(panel1, BorderLayout.CENTER); initRawPanel(); initButtonPanel(); initSourcePanel(); JTabbedPane tabbed = new JTabbedPane(); tabbed.add(rawPanel, Localization.lang("Raw source")); tabbed.add(sourcePanel, Localization.lang("%0 source", frame.getCurrentBasePanel().getBibDatabaseContext().getMode().getFormattedName())); // Panel Layout panel1.setLayout(new BorderLayout()); panel1.add(tabbed, BorderLayout.CENTER); panel1.add(buttons, BorderLayout.SOUTH); // Key bindings: ActionMap am = buttons.getActionMap(); InputMap im = buttons.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }); } // Panel with text import functionality private void initRawPanel() { rawPanel.setLayout(new BorderLayout()); // Textarea textPane.setEditable(false); document = textPane.getStyledDocument(); addStylesToDocument(); try { document.insertString(0, "", document.getStyle("regular")); } catch (BadLocationException ex) { LOGGER.warn("Problem setting style", ex); } OverlayPanel testPanel = new OverlayPanel(textPane, Localization.lang("paste text here")); testPanel.setPreferredSize(new Dimension(450, 255)); testPanel.setMaximumSize(new Dimension(450, Integer.MAX_VALUE)); // Setup fields (required to be done before setting up popup menu) fieldList = new JList<>(getAllFields()); fieldList.setCellRenderer(new SimpleCellRenderer(fieldList.getFont())); ListSelectionModel listSelectionModel = fieldList.getSelectionModel(); listSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); listSelectionModel.addListSelectionListener(new FieldListSelectionHandler()); fieldList.addMouseListener(new FieldListMouseListener()); // After the call to getAllFields initPopupMenuAndToolbar(); //Add listener to components that can bring up popup menus. MouseListener popupListener = new PopupListener(inputMenu); textPane.addMouseListener(popupListener); testPanel.addMouseListener(popupListener); JPanel leftPanel = new JPanel(new BorderLayout()); leftPanel.add(toolBar, BorderLayout.NORTH); leftPanel.add(testPanel, BorderLayout.CENTER); JPanel inputPanel = setUpFieldListPanel(); // parse with FreeCite button parseWithFreeCiteButton.addActionListener(event -> { if (parseWithFreeCiteAndAddEntries()) { okPressed = false; // we do not want to have the super method to handle our entries, we do it on our own dispose(); } }); rawPanel.add(leftPanel, BorderLayout.CENTER); rawPanel.add(inputPanel, BorderLayout.EAST); JLabel desc = new JLabel("<html><h3>" + Localization.lang("Plain text import") + "</h3><p>" + Localization.lang("This is a simple copy and paste dialog. First load or paste some text into " + "the text input area.<br>After that, you can mark text and assign it to a BibTeX field.") + "</p></html>"); desc.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); rawPanel.add(desc, BorderLayout.SOUTH); } private JPanel setUpFieldListPanel() { JPanel inputPanel = new JPanel(); // Panel Layout GridBagLayout gbl = new GridBagLayout(); GridBagConstraints con = new GridBagConstraints(); con.weightx = 0; con.insets = new Insets(5, 5, 0, 5); con.fill = GridBagConstraints.HORIZONTAL; inputPanel.setLayout(gbl); // Border TitledBorder titledBorder1 = new TitledBorder(BorderFactory.createLineBorder(new Color(153, 153, 153), 2), Localization.lang("Work options")); inputPanel.setBorder(titledBorder1); inputPanel.setMinimumSize(new Dimension(10, 10)); JScrollPane fieldScroller = new JScrollPane(fieldList); fieldScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); // insert buttons insertButton.addActionListener(event -> insertTextForTag(override.isSelected())); // Radio buttons append.setToolTipText(Localization.lang("Append the selected text to BibTeX field")); append.setMnemonic(KeyEvent.VK_A); append.setSelected(true); override.setToolTipText(Localization.lang("Override the BibTeX field by the selected text")); override.setMnemonic(KeyEvent.VK_O); override.setSelected(false); //Group the radio buttons. ButtonGroup group = new ButtonGroup(); group.add(append); group.add(override); JPanel radioPanel = new JPanel(new GridLayout(0, 1)); radioPanel.add(append); radioPanel.add(override); // insert sub components JLabel label1 = new JLabel(Localization.lang("Available BibTeX fields")); con.gridwidth = GridBagConstraints.REMAINDER; gbl.setConstraints(label1, con); inputPanel.add(label1); con.gridwidth = GridBagConstraints.REMAINDER; con.gridheight = 8; con.weighty = 1; con.fill = GridBagConstraints.BOTH; gbl.setConstraints(fieldScroller, con); inputPanel.add(fieldScroller); con.fill = GridBagConstraints.HORIZONTAL; con.weighty = 0; con.gridwidth = 2; gbl.setConstraints(radioPanel, con); inputPanel.add(radioPanel); con.gridwidth = GridBagConstraints.REMAINDER; gbl.setConstraints(insertButton, con); inputPanel.add(insertButton); return inputPanel; } private void initPopupMenuAndToolbar() { // copy/paste Menu PasteAction pasteAction = new PasteAction(); ClearAction clearAction = new ClearAction(); JMenuItem pasteMI = new JMenuItem(pasteAction); inputMenu.add(clearAction); inputMenu.addSeparator(); inputMenu.add(pasteMI); inputMenu.addSeparator(); // Right-click append/override JMenu appendMenu = new JMenu(Localization.lang("Append")); appendMenu.setToolTipText(Localization.lang("Append the selected text to BibTeX field")); JMenu overrideMenu = new JMenu(Localization.lang("Override")); overrideMenu.setToolTipText(Localization.lang("Override the BibTeX field by the selected text")); for (String field : allFields) { appendMenu.add(new JMenuItem(new MenuTextForTagAction(field, false))); overrideMenu.add(new JMenuItem(new MenuTextForTagAction(field, true))); } inputMenu.add(appendMenu); inputMenu.add(overrideMenu); // Toolbar toolBar.add(clearAction); toolBar.setBorderPainted(false); toolBar.addSeparator(); toolBar.add(pasteAction); toolBar.add(new LoadAction()); } private void initButtonPanel() { okButton.addActionListener(event -> { okPressed = true; dispose(); }); cancelButton.addActionListener(event -> dispose()); ButtonBarBuilder bb = new ButtonBarBuilder(buttons); buttons.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); bb.addGlue(); bb.addButton(okButton); bb.addButton(parseWithFreeCiteButton); bb.addButton(cancelButton); bb.addGlue(); } // Panel with BibTeX source code private void initSourcePanel() { sourcePreview.setEditable(false); sourcePreview.setFont(new Font("Monospaced", Font.PLAIN, Globals.prefs.getInt(JabRefPreferences.FONT_SIZE))); JScrollPane paneScrollPane = new JScrollPane(sourcePreview); paneScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); paneScrollPane.setPreferredSize(new Dimension(500, 255)); paneScrollPane.setMinimumSize(new Dimension(10, 10)); sourcePanel.setLayout(new BorderLayout()); sourcePanel.add(paneScrollPane, BorderLayout.CENTER); } private void addStylesToDocument() { //Initialize some styles. Style defaultStyle = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); Style regularStyle = document.addStyle("regular", defaultStyle); StyleConstants.setFontFamily(defaultStyle, "SansSerif"); StyleConstants.setFontSize(defaultStyle, Globals.prefs.getInt(JabRefPreferences.FONT_SIZE)); Style s = document.addStyle("used", regularStyle); StyleConstants.setBold(s, true); StyleConstants.setForeground(s, Color.blue); s = document.addStyle("marked", regularStyle); StyleConstants.setBold(s, true); StyleConstants.setForeground(s, Color.red); } private void insertTextForTag(boolean overrideField) { String fieldName = fieldList.getSelectedValue(); if (fieldName != null) { String txt = textPane.getSelectedText(); if (txt != null) { int selectionStart = textPane.getSelectionStart(); int selectionEnd = textPane.getSelectionEnd(); // unselect text textPane.setSelectionEnd(selectionStart); // mark the selected text as "used" document.setCharacterAttributes(selectionStart, selectionEnd - selectionStart, document.getStyle("marked"), true); // override an existing entry if (overrideField) { entry.setField(fieldName, txt); // erase old text selection markedTextStore.setStyleForTag(fieldName, "regular", document); // delete all previous styles markedTextStore.insertPosition(fieldName, selectionStart, selectionEnd); // insert new selection style } else { // memorize the selection for text highlighting markedTextStore.appendPosition(fieldName, selectionStart, selectionEnd); // get old text from BibTeX tag Optional<String> old = entry.getField(fieldName); // merge old and selected text if (old.isPresent()) { // insert a new name with an additional "and" if (InternalBibtexFields.getFieldProperties(fieldName).contains(FieldProperty.PERSON_NAMES)) { entry.setField(fieldName, old.get() + " and " + txt); } else if (FieldName.KEYWORDS.equals(fieldName)) { // Add keyword entry.addKeyword(txt, Globals.prefs.getKeywordDelimiter()); } else { entry.setField(fieldName, old.get() + txt); } } else { // "null"+"txt" Strings forbidden entry.setField(fieldName, txt); } } // make the new data in BibTeX source code visible updateSourceView(); } } } public boolean okPressed() { return okPressed; } /** * tries to parse the pasted reference with freecite * * @return true if successful, false otherwise */ private boolean parseWithFreeCiteAndAddEntries() { FreeCiteImporter fimp = new FreeCiteImporter(Globals.prefs.getImportFormatPreferences()); String text = textPane.getText(); // we have to remove line breaks (but keep empty lines) // otherwise, the result is broken text = text.replace(OS.NEWLINE.concat(OS.NEWLINE), "##NEWLINE##"); // possible URL line breaks are removed completely. text = text.replace("/".concat(OS.NEWLINE), "/"); text = text.replace(OS.NEWLINE, " "); text = text.replace("##NEWLINE##", OS.NEWLINE); ParserResult importerResult = fimp.importEntries(text); if (importerResult.hasWarnings()) { frame.showMessage(importerResult.getErrorMessage()); } List<BibEntry> importedEntries = importerResult.getDatabase().getEntries(); if (importedEntries.isEmpty()) { return false; } else { UpdateField.setAutomaticFields(importedEntries, false, false, Globals.prefs.getUpdateFieldPreferences()); boolean markEntries = EntryMarker.shouldMarkEntries(); for (BibEntry e : importedEntries) { if (markEntries) { EntryMarker.markEntry(entry, EntryMarker.IMPORT_MARK_LEVEL, false, new NamedCompound("")); } frame.getCurrentBasePanel().insertEntry(e); } return true; } } // update the bibtex source view and available List private void updateSourceView() { StringWriter sw = new StringWriter(200); try { new BibEntryWriter(new LatexFieldFormatter(Globals.prefs.getLatexFieldFormatterPreferences()), false).write(entry, sw, frame.getCurrentBasePanel().getBibDatabaseContext().getMode()); sourcePreview.setText(sw.getBuffer().toString()); } catch (IOException ex) { LOGGER.error("Error in entry" + ": " + ex.getMessage(), ex); } fieldList.clearSelection(); } private String[] getAllFields() { Optional<EntryType> type = EntryTypes.getType(entry.getType(), frame.getCurrentBasePanel().getBibDatabaseContext().getMode()); if (type.isPresent()) { allFields.addAll(type.get().getAllFields()); requiredFields.addAll(type.get().getRequiredFieldsFlat()); optionalFields.addAll(type.get().getPrimaryOptionalFields()); } for (String field : InternalBibtexFields.getAllPublicFieldNames()) { if (!allFields.contains(field)) { allFields.add(field); } } return allFields.toArray(new String[allFields.size()]); } private class PasteAction extends BasicAction { public PasteAction() { super(Localization.lang("Paste"), Localization.lang("Paste from clipboard"), IconTheme.JabRefIcon.PASTE.getIcon()); } @Override public void actionPerformed(ActionEvent e) { String data = new ClipBoardManager().getClipboardContents(); int selStart = textPane.getSelectionStart(); int selEnd = textPane.getSelectionEnd(); if ((selEnd - selStart) > 0) { textPane.replaceSelection(""); } int cPos = textPane.getCaretPosition(); try { document.insertString(cPos, data, document.getStyle("regular")); } catch (BadLocationException ex) { LOGGER.warn("Could not paste text", ex); } } } private class LoadAction extends BasicAction { public LoadAction() { super(Localization.lang("Open"), Localization.lang("Open file"), IconTheme.JabRefIcon.OPEN.getIcon()); } @Override public void actionPerformed(ActionEvent e) { try { FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .addExtensionFilter(FileExtensions.TXT) .withDefaultExtension(FileExtensions.TXT) .withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build(); DialogService ds = new FXDialogService(); Optional<Path> path = DefaultTaskExecutor .runInJavaFXThread(() -> ds.showFileOpenDialog(fileDialogConfiguration)); if (path.isPresent()) { Path file = path.get(); document.remove(0, document.getLength()); EditorKit eKit = textPane.getEditorKit(); if (eKit != null) { try (InputStream fis = Files.newInputStream(file)) { eKit.read(fis, document, 0); document.setLogicalStyle(0, document.getStyle("regular")); } } } } catch (BadLocationException | IOException ex) { LOGGER.warn("Problem reading or inserting file", ex); } } } private class ClearAction extends BasicAction { public ClearAction() { super(Localization.lang("Clear"), Localization.lang("Clear inputarea"), IconTheme.JabRefIcon.NEW.getIcon()); } @Override public void actionPerformed(ActionEvent e) { textPane.setText(""); } } class FieldListSelectionHandler implements ListSelectionListener { private int lastIndex = -1; @Override public void valueChanged(ListSelectionEvent e) { ListSelectionModel lsm = (ListSelectionModel) e.getSource(); int index = lsm.getAnchorSelectionIndex(); if (index != lastIndex) { boolean isAdjusting = e.getValueIsAdjusting(); if (!isAdjusting) { if (lastIndex > -1) { String tag1 = fieldList.getModel().getElementAt(lastIndex); markedTextStore.setStyleForTag(tag1, "used", document); } String tag2 = fieldList.getModel().getElementAt(index); markedTextStore.setStyleForTag(tag2, "marked", document); lastIndex = index; } } } } // simple JList Renderer // based on : Advanced JList Programming at developers.sun.com private class SimpleCellRenderer extends DefaultListCellRenderer { private final Font baseFont; private final Font usedFont; private final Icon okIcon = IconTheme.JabRefIcon.PLAIN_TEXT_IMPORT_DONE.getSmallIcon(); private final Icon needIcon = IconTheme.JabRefIcon.PLAIN_TEXT_IMPORT_TODO.getSmallIcon(); private final Color requiredColor = Globals.prefs.getColor(JabRefPreferences.TABLE_REQ_FIELD_BACKGROUND); private final Color optionalColor = Globals.prefs.getColor(JabRefPreferences.TABLE_OPT_FIELD_BACKGROUND); public SimpleCellRenderer(Font normFont) { baseFont = normFont; usedFont = baseFont.deriveFont(Font.ITALIC); } /* This is the only method defined by ListCellRenderer. We just * reconfigure the Jlabel each time we're called. */ @Override public Component getListCellRendererComponent(JList<?> list, Object value, // value to display int index, // cell index boolean iss, // is the cell selected boolean chf) // the list and the cell have the focus { /* The DefaultListCellRenderer class will take care of * the JLabels text property, it's foreground and background * colors, and so on. */ super.getListCellRendererComponent(list, value, index, iss, chf); /* We additionally set the JLabels icon property here. */ String s = value.toString(); if (entry.hasField(s)) { this.setForeground(Color.gray); this.setFont(usedFont); this.setIcon(okIcon); this.setToolTipText(Localization.lang("Filled")); } else { this.setIcon(needIcon); this.setToolTipText(Localization.lang("Field is missing")); } if (requiredFields.contains(s)) { this.setBackground(requiredColor); } else if (optionalFields.contains(s)) { this.setBackground(optionalColor); } return this; } } private class FieldListMouseListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { insertTextForTag(override.isSelected()); } } } private class MenuTextForTagAction extends AbstractAction { private final String field; private final Boolean overrideField; public MenuTextForTagAction(String field, Boolean overrideField) { super(field); this.field = field; this.overrideField = overrideField; } @Override public void actionPerformed(ActionEvent e) { // To enable correct marking of used values fieldList.setSelectedValue(field, false); insertTextForTag(overrideField); } } } class PopupListener extends MouseAdapter { private final JPopupMenu popMenu; public PopupListener(JPopupMenu menu) { popMenu = menu; } @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popMenu.show(e.getComponent(), e.getX(), e.getY()); } } } abstract class BasicAction extends AbstractAction { public BasicAction(String text, String description, Icon icon) { super(text, icon); putValue(Action.SHORT_DESCRIPTION, description); } public BasicAction(String text) { super(text); } @Override public abstract void actionPerformed(ActionEvent e); }