package org.jabref.gui.search; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.event.ActionEvent; import java.io.File; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import org.jabref.Globals; import org.jabref.gui.BasePanel; import org.jabref.gui.GUIGlobals; import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefFrame; import org.jabref.gui.OSXCompatibleToolbar; import org.jabref.gui.autocompleter.AutoCompleteSupport; import org.jabref.gui.help.HelpAction; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.maintable.MainTable; import org.jabref.gui.maintable.MainTableDataModel; import org.jabref.gui.util.component.JTextFieldWithPlaceholder; import org.jabref.logic.autocompleter.AutoCompleter; import org.jabref.logic.help.HelpFile; import org.jabref.logic.l10n.Localization; import org.jabref.logic.search.SearchQuery; import org.jabref.logic.search.SearchQueryHighlightObservable; import org.jabref.logic.util.OS; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.SearchPreferences; public class GlobalSearchBar extends JPanel { private static final Color NEUTRAL_COLOR = Color.WHITE; private static final Color NO_RESULTS_COLOR = new Color(232, 202, 202); private static final Color RESULTS_FOUND_COLOR = new Color(217, 232, 202); private static final Color ADVANCED_SEARCH_COLOR = new Color(102, 255, 255); private final JabRefFrame frame; private final JLabel searchIcon = new JLabel(IconTheme.JabRefIcon.SEARCH.getSmallIcon()); private final JTextFieldWithPlaceholder searchField = new JTextFieldWithPlaceholder(Localization.lang("Search") + "..."); private JButton openCurrentResultsInDialog = new JButton(IconTheme.JabRefIcon.OPEN_IN_NEW_WINDOW.getSmallIcon()); private final JToggleButton caseSensitive; private final JToggleButton regularExp; private final JButton searchModeButton = new JButton(); private final JLabel currentResults = new JLabel(""); private AutoCompleteSupport<String> autoCompleteSupport = new AutoCompleteSupport<>(searchField); private final SearchQueryHighlightObservable searchQueryHighlightObservable = new SearchQueryHighlightObservable(); private SearchWorker searchWorker; private GlobalSearchWorker globalSearchWorker; private SearchResultFrame searchResultFrame; private SearchDisplayMode searchDisplayMode; /** * if this flag is set the searchbar won't be selected after the next search */ private boolean dontSelectSearchBar; public GlobalSearchBar(JabRefFrame frame) { super(); this.frame = Objects.requireNonNull(frame); SearchPreferences searchPreferences = new SearchPreferences(Globals.prefs); searchDisplayMode = searchPreferences.getSearchMode(); // fits the standard "found x entries"-message thus hinders the searchbar to jump around while searching if the frame width is too small currentResults.setPreferredSize(new Dimension(150, 5)); currentResults.setFont(currentResults.getFont().deriveFont(Font.BOLD)); searchField.setColumns(30); JToggleButton globalSearch = new JToggleButton(IconTheme.JabRefIcon.GLOBAL_SEARCH.getSmallIcon(), searchPreferences.isGlobalSearch()); globalSearch.setToolTipText(Localization.lang("Search in all open libraries")); // default action to be performed for toggling globalSearch AbstractAction globalSearchStandardAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { searchPreferences.setGlobalSearch(globalSearch.isSelected()); updateOpenCurrentResultsTooltip(globalSearch.isSelected()); } }; // additional action for global search shortcut AbstractAction globalSearchShortCutAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { globalSearch.setSelected(true); globalSearchStandardAction.actionPerformed(new ActionEvent(this, 0, "fire standard action")); focus(); } }; String searchGlobalByKey = "searchGlobalByKey"; globalSearch.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(Globals.getKeyPrefs().getKey(KeyBinding.GLOBAL_SEARCH), searchGlobalByKey); globalSearch.getActionMap().put(searchGlobalByKey, globalSearchShortCutAction); globalSearch.addActionListener(globalSearchStandardAction); openCurrentResultsInDialog.setDisabledIcon(IconTheme.JabRefIcon.OPEN_IN_NEW_WINDOW.getSmallIcon().createDisabledIcon()); openCurrentResultsInDialog.addActionListener(event -> { if (globalSearch.isSelected()) { performGlobalSearch(); } else { openLocalFindingsInExternalPanel(); } }); openCurrentResultsInDialog.setEnabled(false); updateOpenCurrentResultsTooltip(globalSearch.isSelected()); regularExp = new JToggleButton(IconTheme.JabRefIcon.REG_EX.getSmallIcon(), searchPreferences.isRegularExpression()); regularExp.setToolTipText(Localization.lang("regular expression")); regularExp.addActionListener(event -> { searchPreferences.setRegularExpression(regularExp.isSelected()); performSearch(); }); caseSensitive = new JToggleButton(IconTheme.JabRefIcon.CASE_SENSITIVE.getSmallIcon(), searchPreferences.isCaseSensitive()); caseSensitive.setToolTipText(Localization.lang("Case sensitive")); caseSensitive.addActionListener(event -> { searchPreferences.setCaseSensitive(caseSensitive.isSelected()); performSearch(); }); updateSearchModeButtonText(); searchModeButton.addActionListener(event -> toggleSearchModeAndSearch()); JButton clearSearchButton = new JButton(IconTheme.JabRefIcon.CLOSE.getSmallIcon()); clearSearchButton.setToolTipText(Localization.lang("Clear")); clearSearchButton.addActionListener(event -> endSearch()); searchField.addFocusListener(Globals.getFocusListener()); searchField.addActionListener(event -> performSearch()); JTextFieldChangeListenerUtil.addChangeListener(searchField, e -> performSearch()); String endSearch = "endSearch"; searchField.getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.CLEAR_SEARCH), endSearch); searchField.getActionMap().put(endSearch, new AbstractAction() { @Override public void actionPerformed(ActionEvent event) { if (autoCompleteSupport.isVisible()) { autoCompleteSupport.setVisible(false); } else { endSearch(); } } }); autoCompleteSupport.install(); String acceptSearch = "acceptSearch"; searchField.getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.ACCEPT), acceptSearch); searchField.getActionMap().put(acceptSearch, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { autoCompleteSupport.setVisible(false); BasePanel currentBasePanel = frame.getCurrentBasePanel(); Globals.getFocusListener().setFocused(currentBasePanel.getMainTable()); currentBasePanel.getMainTable().requestFocus(); } }); setLayout(new FlowLayout(FlowLayout.RIGHT)); JToolBar toolBar = new OSXCompatibleToolbar(); toolBar.setFloatable(false); if (OS.OS_X) { searchField.putClientProperty("JTextField.variant", "search"); toolBar.add(searchField); } else { toolBar.add(searchIcon); toolBar.add(searchField); toolBar.add(clearSearchButton); } toolBar.addSeparator(); toolBar.add(openCurrentResultsInDialog); toolBar.addSeparator(); toolBar.add(globalSearch); toolBar.add(regularExp); toolBar.add(caseSensitive); toolBar.add(searchModeButton); toolBar.addSeparator(); toolBar.add(new HelpAction(HelpFile.SEARCH)); toolBar.addSeparator(); toolBar.add(currentResults); this.add(toolBar); } public void performGlobalSearch() { BasePanel currentBasePanel = frame.getCurrentBasePanel(); if (currentBasePanel == null || validateSearchResultFrame(true)) { return; } if (globalSearchWorker != null) { globalSearchWorker.cancel(true); } if (searchField.getText().isEmpty()) { focus(); return; } globalSearchWorker = new GlobalSearchWorker(currentBasePanel.frame(), getSearchQuery()); globalSearchWorker.execute(); } private void openLocalFindingsInExternalPanel() { BasePanel currentBasePanel = frame.getCurrentBasePanel(); if (currentBasePanel == null || validateSearchResultFrame(false)) { return; } if (searchField.getText().isEmpty()) { focus(); return; } SearchResultFrame searchDialog = new SearchResultFrame(currentBasePanel.frame(), Localization.lang("Search results in library %0 for %1", currentBasePanel.getBibDatabaseContext() .getDatabaseFile().map(File::getName).orElse(GUIGlobals.UNTITLED_TITLE), this.getSearchQuery().localize()), getSearchQuery(), false); List<BibEntry> entries = currentBasePanel.getDatabase().getEntries().stream() .filter(BibEntry::isSearchHit) .collect(Collectors.toList()); searchDialog.addEntries(entries, currentBasePanel); searchDialog.selectFirstEntry(); searchDialog.setVisible(true); } private boolean validateSearchResultFrame(boolean globalSearch) { if (searchResultFrame != null) { if (searchResultFrame.isGlobalSearch() == globalSearch && isStillValidQuery(searchResultFrame.getSearchQuery())) { searchResultFrame.focus(); return true; } else { searchResultFrame.dispose(); return false; } } return false; } private void toggleSearchModeAndSearch() { int nextSearchMode = (searchDisplayMode.ordinal() + 1) % SearchDisplayMode.values().length; searchDisplayMode = SearchDisplayMode.values()[nextSearchMode]; new SearchPreferences(Globals.prefs).setSearchMode(searchDisplayMode); updateSearchModeButtonText(); performSearch(); } private void updateSearchModeButtonText() { searchModeButton.setText(searchDisplayMode.getDisplayName()); searchModeButton.setToolTipText(searchDisplayMode.getToolTipText()); } public void endSearch() { BasePanel currentBasePanel = frame.getCurrentBasePanel(); if (currentBasePanel != null) { clearSearch(currentBasePanel); MainTable mainTable = frame.getCurrentBasePanel().getMainTable(); Globals.getFocusListener().setFocused(mainTable); mainTable.requestFocus(); SwingUtilities.invokeLater(() -> mainTable.ensureVisible(mainTable.getSelectedRow())); } } /** * Focuses the search field if it is not focused. */ public void focus() { if (!searchField.hasFocus()) { searchField.requestFocus(); } searchField.selectAll(); } private void clearSearch(BasePanel currentBasePanel) { currentResults.setText(""); searchField.setText(""); searchField.setBackground(NEUTRAL_COLOR); searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getSmallIcon()); searchQueryHighlightObservable.reset(); openCurrentResultsInDialog.setEnabled(false); if (currentBasePanel != null) { currentBasePanel.getMainTable().getTableModel().updateSearchState(MainTableDataModel.DisplayOption.DISABLED); currentBasePanel.setCurrentSearchQuery(null); } if (dontSelectSearchBar) { dontSelectSearchBar = false; return; } focus(); } public void performSearch() { BasePanel currentBasePanel = frame.getCurrentBasePanel(); if (currentBasePanel == null) { return; } if (searchWorker != null) { searchWorker.cancel(true); } // An empty search field should cause the search to be cleared. if (searchField.getText().isEmpty()) { clearSearch(currentBasePanel); return; } SearchQuery searchQuery = getSearchQuery(); if (!searchQuery.isValid()) { informUserAboutInvalidSearchQuery(); return; } searchWorker = new SearchWorker(currentBasePanel, searchQuery, searchDisplayMode); searchWorker.execute(); } private void informUserAboutInvalidSearchQuery() { searchField.setBackground(NO_RESULTS_COLOR); searchQueryHighlightObservable.reset(); BasePanel currentBasePanel = frame.getCurrentBasePanel(); currentBasePanel.getMainTable().getTableModel().updateSearchState(MainTableDataModel.DisplayOption.DISABLED); searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getSmallIcon().createWithNewColor(NO_RESULTS_COLOR)); String illegalSearch = Localization.lang("Search failed: illegal search expression"); searchIcon.setToolTipText(illegalSearch); currentResults.setText(illegalSearch); openCurrentResultsInDialog.setEnabled(false); } public void setAutoCompleter(AutoCompleter<String> searchCompleter) { this.autoCompleteSupport.setAutoCompleter(searchCompleter); } public SearchQueryHighlightObservable getSearchQueryHighlightObservable() { return searchQueryHighlightObservable; } public boolean isStillValidQuery(SearchQuery query) { return query.getQuery().equals(this.searchField.getText()) && (query.isRegularExpression() == regularExp.isSelected()) && (query.isCaseSensitive() == caseSensitive.isSelected()); } private SearchQuery getSearchQuery() { SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), this.caseSensitive.isSelected(), this.regularExp.isSelected()); this.frame.getCurrentBasePanel().setCurrentSearchQuery(searchQuery); return searchQuery; } public void updateResults(int matched, String description, boolean grammarBasedSearch) { if (matched == 0) { currentResults.setText(Localization.lang("No results found.")); this.searchField.setBackground(NO_RESULTS_COLOR); } else { currentResults.setText(Localization.lang("Found %0 results.", String.valueOf(matched))); this.searchField.setBackground(RESULTS_FOUND_COLOR); } this.searchField.setToolTipText("<html>" + description + "</html>"); if (grammarBasedSearch) { searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getSmallIcon().createWithNewColor(ADVANCED_SEARCH_COLOR)); searchIcon.setToolTipText(Localization.lang("Advanced search active.")); } else { searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getSmallIcon()); searchIcon.setToolTipText(Localization.lang("Normal search active.")); } openCurrentResultsInDialog.setEnabled(true); } public void setSearchResultFrame(SearchResultFrame searchResultFrame) { this.searchResultFrame = searchResultFrame; } public void setSearchTerm(String searchTerm, boolean dontSelectSearchBar) { if (searchTerm.equals(searchField.getText())) { return; } setDontSelectSearchBar(dontSelectSearchBar); searchField.setText(searchTerm); // to hinder the autocomplete window to popup autoCompleteSupport.setVisible(false); } public void setDontSelectSearchBar(boolean dontSelectSearchBar) { this.dontSelectSearchBar = dontSelectSearchBar; } private void updateOpenCurrentResultsTooltip(boolean globalSearchEnabled) { if (globalSearchEnabled) { openCurrentResultsInDialog.setToolTipText(Localization.lang("Show global search results in a window")); } else { openCurrentResultsInDialog.setToolTipText(Localization.lang("Show search results in a window")); } } }