package org.jabref.gui; import java.awt.BorderLayout; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TimerTask; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javafx.application.Platform; import org.jabref.Globals; import org.jabref.JabRefExecutorService; import org.jabref.collab.ChangeScanner; import org.jabref.collab.FileUpdateListener; import org.jabref.collab.FileUpdatePanel; import org.jabref.gui.actions.Actions; import org.jabref.gui.actions.BaseAction; import org.jabref.gui.actions.CleanupAction; import org.jabref.gui.actions.CopyBibTeXKeyAndLinkAction; import org.jabref.gui.bibtexkeypattern.SearchFixDuplicateLabels; import org.jabref.gui.contentselector.ContentSelectorDialog; import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.entryeditor.EntryEditor; import org.jabref.gui.exporter.ExportToClipboardAction; import org.jabref.gui.exporter.SaveDatabaseAction; import org.jabref.gui.externalfiles.FindFullTextAction; import org.jabref.gui.externalfiles.SynchronizeFileField; import org.jabref.gui.externalfiles.WriteXMPAction; import org.jabref.gui.externalfiletype.ExternalFileMenuItem; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.fieldeditors.FieldEditor; import org.jabref.gui.filelist.AttachFileAction; import org.jabref.gui.filelist.FileListEntry; import org.jabref.gui.filelist.FileListTableModel; import org.jabref.gui.groups.GroupAddRemoveDialog; import org.jabref.gui.groups.GroupSelector; import org.jabref.gui.groups.GroupTreeNodeViewModel; import org.jabref.gui.importer.actions.AppendDatabaseAction; import org.jabref.gui.journals.AbbreviateAction; import org.jabref.gui.journals.UnabbreviateAction; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.maintable.MainTable; import org.jabref.gui.maintable.MainTableDataModel; import org.jabref.gui.maintable.MainTableFormat; import org.jabref.gui.maintable.MainTableSelectionListener; import org.jabref.gui.mergeentries.MergeEntriesDialog; import org.jabref.gui.mergeentries.MergeWithFetchedEntryAction; import org.jabref.gui.plaintextimport.TextInputDialog; import org.jabref.gui.specialfields.SpecialFieldDatabaseChangeListener; import org.jabref.gui.specialfields.SpecialFieldValueViewModel; import org.jabref.gui.specialfields.SpecialFieldViewModel; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.UndoableChangeType; import org.jabref.gui.undo.UndoableFieldChange; import org.jabref.gui.undo.UndoableInsertEntry; import org.jabref.gui.undo.UndoableKeyChange; import org.jabref.gui.undo.UndoableRemoveEntry; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.component.CheckBoxMessage; import org.jabref.gui.worker.AbstractWorker; import org.jabref.gui.worker.CallBack; import org.jabref.gui.worker.CitationStyleToClipboardWorker; import org.jabref.gui.worker.MarkEntriesAction; import org.jabref.gui.worker.SendAsEMailAction; import org.jabref.logic.autocompleter.AutoCompletePreferences; import org.jabref.logic.autocompleter.AutoCompleter; import org.jabref.logic.autocompleter.AutoCompleterFactory; import org.jabref.logic.autocompleter.ContentAutoCompleters; import org.jabref.logic.bibtexkeypattern.BibtexKeyPatternUtil; import org.jabref.logic.citationstyle.CitationStyleCache; import org.jabref.logic.citationstyle.CitationStyleOutputFormat; import org.jabref.logic.exporter.BibtexDatabaseWriter; import org.jabref.logic.exporter.FileSaveSession; import org.jabref.logic.exporter.SaveException; import org.jabref.logic.exporter.SavePreferences; import org.jabref.logic.exporter.SaveSession; import org.jabref.logic.l10n.Encodings; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.Layout; import org.jabref.logic.layout.LayoutHelper; import org.jabref.logic.pdf.FileAnnotationCache; import org.jabref.logic.search.SearchQuery; import org.jabref.logic.util.FileExtensions; import org.jabref.logic.util.UpdateField; import org.jabref.logic.util.io.FileBasedLock; import org.jabref.logic.util.io.FileFinder; import org.jabref.logic.util.io.FileFinders; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.FieldChange; import org.jabref.model.bibtexkeypattern.AbstractBibtexKeyPattern; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.DatabaseLocation; import org.jabref.model.database.KeyCollisionException; import org.jabref.model.database.event.EntryAddedEvent; import org.jabref.model.database.event.EntryRemovedEvent; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.EntryType; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.event.EntryChangedEvent; import org.jabref.model.entry.event.EntryEventSource; import org.jabref.model.entry.specialfields.SpecialField; import org.jabref.model.entry.specialfields.SpecialFieldValue; import org.jabref.preferences.JabRefPreferences; import org.jabref.preferences.PreviewPreferences; import org.jabref.shared.DBMSSynchronizer; import com.google.common.eventbus.Subscribe; 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 BasePanel extends JPanel implements ClipboardOwner, FileUpdateListener { private static final Log LOGGER = LogFactory.getLog(BasePanel.class); // Divider size for BaseFrame split pane. 0 means non-resizable. private static final int SPLIT_PANE_DIVIDER_SIZE = 4; private final BibDatabaseContext bibDatabaseContext; private final MainTableDataModel tableModel; private final CitationStyleCache citationStyleCache; private final FileAnnotationCache annotationCache; private final JabRefFrame frame; // The undo manager. private final UndoAction undoAction = new UndoAction(); private final RedoAction redoAction = new RedoAction(); private final CountingUndoManager undoManager = new CountingUndoManager(); private final List<BibEntry> previousEntries = new ArrayList<>(); private final List<BibEntry> nextEntries = new ArrayList<>(); // Keeps track of the string dialog if it is open. private final Map<String, Object> actions = new HashMap<>(); private final SidePaneManager sidePaneManager; // To contain instantiated entry editors. This is to save time // As most enums, this must not be null private BasePanelMode mode = BasePanelMode.SHOWING_NOTHING; private EntryEditor currentEditor; private MainTableSelectionListener selectionListener; private JSplitPane splitPane; private String fileMonitorHandle; private boolean saving; private boolean updatedExternally; // AutoCompleter used in the search bar private AutoCompleter<String> searchAutoCompleter; private boolean baseChanged; private boolean nonUndoableChange; // Used to track whether the base has changed since last save. private MainTable mainTable; private MainTableFormat tableFormat; private BibEntry showing; // Variable to prevent erroneous update of back/forward histories at the time // when a Back or Forward operation is being processed: private boolean backOrForwardInProgress; // in switching between entries. private PreambleEditor preambleEditor; // Keeps track of the preamble dialog if it is open. private StringDialog stringDialog; private ContentAutoCompleters autoCompleters; /** the query the user searches when this basepanel is active */ private Optional<SearchQuery> currentSearchQuery = Optional.empty(); public BasePanel(JabRefFrame frame, BibDatabaseContext bibDatabaseContext) { Objects.requireNonNull(frame); Objects.requireNonNull(bibDatabaseContext); this.bibDatabaseContext = bibDatabaseContext; this.sidePaneManager = frame.getSidePaneManager(); this.frame = frame; this.tableModel = new MainTableDataModel(getBibDatabaseContext()); citationStyleCache = new CitationStyleCache(bibDatabaseContext); annotationCache = new FileAnnotationCache(bibDatabaseContext); setupMainPanel(); setupActions(); this.getDatabase().registerListener(new SearchListener()); // ensure that at each addition of a new entry, the entry is added to the groups interface this.bibDatabaseContext.getDatabase().registerListener(new GroupTreeListener()); Optional<File> file = bibDatabaseContext.getDatabaseFile(); if (file.isPresent()) { // Register so we get notifications about outside changes to the file. try { fileMonitorHandle = Globals.getFileUpdateMonitor().addUpdateListener(this, file.get()); } catch (IOException ex) { LOGGER.warn("Could not register FileUpdateMonitor", ex); } } else { if (bibDatabaseContext.getDatabase().hasEntries()) { // if the database is not empty and no file is assigned, // the database came from an import and has to be treated somehow // -> mark as changed this.baseChanged = true; } } } public static void runWorker(AbstractWorker worker) throws Exception { // This part uses Spin's features: Runnable wrk = worker.getWorker(); // The Worker returned by getWorker() has been wrapped // by Spin.off(), which makes its methods be run in // a different thread from the EDT. CallBack clb = worker.getCallBack(); worker.init(); // This method runs in this same thread, the EDT. // Useful for initial GUI actions, like printing a message. // The CallBack returned by getCallBack() has been wrapped // by Spin.over(), which makes its methods be run on // the EDT. wrk.run(); // Runs the potentially time-consuming action // without freezing the GUI. The magic is that THIS line // of execution will not continue until run() is finished. clb.update(); // Runs the update() method on the EDT. } // Returns a collection of AutoCompleters, which are populated from the current library public ContentAutoCompleters getAutoCompleters() { return autoCompleters; } public String getTabTitle() { StringBuilder title = new StringBuilder(); DatabaseLocation databaseLocation = this.bibDatabaseContext.getLocation(); boolean isAutosaveEnabled = Globals.prefs.getBoolean(JabRefPreferences.LOCAL_AUTO_SAVE); if (databaseLocation == DatabaseLocation.LOCAL) { if (this.bibDatabaseContext.getDatabaseFile().isPresent()) { // check if file is modified String changeFlag = isModified() && !isAutosaveEnabled ? "*" : ""; title.append(this.bibDatabaseContext.getDatabaseFile().get().getName()).append(changeFlag); } else { title.append(GUIGlobals.UNTITLED_TITLE); if (getDatabase().hasEntries()) { // if the database is not empty and no file is assigned, // the database came from an import and has to be treated somehow // -> mark as changed // This also happens internally at basepanel to ensure consistency line 224 title.append('*'); } } } else if (databaseLocation == DatabaseLocation.SHARED) { title.append( this.bibDatabaseContext.getDBMSSynchronizer().getDBName() + " [" + Localization.lang("shared") + "]"); } return title.toString(); } public boolean isModified() { return baseChanged; } public BasePanelMode getMode() { return mode; } public void setMode(BasePanelMode mode) { this.mode = mode; } public JabRefFrame frame() { return frame; } public void output(String s) { frame.output(s); } private void setupActions() { SaveDatabaseAction saveAction = new SaveDatabaseAction(this); CleanupAction cleanUpAction = new CleanupAction(this, Globals.prefs); actions.put(Actions.UNDO, undoAction); actions.put(Actions.REDO, redoAction); actions.put(Actions.FOCUS_TABLE, (BaseAction) () -> { mainTable.requestFocus(); }); // The action for opening an entry editor. actions.put(Actions.EDIT, (BaseAction) selectionListener::editSignalled); // The action for saving a database. actions.put(Actions.SAVE, saveAction); actions.put(Actions.SAVE_AS, (BaseAction) saveAction::saveAs); actions.put(Actions.SAVE_SELECTED_AS, new SaveSelectedAction(SavePreferences.DatabaseSaveType.ALL)); actions.put(Actions.SAVE_SELECTED_AS_PLAIN, new SaveSelectedAction(SavePreferences.DatabaseSaveType.PLAIN_BIBTEX)); // The action for copying selected entries. actions.put(Actions.COPY, (BaseAction) () -> copy()); actions.put(Actions.PRINT_PREVIEW, new PrintPreviewAction()); actions.put(Actions.CUT, (BaseAction) this::cut); //when you modify this action be sure to adjust Actions.CUT, //they are the same except of the Localization, delete confirmation and Actions.COPY call actions.put(Actions.DELETE, (BaseAction) () -> delete(false)); // The action for pasting entries or cell contents. // - more robust detection of available content flavors (doesn't only look at first one offered) // - support for parsing string-flavor clipboard contents which are bibtex entries. // This allows you to (a) paste entire bibtex entries from a text editor, web browser, etc // (b) copy and paste entries between multiple instances of JabRef (since // only the text representation seems to get as far as the X clipboard, at least on my system) actions.put(Actions.PASTE, (BaseAction) () -> paste()); actions.put(Actions.SELECT_ALL, (BaseAction) mainTable::selectAll); // The action for opening the preamble editor actions.put(Actions.EDIT_PREAMBLE, (BaseAction) () -> { if (preambleEditor == null) { PreambleEditor form = new PreambleEditor(frame, BasePanel.this, bibDatabaseContext.getDatabase()); form.setLocationRelativeTo(frame); form.setVisible(true); preambleEditor = form; } else { preambleEditor.setVisible(true); } }); // The action for opening the string editor actions.put(Actions.EDIT_STRINGS, (BaseAction) () -> { if (stringDialog == null) { StringDialog form = new StringDialog(frame, BasePanel.this, bibDatabaseContext.getDatabase()); form.setVisible(true); stringDialog = form; } else { stringDialog.setVisible(true); } }); actions.put(FindUnlinkedFilesDialog.ACTION_COMMAND, (BaseAction) () -> { final FindUnlinkedFilesDialog dialog = new FindUnlinkedFilesDialog(frame, frame, BasePanel.this); dialog.setLocationRelativeTo(frame); dialog.setVisible(true); }); // The action for auto-generating keys. actions.put(Actions.MAKE_KEY, new AbstractWorker() { List<BibEntry> entries; int numSelected; boolean canceled; // Run first, in EDT: @Override public void init() { entries = getSelectedEntries(); numSelected = entries.size(); if (entries.isEmpty()) { // None selected. Inform the user to select entries first. JOptionPane.showMessageDialog(frame, Localization.lang("First select the entries you want keys to be generated for."), Localization.lang("Autogenerate BibTeX keys"), JOptionPane.INFORMATION_MESSAGE); return; } frame.block(); output(formatOutputMessage(Localization.lang("Generating BibTeX key for"), numSelected)); } // Run second, on a different thread: @Override public void run() { // We don't want to generate keys for entries which already have one thus remove the entries if (Globals.prefs.getBoolean(JabRefPreferences.AVOID_OVERWRITING_KEY)) { entries.removeIf(BibEntry::hasCiteKey); // if we're going to override some cite keys warn the user about it } else if (Globals.prefs.getBoolean(JabRefPreferences.WARN_BEFORE_OVERWRITING_KEY)) { if (entries.parallelStream().anyMatch(BibEntry::hasCiteKey)) { CheckBoxMessage cbm = new CheckBoxMessage( Localization.lang("One or more keys will be overwritten. Continue?"), Localization.lang("Disable this confirmation dialog"), false); final int answer = JOptionPane.showConfirmDialog(frame, cbm, Localization.lang("Overwrite keys"), JOptionPane.YES_NO_OPTION); Globals.prefs.putBoolean(JabRefPreferences.WARN_BEFORE_OVERWRITING_KEY, !cbm.isSelected()); // The user doesn't want to overide cite keys if (answer == JOptionPane.NO_OPTION) { canceled = true; return; } } } // generate the new cite keys for each entry final NamedCompound ce = new NamedCompound(Localization.lang("Autogenerate BibTeX keys")); AbstractBibtexKeyPattern citeKeyPattern = bibDatabaseContext.getMetaData() .getCiteKeyPattern(Globals.prefs.getBibtexKeyPatternPreferences().getKeyPattern()); for (BibEntry entry : entries) { String oldCiteKey = entry.getCiteKeyOptional().orElse(""); BibtexKeyPatternUtil.makeAndSetLabel(citeKeyPattern, bibDatabaseContext.getDatabase(), entry, Globals.prefs.getBibtexKeyPatternPreferences()); String newCiteKey = entry.getCiteKeyOptional().orElse(""); if (!oldCiteKey.equals(newCiteKey)) { ce.addEdit(new UndoableKeyChange(entry, oldCiteKey, newCiteKey)); } } ce.end(); // register the undo event only if new cite keys were generated if (ce.hasEdits()) { getUndoManager().addEdit(ce); } } // Run third, on EDT: @Override public void update() { if (canceled) { frame.unblock(); return; } markBaseChanged(); numSelected = entries.size(); //////////////////////////////////////////////////////////////////////////////// // Prevent selection loss for autogenerated BibTeX-Keys //////////////////////////////////////////////////////////////////////////////// for (final BibEntry bibEntry : entries) { SwingUtilities.invokeLater(() -> { final int row = mainTable.findEntry(bibEntry); if ((row >= 0) && (mainTable.getSelectedRowCount() < entries.size())) { mainTable.addRowSelectionInterval(row, row); } }); } //////////////////////////////////////////////////////////////////////////////// output(formatOutputMessage(Localization.lang("Generated BibTeX key for"), numSelected)); frame.unblock(); } }); // The action for cleaning up entry. actions.put(Actions.CLEANUP, cleanUpAction); actions.put(Actions.MERGE_ENTRIES, (BaseAction) () -> new MergeEntriesDialog(BasePanel.this)); actions.put(Actions.SEARCH, (BaseAction) frame.getGlobalSearchBar()::focus); actions.put(Actions.GLOBAL_SEARCH, (BaseAction) frame.getGlobalSearchBar()::performGlobalSearch); // The action for copying the selected entry's key. actions.put(Actions.COPY_KEY, (BaseAction) () -> copyKey()); // The action for copying the selected entry's title. actions.put(Actions.COPY_TITLE, (BaseAction) () -> copyTitle()); // The action for copying a cite for the selected entry. actions.put(Actions.COPY_CITE_KEY, (BaseAction) () -> copyCiteKey()); // The action for copying the BibTeX key and the title for the first selected entry actions.put(Actions.COPY_KEY_AND_TITLE, (BaseAction) () -> copyKeyAndTitle()); actions.put(Actions.COPY_CITATION_ASCII_DOC, (BaseAction) () -> copyCitationToClipboard(CitationStyleOutputFormat.ASCII_DOC)); actions.put(Actions.COPY_CITATION_XSLFO, (BaseAction) () -> copyCitationToClipboard(CitationStyleOutputFormat.XSL_FO)); actions.put(Actions.COPY_CITATION_HTML, (BaseAction) () -> copyCitationToClipboard(CitationStyleOutputFormat.HTML)); actions.put(Actions.COPY_CITATION_RTF, (BaseAction) () -> copyCitationToClipboard(CitationStyleOutputFormat.RTF)); actions.put(Actions.COPY_CITATION_TEXT, (BaseAction) () -> copyCitationToClipboard(CitationStyleOutputFormat.TEXT)); // The action for copying the BibTeX keys as hyperlinks to the urls of the selected entries actions.put(Actions.COPY_KEY_AND_LINK, new CopyBibTeXKeyAndLinkAction(mainTable)); actions.put(Actions.MERGE_DATABASE, new AppendDatabaseAction(frame, this)); actions.put(Actions.ADD_FILE_LINK, new AttachFileAction(this)); actions.put(Actions.OPEN_EXTERNAL_FILE, (BaseAction) () -> openExternalFile()); actions.put(Actions.OPEN_FOLDER, (BaseAction) () -> JabRefExecutorService.INSTANCE.execute(() -> { final List<Path> files = FileUtil.getListOfLinkedFiles(mainTable.getSelectedEntries(), bibDatabaseContext.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences())); for (final Path f : files) { try { JabRefDesktop.openFolderAndSelectFile(f.toAbsolutePath()); } catch (IOException e) { LOGGER.info("Could not open folder", e); } } })); actions.put(Actions.OPEN_CONSOLE, (BaseAction) () -> JabRefDesktop .openConsole(frame.getCurrentBasePanel().getBibDatabaseContext().getDatabaseFile().orElse(null))); actions.put(Actions.PULL_CHANGES_FROM_SHARED_DATABASE, (BaseAction) () -> { DBMSSynchronizer dbmsSynchronizer = frame.getCurrentBasePanel().getBibDatabaseContext() .getDBMSSynchronizer(); dbmsSynchronizer.pullChanges(); }); actions.put(Actions.OPEN_URL, new OpenURLAction()); actions.put(Actions.MERGE_WITH_FETCHED_ENTRY, new MergeWithFetchedEntryAction(this)); actions.put(Actions.REPLACE_ALL, (BaseAction) () -> { final ReplaceStringDialog rsd = new ReplaceStringDialog(frame); rsd.setVisible(true); if (!rsd.okPressed()) { return; } int counter = 0; final NamedCompound ce = new NamedCompound(Localization.lang("Replace string")); if (rsd.selOnly()) { for (BibEntry be : mainTable.getSelectedEntries()) { counter += rsd.replace(be, ce); } } else { for (BibEntry entry : bibDatabaseContext.getDatabase().getEntries()) { counter += rsd.replace(entry, ce); } } output(Localization.lang("Replaced") + ' ' + counter + ' ' + (counter == 1 ? Localization.lang("occurrence") : Localization.lang("occurrences")) + '.'); if (counter > 0) { ce.end(); getUndoManager().addEdit(ce); markBaseChanged(); } }); actions.put(Actions.DUPLI_CHECK, (BaseAction) () -> JabRefExecutorService.INSTANCE.execute(new DuplicateSearch(BasePanel.this))); actions.put(Actions.PLAIN_TEXT_IMPORT, (BaseAction) () -> { // get Type of new entry EntryTypeDialog etd = new EntryTypeDialog(frame); etd.setLocationRelativeTo(BasePanel.this); etd.setVisible(true); EntryType tp = etd.getChoice(); if (tp == null) { return; } BibEntry bibEntry = new BibEntry(tp.getName()); TextInputDialog tidialog = new TextInputDialog(frame, bibEntry); tidialog.setLocationRelativeTo(BasePanel.this); tidialog.setVisible(true); if (tidialog.okPressed()) { UpdateField.setAutomaticFields(Collections.singletonList(bibEntry), false, false, Globals.prefs.getUpdateFieldPreferences()); insertEntry(bibEntry); } }); actions.put(Actions.MARK_ENTRIES, new MarkEntriesAction(frame, 0)); actions.put(Actions.UNMARK_ENTRIES, (BaseAction) () -> { try { List<BibEntry> bes = mainTable.getSelectedEntries(); if (bes.isEmpty()) { output(Localization.lang("This operation requires one or more entries to be selected.")); return; } NamedCompound ce = new NamedCompound(Localization.lang("Unmark entries")); for (BibEntry be : bes) { EntryMarker.unmarkEntry(be, false, bibDatabaseContext.getDatabase(), ce); } ce.end(); getUndoManager().addEdit(ce); markBaseChanged(); String outputStr; if (bes.size() == 1) { outputStr = Localization.lang("Unmarked selected entry"); } else { outputStr = Localization.lang("Unmarked all %0 selected entries", Integer.toString(bes.size())); } output(outputStr); } catch (Throwable ex) { LOGGER.warn("Could not unmark", ex); } }); actions.put(Actions.UNMARK_ALL, (BaseAction) () -> { NamedCompound ce = new NamedCompound(Localization.lang("Unmark all")); for (BibEntry be : bibDatabaseContext.getDatabase().getEntries()) { EntryMarker.unmarkEntry(be, false, bibDatabaseContext.getDatabase(), ce); } ce.end(); getUndoManager().addEdit(ce); markBaseChanged(); output(Localization.lang("Unmarked all entries")); }); // Note that we can't put the number of entries that have been reverted into the undoText as the concrete number cannot be injected actions.put(new SpecialFieldValueViewModel(SpecialField.RELEVANCE.getValues().get(0)).getActionName(), new SpecialFieldViewModel(SpecialField.RELEVANCE).getSpecialFieldAction( SpecialField.RELEVANCE.getValues().get(0), frame)); actions.put(new SpecialFieldValueViewModel(SpecialField.QUALITY.getValues().get(0)).getActionName(), new SpecialFieldViewModel(SpecialField.QUALITY) .getSpecialFieldAction(SpecialField.QUALITY.getValues().get(0), frame)); actions.put(new SpecialFieldValueViewModel(SpecialField.PRINTED.getValues().get(0)).getActionName(), new SpecialFieldViewModel(SpecialField.PRINTED).getSpecialFieldAction( SpecialField.PRINTED.getValues().get(0), frame)); for (SpecialFieldValue prio : SpecialField.PRIORITY.getValues()) { actions.put(new SpecialFieldValueViewModel(prio).getActionName(), new SpecialFieldViewModel(SpecialField.PRIORITY).getSpecialFieldAction(prio, this.frame)); } for (SpecialFieldValue rank : SpecialField.RANKING.getValues()) { actions.put(new SpecialFieldValueViewModel(rank).getActionName(), new SpecialFieldViewModel(SpecialField.RANKING).getSpecialFieldAction(rank, this.frame)); } for (SpecialFieldValue status : SpecialField.READ_STATUS.getValues()) { actions.put(new SpecialFieldValueViewModel(status).getActionName(), new SpecialFieldViewModel(SpecialField.READ_STATUS).getSpecialFieldAction(status, this.frame)); } actions.put(Actions.TOGGLE_PREVIEW, (BaseAction) () -> { PreviewPreferences previewPreferences = Globals.prefs.getPreviewPreferences(); boolean enabled = !previewPreferences.isPreviewPanelEnabled(); PreviewPreferences newPreviewPreferences = previewPreferences .getBuilder() .withPreviewPanelEnabled(enabled) .build(); Globals.prefs.storePreviewPreferences(newPreviewPreferences); setPreviewActiveBasePanels(enabled); frame.setPreviewToggle(enabled); }); actions.put(Actions.NEXT_PREVIEW_STYLE, (BaseAction) selectionListener::nextPreviewStyle); actions.put(Actions.PREVIOUS_PREVIEW_STYLE, (BaseAction) selectionListener::previousPreviewStyle); actions.put(Actions.MANAGE_SELECTORS, (BaseAction) () -> { ContentSelectorDialog csd = new ContentSelectorDialog(frame, frame, BasePanel.this, false, null); csd.setLocationRelativeTo(frame); csd.setVisible(true); }); actions.put(Actions.EXPORT_TO_CLIPBOARD, new ExportToClipboardAction(frame)); actions.put(Actions.SEND_AS_EMAIL, new SendAsEMailAction(frame)); actions.put(Actions.WRITE_XMP, new WriteXMPAction(this)); actions.put(Actions.ABBREVIATE_ISO, new AbbreviateAction(this, true)); actions.put(Actions.ABBREVIATE_MEDLINE, new AbbreviateAction(this, false)); actions.put(Actions.UNABBREVIATE, new UnabbreviateAction(this)); actions.put(Actions.AUTO_SET_FILE, new SynchronizeFileField(this)); actions.put(Actions.BACK, (BaseAction) BasePanel.this::back); actions.put(Actions.FORWARD, (BaseAction) BasePanel.this::forward); actions.put(Actions.RESOLVE_DUPLICATE_KEYS, new SearchFixDuplicateLabels(this)); actions.put(Actions.ADD_TO_GROUP, new GroupAddRemoveDialog(this, true, false)); actions.put(Actions.REMOVE_FROM_GROUP, new GroupAddRemoveDialog(this, false, false)); actions.put(Actions.MOVE_TO_GROUP, new GroupAddRemoveDialog(this, true, true)); actions.put(Actions.DOWNLOAD_FULL_TEXT, new FindFullTextAction(this)); } /** * Generates and copies citations based on the selected entries to the clipboard * @param outputFormat the desired {@link CitationStyleOutputFormat} */ private void copyCitationToClipboard(CitationStyleOutputFormat outputFormat) { new CitationStyleToClipboardWorker(this, outputFormat).execute(); } private void copy() { List<BibEntry> bes = mainTable.getSelectedEntries(); if (bes.isEmpty()) { // The user maybe selected a single cell. // TODO: Check if this can actually happen int[] rows = mainTable.getSelectedRows(); int[] cols = mainTable.getSelectedColumns(); if ((cols.length == 1) && (rows.length == 1)) { // Copy single value. Object o = mainTable.getValueAt(rows[0], cols[0]); if (o != null) { StringSelection ss = new StringSelection(o.toString()); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, BasePanel.this); output(Localization.lang("Copied cell contents") + '.'); } } } else { TransferableBibtexEntry trbe = new TransferableBibtexEntry(bes); // ! look at ClipBoardManager Toolkit.getDefaultToolkit().getSystemClipboard().setContents(trbe, BasePanel.this); output(formatOutputMessage(Localization.lang("Copied"), bes.size())); } } private void cut() { runCommand(Actions.COPY); // cannot call runCommand(Actions.DELETE), b/c it will call delete(false) with the wrong parameter delete(true); } /** * Removes the selected entries from the database * @param cut If false the user will get asked if he really wants to delete the entries, and it will be localized * as "deleted". * If true the action will be localized as "cut" */ private void delete(boolean cut) { List<BibEntry> entries = mainTable.getSelectedEntries(); if (entries.isEmpty()) { return; } if (!cut && !showDeleteConfirmationDialog(entries.size())) { return; } // select the next entry to stay at the same place as before (or the previous if we're already at the end) if (mainTable.getSelectedRow() != (mainTable.getRowCount() - 1)) { selectNextEntry(); } else { selectPreviousEntry(); } NamedCompound compound; if (cut) { compound = new NamedCompound( (entries.size() > 1 ? Localization.lang("cut entries") : Localization.lang("cut entry"))); } else { compound = new NamedCompound( (entries.size() > 1 ? Localization.lang("delete entries") : Localization.lang("delete entry"))); } for (BibEntry entry : entries) { compound.addEdit(new UndoableRemoveEntry(bibDatabaseContext.getDatabase(), entry, BasePanel.this)); bibDatabaseContext.getDatabase().removeEntry(entry); ensureNotShowingBottomPanel(entry); } compound.end(); getUndoManager().addEdit(compound); markBaseChanged(); frame.output( formatOutputMessage(cut ? Localization.lang("Cut") : Localization.lang("Deleted"), entries.size())); // prevent the main table from loosing focus mainTable.requestFocus(); } private void paste() { Collection<BibEntry> bes = new ClipBoardManager().extractBibEntriesFromClipboard(); // finally we paste in the entries (if any), which either came from TransferableBibtexEntries // or were parsed from a string if (!bes.isEmpty()) { NamedCompound ce = new NamedCompound( (bes.size() > 1 ? Localization.lang("paste entries") : Localization.lang("paste entry"))); // Store the first inserted bibtexentry. // bes[0] does not work as bes[0] is first clonded, // then inserted. // This entry is used to open up an entry editor // for the first inserted entry. BibEntry firstBE = null; for (BibEntry be1 : bes) { BibEntry be = (BibEntry) be1.clone(); if (firstBE == null) { firstBE = be; } UpdateField.setAutomaticFields(be, Globals.prefs.getUpdateFieldPreferences()); // We have to clone the // entries, since the pasted // entries must exist // independently of the copied // ones. bibDatabaseContext.getDatabase().insertEntry(be); ce.addEdit(new UndoableInsertEntry(bibDatabaseContext.getDatabase(), be, BasePanel.this)); } ce.end(); getUndoManager().addEdit(ce); output(formatOutputMessage(Localization.lang("Pasted"), bes.size())); markBaseChanged(); highlightEntry(firstBE); mainTable.requestFocus(); if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_OPEN_FORM)) { selectionListener.editSignalled(firstBE); } } } private void copyTitle() { List<BibEntry> selectedBibEntries = mainTable.getSelectedEntries(); if (!selectedBibEntries.isEmpty()) { storeCurrentEdit(); // Collect all non-null titles. List<String> titles = selectedBibEntries.stream() .filter(bibEntry -> bibEntry.getTitle().isPresent()) .map(bibEntry -> bibEntry.getTitle().get()) .collect(Collectors.toList()); if (titles.isEmpty()) { output(Localization.lang("None of the selected entries have titles.")); return; } StringSelection ss = new StringSelection(String.join("\n", titles)); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, BasePanel.this); if (titles.size() == selectedBibEntries.size()) { // All entries had titles. output((selectedBibEntries.size() > 1 ? Localization.lang("Copied titles") : Localization .lang("Copied title")) + '.'); } else { output(Localization.lang("Warning: %0 out of %1 entries have undefined title.", Integer.toString(selectedBibEntries.size() - titles.size()), Integer.toString(selectedBibEntries.size()))); } } } private void copyCiteKey() { List<BibEntry> bes = mainTable.getSelectedEntries(); if (!bes.isEmpty()) { storeCurrentEdit(); List<String> keys = new ArrayList<>(bes.size()); // Collect all non-null keys. for (BibEntry be : bes) { be.getCiteKeyOptional().ifPresent(keys::add); } if (keys.isEmpty()) { output(Localization.lang("None of the selected entries have BibTeX keys.")); return; } String sb = String.join(",", keys); StringSelection ss = new StringSelection("\\cite{" + sb + '}'); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, BasePanel.this); if (keys.size() == bes.size()) { // All entries had keys. output(bes.size() > 1 ? Localization.lang("Copied keys") : Localization.lang("Copied key") + '.'); } else { output(Localization.lang("Warning: %0 out of %1 entries have undefined BibTeX key.", Integer.toString(bes.size() - keys.size()), Integer.toString(bes.size()))); } } } private void copyKey() { List<BibEntry> bes = mainTable.getSelectedEntries(); if (!bes.isEmpty()) { storeCurrentEdit(); List<String> keys = new ArrayList<>(bes.size()); // Collect all non-null keys. for (BibEntry be : bes) { be.getCiteKeyOptional().ifPresent(keys::add); } if (keys.isEmpty()) { output(Localization.lang("None of the selected entries have BibTeX keys.")); return; } StringSelection ss = new StringSelection(String.join(",", keys)); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, BasePanel.this); if (keys.size() == bes.size()) { // All entries had keys. output((bes.size() > 1 ? Localization.lang("Copied keys") : Localization.lang("Copied key")) + '.'); } else { output(Localization.lang("Warning: %0 out of %1 entries have undefined BibTeX key.", Integer.toString(bes.size() - keys.size()), Integer.toString(bes.size()))); } } } private void copyKeyAndTitle() { List<BibEntry> bes = mainTable.getSelectedEntries(); if (!bes.isEmpty()) { storeCurrentEdit(); // OK: in a future version, this string should be configurable to allow arbitrary exports StringReader sr = new StringReader( "\\bibtexkey - \\begin{title}\\format[RemoveBrackets]{\\title}\\end{title}\n"); Layout layout; try { layout = new LayoutHelper(sr, Globals.prefs.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader)) .getLayoutFromText(); } catch (IOException e) { LOGGER.info("Could not get layout", e); return; } StringBuilder sb = new StringBuilder(); int copied = 0; // Collect all non-null keys. for (BibEntry be : bes) { if (be.hasCiteKey()) { copied++; sb.append(layout.doLayout(be, bibDatabaseContext.getDatabase())); } } if (copied == 0) { output(Localization.lang("None of the selected entries have BibTeX keys.")); return; } final StringSelection ss = new StringSelection(sb.toString()); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, BasePanel.this); if (copied == bes.size()) { // All entries had keys. output((bes.size() > 1 ? Localization.lang("Copied keys") : Localization.lang("Copied key")) + '.'); } else { output(Localization.lang("Warning: %0 out of %1 entries have undefined BibTeX key.", Integer.toString(bes.size() - copied), Integer.toString(bes.size()))); } } } private void openExternalFile() { JabRefExecutorService.INSTANCE.execute(() -> { final List<BibEntry> bes = mainTable.getSelectedEntries(); if (bes.size() != 1) { output(Localization.lang("This operation requires exactly one item to be selected.")); return; } final BibEntry entry = bes.get(0); if (!entry.hasField(FieldName.FILE)) { // no bibtex field new SearchAndOpenFile(entry, BasePanel.this).searchAndOpen(); return; } FileListTableModel fileListTableModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(fileListTableModel::setContent); if (fileListTableModel.getRowCount() == 0) { // content in BibTeX field is not readable new SearchAndOpenFile(entry, BasePanel.this).searchAndOpen(); return; } FileListEntry flEntry = fileListTableModel.getEntry(0); ExternalFileMenuItem item = new ExternalFileMenuItem(frame(), entry, "", flEntry.getLink(), flEntry.getType().get().getIcon(), bibDatabaseContext, flEntry.getType()); item.doClick(); }); } /** * This method is called from JabRefFrame if a database specific action is requested by the user. Runs the command * if it is defined, or prints an error message to the standard error stream. * * @param _command The name of the command to run. */ public void runCommand(final String _command) { if (!actions.containsKey(_command)) { LOGGER.info("No action defined for '" + _command + '\''); return; } Object o = actions.get(_command); try { if (o instanceof BaseAction) { ((BaseAction) o).action(); } else { runWorker((AbstractWorker) o); } } catch (Throwable ex) { // If the action has blocked the JabRefFrame before crashing, we need to unblock it. // The call to unblock will simply hide the glasspane, so there is no harm in calling // it even if the frame hasn't been blocked. frame.unblock(); LOGGER.error("runCommand error: " + ex.getMessage(), ex); } } private boolean saveDatabase(File file, boolean selectedOnly, Charset enc, SavePreferences.DatabaseSaveType saveType) throws SaveException { SaveSession session; frame.block(); final String SAVE_DATABASE = Localization.lang("Save library"); try { SavePreferences prefs = SavePreferences.loadForSaveFromPreferences(Globals.prefs).withEncoding(enc) .withSaveType(saveType); BibtexDatabaseWriter<SaveSession> databaseWriter = new BibtexDatabaseWriter<>( FileSaveSession::new); if (selectedOnly) { session = databaseWriter.savePartOfDatabase(bibDatabaseContext, mainTable.getSelectedEntries(), prefs); } else { session = databaseWriter.saveDatabase(bibDatabaseContext, prefs); } registerUndoableChanges(session); } // FIXME: not sure if this is really thrown anywhere catch (UnsupportedCharsetException ex) { JOptionPane.showMessageDialog(frame, Localization.lang("Could not save file.") + ' ' + Localization.lang("Character encoding '%0' is not supported.", enc.displayName()), SAVE_DATABASE, JOptionPane.ERROR_MESSAGE); throw new SaveException("rt"); } catch (SaveException ex) { if (ex.specificEntry()) { // Error occurred during processing of the entry. Highlight it: highlightEntry(ex.getEntry()); showEntry(ex.getEntry()); } else { LOGGER.warn("Could not save", ex); } JOptionPane.showMessageDialog(frame, Localization.lang("Could not save file.") + "\n" + ex.getMessage(), SAVE_DATABASE, JOptionPane.ERROR_MESSAGE); throw new SaveException("rt"); } finally { frame.unblock(); } boolean commit = true; if (!session.getWriter().couldEncodeAll()) { FormBuilder builder = FormBuilder.create() .layout(new FormLayout("left:pref, 4dlu, fill:pref", "pref, 4dlu, pref")); JTextArea ta = new JTextArea(session.getWriter().getProblemCharacters()); ta.setEditable(false); builder.add(Localization.lang("The chosen encoding '%0' could not encode the following characters:", session.getEncoding().displayName())).xy(1, 1); builder.add(ta).xy(3, 1); builder.add(Localization.lang("What do you want to do?")).xy(1, 3); String tryDiff = Localization.lang("Try different encoding"); int answer = JOptionPane.showOptionDialog(frame, builder.getPanel(), SAVE_DATABASE, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] {Localization.lang("Save"), tryDiff, Localization.lang("Cancel")}, tryDiff); if (answer == JOptionPane.NO_OPTION) { // The user wants to use another encoding. Object choice = JOptionPane.showInputDialog(frame, Localization.lang("Select encoding"), SAVE_DATABASE, JOptionPane.QUESTION_MESSAGE, null, Encodings.ENCODINGS_DISPLAYNAMES, enc); if (choice == null) { commit = false; } else { Charset newEncoding = Charset.forName((String) choice); return saveDatabase(file, selectedOnly, newEncoding, saveType); } } else if (answer == JOptionPane.CANCEL_OPTION) { commit = false; } } if (commit) { session.commit(file.toPath()); this.bibDatabaseContext.getMetaData().setEncoding(enc); // Make sure to remember which encoding we used. } else { session.cancel(); } return commit; } public void registerUndoableChanges(SaveSession session) { NamedCompound ce = new NamedCompound(Localization.lang("Save actions")); for (FieldChange change : session.getFieldChanges()) { ce.addEdit(new UndoableFieldChange(change)); } ce.end(); if (ce.hasEdits()) { getUndoManager().addEdit(ce); } } /** * This method is called from JabRefFrame when the user wants to create a new entry. If the argument is null, the * user is prompted for an entry type. * * @param type The type of the entry to create. * @return The newly created BibEntry or null the operation was canceled by the user. */ public BibEntry newEntry(EntryType type) { EntryType actualType = type; if (actualType == null) { // Find out what type is wanted. final EntryTypeDialog etd = new EntryTypeDialog(frame); // We want to center the dialog, to make it look nicer. etd.setLocationRelativeTo(frame); etd.setVisible(true); actualType = etd.getChoice(); } if (actualType != null) { // Only if the dialog was not canceled. final BibEntry be = new BibEntry(actualType.getName()); try { bibDatabaseContext.getDatabase().insertEntry(be); // Set owner/timestamp if options are enabled: List<BibEntry> list = new ArrayList<>(); list.add(be); UpdateField.setAutomaticFields(list, true, true, Globals.prefs.getUpdateFieldPreferences()); // Create an UndoableInsertEntry object. getUndoManager().addEdit(new UndoableInsertEntry(bibDatabaseContext.getDatabase(), be, BasePanel.this)); output(Localization.lang("Added new '%0' entry.", actualType.getName().toLowerCase(Locale.ROOT))); // We are going to select the new entry. Before that, make sure that we are in // show-entry mode. If we aren't already in that mode, enter the WILL_SHOW_EDITOR // mode which makes sure the selection will trigger display of the entry editor // and adjustment of the splitter. if (mode != BasePanelMode.SHOWING_EDITOR) { mode = BasePanelMode.WILL_SHOW_EDITOR; } highlightEntry(be); // The database just changed. markBaseChanged(); final EntryEditor entryEditor = getEntryEditor(be); this.showEntryEditor(entryEditor); entryEditor.requestFocus(); return be; } catch (KeyCollisionException ex) { LOGGER.info(ex.getMessage(), ex); } } return null; } /** * This method is called from JabRefFrame when the user wants to create a new entry. * * @param bibEntry The new entry. */ public void insertEntry(final BibEntry bibEntry) { if (bibEntry != null) { try { bibDatabaseContext.getDatabase().insertEntry(bibEntry); if (Globals.prefs.getBoolean(JabRefPreferences.USE_OWNER)) { // Set owner field to default value UpdateField.setAutomaticFields(bibEntry, true, true, Globals.prefs.getUpdateFieldPreferences()); } // Create an UndoableInsertEntry object. getUndoManager().addEdit(new UndoableInsertEntry(bibDatabaseContext.getDatabase(), bibEntry, BasePanel.this)); output(Localization.lang("Added new '%0' entry.", bibEntry.getType())); markBaseChanged(); // The database just changed. if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_OPEN_FORM)) { selectionListener.editSignalled(bibEntry); } highlightEntry(bibEntry); } catch (KeyCollisionException ex) { LOGGER.info("Collision for bibtex key" + bibEntry.getId(), ex); } } } public void editEntryByIdAndFocusField(final String entryId, final String fieldName) { final Optional<BibEntry> entry = bibDatabaseContext.getDatabase().getEntryById(entryId); entry.ifPresent(e -> { mainTable.setSelected(mainTable.findEntry(e)); selectionListener.editSignalled(); final EntryEditor editor = getEntryEditor(e); editor.setFocusToField(fieldName); this.showEntryEditor(editor); editor.requestFocus(); }); } public void updateTableFont() { mainTable.updateFont(); } private void createMainTable() { bibDatabaseContext.getDatabase().registerListener(tableModel.getListSynchronizer()); bibDatabaseContext.getDatabase().registerListener(SpecialFieldDatabaseChangeListener.getInstance()); tableFormat = new MainTableFormat(bibDatabaseContext.getDatabase()); tableFormat.updateTableFormat(); mainTable = new MainTable(tableFormat, tableModel, frame, this); selectionListener = new MainTableSelectionListener(this, mainTable); mainTable.updateFont(); mainTable.addSelectionListener(selectionListener); mainTable.addMouseListener(selectionListener); mainTable.addKeyListener(selectionListener); mainTable.addFocusListener(selectionListener); // Add the listener that binds selection to state manager (TODO: should be replaced by proper JavaFX binding as soon as table is implemented in JavaFX) mainTable.addSelectionListener(listEvent -> Platform .runLater(() -> Globals.stateManager.setSelectedEntries(mainTable.getSelectedEntries()))); String clearSearch = "clearSearch"; mainTable.getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.CLEAR_SEARCH), clearSearch); mainTable.getActionMap().put(clearSearch, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { // need to close these here, b/c this action overshadows the responsible actions when the main table is selected switch (mode) { case SHOWING_NOTHING: frame.getGlobalSearchBar().endSearch(); break; case SHOWING_PREVIEW: getPreviewPanel().close(); break; case SHOWING_EDITOR: case WILL_SHOW_EDITOR: getCurrentEditor().close(); break; default: LOGGER.warn("unknown BasePanelMode: '" + mode + "', doing nothing"); break; } } }); mainTable.getActionMap().put(Actions.CUT, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { try { runCommand(Actions.CUT); } catch (Throwable ex) { LOGGER.warn("Could not cut", ex); } } }); mainTable.getActionMap().put(Actions.COPY, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { try { runCommand(Actions.COPY); } catch (Throwable ex) { LOGGER.warn("Could not copy", ex); } } }); mainTable.getActionMap().put(Actions.PASTE, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { try { runCommand(Actions.PASTE); } catch (Throwable ex) { LOGGER.warn("Could not paste", ex); } } }); mainTable.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { final int keyCode = e.getKeyCode(); final TreePath path = frame.getGroupSelector().getSelectionPath(); final GroupTreeNodeViewModel node = path == null ? null : (GroupTreeNodeViewModel) path .getLastPathComponent(); if (e.isControlDown()) { switch (keyCode) { case KeyEvent.VK_PAGE_DOWN: frame.nextTab.actionPerformed(null); e.consume(); break; case KeyEvent.VK_PAGE_UP: frame.prevTab.actionPerformed(null); e.consume(); break; default: break; } } else if (keyCode == KeyEvent.VK_ENTER) { e.consume(); try { runCommand(Actions.EDIT); } catch (Throwable ex) { LOGGER.warn("Could not run action based on key press", ex); } } } }); } public void setupMainPanel() { splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.setDividerSize(SPLIT_PANE_DIVIDER_SIZE); adjustSplitter(); // restore last splitting state (before mainTable is created as creation affects the stored size of the entryEditors) // check whether a mainTable already existed and a floatSearch was active boolean floatSearchActive = (mainTable != null) && (this.tableModel.getSearchState() == MainTableDataModel.DisplayOption.FLOAT); createMainTable(); splitPane.setTopComponent(mainTable.getPane()); // Remove borders splitPane.setBorder(BorderFactory.createEmptyBorder()); setBorder(BorderFactory.createEmptyBorder()); // If an entry is currently being shown, make sure it stays shown, // otherwise set the bottom component to null. if (mode == BasePanelMode.SHOWING_PREVIEW) { mode = BasePanelMode.SHOWING_NOTHING; highlightEntry(selectionListener.getPreview().getEntry()); } else if (mode == BasePanelMode.SHOWING_EDITOR) { mode = BasePanelMode.SHOWING_NOTHING; } else { splitPane.setBottomComponent(null); } setLayout(new BorderLayout()); removeAll(); add(splitPane, BorderLayout.CENTER); // Set up name autocompleter for search: instantiateSearchAutoCompleter(); this.getDatabase().registerListener(new SearchAutoCompleteListener()); AutoCompletePreferences autoCompletePreferences = new AutoCompletePreferences(Globals.prefs); // Set up AutoCompleters for this panel: if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_COMPLETE)) { autoCompleters = new ContentAutoCompleters(getDatabase(), bibDatabaseContext.getMetaData(), autoCompletePreferences, Globals.journalAbbreviationLoader); // ensure that the autocompleters are in sync with entries this.getDatabase().registerListener(new AutoCompleteListener()); } else { // create empty ContentAutoCompleters() if autoCompletion is deactivated autoCompleters = new ContentAutoCompleters(); } // restore floating search result // (needed if preferences have been changed which causes a recreation of the main table) if (floatSearchActive) { mainTable.showFloatSearch(); } splitPane.revalidate(); revalidate(); repaint(); // saves the divider position as soon as it changes splitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, event -> saveDividerLocation()); } public void updateSearchManager() { frame.getGlobalSearchBar().setAutoCompleter(searchAutoCompleter); } private void instantiateSearchAutoCompleter() { AutoCompletePreferences autoCompletePreferences = new AutoCompletePreferences(Globals.prefs); AutoCompleterFactory autoCompleterFactory = new AutoCompleterFactory(autoCompletePreferences, Globals.journalAbbreviationLoader); searchAutoCompleter = autoCompleterFactory.getPersonAutoCompleter(); for (BibEntry entry : bibDatabaseContext.getDatabase().getEntries()) { searchAutoCompleter.addBibtexEntry(entry); } } public void updatePreamble() { if (preambleEditor != null) { preambleEditor.updatePreamble(); } } public void assureStringDialogNotEditing() { if (stringDialog != null) { stringDialog.assureNotEditing(); } } public void updateStringDialog() { if (stringDialog != null) { stringDialog.refreshTable(); } } public void adjustSplitter() { if (mode == BasePanelMode.SHOWING_PREVIEW) { splitPane.setDividerLocation( splitPane.getHeight() - Globals.prefs.getPreviewPreferences().getPreviewPanelHeight()); } else { splitPane.setDividerLocation( splitPane.getHeight() - Globals.prefs.getInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT)); } } private boolean isShowingEditor() { return (splitPane.getBottomComponent() != null) && (splitPane.getBottomComponent() instanceof EntryEditor); } public void showEntry(final BibEntry be) { if (getShowing() == be) { if (splitPane.getBottomComponent() == null) { // This is the special occasion when showing is set to an // entry, but no entry editor is in fact shown. This happens // after Preferences dialog is closed, and it means that we // must make sure the same entry is shown again. We do this by // setting showing to null, and recursively calling this method. newEntryShowing(null); showEntry(be); } return; } String visName = null; if ((getShowing() != null) && isShowingEditor()) { visName = ((EntryEditor) splitPane.getBottomComponent()).getVisiblePanelName(); } // We must instantiate a new editor. EntryEditor entryEditor = new EntryEditor(frame, BasePanel.this, be); if (visName != null) { entryEditor.setVisiblePanel(visName); } showEntryEditor(entryEditor); newEntryShowing(be); setEntryEditorEnabled(true); // Make sure it is enabled. } /** * Get an entry editor ready to edit the given entry. If an appropriate editor is already cached, it will be updated * and returned. * * @param entry The entry to be edited. * @return A suitable entry editor. */ public EntryEditor getEntryEditor(BibEntry entry) { // We must instantiate a new editor. First make sure the old one stores its last edit: storeCurrentEdit(); // Then start the new one: return new EntryEditor(frame, BasePanel.this, entry); } public EntryEditor getCurrentEditor() { return currentEditor; } /** * Sets the given entry editor as the bottom component in the split pane. If an entry editor already was shown, * makes sure that the divider doesn't move. Updates the mode to SHOWING_EDITOR. * * @param editor The entry editor to add. */ public void showEntryEditor(EntryEditor editor) { if (mode == BasePanelMode.SHOWING_EDITOR) { Globals.prefs.putInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); } mode = BasePanelMode.SHOWING_EDITOR; if (currentEditor != null) { currentEditor.setMovingToDifferentEntry(); } currentEditor = editor; splitPane.setBottomComponent(editor); if (editor.getEntry() != getShowing()) { newEntryShowing(editor.getEntry()); } adjustSplitter(); } /** * Sets the given preview panel as the bottom component in the split panel. Updates the mode to SHOWING_PREVIEW. * * @param preview The preview to show. */ public void showPreview(PreviewPanel preview) { mode = BasePanelMode.SHOWING_PREVIEW; splitPane.setBottomComponent(preview); adjustSplitter(); } /** * Removes the bottom component. */ public void hideBottomComponent() { mode = BasePanelMode.SHOWING_NOTHING; splitPane.setBottomComponent(null); } /** * This method selects the given entry, and scrolls it into view in the table. If an entryEditor is shown, it is * given focus afterwards. */ public void highlightEntry(final BibEntry bibEntry) { highlightEntry(mainTable.findEntry(bibEntry)); } /** * This method selects the entry on the given position, and scrolls it into view in the table. * If an entryEditor is shown, it is given focus afterwards. */ public void highlightEntry(int pos) { if ((pos >= 0) && (pos < mainTable.getRowCount())) { mainTable.setRowSelectionInterval(pos, pos); mainTable.ensureVisible(pos); } } public void selectPreviousEntry() { highlightEntry(((mainTable.getSelectedRow() - 1) + mainTable.getRowCount()) % mainTable.getRowCount()); } public void selectNextEntry() { highlightEntry((mainTable.getSelectedRow() + 1) % mainTable.getRowCount()); } public void selectFirstEntry() { highlightEntry(0); } public void selectLastEntry() { highlightEntry(mainTable.getRowCount() - 1); } /** * This method is called from an EntryEditor when it should be closed. We relay to the selection listener, which * takes care of the rest. * * @param editor The entry editor to close. */ public void entryEditorClosing(EntryEditor editor) { // Store divider location for next time: Globals.prefs.putInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); selectionListener.entryEditorClosing(editor); } /** * Closes the entry editor or preview panel if it is showing the given entry. */ public void ensureNotShowingBottomPanel(BibEntry entry) { if (((mode == BasePanelMode.SHOWING_EDITOR) && (currentEditor.getEntry() == entry)) || ((mode == BasePanelMode.SHOWING_PREVIEW) && (selectionListener.getPreview().getEntry() == entry))) { hideBottomComponent(); } } public void updateEntryEditorIfShowing() { if (mode == BasePanelMode.SHOWING_EDITOR) { if (currentEditor.getDisplayedBibEntryType().equals(currentEditor.getEntry().getType())) { currentEditor.updateSource(); } else { // The entry has changed type, so we must get a new editor. newEntryShowing(null); final EntryEditor newEditor = getEntryEditor(currentEditor.getEntry()); showEntryEditor(newEditor); } } } /** * If an entry editor is showing, make sure its currently focused field stores its changes, if any. */ public void storeCurrentEdit() { if (isShowingEditor()) { final EntryEditor editor = (EntryEditor) splitPane.getBottomComponent(); editor.storeCurrentEdit(); } } public void markBaseChanged() { baseChanged = true; if (SwingUtilities.isEventDispatchThread()) { markBasedChangedInternal(); } else { try { SwingUtilities.invokeAndWait(() -> markBasedChangedInternal()); } catch (InvocationTargetException | InterruptedException e) { LOGGER.info("Problem marking database as changed", e); } } } private void markBasedChangedInternal() { // Put an asterisk behind the filename to indicate the database has changed. frame.setWindowTitle(); frame.updateAllTabTitles(); // If the status line states that the base has been saved, we // remove this message, since it is no longer relevant. If a // different message is shown, we leave it. if (frame.getStatusLineText().startsWith(Localization.lang("Saved library"))) { frame.output(" "); } } public void markNonUndoableBaseChanged() { nonUndoableChange = true; markBaseChanged(); } public void rebuildAllEntryEditors() { if (currentEditor != null) { currentEditor.rebuildPanels(); } } private synchronized void markChangedOrUnChanged() { if (getUndoManager().hasChanged()) { if (!baseChanged) { markBaseChanged(); } } else if (baseChanged && !nonUndoableChange) { baseChanged = false; if (getBibDatabaseContext().getDatabaseFile().isPresent()) { frame.setTabTitle(this, getTabTitle(), getBibDatabaseContext().getDatabaseFile().get().getAbsolutePath()); } else { frame.setTabTitle(this, GUIGlobals.UNTITLED_TITLE, null); } } frame.setWindowTitle(); } public BibDatabase getDatabase() { return bibDatabaseContext.getDatabase(); } public void preambleEditorClosing() { preambleEditor = null; } public void stringsClosing() { stringDialog = null; } public void changeTypeOfSelectedEntries(String newType) { List<BibEntry> bes = mainTable.getSelectedEntries(); changeType(bes, newType); } private void changeType(List<BibEntry> entries, String newType) { if ((entries == null) || (entries.isEmpty())) { LOGGER.error("At least one entry must be selected to be able to change the type."); return; } if (entries.size() > 1) { int choice = JOptionPane.showConfirmDialog(this, Localization.lang("Multiple entries selected. Do you want to change the type of all these to '%0'?", newType), Localization.lang("Change entry type"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (choice == JOptionPane.NO_OPTION) { return; } } NamedCompound compound = new NamedCompound(Localization.lang("Change entry type")); for (BibEntry entry : entries) { compound.addEdit(new UndoableChangeType(entry, entry.getType(), newType)); entry.setType(newType); } output(formatOutputMessage(Localization.lang("Changed type to '%0' for", newType), entries.size())); compound.end(); getUndoManager().addEdit(compound); markBaseChanged(); updateEntryEditorIfShowing(); } public boolean showDeleteConfirmationDialog(int numberOfEntries) { if (Globals.prefs.getBoolean(JabRefPreferences.CONFIRM_DELETE)) { String msg; msg = Localization.lang("Really delete the selected entry?"); String title = Localization.lang("Delete entry"); if (numberOfEntries > 1) { msg = Localization.lang("Really delete the %0 selected entries?", Integer.toString(numberOfEntries)); title = Localization.lang("Delete multiple entries"); } CheckBoxMessage cb = new CheckBoxMessage(msg, Localization.lang("Disable this confirmation dialog"), false); int answer = JOptionPane.showConfirmDialog(frame, cb, title, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (cb.isSelected()) { Globals.prefs.putBoolean(JabRefPreferences.CONFIRM_DELETE, false); } return answer == JOptionPane.YES_OPTION; } else { return true; } } /** * If the relevant option is set, autogenerate keys for all entries that are lacking keys. */ public void autoGenerateKeysBeforeSaving() { if (Globals.prefs.getBoolean(JabRefPreferences.GENERATE_KEYS_BEFORE_SAVING)) { NamedCompound ce = new NamedCompound(Localization.lang("Autogenerate BibTeX keys")); for (BibEntry bes : bibDatabaseContext.getDatabase().getEntries()) { Optional<String> oldKey = bes.getCiteKeyOptional(); if (!(oldKey.isPresent()) || oldKey.get().isEmpty()) { BibtexKeyPatternUtil.makeAndSetLabel(bibDatabaseContext.getMetaData() .getCiteKeyPattern(Globals.prefs.getBibtexKeyPatternPreferences().getKeyPattern()), bibDatabaseContext.getDatabase(), bes, Globals.prefs.getBibtexKeyPatternPreferences()); bes.getCiteKeyOptional().ifPresent( newKey -> ce.addEdit(new UndoableKeyChange(bes, oldKey.orElse(""), newKey))); } } // Store undo information, if any: if (ce.hasEdits()) { ce.end(); getUndoManager().addEdit(ce); } } } /** * Activates or deactivates the entry preview, depending on the argument. When deactivating, makes sure that any * visible preview is hidden. * * @param enabled */ private void setPreviewActive(boolean enabled) { selectionListener.setPreviewActive(enabled); } /** * Depending on whether a preview or an entry editor is showing, save the current divider location in the correct * preference setting. */ public void saveDividerLocation() { if (mode == BasePanelMode.SHOWING_PREVIEW) { int previewPanelHeight = splitPane.getHeight() - splitPane.getDividerLocation(); PreviewPreferences previewPreferences = Globals.prefs.getPreviewPreferences() .getBuilder() .withPreviewPanelHeight(previewPanelHeight) .build(); Globals.prefs.storePreviewPreferences(previewPreferences); } else if (mode == BasePanelMode.SHOWING_EDITOR) { Globals.prefs.putInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); } } // Method pertaining to the ClipboardOwner interface. @Override public void lostOwnership(Clipboard clipboard, Transferable contents) { // Nothing } private void setEntryEditorEnabled(boolean enabled) { if ((getShowing() != null) && (splitPane.getBottomComponent() instanceof EntryEditor)) { EntryEditor ed = (EntryEditor) splitPane.getBottomComponent(); if (ed.isEnabled() != enabled) { ed.setEnabled(enabled); } } } public String fileMonitorHandle() { return fileMonitorHandle; } @Override public void fileUpdated() { if (saving) { // We are just saving the file, so this message is most likely due to bad timing. // If not, we'll handle it on the next polling. return; } updatedExternally = true; final ChangeScanner scanner = new ChangeScanner(frame, BasePanel.this, getBibDatabaseContext().getDatabaseFile().orElse(null)); // Test: running scan automatically in background if ((getBibDatabaseContext().getDatabaseFile().isPresent()) && !FileBasedLock.waitForFileLock(getBibDatabaseContext().getDatabaseFile().get().toPath())) { // The file is locked even after the maximum wait. Do nothing. LOGGER.error("File updated externally, but change scan failed because the file is locked."); // Perturb the stored timestamp so successive checks are made: Globals.getFileUpdateMonitor().perturbTimestamp(getFileMonitorHandle()); return; } JabRefExecutorService.INSTANCE.executeInterruptableTaskAndWait(scanner); // Adding the sidepane component is Swing work, so we must do this in the Swing // thread: Runnable t = () -> { // Check if there is already a notification about external // changes: boolean hasAlready = sidePaneManager.hasComponent(FileUpdatePanel.class); if (hasAlready) { sidePaneManager.hideComponent(FileUpdatePanel.class); sidePaneManager.unregisterComponent(FileUpdatePanel.class); } FileUpdatePanel pan = new FileUpdatePanel(BasePanel.this, sidePaneManager, getBibDatabaseContext().getDatabaseFile().orElse(null), scanner); sidePaneManager.register(pan); sidePaneManager.show(FileUpdatePanel.class); }; if (scanner.changesFound()) { SwingUtilities.invokeLater(t); } else { setUpdatedExternally(false); } } @Override public void fileRemoved() { LOGGER.info("File '" + getBibDatabaseContext().getDatabaseFile().get().getPath() + "' has been deleted."); } /** * Perform necessary cleanup when this BasePanel is closed. */ public void cleanUp() { if (fileMonitorHandle != null) { Globals.getFileUpdateMonitor().removeUpdateListener(fileMonitorHandle); } // Check if there is a FileUpdatePanel for this BasePanel being shown. If so, // remove it: if (sidePaneManager.hasComponent(FileUpdatePanel.class)) { FileUpdatePanel fup = (FileUpdatePanel) sidePaneManager.getComponent(FileUpdatePanel.class); if (fup.getPanel() == this) { sidePaneManager.hideComponent(FileUpdatePanel.class); } } } /** * Get an array containing the currently selected entries. The array is stable and not changed if the selection * changes * * @return A list containing the selected entries. Is never null. */ public List<BibEntry> getSelectedEntries() { return mainTable.getSelectedEntries(); } public BibDatabaseContext getBibDatabaseContext() { return this.bibDatabaseContext; } public GroupSelector getGroupSelector() { return frame.getGroupSelector(); } public boolean isUpdatedExternally() { return updatedExternally; } public void setUpdatedExternally(boolean b) { updatedExternally = b; } public String getFileMonitorHandle() { return fileMonitorHandle; } public void setFileMonitorHandle(String fileMonitorHandle) { this.fileMonitorHandle = fileMonitorHandle; } public SidePaneManager getSidePaneManager() { return sidePaneManager; } public void setNonUndoableChange(boolean nonUndoableChange) { this.nonUndoableChange = nonUndoableChange; } public void setBaseChanged(boolean baseChanged) { this.baseChanged = baseChanged; } public boolean isSaving() { return saving; } public void setSaving(boolean saving) { this.saving = saving; } private BibEntry getShowing() { return showing; } /** * Update the pointer to the currently shown entry in all cases where the user has moved to a new entry, except when * using Back and Forward commands. Also updates history for Back command, and clears history for Forward command. * * @param entry The entry that is now to be shown. */ public void newEntryShowing(BibEntry entry) { // If this call is the result of a Back or Forward operation, we must take // care not to make any history changes, since the necessary changes will // already have been done in the back() or forward() method: if (backOrForwardInProgress) { showing = entry; backOrForwardInProgress = false; setBackAndForwardEnabledState(); return; } nextEntries.clear(); if (!Objects.equals(entry, showing)) { // Add the entry we are leaving to the history: if (showing != null) { previousEntries.add(showing); if (previousEntries.size() > GUIGlobals.MAX_BACK_HISTORY_SIZE) { previousEntries.remove(0); } } showing = entry; setBackAndForwardEnabledState(); } } /** * Go back (if there is any recorded history) and update the histories for the Back and Forward commands. */ private void back() { if (!previousEntries.isEmpty()) { BibEntry toShow = previousEntries.get(previousEntries.size() - 1); previousEntries.remove(previousEntries.size() - 1); // Add the entry we are going back from to the Forward history: if (showing != null) { nextEntries.add(showing); } backOrForwardInProgress = true; // to avoid the history getting updated erroneously highlightEntry(toShow); } } private void forward() { if (!nextEntries.isEmpty()) { BibEntry toShow = nextEntries.get(nextEntries.size() - 1); nextEntries.remove(nextEntries.size() - 1); // Add the entry we are going forward from to the Back history: if (showing != null) { previousEntries.add(showing); } backOrForwardInProgress = true; // to avoid the history getting updated erroneously highlightEntry(toShow); } } public void setBackAndForwardEnabledState() { frame.getBackAction().setEnabled(!previousEntries.isEmpty()); frame.getForwardAction().setEnabled(!nextEntries.isEmpty()); } private String formatOutputMessage(String start, int count) { return String.format("%s %d %s.", start, count, (count > 1 ? Localization.lang("entries") : Localization.lang("entry"))); } /** * This method iterates through all existing entry editors in this BasePanel, telling each to update all its * instances of FieldContentSelector. This is done to ensure that the list of words in each selector is up-to-date * after the user has made changes in the Manage dialog. */ public void updateAllContentSelectors() { currentEditor.updateAllContentSelectors(); } /** * Set the preview active state for all BasePanel instances. */ private void setPreviewActiveBasePanels(boolean enabled) { for (int i = 0; i < frame.getTabbedPane().getTabCount(); i++) { frame.getBasePanelAt(i).setPreviewActive(enabled); } } public CountingUndoManager getUndoManager() { return undoManager; } public MainTable getMainTable() { return mainTable; } public BibDatabaseContext getDatabaseContext() { return bibDatabaseContext; } public Optional<SearchQuery> getCurrentSearchQuery() { return currentSearchQuery; } /** * Set the query the user currently searches while this basepanel is active * * @param currentSearchQuery can be null */ public void setCurrentSearchQuery(SearchQuery currentSearchQuery) { this.currentSearchQuery = Optional.ofNullable(currentSearchQuery); } public CitationStyleCache getCitationStyleCache() { return citationStyleCache; } public PreviewPanel getPreviewPanel() { if (selectionListener == null) { // only occurs if this is called while instantiating this BasePanel return null; } return selectionListener.getPreview(); } public FileAnnotationCache getAnnotationCache() { return annotationCache; } private static class SearchAndOpenFile { private final BibEntry entry; private final BasePanel basePanel; public SearchAndOpenFile(final BibEntry entry, final BasePanel basePanel) { this.entry = entry; this.basePanel = basePanel; } public void searchAndOpen() { if (!Globals.prefs.getBoolean(JabRefPreferences.RUN_AUTOMATIC_FILE_SEARCH)) { /* The search can lead to an unexpected 100% CPU usage which is perceived as a bug, if the search incidentally starts at a directory with lots of stuff below. It is now disabled by default. */ return; } final Set<ExternalFileType> types = ExternalFileTypes.getInstance().getExternalFileTypeSelection(); final List<Path> dirs = basePanel.getBibDatabaseContext().getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()); final List<String> extensions = types.stream().map(ExternalFileType::getExtension).collect(Collectors.toList()); // Run the search operation: FileFinder fileFinder = FileFinders.constructFromConfiguration(Globals.prefs.getAutoLinkPreferences()); List<Path> files = fileFinder.findAssociatedFiles(entry, dirs, extensions); if (!files.isEmpty()) { Path file = files.get(0); Optional<ExternalFileType> type = ExternalFileTypes.getInstance().getExternalFileTypeByFile(file); if (type.isPresent()) { try { JabRefDesktop.openExternalFileAnyFormat(file, basePanel.getBibDatabaseContext(), type); basePanel.output(Localization.lang("External viewer called") + '.'); } catch (IOException ex) { basePanel.output(Localization.lang("Error") + ": " + ex.getMessage()); } } } } } private class GroupTreeListener { private final Runnable task = new Runnable() { @Override public void run() { // Update group display (for example to reflect that the number of contained entries has changed) frame.getGroupSelector().revalidateGroups(); } }; /** * Only access when you have the lock of the task instance * * Guarded by "task" */ private TimerTask timerTask = new TimerTask() { @Override public void run() { task.run(); } }; @Subscribe public void listen(EntryAddedEvent addedEntryEvent) { // if the added entry is an undo don't add it to the current group if (addedEntryEvent.getEntryEventSource() == EntryEventSource.UNDO) { scheduleUpdate(); return; } // Automatically add new entry to the selected group (or set of groups) if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_ASSIGN_GROUP) && frame.getGroupSelector().getToggleAction().isSelected()) { final List<BibEntry> entries = Collections.singletonList(addedEntryEvent.getBibEntry()); final TreePath[] selection = frame.getGroupSelector().getGroupsTree().getSelectionPaths(); if (selection != null) { // it is possible that the user selected nothing. Therefore, checked for "!= null" for (final TreePath tree : selection) { ((GroupTreeNodeViewModel) tree.getLastPathComponent()).addEntriesToGroup(entries); } } SwingUtilities.invokeLater(() -> BasePanel.this.getGroupSelector().valueChanged(null)); } scheduleUpdate(); } private void scheduleUpdate() { // This is a quickfix/dirty hack. // a better solution would be using RxJava or something reactive instead // nevertheless it works correctly synchronized (task) { timerTask.cancel(); timerTask = new TimerTask() { @Override public void run() { task.run(); } }; JabRefExecutorService.INSTANCE.submit(timerTask, 200); } } } /** * Ensures that the search auto completer is up to date when entries are changed AKA Let the auto completer, if any, * harvest words from the entry */ private class SearchAutoCompleteListener { @Subscribe public void listen(EntryAddedEvent addedEntryEvent) { searchAutoCompleter.addBibtexEntry(addedEntryEvent.getBibEntry()); } @Subscribe public void listen(EntryChangedEvent entryChangedEvent) { searchAutoCompleter.addBibtexEntry(entryChangedEvent.getBibEntry()); } } /** * Ensures that auto completers are up to date when entries are changed AKA Let the auto completer, if any, harvest * words from the entry */ private class AutoCompleteListener { @Subscribe public void listen(EntryAddedEvent addedEntryEvent) { BasePanel.this.autoCompleters.addEntry(addedEntryEvent.getBibEntry()); } @Subscribe public void listen(EntryChangedEvent entryChangedEvent) { BasePanel.this.autoCompleters.addEntry(entryChangedEvent.getBibEntry()); } } /** * Ensures that the results of the current search are updated when a new entry is inserted into the database */ private class SearchListener { @Subscribe public void listen(EntryAddedEvent addedEntryEvent) { frame.getGlobalSearchBar().performSearch(); } @Subscribe public void listen(EntryChangedEvent entryChangedEvent) { frame.getGlobalSearchBar().setDontSelectSearchBar(true); frame.getGlobalSearchBar().performSearch(); } @Subscribe public void listen(EntryRemovedEvent removedEntryEvent) { // IMO only used to update the status (found X entries) frame.getGlobalSearchBar().performSearch(); } } private class UndoAction implements BaseAction { @Override public void action() { try { JComponent focused = Globals.getFocusListener().getFocused(); if ((focused != null) && (focused instanceof FieldEditor) && focused.hasFocus()) { // User is currently editing a field: // Check if it is the preamble: if ((preambleEditor != null) && (focused == preambleEditor.getFieldEditor())) { preambleEditor.storeCurrentEdit(); } else { storeCurrentEdit(); } } getUndoManager().undo(); markBaseChanged(); frame.output(Localization.lang("Undo")); } catch (CannotUndoException ex) { LOGGER.warn("Nothing to undo", ex); frame.output(Localization.lang("Nothing to undo") + '.'); } markChangedOrUnChanged(); } } private class OpenURLAction implements BaseAction { @Override public void action() { final List<BibEntry> bes = mainTable.getSelectedEntries(); if (bes.size() == 1) { String field = FieldName.DOI; Optional<String> link = bes.get(0).getField(FieldName.DOI); if (bes.get(0).hasField(FieldName.URL)) { link = bes.get(0).getField(FieldName.URL); field = FieldName.URL; } if (link.isPresent()) { try { JabRefDesktop.openExternalViewer(bibDatabaseContext, link.get(), field); output(Localization.lang("External viewer called") + '.'); } catch (IOException ex) { output(Localization.lang("Error") + ": " + ex.getMessage()); } } else { // No URL or DOI found in the "url" and "doi" fields. // Look for web links in the "file" field as a fallback: FileListEntry entry = null; FileListTableModel tm = new FileListTableModel(); bes.get(0).getField(FieldName.FILE).ifPresent(tm::setContent); for (int i = 0; i < tm.getRowCount(); i++) { FileListEntry flEntry = tm.getEntry(i); if (FieldName.URL.equalsIgnoreCase(flEntry.getType().get().getName()) || FieldName.PS.equalsIgnoreCase(flEntry.getType().get().getName()) || FieldName.PDF.equalsIgnoreCase(flEntry.getType().get().getName())) { entry = flEntry; break; } } if (entry == null) { output(Localization.lang("No URL defined") + '.'); } else { try { JabRefDesktop.openExternalFileAnyFormat(bibDatabaseContext, entry.getLink(), entry.getType()); output(Localization.lang("External viewer called") + '.'); } catch (IOException e) { output(Localization.lang("Could not open link")); LOGGER.info("Could not open link", e); } } } } else { output(Localization.lang("This operation requires exactly one item to be selected.")); } } } private class RedoAction implements BaseAction { @Override public void action() { try { JComponent focused = Globals.getFocusListener().getFocused(); if ((focused != null) && (focused instanceof FieldEditor) && focused.hasFocus()) { // User is currently editing a field: storeCurrentEdit(); } getUndoManager().redo(); markBaseChanged(); frame.output(Localization.lang("Redo")); } catch (CannotRedoException ex) { frame.output(Localization.lang("Nothing to redo") + '.'); } markChangedOrUnChanged(); } } private class PrintPreviewAction implements BaseAction { @Override public void action() throws Exception { selectionListener.setPreviewActive(true); showPreview(selectionListener.getPreview()); selectionListener.getPreview().getPrintAction().actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null)); } } private class SaveSelectedAction implements BaseAction { private final SavePreferences.DatabaseSaveType saveType; public SaveSelectedAction(SavePreferences.DatabaseSaveType saveType) { this.saveType = saveType; } @Override public void action() throws SaveException { FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .withDefaultExtension(FileExtensions.BIBTEX_DB) .addExtensionFilter(FileExtensions.BIBTEX_DB) .withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build(); DialogService ds = new FXDialogService(); Optional<Path> chosenFile = DefaultTaskExecutor .runInJavaFXThread(() -> ds.showFileSaveDialog(fileDialogConfiguration)); if (chosenFile.isPresent()) { Path path = chosenFile.get(); saveDatabase(path.toFile(), true, Globals.prefs.getDefaultEncoding(), saveType); frame.getFileHistory().newFile(path.toString()); frame.output(Localization.lang("Saved selected to '%0'.", path.toString())); } } } }