d2a23ef589b72407efbb39a506682ea1a7d3b8ca
[debian/jabref.git] / src / java / net / sf / jabref / SearchManager2.java
1 /*
2 Copyright (C) 2003 JabRef team
3
4 All programs in this directory and
5 subdirectories are published under the GNU General Public License as
6 described below.
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or (at
11 your option) any later version.
12
13 This program is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21 USA
22
23 Further information about the GNU GPL is available at:
24 http://www.gnu.org/copyleft/gpl.ja.html
25
26 */
27 package net.sf.jabref;
28
29 import java.util.Hashtable;
30 import java.util.Iterator;
31 import java.util.Collection;
32 import java.awt.*;
33 import java.awt.event.*;
34 import javax.swing.*;
35 import javax.swing.event.*;
36 import javax.swing.event.ChangeListener;
37 import javax.swing.event.ChangeEvent;
38
39 import net.sf.jabref.search.*;
40 import net.sf.jabref.search.SearchExpression;
41 import ca.odell.glazedlists.matchers.Matcher;
42 import ca.odell.glazedlists.EventList;
43
44 class SearchManager2 extends SidePaneComponent
45     implements ActionListener, KeyListener, ItemListener, CaretListener, ErrorMessageDisplay {
46
47     private JabRefFrame frame;
48
49     GridBagLayout gbl = new GridBagLayout() ;
50     GridBagConstraints con = new GridBagConstraints() ;
51
52     IncrementalSearcher incSearcher;
53
54     //private JabRefFrame frame;
55     private JTextField searchField = new JTextField("", 12);
56     private JLabel lab = //new JLabel(Globals.lang("Search")+":");
57     new JLabel(new ImageIcon(GUIGlobals.searchIconFile));
58     private JPopupMenu settings = new JPopupMenu();
59     private JButton openset = new JButton(Globals.lang("Settings"));
60     private JButton escape = new JButton(Globals.lang("Clear"));
61     private JButton help = new JButton(new ImageIcon(GUIGlobals.helpIconFile));
62     /** This button's text will be set later. */
63     private JButton search = new JButton();
64     private JCheckBoxMenuItem searchReq, searchOpt, searchGen,
65     searchAll, caseSensitive, regExpSearch;
66
67     private JRadioButton increment, floatSearch, hideSearch;
68     private JCheckBoxMenuItem select;
69     private ButtonGroup types = new ButtonGroup();
70     private boolean incSearch = false, startedFloatSearch=false, startedFilterSearch=false;
71
72     private int incSearchPos = -1; // To keep track of where we are in
73                    // an incremental search. -1 means
74                    // that the search is inactive.
75
76
77     public SearchManager2(JabRefFrame frame, SidePaneManager manager) {
78     super(manager, GUIGlobals.searchIconFile, Globals.lang("Search"));
79
80         this.frame = frame;
81     incSearcher = new IncrementalSearcher(Globals.prefs);
82
83
84
85     //setBorder(BorderFactory.createMatteBorder(1,1,1,1,Color.magenta));
86
87         searchReq = new JCheckBoxMenuItem
88         (Globals.lang("Search required fields"),
89          Globals.prefs.getBoolean("searchReq"));
90     searchOpt = new JCheckBoxMenuItem
91         (Globals.lang("Search optional fields"),
92          Globals.prefs.getBoolean("searchOpt"));
93     searchGen = new JCheckBoxMenuItem
94         (Globals.lang("Search general fields"),
95          Globals.prefs.getBoolean("searchGen"));
96         searchAll = new JCheckBoxMenuItem
97         (Globals.lang("Search all fields"),
98          Globals.prefs.getBoolean("searchAll"));
99         regExpSearch = new JCheckBoxMenuItem
100         (Globals.lang("Use regular expressions"),
101          Globals.prefs.getBoolean("regExpSearch"));
102
103
104     increment = new JRadioButton(Globals.lang("Incremental"), false);
105     floatSearch = new JRadioButton(Globals.lang("Float"), true);
106     hideSearch = new JRadioButton(Globals.lang("Filter"), true);
107     types.add(increment);
108     types.add(floatSearch);
109         types.add(hideSearch);
110
111         select = new JCheckBoxMenuItem(Globals.lang("Select matches"), false);
112         increment.setToolTipText(Globals.lang("Incremental search"));
113         floatSearch.setToolTipText(Globals.lang("Gray out non-matching entries"));
114         hideSearch.setToolTipText(Globals.lang("Hide non-matching entries"));
115
116     // Add an item listener that makes sure we only listen for key events
117     // when incremental search is turned on.
118     increment.addItemListener(this);
119         floatSearch.addItemListener(this);
120         hideSearch.addItemListener(this);
121
122         // Add the global focus listener, so a menu item can see if this field was focused when
123         // an action was called.
124         searchField.addFocusListener(Globals.focusListener);
125
126
127     if (searchAll.isSelected()) {
128         searchReq.setEnabled(false);
129         searchOpt.setEnabled(false);
130         searchGen.setEnabled(false);
131     }
132     searchAll.addChangeListener(new ChangeListener() {
133         public void stateChanged(ChangeEvent event) {
134             boolean state = !searchAll.isSelected();
135             searchReq.setEnabled(state);
136             searchOpt.setEnabled(state);
137             searchGen.setEnabled(state);
138         }
139     });
140
141         caseSensitive = new JCheckBoxMenuItem(Globals.lang("Case sensitive"),
142                       Globals.prefs.getBoolean("caseSensitiveSearch"));
143 settings.add(select);
144
145     // 2005.03.29, trying to remove field category searches, to simplify
146         // search usability.
147     //settings.addSeparator();
148     //settings.add(searchReq);
149     //settings.add(searchOpt);
150     //settings.add(searchGen);
151     //settings.addSeparator();
152     //settings.add(searchAll);
153     // ---------------------------------------------------------------
154     settings.addSeparator();
155         settings.add(caseSensitive);
156     settings.add(regExpSearch);
157     //settings.addSeparator();
158
159
160     searchField.addActionListener(this);
161     searchField.addCaretListener(this);
162         search.addActionListener(this);
163     searchField.addFocusListener(new FocusAdapter() {
164           public void focusGained(FocusEvent e) {
165             if (increment.isSelected())
166               searchField.setText("");
167           }
168         public void focusLost(FocusEvent e) {
169             incSearch = false;
170             incSearchPos = -1; // Reset incremental
171                        // search. This makes the
172                        // incremental search reset
173                        // once the user moves focus to
174                        // somewhere else.
175                     if (increment.isSelected()) {
176                       //searchField.setText("");
177                       //System.out.println("focuslistener");
178                     }
179         }
180         });
181     escape.addActionListener(this);
182     escape.setEnabled(false); // enabled after searching
183
184     openset.addActionListener(new ActionListener() {
185         public void actionPerformed(ActionEvent e) {
186                   if (settings.isVisible()) {
187                     //System.out.println("oee");
188                     //settings.setVisible(false);
189                   }
190                   else {
191                     JButton src = (JButton) e.getSource();
192                     settings.show(src, 0, openset.getHeight());
193                   }
194         }
195         });
196
197             Insets margin = new Insets(0, 2, 0, 2);
198             //search.setMargin(margin);
199             escape.setMargin(margin);
200             openset.setMargin(margin);
201             Dimension butDim = new Dimension(20, 20);
202             help.setPreferredSize(butDim);
203             help.setMinimumSize(butDim);
204             help.setMargin(margin);
205             help.addActionListener(new HelpAction(Globals.helpDiag, GUIGlobals.searchHelp, "Help"));
206
207     if (Globals.prefs.getBoolean("incrementS"))
208         increment.setSelected(true);
209
210     JPanel main = new JPanel();
211     main.setLayout(gbl);
212     //SidePaneHeader header = new SidePaneHeader("Search", GUIGlobals.searchIconFile, this);
213     con.gridwidth = GridBagConstraints.REMAINDER;
214     con.fill = GridBagConstraints.BOTH;
215         con.weightx = 1;
216     //con.insets = new Insets(0, 0, 2,  0);
217     //gbl.setConstraints(header, con);
218     //add(header);
219         //con.insets = new Insets(0, 0, 0,  0);
220         gbl.setConstraints(searchField,con);
221         main.add(searchField) ;
222         //con.gridwidth = 1;
223         gbl.setConstraints(search,con);
224         main.add(search) ;
225         con.gridwidth = GridBagConstraints.REMAINDER;
226         gbl.setConstraints(escape,con);
227         main.add(escape) ;
228         con.insets = new Insets(0, 2, 0,  0);
229         gbl.setConstraints(increment, con);
230         main.add(increment);
231         gbl.setConstraints(floatSearch, con);
232         main.add(floatSearch);
233         gbl.setConstraints(hideSearch, con);
234         main.add(hideSearch);
235     con.insets = new Insets(0, 0, 0,  0);
236         JPanel pan = new JPanel();
237         GridBagLayout gb = new GridBagLayout();
238         gbl.setConstraints(pan, con);
239         pan.setLayout(gb);
240         con.weightx = 1;
241         con.gridwidth = 1;
242         gb.setConstraints(openset, con);
243         pan.add(openset);
244         con.weightx = 0;
245         gb.setConstraints(help, con);
246         pan.add(help);
247         main.add(pan);
248         main.setBorder(BorderFactory.createEmptyBorder(1,1,1,1));
249         add(main, BorderLayout.CENTER);
250
251     searchField.getInputMap().put(Globals.prefs.getKey("Repeat incremental search"),
252                       "repeat");
253
254     searchField.getActionMap().put("repeat", new AbstractAction() {
255         public void actionPerformed(ActionEvent e) {
256             if (increment.isSelected())
257             repeatIncremental();
258         }
259         });
260     searchField.getInputMap().put(Globals.prefs.getKey("Clear search"), "escape");
261     searchField.getActionMap().put("escape", new AbstractAction() {
262         public void actionPerformed(ActionEvent e) {
263             hideAway();
264             //SearchManager2.this.actionPerformed(new ActionEvent(escape, 0, ""));
265         }
266         });
267     setSearchButtonSizes();
268     updateSearchButtonText();
269     }
270
271     /** force the search button to be large enough for
272      * the longer of the two texts */
273     private void setSearchButtonSizes() {
274         search.setText(Globals.lang("Search Specified Field(s)"));
275         Dimension size1 = search.getPreferredSize();
276         search.setText(Globals.lang("Search All Fields"));
277         Dimension size2 = search.getPreferredSize();
278         size2.width = Math.max(size1.width,size2.width);
279         search.setMinimumSize(size2);
280         search.setPreferredSize(size2);
281     }
282
283     public void updatePrefs() {
284     Globals.prefs.putBoolean("searchReq", searchReq.isSelected());
285     Globals.prefs.putBoolean("searchOpt", searchOpt.isSelected());
286     Globals.prefs.putBoolean("searchGen", searchGen.isSelected());
287     Globals.prefs.putBoolean("searchAll", searchAll.isSelected());
288     Globals.prefs.putBoolean("incrementS", increment.isSelected());
289     Globals.prefs.putBoolean("selectS", select.isSelected());
290     Globals.prefs.putBoolean("grayOutNonHits", floatSearch.isSelected());
291     Globals.prefs.putBoolean("caseSensitiveSearch",
292              caseSensitive.isSelected());
293     Globals.prefs.putBoolean("regExpSearch", regExpSearch.isSelected());
294
295     }
296
297     public void startIncrementalSearch() {
298     increment.setSelected(true);
299     searchField.setText("");
300         //System.out.println("startIncrementalSearch");
301     searchField.requestFocus();
302     }
303
304     /**
305      * Clears and focuses the search field if it is not
306      * focused. Otherwise, cycles to the next search type.
307      */
308     public void startSearch() {
309     if (increment.isSelected() && incSearch) {
310         repeatIncremental();
311         return;
312     }
313     if (!searchField.hasFocus()) {
314         //searchField.setText("");
315             searchField.selectAll();
316         searchField.requestFocus();
317     } else {
318         if (increment.isSelected())
319             floatSearch.setSelected(true);
320         else if (floatSearch.isSelected())
321             hideSearch.setSelected(true);
322         else {
323         increment.setSelected(true);
324         }
325         increment.revalidate();
326         increment.repaint();
327
328         searchField.requestFocus();
329
330     }
331     }
332
333     public void actionPerformed(ActionEvent e) {
334     if (e.getSource() == escape) {
335         incSearch = false;
336         if (panel != null) {
337             Thread t = new Thread() {
338                 public void run() {
339                     clearSearch();
340                 }
341             };
342             // do this after the button action is over
343             SwingUtilities.invokeLater(t);
344         }
345     }
346     else if (((e.getSource() == searchField) || (e.getSource() == search))
347          && !increment.isSelected()
348          && (panel != null)) {
349         updatePrefs(); // Make sure the user's choices are recorded.
350             if (searchField.getText().equals("")) {
351               // An empty search field should cause the search to be cleared.
352               panel.stopShowingSearchResults();
353               return;
354             }
355         // Setup search parameters common to both normal and float.
356         Hashtable searchOptions = new Hashtable();
357         searchOptions.put("option",searchField.getText()) ;
358         SearchRuleSet searchRules = new SearchRuleSet() ;
359         SearchRule rule1;
360         if (Globals.prefs.getBoolean("regExpSearch"))
361             rule1 = new RegExpRule(
362                     Globals.prefs.getBoolean("caseSensitiveSearch"));
363         else
364             rule1 = new SimpleSearchRule(
365                     Globals.prefs.getBoolean("caseSensitiveSearch"));
366
367         try {
368             // this searches specified fields if specified,
369             // and all fields otherwise
370             rule1 = new SearchExpression(Globals.prefs,searchOptions);
371         } catch (Exception ex) {
372             // we'll do a search in all fields
373         }
374
375         searchRules.addRule(rule1) ;
376         SearchWorker worker = new SearchWorker(searchRules, searchOptions);
377         worker.getWorker().run();
378         worker.getCallBack().update();
379         escape.setEnabled(true);
380     }
381     }
382
383     class SearchWorker extends AbstractWorker {
384         private SearchRuleSet rules;
385         Hashtable searchTerm;
386         int hits = 0;
387         public SearchWorker(SearchRuleSet rules, Hashtable searchTerm) {
388             this.rules = rules;
389             this.searchTerm = searchTerm;
390         }
391
392         public void run() {
393             Collection entries = panel.getDatabase().getEntries();
394             for (Iterator i=entries.iterator(); i.hasNext();) {
395                 BibtexEntry entry = (BibtexEntry)i.next();
396                 boolean hit = rules.applyRule(searchTerm, entry) > 0;
397                 entry.setSearchHit(hit);
398                 if (hit) hits++;
399             }
400         }
401
402         public void update() {
403             panel.output(Globals.lang("Searched database. Number of hits")
404                     + ": " + hits);
405
406             // Show the result in the chosen way:
407             if (hideSearch.isSelected()) {
408                 // Filtering search - removes non-hits from the table:
409                 if (startedFloatSearch) {
410                     panel.mainTable.stopShowingFloatSearch();
411                     startedFloatSearch = false;
412                 }
413                 startedFilterSearch = true;
414                 panel.setSearchMatcher(SearchMatcher.INSTANCE);
415
416             } else {
417                 // Float search - floats hits to the top of the table:
418                 if (startedFilterSearch) {
419                     panel.stopShowingSearchResults();
420                     startedFilterSearch = false;
421                 }
422                 startedFloatSearch = true;
423                 panel.mainTable.showFloatSearch(SearchMatcher.INSTANCE);
424
425             }
426
427             // Afterwards, select all text in the search field.
428             searchField.select(0, searchField.getText().length());
429
430         }
431     }
432
433     public void clearSearch() {
434         if (startedFloatSearch) {
435             startedFloatSearch = false;
436             panel.mainTable.stopShowingFloatSearch();
437         } else if (startedFilterSearch) {
438             startedFilterSearch = false;
439             panel.stopShowingSearchResults();
440         }
441         // disable "Cancel" button to signal this to the user
442         escape.setEnabled(false);
443     }
444     public void itemStateChanged(ItemEvent e) {
445     if (e.getSource() == increment) {
446         if (startedFilterSearch || startedFloatSearch) {
447             clearSearch();
448         }
449         updateSearchButtonText();
450         if (increment.isSelected())
451         searchField.addKeyListener(this);
452         else
453         searchField.removeKeyListener(this);
454     } else /*if (e.getSource() == normal)*/ {
455         updateSearchButtonText();
456
457         // If this search type is disabled, remove reordering from
458         // all databases.
459         /*if ((panel != null) && increment.isSelected()) {
460             clearSearch();
461         } */
462     }
463     }
464
465     private void repeatIncremental() {
466     incSearchPos++;
467     if (panel != null)
468         goIncremental();
469     }
470
471     /**
472      * Used for incremental search. Only activated when incremental
473      * is selected.
474      *
475      * The variable incSearchPos keeps track of which entry was last
476      * checked.
477      */
478     public void keyTyped(KeyEvent e) {
479     if (e.isControlDown()) {
480         return;
481     }
482     if (panel != null)
483         goIncremental();
484     }
485
486     private void goIncremental() {
487     incSearch = true;
488     escape.setEnabled(true);
489     SwingUtilities.invokeLater(new Thread() {
490         public void run() {
491             String text = searchField.getText();
492
493
494             if (incSearchPos >= panel.getDatabase().getEntryCount()) {
495             panel.output("'"+text+"' : "+Globals.lang
496
497                      ("Incremental search failed. Repeat to search from top.")+".");
498             incSearchPos = -1;
499             return;
500             }
501
502             if (searchField.getText().equals("")) return;
503             if (incSearchPos < 0)
504             incSearchPos = 0;
505             BibtexEntry be = panel.mainTable.getEntryAt(incSearchPos);
506             while (!incSearcher.search(text, be)) {
507                 incSearchPos++;
508                 if (incSearchPos < panel.getDatabase().getEntryCount())
509                     be = panel.mainTable.getEntryAt(incSearchPos);
510             else {
511                 panel.output("'"+text+"' : "+Globals.lang
512                      ("Incremental search failed. Repeat to search from top."));
513                 incSearchPos = -1;
514                 return;
515             }
516             }
517             if (incSearchPos >= 0) {
518
519             panel.selectSingleEntry(incSearchPos);
520             panel.output("'"+text+"' "+Globals.lang
521
522                      ("found")+".");
523
524             }
525         }
526         });
527     }
528
529     public void componentClosing() {
530     frame.searchToggle.setSelected(false);
531         if (panel != null) {
532             if (startedFilterSearch || startedFloatSearch)
533                 clearSearch();
534         }
535     }
536
537
538     public void keyPressed(KeyEvent e) {}
539     public void keyReleased(KeyEvent e) {}
540
541     public void caretUpdate(CaretEvent e) {
542         if (e.getSource() == searchField) {
543             updateSearchButtonText();
544         }
545     }
546
547     /** Updates the text on the search button to reflect
548       * the type of search that will happen on click. */
549     private void updateSearchButtonText() {
550         search.setText(!increment.isSelected()
551                 && SearchExpressionParser.checkSyntax(
552                 searchField.getText(),
553                 caseSensitive.isSelected(),
554                 regExpSearch.isSelected()) != null
555                 ? Globals.lang("Search Specified Field(s)")
556                 : Globals.lang("Search All Fields"));
557     }
558
559     /**
560      * This method is required by the ErrorMessageDisplay interface, and lets this class
561      * serve as a callback for regular expression exceptions happening in DatabaseSearch.
562      * @param errorMessage
563      */
564     public void reportError(String errorMessage) {
565         JOptionPane.showMessageDialog(panel, errorMessage, Globals.lang("Search error"),
566                 JOptionPane.ERROR_MESSAGE);
567     }
568
569     /**
570      * This method is required by the ErrorMessageDisplay interface, and lets this class
571      * serve as a callback for regular expression exceptions happening in DatabaseSearch.
572      * @param errorMessage
573      */
574     public void reportError(String errorMessage, Exception exception) {
575         reportError(errorMessage);
576     }
577 }