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