d146fbedef1ccaa7cd6dce8a21e37fced8094fcd
[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(GUIGlobals.getImage("search"));
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(GUIGlobals.getImage("help"));
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.getIconUrl("search"), 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             int butSize = help.getIcon().getIconHeight() + 5;
202             Dimension butDim = new Dimension(butSize, butSize);
203             help.setPreferredSize(butDim);
204             help.setMinimumSize(butDim);
205             help.setMargin(margin);
206             help.addActionListener(new HelpAction(Globals.helpDiag, GUIGlobals.searchHelp, "Help"));
207
208     if (Globals.prefs.getBoolean("incrementS"))
209         increment.setSelected(true);
210
211     JPanel main = new JPanel();
212     main.setLayout(gbl);
213     //SidePaneHeader header = new SidePaneHeader("Search", GUIGlobals.searchIconFile, this);
214     con.gridwidth = GridBagConstraints.REMAINDER;
215     con.fill = GridBagConstraints.BOTH;
216         con.weightx = 1;
217     //con.insets = new Insets(0, 0, 2,  0);
218     //gbl.setConstraints(header, con);
219     //add(header);
220         //con.insets = new Insets(0, 0, 0,  0);
221         gbl.setConstraints(searchField,con);
222         main.add(searchField) ;
223         //con.gridwidth = 1;
224         gbl.setConstraints(search,con);
225         main.add(search) ;
226         con.gridwidth = GridBagConstraints.REMAINDER;
227         gbl.setConstraints(escape,con);
228         main.add(escape) ;
229         con.insets = new Insets(0, 2, 0,  0);
230         gbl.setConstraints(increment, con);
231         main.add(increment);
232         gbl.setConstraints(floatSearch, con);
233         main.add(floatSearch);
234         gbl.setConstraints(hideSearch, con);
235         main.add(hideSearch);
236     con.insets = new Insets(0, 0, 0,  0);
237         JPanel pan = new JPanel();
238         GridBagLayout gb = new GridBagLayout();
239         gbl.setConstraints(pan, con);
240         pan.setLayout(gb);
241         con.weightx = 1;
242         con.gridwidth = 1;
243         gb.setConstraints(openset, con);
244         pan.add(openset);
245         con.weightx = 0;
246         gb.setConstraints(help, con);
247         pan.add(help);
248         main.add(pan);
249         main.setBorder(BorderFactory.createEmptyBorder(1,1,1,1));
250         
251         setContent(main);
252
253     searchField.getInputMap().put(Globals.prefs.getKey("Repeat incremental search"),
254                       "repeat");
255
256     searchField.getActionMap().put("repeat", new AbstractAction() {
257         public void actionPerformed(ActionEvent e) {
258             if (increment.isSelected())
259             repeatIncremental();
260         }
261         });
262     searchField.getInputMap().put(Globals.prefs.getKey("Clear search"), "escape");
263     searchField.getActionMap().put("escape", new AbstractAction() {
264         public void actionPerformed(ActionEvent e) {
265             hideAway();
266             //SearchManager2.this.actionPerformed(new ActionEvent(escape, 0, ""));
267         }
268         });
269     setSearchButtonSizes();
270     updateSearchButtonText();
271     }
272
273     /** force the search button to be large enough for
274      * the longer of the two texts */
275     private void setSearchButtonSizes() {
276         search.setText(Globals.lang("Search Specified Field(s)"));
277         Dimension size1 = search.getPreferredSize();
278         search.setText(Globals.lang("Search All Fields"));
279         Dimension size2 = search.getPreferredSize();
280         size2.width = Math.max(size1.width,size2.width);
281         search.setMinimumSize(size2);
282         search.setPreferredSize(size2);
283     }
284
285     public void updatePrefs() {
286     Globals.prefs.putBoolean("searchReq", searchReq.isSelected());
287     Globals.prefs.putBoolean("searchOpt", searchOpt.isSelected());
288     Globals.prefs.putBoolean("searchGen", searchGen.isSelected());
289     Globals.prefs.putBoolean("searchAll", searchAll.isSelected());
290     Globals.prefs.putBoolean("incrementS", increment.isSelected());
291     Globals.prefs.putBoolean("selectS", select.isSelected());
292     Globals.prefs.putBoolean("grayOutNonHits", floatSearch.isSelected());
293     Globals.prefs.putBoolean("caseSensitiveSearch",
294              caseSensitive.isSelected());
295     Globals.prefs.putBoolean("regExpSearch", regExpSearch.isSelected());
296
297     }
298
299     public void startIncrementalSearch() {
300     increment.setSelected(true);
301     searchField.setText("");
302         //System.out.println("startIncrementalSearch");
303     searchField.requestFocus();
304     }
305
306     /**
307      * Clears and focuses the search field if it is not
308      * focused. Otherwise, cycles to the next search type.
309      */
310     public void startSearch() {
311     if (increment.isSelected() && incSearch) {
312         repeatIncremental();
313         return;
314     }
315     if (!searchField.hasFocus()) {
316         //searchField.setText("");
317             searchField.selectAll();
318         searchField.requestFocus();
319     } else {
320         if (increment.isSelected())
321             floatSearch.setSelected(true);
322         else if (floatSearch.isSelected())
323             hideSearch.setSelected(true);
324         else {
325         increment.setSelected(true);
326         }
327         increment.revalidate();
328         increment.repaint();
329
330         searchField.requestFocus();
331
332     }
333     }
334
335     public void actionPerformed(ActionEvent e) {
336     if (e.getSource() == escape) {
337         incSearch = false;
338         if (panel != null) {
339             Thread t = new Thread() {
340                 public void run() {
341                     clearSearch();
342                 }
343             };
344             // do this after the button action is over
345             SwingUtilities.invokeLater(t);
346         }
347     }
348     else if (((e.getSource() == searchField) || (e.getSource() == search))
349          && !increment.isSelected()
350          && (panel != null)) {
351         updatePrefs(); // Make sure the user's choices are recorded.
352             if (searchField.getText().equals("")) {
353               // An empty search field should cause the search to be cleared.
354               panel.stopShowingSearchResults();
355               return;
356             }
357         // Setup search parameters common to both normal and float.
358         Hashtable searchOptions = new Hashtable();
359         searchOptions.put("option",searchField.getText()) ;
360         SearchRuleSet searchRules = new SearchRuleSet() ;
361         SearchRule rule1;
362
363         rule1 = new BasicSearch(Globals.prefs.getBoolean("caseSensitiveSearch"),
364                 Globals.prefs.getBoolean("regExpSearch"));
365
366         /*
367         if (Globals.prefs.getBoolean("regExpSearch"))
368             rule1 = new RegExpRule(
369                     Globals.prefs.getBoolean("caseSensitiveSearch"));
370         else {
371             rule1 = new SimpleSearchRule(
372                     Globals.prefs.getBoolean("caseSensitiveSearch"));
373
374         }
375         */
376         try {
377             // this searches specified fields if specified,
378             // and all fields otherwise
379             rule1 = new SearchExpression(Globals.prefs,searchOptions);
380         } catch (Exception ex) {
381             // we'll do a search in all fields
382         }
383
384         searchRules.addRule(rule1) ;
385         SearchWorker worker = new SearchWorker(searchRules, searchOptions);
386         worker.getWorker().run();
387         worker.getCallBack().update();
388         escape.setEnabled(true);
389     }
390     }
391
392     class SearchWorker extends AbstractWorker {
393         private SearchRuleSet rules;
394         Hashtable searchTerm;
395         int hits = 0;
396         public SearchWorker(SearchRuleSet rules, Hashtable searchTerm) {
397             this.rules = rules;
398             this.searchTerm = searchTerm;
399         }
400
401         public void run() {
402             Collection entries = panel.getDatabase().getEntries();
403             for (Iterator i=entries.iterator(); i.hasNext();) {
404                 BibtexEntry entry = (BibtexEntry)i.next();
405                 boolean hit = rules.applyRule(searchTerm, entry) > 0;
406                 entry.setSearchHit(hit);
407                 if (hit) hits++;
408             }
409         }
410
411         public void update() {
412             panel.output(Globals.lang("Searched database. Number of hits")
413                     + ": " + hits);
414
415             // Show the result in the chosen way:
416             if (hideSearch.isSelected()) {
417                 // Filtering search - removes non-hits from the table:
418                 if (startedFloatSearch) {
419                     panel.mainTable.stopShowingFloatSearch();
420                     startedFloatSearch = false;
421                 }
422                 startedFilterSearch = true;
423                 panel.setSearchMatcher(SearchMatcher.INSTANCE);
424
425             } else {
426                 // Float search - floats hits to the top of the table:
427                 if (startedFilterSearch) {
428                     panel.stopShowingSearchResults();
429                     startedFilterSearch = false;
430                 }
431                 startedFloatSearch = true;
432                 panel.mainTable.showFloatSearch(SearchMatcher.INSTANCE);
433
434             }
435
436             // Afterwards, select all text in the search field.
437             searchField.select(0, searchField.getText().length());
438
439         }
440     }
441
442     public void clearSearch() {
443         if (startedFloatSearch) {
444             startedFloatSearch = false;
445             panel.mainTable.stopShowingFloatSearch();
446         } else if (startedFilterSearch) {
447             startedFilterSearch = false;
448             panel.stopShowingSearchResults();
449         }
450         // disable "Cancel" button to signal this to the user
451         escape.setEnabled(false);
452     }
453     public void itemStateChanged(ItemEvent e) {
454     if (e.getSource() == increment) {
455         if (startedFilterSearch || startedFloatSearch) {
456             clearSearch();
457         }
458         updateSearchButtonText();
459         if (increment.isSelected())
460         searchField.addKeyListener(this);
461         else
462         searchField.removeKeyListener(this);
463     } else /*if (e.getSource() == normal)*/ {
464         updateSearchButtonText();
465
466         // If this search type is disabled, remove reordering from
467         // all databases.
468         /*if ((panel != null) && increment.isSelected()) {
469             clearSearch();
470         } */
471     }
472     }
473
474     private void repeatIncremental() {
475     incSearchPos++;
476     if (panel != null)
477         goIncremental();
478     }
479
480     /**
481      * Used for incremental search. Only activated when incremental
482      * is selected.
483      *
484      * The variable incSearchPos keeps track of which entry was last
485      * checked.
486      */
487     public void keyTyped(KeyEvent e) {
488     if (e.isControlDown()) {
489         return;
490     }
491     if (panel != null)
492         goIncremental();
493     }
494
495     private void goIncremental() {
496     incSearch = true;
497     escape.setEnabled(true);
498     SwingUtilities.invokeLater(new Thread() {
499         public void run() {
500             String text = searchField.getText();
501
502
503             if (incSearchPos >= panel.getDatabase().getEntryCount()) {
504             panel.output("'"+text+"' : "+Globals.lang
505
506                      ("Incremental search failed. Repeat to search from top.")+".");
507             incSearchPos = -1;
508             return;
509             }
510
511             if (searchField.getText().equals("")) return;
512             if (incSearchPos < 0)
513             incSearchPos = 0;
514             BibtexEntry be = panel.mainTable.getEntryAt(incSearchPos);
515             while (!incSearcher.search(text, be)) {
516                 incSearchPos++;
517                 if (incSearchPos < panel.getDatabase().getEntryCount())
518                     be = panel.mainTable.getEntryAt(incSearchPos);
519             else {
520                 panel.output("'"+text+"' : "+Globals.lang
521                      ("Incremental search failed. Repeat to search from top."));
522                 incSearchPos = -1;
523                 return;
524             }
525             }
526             if (incSearchPos >= 0) {
527
528             panel.selectSingleEntry(incSearchPos);
529             panel.output("'"+text+"' "+Globals.lang
530
531                      ("found")+".");
532
533             }
534         }
535         });
536     }
537
538     public void componentClosing() {
539     frame.searchToggle.setSelected(false);
540         if (panel != null) {
541             if (startedFilterSearch || startedFloatSearch)
542                 clearSearch();
543         }
544     }
545
546
547     public void keyPressed(KeyEvent e) {}
548     public void keyReleased(KeyEvent e) {}
549
550     public void caretUpdate(CaretEvent e) {
551         if (e.getSource() == searchField) {
552             updateSearchButtonText();
553         }
554     }
555
556     /** Updates the text on the search button to reflect
557       * the type of search that will happen on click. */
558     private void updateSearchButtonText() {
559         search.setText(!increment.isSelected()
560                 && SearchExpressionParser.checkSyntax(
561                 searchField.getText(),
562                 caseSensitive.isSelected(),
563                 regExpSearch.isSelected()) != null
564                 ? Globals.lang("Search Specified Field(s)")
565                 : Globals.lang("Search All Fields"));
566     }
567
568     /**
569      * This method is required by the ErrorMessageDisplay interface, and lets this class
570      * serve as a callback for regular expression exceptions happening in DatabaseSearch.
571      * @param errorMessage
572      */
573     public void reportError(String errorMessage) {
574         JOptionPane.showMessageDialog(panel, errorMessage, Globals.lang("Search error"),
575                 JOptionPane.ERROR_MESSAGE);
576     }
577
578     /**
579      * This method is required by the ErrorMessageDisplay interface, and lets this class
580      * serve as a callback for regular expression exceptions happening in DatabaseSearch.
581      * @param errorMessage
582      */
583     public void reportError(String errorMessage, Exception exception) {
584         reportError(errorMessage);
585     }
586 }