Java tutorial
/* Copyright (C) 2003-2016 JabRef contributors. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package net.sf.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.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.TimerTask; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JFileChooser; 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 net.sf.jabref.BibDatabaseContext; import net.sf.jabref.Globals; import net.sf.jabref.JabRefExecutorService; import net.sf.jabref.collab.ChangeScanner; import net.sf.jabref.collab.FileUpdateListener; import net.sf.jabref.collab.FileUpdatePanel; import net.sf.jabref.external.AttachFileAction; import net.sf.jabref.external.ExternalFileMenuItem; import net.sf.jabref.external.ExternalFileType; import net.sf.jabref.external.ExternalFileTypes; import net.sf.jabref.external.FindFullTextAction; import net.sf.jabref.external.RegExpFileSearch; import net.sf.jabref.external.SynchronizeFileField; import net.sf.jabref.external.WriteXMPAction; import net.sf.jabref.gui.actions.Actions; import net.sf.jabref.gui.actions.BaseAction; import net.sf.jabref.gui.actions.CleanupAction; import net.sf.jabref.gui.desktop.JabRefDesktop; import net.sf.jabref.gui.entryeditor.EntryEditor; import net.sf.jabref.gui.exporter.ExportToClipboardAction; import net.sf.jabref.gui.exporter.SaveDatabaseAction; import net.sf.jabref.gui.fieldeditors.FieldEditor; import net.sf.jabref.gui.groups.GroupAddRemoveDialog; import net.sf.jabref.gui.groups.GroupSelector; import net.sf.jabref.gui.groups.GroupTreeNodeViewModel; import net.sf.jabref.gui.journals.AbbreviateAction; import net.sf.jabref.gui.journals.UnabbreviateAction; import net.sf.jabref.gui.labelpattern.SearchFixDuplicateLabels; import net.sf.jabref.gui.maintable.MainTable; import net.sf.jabref.gui.maintable.MainTableDataModel; import net.sf.jabref.gui.maintable.MainTableFormat; import net.sf.jabref.gui.maintable.MainTableSelectionListener; import net.sf.jabref.gui.mergeentries.MergeEntriesDialog; import net.sf.jabref.gui.mergeentries.MergeEntryDOIDialog; import net.sf.jabref.gui.plaintextimport.TextInputDialog; import net.sf.jabref.gui.search.SearchBar; import net.sf.jabref.gui.undo.CountingUndoManager; import net.sf.jabref.gui.undo.NamedCompound; import net.sf.jabref.gui.undo.UndoableChangeType; import net.sf.jabref.gui.undo.UndoableFieldChange; import net.sf.jabref.gui.undo.UndoableInsertEntry; import net.sf.jabref.gui.undo.UndoableKeyChange; import net.sf.jabref.gui.undo.UndoableRemoveEntry; import net.sf.jabref.gui.util.FocusRequester; import net.sf.jabref.gui.util.component.CheckBoxMessage; import net.sf.jabref.gui.worker.AbstractWorker; import net.sf.jabref.gui.worker.CallBack; import net.sf.jabref.gui.worker.MarkEntriesAction; import net.sf.jabref.gui.worker.SendAsEMailAction; import net.sf.jabref.gui.worker.Worker; import net.sf.jabref.importer.AppendDatabaseAction; import net.sf.jabref.logic.autocompleter.AutoCompletePreferences; import net.sf.jabref.logic.autocompleter.AutoCompleter; import net.sf.jabref.logic.autocompleter.AutoCompleterFactory; import net.sf.jabref.logic.autocompleter.ContentAutoCompleters; import net.sf.jabref.logic.exporter.BibtexDatabaseWriter; import net.sf.jabref.logic.exporter.FileSaveSession; import net.sf.jabref.logic.exporter.SaveException; import net.sf.jabref.logic.exporter.SavePreferences; import net.sf.jabref.logic.exporter.SaveSession; import net.sf.jabref.logic.l10n.Encodings; import net.sf.jabref.logic.l10n.Localization; import net.sf.jabref.logic.labelpattern.LabelPatternUtil; import net.sf.jabref.logic.layout.Layout; import net.sf.jabref.logic.layout.LayoutHelper; import net.sf.jabref.logic.util.UpdateField; import net.sf.jabref.logic.util.io.FileBasedLock; import net.sf.jabref.logic.util.io.FileUtil; import net.sf.jabref.model.FieldChange; import net.sf.jabref.model.database.BibDatabase; import net.sf.jabref.model.database.KeyCollisionException; import net.sf.jabref.model.entry.BibEntry; import net.sf.jabref.model.entry.EntryType; import net.sf.jabref.model.entry.FieldName; import net.sf.jabref.model.entry.IdGenerator; import net.sf.jabref.model.event.EntryAddedEvent; import net.sf.jabref.model.event.EntryChangedEvent; import net.sf.jabref.preferences.HighlightMatchingGroupPreferences; import net.sf.jabref.preferences.JabRefPreferences; import net.sf.jabref.specialfields.Printed; import net.sf.jabref.specialfields.Priority; import net.sf.jabref.specialfields.Quality; import net.sf.jabref.specialfields.Rank; import net.sf.jabref.specialfields.ReadStatus; import net.sf.jabref.specialfields.Relevance; import net.sf.jabref.specialfields.SpecialFieldAction; import net.sf.jabref.specialfields.SpecialFieldDatabaseChangeListener; import net.sf.jabref.specialfields.SpecialFieldValue; import net.sf.jabref.sql.DBConnectDialog; import net.sf.jabref.sql.DBExporterAndImporterFactory; import net.sf.jabref.sql.DBStrings; import net.sf.jabref.sql.DbConnectAction; import net.sf.jabref.sql.SQLUtil; import net.sf.jabref.sql.exporter.DatabaseExporter; import ca.odell.glazedlists.event.ListEventListener; 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 BibDatabase database; private final BibDatabaseContext bibDatabaseContext; private final MainTableDataModel tableModel; // 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 PreviewPanel currentPreview; private MainTableSelectionListener selectionListener; private ListEventListener<BibEntry> groupsHighlightListener; private JSplitPane splitPane; private final JabRefFrame frame; private String fileMonitorHandle; private boolean saving; private boolean updatedExternally; // AutoCompleter used in the search bar private AutoCompleter<String> searchAutoCompleter; // The undo manager. private final CountingUndoManager undoManager = new CountingUndoManager(this); private final UndoAction undoAction = new UndoAction(); private final RedoAction redoAction = new RedoAction(); private final List<BibEntry> previousEntries = new ArrayList<>(); private final List<BibEntry> nextEntries = new ArrayList<>(); 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; // To indicate which entry is currently shown. private final Map<String, EntryEditor> entryEditors = new HashMap<>(); // in switching between entries. private PreambleEditor preambleEditor; // Keeps track of the preamble dialog if it is open. private StringDialog stringDialog; // Keeps track of the string dialog if it is open. private final Map<String, Object> actions = new HashMap<>(); private final SidePaneManager sidePaneManager; private final SearchBar searchBar; private ContentAutoCompleters autoCompleters; public BasePanel(JabRefFrame frame, BibDatabaseContext bibDatabaseContext) { Objects.requireNonNull(frame); Objects.requireNonNull(bibDatabaseContext); this.bibDatabaseContext = bibDatabaseContext; this.sidePaneManager = frame.getSidePaneManager(); this.frame = frame; this.database = bibDatabaseContext.getDatabase(); this.tableModel = new MainTableDataModel(getBibDatabaseContext()); searchBar = new SearchBar(this); setupMainPanel(); setupActions(); File file = bibDatabaseContext.getDatabaseFile(); // ensure that at each addition of a new entry, the entry is added to the groups interface this.database.registerListener(new GroupTreeListener()); if (file == null) { if (database.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; } } else { // Register so we get notifications about outside changes to the file. try { fileMonitorHandle = Globals.getFileUpdateMonitor().addUpdateListener(this, file); } catch (IOException ex) { LOGGER.warn("Could not register FileUpdateMonitor", ex); } } } // Returns a collection of AutoCompleters, which are populated from the current database public ContentAutoCompleters getAutoCompleters() { return autoCompleters; } public String getTabTitle() { StringBuilder title = new StringBuilder(); if (getBibDatabaseContext().getDatabaseFile() == null) { 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 { // check if file is modified String changeFlag = isModified() ? "*" : ""; title.append(getBibDatabaseContext().getDatabaseFile().getName()).append(changeFlag); } 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) () -> new FocusRequester(mainTable)); // 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()); //when you modify this action be sure to adjust Actions.DELETE //they are the same except of the Localization, delete confirmation and Actions.COPY call actions.put(Actions.CUT, (BaseAction) () -> { runCommand(Actions.COPY); List<BibEntry> entries = mainTable.getSelectedEntries(); if (entries.isEmpty()) { return; } NamedCompound compound = new NamedCompound( (entries.size() > 1 ? Localization.lang("cut entries") : Localization.lang("cut entry"))); for (BibEntry entry : entries) { compound.addEdit(new UndoableRemoveEntry(database, entry, BasePanel.this)); database.removeEntry(entry); ensureNotShowingBottomPanel(entry); } compound.end(); getUndoManager().addEdit(compound); frame.output(formatOutputMessage(Localization.lang("Cut"), entries.size())); markBaseChanged(); }); //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()); // 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, database); 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, database); form.setVisible(true); stringDialog = form; } else { stringDialog.setVisible(true); } }); // The action for toggling the groups interface actions.put(Actions.TOGGLE_GROUPS, (BaseAction) () -> { sidePaneManager.toggle("groups"); frame.groupToggle.setSelected(sidePaneManager.isComponentVisible("groups")); }); // action for collecting database strings from user actions.put(Actions.DB_CONNECT, new DbConnectAction(this)); // action for exporting database to external SQL database actions.put(Actions.DB_EXPORT, new AbstractWorker() { String errorMessage = ""; boolean connectedToDB; // run first, in EDT: @Override public void init() { DBStrings dbs = bibDatabaseContext.getMetaData().getDBStrings(); // get DBStrings from user if necessary if (dbs.isConfigValid()) { connectedToDB = true; } else { // init DB strings if necessary if (!dbs.isInitialized()) { dbs.initialize(); } // show connection dialog DBConnectDialog dbd = new DBConnectDialog(frame(), dbs); dbd.setLocationRelativeTo(BasePanel.this); dbd.setVisible(true); connectedToDB = dbd.isConnectedToDB(); // store database strings if (connectedToDB) { dbs = dbd.getDBStrings(); bibDatabaseContext.getMetaData().setDBStrings(dbs); dbd.dispose(); } } } // run second, on a different thread: @Override public void run() { if (!connectedToDB) { return; } final DBStrings dbs = bibDatabaseContext.getMetaData().getDBStrings(); try { frame.output(Localization.lang("Attempting SQL export...")); final DBExporterAndImporterFactory factory = new DBExporterAndImporterFactory(); final DatabaseExporter exporter = factory.getExporter(dbs.getDbPreferences().getServerType()); exporter.exportDatabaseToDBMS(bibDatabaseContext, getDatabase().getEntries(), dbs, frame); dbs.isConfigValid(true); } catch (Exception ex) { final String preamble = Localization .lang("Could not export to SQL database for the following reason:"); errorMessage = SQLUtil.getExceptionMessage(ex); LOGGER.info("Could not export to SQL database", ex); dbs.isConfigValid(false); JOptionPane.showMessageDialog(frame, preamble + '\n' + errorMessage, Localization.lang("Export to SQL database"), JOptionPane.ERROR_MESSAGE); } bibDatabaseContext.getMetaData().setDBStrings(dbs); } // run third, on EDT: @Override public void update() { // if no error, report success if (errorMessage.isEmpty()) { if (connectedToDB) { final DBStrings dbs = bibDatabaseContext.getMetaData().getDBStrings(); frame.output(Localization.lang("%0 export successful", dbs.getDbPreferences().getServerType().getFormattedName())); } } else { // show an error dialog if an error occurred final String preamble = Localization .lang("Could not export to SQL database for the following reason:"); frame.output(preamble + " " + errorMessage); JOptionPane.showMessageDialog(frame, preamble + '\n' + errorMessage, Localization.lang("Export to SQL database"), JOptionPane.ERROR_MESSAGE); errorMessage = ""; } } }); 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() { BibEntry bes; // First check if any entries have keys set already. If so, possibly remove // them from consideration, or warn about overwriting keys. // This is a partial clone of net.sf.jabref.gui.entryeditor.EntryEditor.GenerateKeyAction.actionPerformed(ActionEvent) for (final Iterator<BibEntry> i = entries.iterator(); i.hasNext();) { bes = i.next(); if (bes.getCiteKey() != null) { if (Globals.prefs.getBoolean(JabRefPreferences.AVOID_OVERWRITING_KEY)) { // Remove the entry, because its key is already set: i.remove(); } else if (Globals.prefs.getBoolean(JabRefPreferences.WARN_BEFORE_OVERWRITING_KEY)) { // Ask if the user wants to cancel the operation: 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); if (cbm.isSelected()) { Globals.prefs.putBoolean(JabRefPreferences.WARN_BEFORE_OVERWRITING_KEY, false); } if (answer == JOptionPane.NO_OPTION) { // Ok, break off the operation. canceled = true; return; } // No need to check more entries, because the user has already confirmed // that it's ok to overwrite keys: break; } } } Map<BibEntry, Object> oldvals = new HashMap<>(); // Iterate again, removing already set keys. This is skipped if overwriting // is disabled, since all entries with keys set will have been removed. if (!Globals.prefs.getBoolean(JabRefPreferences.AVOID_OVERWRITING_KEY)) { for (BibEntry entry : entries) { bes = entry; // Store the old value: oldvals.put(bes, bes.getCiteKey()); database.setCiteKeyForEntry(bes, null); } } final NamedCompound ce = new NamedCompound(Localization.lang("Autogenerate BibTeX keys")); // Finally, set the new keys: for (BibEntry entry : entries) { bes = entry; LabelPatternUtil.makeLabel(bibDatabaseContext.getMetaData(), database, bes, Globals.prefs); ce.addEdit(new UndoableKeyChange(database, bes, (String) oldvals.get(bes), bes.getCiteKey())); } ce.end(); 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) searchBar::focus); // The action for copying the selected entry's key. actions.put(Actions.COPY_KEY, (BaseAction) () -> copyKey()); // 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.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<File> files = FileUtil.getListOfLinkedFiles(mainTable.getSelectedEntries(), bibDatabaseContext.getFileDirectory()); for (final File f : files) { try { JabRefDesktop.openFolderAndSelectFile(f.getAbsolutePath()); } catch (IOException e) { LOGGER.info("Could not open folder", e); } } })); actions.put(Actions.OPEN_CONSOLE, (BaseAction) () -> JabRefDesktop .openConsole(frame.getCurrentBasePanel().getBibDatabaseContext().getDatabaseFile())); actions.put(Actions.OPEN_URL, new OpenURLAction()); actions.put(Actions.MERGE_DOI, (BaseAction) () -> new MergeEntryDOIDialog(BasePanel.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 : database.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; } String id = IdGenerator.next(); BibEntry bibEntry = new BibEntry(id, 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); 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, database, 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 : database.getEntries()) { EntryMarker.unmarkEntry(be, false, database, 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(Relevance.getInstance().getValues().get(0).getActionName(), new SpecialFieldAction(frame, Relevance.getInstance(), Relevance.getInstance().getValues().get(0).getFieldValue().get(), true, Localization.lang("Toggle relevance"))); actions.put(Quality.getInstance().getValues().get(0).getActionName(), new SpecialFieldAction(frame, Quality.getInstance(), Quality.getInstance().getValues().get(0).getFieldValue().get(), true, Localization.lang("Toggle quality assured"))); actions.put(Printed.getInstance().getValues().get(0).getActionName(), new SpecialFieldAction(frame, Printed.getInstance(), Printed.getInstance().getValues().get(0).getFieldValue().get(), true, Localization.lang("Toggle print status"))); for (SpecialFieldValue prio : Priority.getInstance().getValues()) { actions.put(prio.getActionName(), prio.getAction(this.frame)); } for (SpecialFieldValue rank : Rank.getInstance().getValues()) { actions.put(rank.getActionName(), rank.getAction(this.frame)); } for (SpecialFieldValue status : ReadStatus.getInstance().getValues()) { actions.put(status.getActionName(), status.getAction(this.frame)); } actions.put(Actions.TOGGLE_PREVIEW, (BaseAction) () -> { boolean enabled = !Globals.prefs.getBoolean(JabRefPreferences.PREVIEW_ENABLED); Globals.prefs.putBoolean(JabRefPreferences.PREVIEW_ENABLED, enabled); setPreviewActiveBasePanels(enabled); frame.setPreviewToggle(enabled); }); actions.put(Actions.TOGGLE_HIGHLIGHTS_GROUPS_MATCHING_ANY, (BaseAction) () -> { new HighlightMatchingGroupPreferences(Globals.prefs).setToAny(); // ping the listener so it updates: groupsHighlightListener.listChanged(null); }); actions.put(Actions.TOGGLE_HIGHLIGHTS_GROUPS_MATCHING_ALL, (BaseAction) () -> { new HighlightMatchingGroupPreferences(Globals.prefs).setToAll(); // ping the listener so it updates: groupsHighlightListener.listChanged(null); }); actions.put(Actions.TOGGLE_HIGHLIGHTS_GROUPS_MATCHING_DISABLE, (BaseAction) () -> { new HighlightMatchingGroupPreferences(Globals.prefs).setToDisabled(); // ping the listener so it updates: groupsHighlightListener.listChanged(null); }); actions.put(Actions.SWITCH_PREVIEW, (BaseAction) selectionListener::switchPreview); actions.put(Actions.MANAGE_SELECTORS, (BaseAction) () -> { ContentSelectorDialog2 csd = new ContentSelectorDialog2(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)); } 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())); } } //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 private void delete() { List<BibEntry> entries = mainTable.getSelectedEntries(); if (entries.isEmpty()) { return; } if (!showDeleteConfirmationDialog(entries.size())) { return; } NamedCompound compound = new NamedCompound( (entries.size() > 1 ? Localization.lang("delete entries") : Localization.lang("delete entry"))); for (BibEntry entry : entries) { compound.addEdit(new UndoableRemoveEntry(database, entry, BasePanel.this)); database.removeEntry(entry); ensureNotShowingBottomPanel(entry); } compound.end(); getUndoManager().addEdit(compound); markBaseChanged(); frame.output(formatOutputMessage(Localization.lang("Deleted"), entries.size())); } 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.getBoolean(JabRefPreferences.OVERWRITE_OWNER), Globals.prefs.getBoolean(JabRefPreferences.OVERWRITE_TIME_STAMP)); // We have to clone the // entries, since the pasted // entries must exist // independently of the copied // ones. be.setId(IdGenerator.next()); database.insertEntry(be); ce.addEdit(new UndoableInsertEntry(database, be, BasePanel.this)); } ce.end(); getUndoManager().addEdit(ce); output(formatOutputMessage(Localization.lang("Pasted"), bes.size())); markBaseChanged(); if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_OPEN_FORM)) { selectionListener.editSignalled(firstBE); } highlightEntry(firstBE); } } 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) { if (be.getCiteKey() != null) { keys.add(be.getCiteKey()); } } 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) { if (be.getCiteKey() != null) { keys.add(be.getCiteKey()); } } 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, 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.getCiteKey() != null) { copied++; sb.append(layout.doLayout(be, database)); } } 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 tableModel = new FileListTableModel(); entry.getFieldOptional(FieldName.FILE).ifPresent(tableModel::setContent); if (tableModel.getRowCount() == 0) { // content in bibtex field is not readable new SearchAndOpenFile(entry, BasePanel.this).searchAndOpen(); return; } FileListEntry flEntry = tableModel.getEntry(0); ExternalFileMenuItem item = new ExternalFileMenuItem(frame(), entry, "", flEntry.link, flEntry.type.get().getIcon(), bibDatabaseContext, flEntry.type); item.openLink(); }); } /** * 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 { // This part uses Spin's features: Worker wrk = ((AbstractWorker) o).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 = ((AbstractWorker) o).getCallBack(); ((AbstractWorker) o).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. } } 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 database"); try { SavePreferences prefs = SavePreferences.loadForSaveFromPreferences(Globals.prefs).withEncoding(enc) .withSaveType(saveType); BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(FileSaveSession::new); if (selectedOnly) { session = databaseWriter.savePartOfDatabase(bibDatabaseContext, mainTable.getSelectedEntries(), prefs); } else { session = databaseWriter.saveDatabase(bibDatabaseContext, prefs); } registerUndoableChanges(session); } catch (UnsupportedCharsetException ex2) { 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 // be. Highlight it: final int row = mainTable.findEntry(ex.getEntry()); final int topShow = Math.max(0, row - 3); mainTable.setRowSelectionInterval(row, row); mainTable.scrollTo(topShow); 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. String id = IdGenerator.next(); final BibEntry be = new BibEntry(id, actualType.getName()); try { database.insertEntry(be); // Set owner/timestamp if options are enabled: List<BibEntry> list = new ArrayList<>(); list.add(be); UpdateField.setAutomaticFields(list, true, true); // Create an UndoableInsertEntry object. getUndoManager().addEdit(new UndoableInsertEntry(database, be, BasePanel.this)); output(Localization.lang("Added new '%0' entry.", actualType.getName().toLowerCase())); // 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; } int row = mainTable.findEntry(be); if (row >= 0) { highlightEntry(be); // Selects the entry. The selection listener will open the editor. } else { // The entry is not visible in the table, perhaps due to a filtering search // or group selection. Show the entry editor anyway: showEntry(be); } markBaseChanged(); // The database just changed. new FocusRequester(getEntryEditor(be)); return be; } catch (KeyCollisionException ex) { LOGGER.info(ex.getMessage(), ex); } } return null; } public SearchBar getSearchBar() { return searchBar; } 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.isUndo()) { scheduleUpdate(); return; } // Automatically add new entry to the selected group (or set of groups) if (Globals.prefs.getBoolean(JabRefPreferences.AUTO_ASSIGN_GROUP) && frame.groupToggle.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()); } } /** * 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 { database.insertEntry(bibEntry); if (Globals.prefs.getBoolean(JabRefPreferences.USE_OWNER)) { // Set owner field to default value UpdateField.setAutomaticFields(bibEntry, true, true); } // Create an UndoableInsertEntry object. getUndoManager().addEdit(new UndoableInsertEntry(database, 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 editEntryByKeyAndFocusField(final String bibtexKey, final String fieldName) { final List<BibEntry> entries = database.getEntriesByKey(bibtexKey); if (entries.size() == 1) { mainTable.setSelected(mainTable.findEntry(entries.get(0))); selectionListener.editSignalled(); final EntryEditor editor = getEntryEditor(entries.get(0)); editor.setFocusToField(fieldName); new FocusRequester(editor); } } public void updateTableFont() { mainTable.updateFont(); } private void createMainTable() { database.registerListener(tableModel.getListSynchronizer()); database.registerListener(SpecialFieldDatabaseChangeListener.getInstance()); tableFormat = new MainTableFormat(database); 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 will take care of highlighting groups as the selection changes: groupsHighlightListener = listEvent -> { HighlightMatchingGroupPreferences highlightMatchingGroupPreferences = new HighlightMatchingGroupPreferences( Globals.prefs); if (highlightMatchingGroupPreferences.isAny()) { getGroupSelector().showMatchingGroups(mainTable.getSelectedEntries(), false); } else if (highlightMatchingGroupPreferences.isAll()) { getGroupSelector().showMatchingGroups(mainTable.getSelectedEntries(), true); } else { // no highlight getGroupSelector().showMatchingGroups(null, true); } }; mainTable.addSelectionListener(groupsHighlightListener); 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) { // The up/down/left/rightkeystrokes are displayed in the // GroupSelector's popup menu, so if they are to be changed, // edit GroupSelector.java accordingly! case KeyEvent.VK_UP: e.consume(); if (node != null) { frame.getGroupSelector().moveNodeUp(node, true); } break; case KeyEvent.VK_DOWN: e.consume(); if (node != null) { frame.getGroupSelector().moveNodeDown(node, true); } break; case KeyEvent.VK_LEFT: e.consume(); if (node != null) { frame.getGroupSelector().moveNodeLeft(node, true); } break; case KeyEvent.VK_RIGHT: e.consume(); if (node != null) { frame.getGroupSelector().moveNodeRight(node, true); } break; 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(); for (EntryEditor ee : entryEditors.values()) { ee.validateAllFields(); } 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; int row = mainTable.findEntry(currentPreview.getEntry()); if (row >= 0) { mainTable.setRowSelectionInterval(row, row); } } else if (mode == BasePanelMode.SHOWING_EDITOR) { mode = BasePanelMode.SHOWING_NOTHING; } else { splitPane.setBottomComponent(null); } setLayout(new BorderLayout()); removeAll(); add(searchBar, BorderLayout.NORTH); 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(); } public void updateSearchManager() { searchBar.setAutoCompleter(searchAutoCompleter); } private void instantiateSearchAutoCompleter() { AutoCompletePreferences autoCompletePreferences = new AutoCompletePreferences(Globals.prefs); AutoCompleterFactory autoCompleterFactory = new AutoCompleterFactory(autoCompletePreferences, Globals.journalAbbreviationLoader); searchAutoCompleter = autoCompleterFactory.getPersonAutoCompleter(); for (BibEntry entry : database.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.getInt(JabRefPreferences.PREVIEW_PANEL_HEIGHT)); } 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); } else { // The correct entry is already being shown. Make sure the editor // is updated. ((EntryEditor) splitPane.getBottomComponent()).updateAllFields(); } return; } EntryEditor form; int divLoc = -1; String visName = null; if ((getShowing() != null) && isShowingEditor()) { visName = ((EntryEditor) splitPane.getBottomComponent()).getVisiblePanelName(); } if (getShowing() != null) { divLoc = splitPane.getDividerLocation(); } if (entryEditors.containsKey(be.getType())) { // We already have an editor for this entry type. form = entryEditors.get(be.getType()); form.switchTo(be); if (visName != null) { form.setVisiblePanel(visName); } splitPane.setBottomComponent(form); } else { // We must instantiate a new editor for this type. form = new EntryEditor(frame, BasePanel.this, be); if (visName != null) { form.setVisiblePanel(visName); } splitPane.setBottomComponent(form); entryEditors.put(be.getType(), form); } if (divLoc > 0) { splitPane.setDividerLocation(divLoc); } else { splitPane.setDividerLocation( splitPane.getHeight() - Globals.prefs.getInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT)); } 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) { EntryEditor form; if (entryEditors.containsKey(entry.getType())) { EntryEditor visibleNow = currentEditor; // We already have an editor for this entry type. form = entryEditors.get(entry.getType()); // If the cached editor is not the same as the currently shown one, // make sure the current one stores its current edit: if ((visibleNow != null) && (!(form.equals(visibleNow)))) { visibleNow.storeCurrentEdit(); } form.switchTo(entry); } else { // We must instantiate a new editor for this type. First make sure the old one // stores its last edit: storeCurrentEdit(); // Then start the new one: form = new EntryEditor(frame, BasePanel.this, entry); entryEditors.put(entry.getType(), form); } return form; } 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()); } else if (mode == BasePanelMode.SHOWING_PREVIEW) { Globals.prefs.putInt(JabRefPreferences.PREVIEW_PANEL_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); } mode = BasePanelMode.SHOWING_EDITOR; 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; currentPreview = preview; splitPane.setBottomComponent(preview); } /** * 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 be) { final int row = mainTable.findEntry(be); if (row >= 0) { mainTable.setRowSelectionInterval(row, row); mainTable.ensureVisible(row); } } /** * 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) && (currentPreview.getEntry() == entry))) { hideBottomComponent(); } } public void updateEntryEditorIfShowing() { if (mode == BasePanelMode.SHOWING_EDITOR) { if (currentEditor.getDisplayedBibEntryType().equals(currentEditor.getEntry().getType())) { currentEditor.updateAllFields(); 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(); } } /** * 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() { for (Map.Entry<String, EntryEditor> stringEntryEditorEntry : entryEditors.entrySet()) { EntryEditor ed = stringEntryEditorEntry.getValue(); ed.updateAllContentSelectors(); } } public void rebuildAllEntryEditors() { for (Map.Entry<String, EntryEditor> stringEntryEditorEntry : entryEditors.entrySet()) { EntryEditor ed = stringEntryEditorEntry.getValue(); ed.rebuildPanels(); } } public void markBaseChanged() { baseChanged = true; // 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 database"))) { frame.output(" "); } } public void markNonUndoableBaseChanged() { nonUndoableChange = true; markBaseChanged(); } private synchronized void markChangedOrUnChanged() { if (getUndoManager().hasChanged()) { if (!baseChanged) { markBaseChanged(); } } else if (baseChanged && !nonUndoableChange) { baseChanged = false; if (getBibDatabaseContext().getDatabaseFile() == null) { frame.setTabTitle(this, GUIGlobals.UNTITLED_TITLE, null); } else { frame.setTabTitle(this, getTabTitle(), getBibDatabaseContext().getDatabaseFile().getAbsolutePath()); } } frame.setWindowTitle(); } /** * Selects a single entry, and scrolls the table to center it. * * @param pos Current position of entry to select. */ public void selectSingleEntry(int pos) { mainTable.clearSelection(); mainTable.addRowSelectionInterval(pos, pos); mainTable.scrollToCenter(pos, 0); } public BibDatabase getDatabase() { return database; } 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")); boolean any = false; for (BibEntry bes : database.getEntries()) { String oldKey = bes.getCiteKey(); if ((oldKey == null) || oldKey.isEmpty()) { LabelPatternUtil.makeLabel(bibDatabaseContext.getMetaData(), database, bes, Globals.prefs); ce.addEdit(new UndoableKeyChange(database, bes, null, bes.getCiteKey())); any = true; } } // Store undo information, if any: if (any) { 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) { Globals.prefs.putInt(JabRefPreferences.PREVIEW_PANEL_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); } else if (mode == BasePanelMode.SHOWING_EDITOR) { Globals.prefs.putInt(JabRefPreferences.ENTRY_EDITOR_HEIGHT, splitPane.getHeight() - splitPane.getDividerLocation()); } } 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(); } } String name = getUndoManager().getUndoPresentationName(); getUndoManager().undo(); markBaseChanged(); frame.output(name); } catch (CannotUndoException ex) { LOGGER.warn("Nothing to undo", ex); frame.output(Localization.lang("Nothing to undo") + '.'); } markChangedOrUnChanged(); } } private class OpenURLAction implements BaseAction { private static final String PS_FIELD = "ps"; private static final String PDF_FIELD = "pdf"; @Override public void action() { final List<BibEntry> bes = mainTable.getSelectedEntries(); if (bes.size() == 1) { String field = FieldName.DOI; Optional<String> link = bes.get(0).getFieldOptional(FieldName.DOI); if (bes.get(0).hasField(FieldName.URL)) { link = bes.get(0).getFieldOptional(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).getFieldOptional(FieldName.FILE).ifPresent(tm::setContent); for (int i = 0; i < tm.getRowCount(); i++) { FileListEntry flEntry = tm.getEntry(i); if (FieldName.URL.equalsIgnoreCase(flEntry.type.get().getName()) || PS_FIELD.equalsIgnoreCase(flEntry.type.get().getName()) || PDF_FIELD.equalsIgnoreCase(flEntry.type.get().getName())) { entry = flEntry; break; } } if (entry == null) { output(Localization.lang("No URL defined") + '.'); } else { try { JabRefDesktop.openExternalFileAnyFormat(bibDatabaseContext, entry.link, entry.type); 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(); } String name = getUndoManager().getRedoPresentationName(); getUndoManager().redo(); markBaseChanged(); frame.output(name); } catch (CannotRedoException ex) { frame.output(Localization.lang("Nothing to redo") + '.'); } markChangedOrUnChanged(); } } // 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()); // Test: running scan automatically in background if ((getBibDatabaseContext().getDatabaseFile() != null) && !FileBasedLock.waitForFileLock(getBibDatabaseContext().getDatabaseFile().toPath(), 10)) { // 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.executeWithLowPriorityInOwnThreadAndWait(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.NAME); if (hasAlready) { sidePaneManager.hideComponent(FileUpdatePanel.NAME); sidePaneManager.unregisterComponent(FileUpdatePanel.NAME); } FileUpdatePanel pan = new FileUpdatePanel(BasePanel.this, sidePaneManager, getBibDatabaseContext().getDatabaseFile(), scanner); sidePaneManager.register(FileUpdatePanel.NAME, pan); sidePaneManager.show(FileUpdatePanel.NAME); }; if (scanner.changesFound()) { SwingUtilities.invokeLater(t); } else { setUpdatedExternally(false); } } @Override public void fileRemoved() { LOGGER.info("File '" + getBibDatabaseContext().getDatabaseFile().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("fileUpdate")) { FileUpdatePanel fup = (FileUpdatePanel) sidePaneManager.getComponent("fileUpdate"); if (fup.getPanel() == this) { sidePaneManager.hideComponent("fileUpdate"); } } } public void setUpdatedExternally(boolean b) { updatedExternally = b; } /** * 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 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 void setSaving(boolean saving) { this.saving = saving; } public boolean isSaving() { return 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() > Globals.prefs.getInt(JabRefPreferences.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"))); } private class SaveSelectedAction implements BaseAction { private final SavePreferences.DatabaseSaveType saveType; public SaveSelectedAction(SavePreferences.DatabaseSaveType saveType) { this.saveType = saveType; } @Override public void action() throws SaveException { String chosenFile = FileDialogs.getNewFile(frame, new File(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)), Collections.singletonList(".bib"), JFileChooser.SAVE_DIALOG, false); if (chosenFile != null) { File expFile = new File(chosenFile); if (!expFile.exists() || (JOptionPane.showConfirmDialog(frame, Localization.lang("'%0' exists. Overwrite file?", expFile.getName()), Localization.lang("Save database"), JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION)) { saveDatabase(expFile, true, Globals.prefs.getDefaultEncoding(), saveType); frame.getFileHistory().newFile(expFile.getPath()); frame.output(Localization.lang("Saved selected to '%0'.", expFile.getPath())); } } } } 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 Optional<String> searchAndOpen() { if (!Globals.prefs.getBoolean(JabRefPreferences.RUN_AUTOMATIC_FILE_SEARCH)) { return Optional.empty(); } /* 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. */ // see if we can fall back to a filename based on the bibtex key final Collection<BibEntry> entries = Collections.singleton(entry); final Collection<ExternalFileType> types = ExternalFileTypes.getInstance() .getExternalFileTypeSelection(); final List<File> dirs = new ArrayList<>(); if (!basePanel.getBibDatabaseContext().getFileDirectory().isEmpty()) { final List<String> mdDirs = basePanel.getBibDatabaseContext().getFileDirectory(); for (final String mdDir : mdDirs) { dirs.add(new File(mdDir)); } } final List<String> extensions = new ArrayList<>(); for (final ExternalFileType type : types) { extensions.add(type.getExtension()); } // Run the search operation: Map<BibEntry, List<File>> result; if (Globals.prefs.getBoolean(JabRefPreferences.AUTOLINK_USE_REG_EXP_SEARCH_KEY)) { String regExp = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY); result = RegExpFileSearch.findFilesForSet(entries, extensions, dirs, regExp); } else { result = FileUtil.findAssociatedFiles(entries, extensions, dirs, Globals.prefs); } if (result.containsKey(entry)) { final List<File> res = result.get(entry); if (!res.isEmpty()) { final String filepath = res.get(0).getPath(); final Optional<String> extension = FileUtil.getFileExtension(filepath); if (extension.isPresent()) { Optional<ExternalFileType> type = ExternalFileTypes.getInstance() .getExternalFileTypeByExt(extension.get()); if (type.isPresent()) { try { JabRefDesktop.openExternalFileAnyFormat(basePanel.getBibDatabaseContext(), filepath, type); basePanel.output(Localization.lang("External viewer called") + '.'); return Optional.of(filepath); } catch (IOException ex) { basePanel.output(Localization.lang("Error") + ": " + ex.getMessage()); } } } } } return Optional.empty(); } } /** * Set the preview active state for all BasePanel instances. * * @param enabled */ 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 Map<String, EntryEditor> getEntryEditors() { return entryEditors; } }