2006-10-18 [colin] 2.5.5cvs16
[claws.git] / src / gtk / quicksearch.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2004 Hiroyuki Yamamoto & the Sylpheed-Claws team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23
24 #include <glib.h>
25 #include <glib/gi18n.h>
26 #include <ctype.h>
27
28 #include <gtk/gtk.h>
29 #include <gdk/gdkkeysyms.h>
30
31 #include "utils.h"
32 #include "menu.h"
33 #include "prefs_common.h"
34 #include "description_window.h"
35 #include "matcher.h"
36 #include "matcher_parser.h"
37 #include "quicksearch.h"
38 #include "folderview.h"
39 #include "folder.h"
40 #include "prefs_matcher.h"
41 #include "sylpheed.h"
42 #include "statusbar.h"
43
44 struct _QuickSearch
45 {
46         GtkWidget                       *hbox_search;
47         GtkWidget                       *search_type;
48         GtkWidget                       *search_type_opt;
49         GtkWidget                       *search_string_entry;
50         GtkWidget                       *search_condition_expression;
51         GtkWidget                       *search_description;
52         GtkWidget                       *clear_search;
53
54         gboolean                         active;
55         gchar                           *search_string;
56         MatcherList                     *matcher_list;
57
58         QuickSearchExecuteCallback       callback;
59         gpointer                         callback_data;
60         gboolean                         running;
61         gboolean                         has_focus;
62         gboolean                         matching;
63         gboolean                         deferred_free;
64         FolderItem                      *root_folder_item;
65         gboolean                         is_fast;
66         gboolean                         in_typing;
67         guint                            press_timeout_id;
68 };
69
70 static void quicksearch_set_running(QuickSearch *quicksearch, gboolean run);
71 static void quicksearch_set_active(QuickSearch *quicksearch, gboolean active);
72 static void quicksearch_reset_folder_items(QuickSearch *quicksearch, FolderItem *folder_item);
73
74 gboolean quicksearch_is_fast(QuickSearch *quicksearch)
75 {
76         return quicksearch->is_fast;
77 }
78
79 static void prepare_matcher(QuickSearch *quicksearch)
80 {
81         const gchar *search_string = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry));
82
83         if (search_string == NULL || search_string[0] == '\0') {
84                 quicksearch_set_active(quicksearch, FALSE);
85         }
86
87         if (quicksearch->matcher_list != NULL) {
88                 if (quicksearch->matching) {
89                         quicksearch->deferred_free = TRUE;
90                         return;
91                 }
92                 quicksearch->deferred_free = FALSE;
93                 matcherlist_free(quicksearch->matcher_list);
94                 quicksearch->matcher_list = NULL;
95         }
96
97         if (search_string == NULL || search_string[0] == '\0') {
98                 return;
99         }
100
101         if (prefs_common.summary_quicksearch_type == QUICK_SEARCH_EXTENDED) {
102                 char *newstr = NULL;
103
104                 newstr = expand_search_string(search_string);
105                 if (newstr && newstr[0] != '\0') {
106                         quicksearch->matcher_list = matcher_parser_get_cond(newstr, &quicksearch->is_fast);
107                         g_free(newstr);
108                 } else {
109                         quicksearch->matcher_list = NULL;
110                         quicksearch_set_active(quicksearch, FALSE);
111
112                         return;
113                 }
114         } else {
115                 quicksearch->is_fast = TRUE;
116                 g_free(quicksearch->search_string);
117                 quicksearch->search_string = g_strdup(search_string);
118         }
119
120         quicksearch_set_active(quicksearch, TRUE);
121 }
122
123 static void update_extended_buttons (QuickSearch *quicksearch)
124 {
125         GtkWidget *expr_btn = quicksearch->search_condition_expression;
126         GtkWidget *ext_btn = quicksearch->search_description;
127
128         g_return_if_fail(expr_btn != NULL);
129         g_return_if_fail(ext_btn != NULL);
130
131         if (prefs_common.summary_quicksearch_type == QUICK_SEARCH_EXTENDED) {
132                 gtk_widget_show(expr_btn);
133                 gtk_widget_show(ext_btn);
134         } else {
135                 gtk_widget_hide(expr_btn);
136                 gtk_widget_hide(ext_btn);
137         }
138 }
139
140 static gboolean searchbar_focus_evt_in(GtkWidget *widget, GdkEventFocus *event,
141                                   QuickSearch *qs)
142 {
143         qs->has_focus = TRUE;
144         return FALSE;
145 }
146
147 static gboolean searchbar_focus_evt_out(GtkWidget *widget, GdkEventFocus *event,
148                                   QuickSearch *qs)
149 {
150         qs->has_focus = FALSE;
151         qs->in_typing = FALSE;
152         return FALSE;
153 }
154
155 gboolean quicksearch_has_focus(QuickSearch *quicksearch)
156 {
157         return quicksearch->has_focus;
158 }
159
160 static void searchbar_run(QuickSearch *quicksearch, gboolean run_only_if_fast)
161 {
162         const gchar *search_string = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry));
163
164         /* add to history */
165         if (!quicksearch->in_typing && search_string && strlen(search_string) != 0) {
166                 /* only if there's no syntax error */
167                 if (quicksearch->matcher_list != NULL ||
168                     prefs_common.summary_quicksearch_type != QUICK_SEARCH_EXTENDED) {
169                         prefs_common.summary_quicksearch_history =
170                                 add_history(prefs_common.summary_quicksearch_history,
171                                                 search_string);
172                         gtk_combo_set_popdown_strings(GTK_COMBO(quicksearch->search_string_entry),
173                                 prefs_common.summary_quicksearch_history);
174                 }
175         }
176
177         prepare_matcher(quicksearch);
178         if (run_only_if_fast && !quicksearch->is_fast)
179                 return;
180         if (quicksearch->matcher_list == NULL && 
181             prefs_common.summary_quicksearch_type == QUICK_SEARCH_EXTENDED)
182                 return;
183         quicksearch_set_running(quicksearch, TRUE);
184         if (quicksearch->callback != NULL)
185                 quicksearch->callback(quicksearch, quicksearch->callback_data);
186         quicksearch_set_running(quicksearch, FALSE);
187 }
188
189 static int searchbar_changed_timeout(void *data)
190 {
191         QuickSearch *qs = (QuickSearch *)data;
192         if (qs && prefs_common.summary_quicksearch_dynamic) {
193                 qs->in_typing = TRUE;
194                 searchbar_run(qs, TRUE);
195         }
196         return FALSE;
197 }
198
199 static gboolean searchbar_changed_cb(GtkWidget *widget, QuickSearch *qs)
200 {
201         if (prefs_common.summary_quicksearch_dynamic) {
202                 if (qs->press_timeout_id != -1) {
203                         gtk_timeout_remove(qs->press_timeout_id);
204                 }
205                 qs->press_timeout_id = gtk_timeout_add(500,
206                                 searchbar_changed_timeout, qs);
207         }
208
209         return FALSE;
210 }
211
212 static gboolean searchbar_pressed(GtkWidget *widget, GdkEventKey *event,
213                                   QuickSearch *quicksearch)
214 {
215         if (event != NULL && event->keyval == GDK_Escape) {
216
217                 const gchar *str;
218
219                 quicksearch->in_typing = FALSE;
220
221                 str = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry));
222                 g_return_val_if_fail(str != NULL, TRUE);
223
224                 /* If the string entry is empty -> hide quicksearch bar. If not -> empty it */
225                 if (!*str) {
226                         quicksearch_hide(quicksearch);
227                 } else {
228                         quicksearch_set(quicksearch, prefs_common.summary_quicksearch_type, "");
229                         gtk_widget_grab_focus(
230                                         GTK_WIDGET(GTK_COMBO(quicksearch->search_string_entry)->entry));
231                 }
232
233                 return TRUE;
234         }
235
236         if (event != NULL && event->keyval == GDK_Return) {
237                 quicksearch->in_typing = FALSE;
238                 /* add expression to history list and exec quicksearch */
239                 searchbar_run(quicksearch, FALSE);
240
241                 g_signal_stop_emission_by_name(G_OBJECT(widget), "key_press_event");
242                 return TRUE;
243         }
244
245         return FALSE;
246 }
247
248 static gboolean searchtype_changed(GtkMenuItem *widget, gpointer data)
249 {
250         QuickSearch *quicksearch = (QuickSearch *)data;
251         const gchar *search_string = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry));
252
253         prefs_common.summary_quicksearch_type = GPOINTER_TO_INT(g_object_get_data(
254                                    G_OBJECT(GTK_MENU_ITEM(gtk_menu_get_active(
255                                    GTK_MENU(quicksearch->search_type)))), MENU_VAL_ID));
256
257         /* Show extended search description button, only when Extended is selected */
258         update_extended_buttons(quicksearch);
259
260         if (!search_string || strlen(search_string) == 0) {
261                 return TRUE;
262         }
263
264         prepare_matcher(quicksearch);
265
266         quicksearch_set_running(quicksearch, TRUE);
267         if (quicksearch->callback != NULL)
268                 quicksearch->callback(quicksearch, quicksearch->callback_data);
269         quicksearch_set_running(quicksearch, FALSE);
270         return TRUE;
271 }
272
273 static gboolean searchtype_recursive_changed(GtkMenuItem *widget, gpointer data)
274 {
275         QuickSearch *quicksearch = (QuickSearch *)data;
276         gboolean checked = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
277         const gchar *search_string = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry));
278
279         prefs_common.summary_quicksearch_recurse = checked;
280
281         /* reselect the search type */
282         gtk_option_menu_set_history(GTK_OPTION_MENU(quicksearch->search_type_opt),
283                                     prefs_common.summary_quicksearch_type);
284
285         if (!search_string || strlen(search_string) == 0) {
286                 return TRUE;
287         }
288
289         prepare_matcher(quicksearch);
290
291         quicksearch_set_running(quicksearch, TRUE);
292         if (quicksearch->callback != NULL)
293                 quicksearch->callback(quicksearch, quicksearch->callback_data);
294         quicksearch_set_running(quicksearch, FALSE);
295         return TRUE;
296 }
297
298 static gboolean searchtype_sticky_changed(GtkMenuItem *widget, gpointer data)
299 {
300         QuickSearch *quicksearch = (QuickSearch *)data;
301         gboolean checked = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
302
303         prefs_common.summary_quicksearch_sticky = checked;
304
305         /* reselect the search type */
306         gtk_option_menu_set_history(GTK_OPTION_MENU(quicksearch->search_type_opt),
307                                     prefs_common.summary_quicksearch_type);
308
309         return TRUE;
310 }
311
312 static gboolean searchtype_dynamic_changed(GtkMenuItem *widget, gpointer data)
313 {
314         QuickSearch *quicksearch = (QuickSearch *)data;
315         gboolean checked = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
316
317         prefs_common.summary_quicksearch_dynamic = checked;
318
319         /* reselect the search type */
320         gtk_option_menu_set_history(GTK_OPTION_MENU(quicksearch->search_type_opt),
321                                     prefs_common.summary_quicksearch_type);
322
323         return TRUE;
324 }
325
326 /*
327  * Strings describing how to use Extended Search
328  *
329  * When adding new lines, remember to put 2 strings for each line
330  */
331 static gchar *search_descr_strings[] = {
332         "a",     N_("all messages"),
333         "ag #",  N_("messages whose age is greater than #"),
334         "al #",  N_("messages whose age is less than #"),
335         "b S",   N_("messages which contain S in the message body"),
336         "B S",   N_("messages which contain S in the whole message"),
337         "c S",   N_("messages carbon-copied to S"),
338         "C S",   N_("message is either to: or cc: to S"),
339         "D",     N_("deleted messages"), /** how I can filter deleted messages **/
340         "e S",   N_("messages which contain S in the Sender field"),
341         "E S",   N_("true if execute \"S\" succeeds"),
342         "f S",   N_("messages originating from user S"),
343         "F",     N_("forwarded messages"),
344         "h S",   N_("messages which contain header S"),
345         "i S",   N_("messages which contain S in Message-ID header"),
346         "I S",   N_("messages which contain S in inreplyto header"),
347         "k #",   N_("messages which are marked with color #"),
348         "L",     N_("locked messages"),
349         "n S",   N_("messages which are in newsgroup S"),
350         "N",     N_("new messages"),
351         "O",     N_("old messages"),
352         "p",     N_("incomplete messages (not entirely downloaded)"),
353         "r",     N_("messages which have been replied to"),
354         "R",     N_("read messages"),
355         "s S",   N_("messages which contain S in subject"),
356         "se #",  N_("messages whose score is equal to #"),
357         "sg #",  N_("messages whose score is greater than #"),
358         "sl #",  N_("messages whose score is lower than #"),
359         "Se #",  N_("messages whose size is equal to #"),
360         "Sg #",  N_("messages whose size is greater than #"),
361         "Ss #",  N_("messages whose size is smaller than #"),
362         "t S",   N_("messages which have been sent to S"),
363         "T",     N_("marked messages"),
364         "U",     N_("unread messages"),
365         "x S",   N_("messages which contain S in References header"),
366         "X \"cmd args\"", N_("messages returning 0 when passed to command - %F is message file"),
367         "y S",   N_("messages which contain S in X-Label header"),
368         "",      "" ,
369         "&amp;",         N_("logical AND operator"),
370         "|",     N_("logical OR operator"),
371         "! or ~",       N_("logical NOT operator"),
372         "%",     N_("case sensitive search"),
373         "",      "" ,
374         " ",     N_("all filtering expressions are allowed"),
375         NULL,    NULL
376 };
377
378 static DescriptionWindow search_descr = {
379         NULL,
380         NULL,
381         2,
382         N_("Extended Search"),
383         N_("Extended Search allows the user to define criteria that messages must "
384            "have in order to match and be displayed in the message list.\n\n"
385            "The following symbols can be used:"),
386         search_descr_strings
387 };
388
389 static void search_description_cb(GtkWidget *widget)
390 {
391         description_window_create(&search_descr);
392 };
393
394 static gboolean clear_search_cb(GtkMenuItem *widget, gpointer data)
395 {
396         QuickSearch *quicksearch = (QuickSearch *)data;
397
398         if (!quicksearch->active)
399                 return TRUE;
400
401         quicksearch_set(quicksearch, prefs_common.summary_quicksearch_type, "");
402
403         return TRUE;
404 };
405
406 static void search_condition_expr_done(MatcherList * matchers)
407 {
408         gchar *str;
409
410         g_return_if_fail(
411                         mainwindow_get_mainwindow()->summaryview->quicksearch != NULL);
412
413         if (matchers == NULL)
414                 return;
415
416         str = matcherlist_to_string(matchers);
417
418         if (str != NULL) {
419                 quicksearch_set(mainwindow_get_mainwindow()->summaryview->quicksearch,
420                                 prefs_common.summary_quicksearch_type, str);
421                 g_free(str);
422
423                 /* add expression to history list and exec quicksearch */
424                 searchbar_run(mainwindow_get_mainwindow()->summaryview->quicksearch, FALSE);
425         }
426 }
427
428 static gboolean search_condition_expr(GtkMenuItem *widget, gpointer data)
429 {
430         const gchar * cond_str;
431         MatcherList * matchers = NULL;
432         
433         g_return_val_if_fail(
434                         mainwindow_get_mainwindow()->summaryview->quicksearch != NULL,
435                         FALSE);
436
437         /* re-use it the current quicksearch value if it's a condition expression,
438            otherwise ignore it silently */
439         cond_str = gtk_entry_get_text(
440                         GTK_ENTRY(GTK_COMBO(mainwindow_get_mainwindow()->summaryview->quicksearch->
441                         search_string_entry)->entry));
442         if (*cond_str != '\0') {
443                 matchers = matcher_parser_get_cond((gchar*)cond_str, NULL);
444         }
445
446         prefs_matcher_open(matchers, search_condition_expr_done);
447
448         if (matchers != NULL)
449                 matcherlist_free(matchers);
450
451         return TRUE;
452 };
453
454 QuickSearch *quicksearch_new()
455 {
456         QuickSearch *quicksearch;
457
458         GtkWidget *hbox_search;
459         GtkWidget *search_type_opt;
460         GtkWidget *search_type;
461         GtkWidget *search_string_entry;
462         GtkWidget *search_hbox;
463         GtkWidget *search_description;
464         GtkWidget *clear_search;
465         GtkWidget *search_condition_expression;
466         GtkWidget *menuitem;
467         GtkTooltips *search_cond_expr_tip;
468
469         quicksearch = g_new0(QuickSearch, 1);
470
471         /* quick search */
472         hbox_search = gtk_hbox_new(FALSE, 0);
473
474         search_type_opt = gtk_option_menu_new();
475         gtk_widget_show(search_type_opt);
476         gtk_box_pack_start(GTK_BOX(hbox_search), search_type_opt, FALSE, FALSE, 0);
477
478         search_type = gtk_menu_new();
479         MENUITEM_ADD (search_type, menuitem, _("Subject"), QUICK_SEARCH_SUBJECT);
480         g_signal_connect(G_OBJECT(menuitem), "activate",
481                          G_CALLBACK(searchtype_changed),
482                          quicksearch);
483         MENUITEM_ADD (search_type, menuitem, _("From"), QUICK_SEARCH_FROM);
484         g_signal_connect(G_OBJECT(menuitem), "activate",
485                          G_CALLBACK(searchtype_changed),
486                          quicksearch);
487         MENUITEM_ADD (search_type, menuitem, _("To"), QUICK_SEARCH_TO);
488         g_signal_connect(G_OBJECT(menuitem), "activate",
489                          G_CALLBACK(searchtype_changed),
490                          quicksearch);
491         MENUITEM_ADD (search_type, menuitem, _("Extended"), QUICK_SEARCH_EXTENDED);
492         g_signal_connect(G_OBJECT(menuitem), "activate",
493                          G_CALLBACK(searchtype_changed),
494                          quicksearch);
495
496         gtk_menu_shell_append(GTK_MENU_SHELL(search_type), gtk_separator_menu_item_new());
497
498         menuitem = gtk_check_menu_item_new_with_label(_("Recursive"));
499         gtk_menu_shell_append(GTK_MENU_SHELL(search_type), menuitem);
500
501         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
502                                         prefs_common.summary_quicksearch_recurse);
503
504         g_signal_connect(G_OBJECT(menuitem), "activate",
505                          G_CALLBACK(searchtype_recursive_changed),
506                          quicksearch);
507
508         menuitem = gtk_check_menu_item_new_with_label(_("Sticky"));
509         gtk_menu_shell_append(GTK_MENU_SHELL(search_type), menuitem);
510
511         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
512                                         prefs_common.summary_quicksearch_sticky);
513
514         g_signal_connect(G_OBJECT(menuitem), "activate",
515                          G_CALLBACK(searchtype_sticky_changed),
516                          quicksearch);
517
518         menuitem = gtk_check_menu_item_new_with_label(_("Type-ahead"));
519         gtk_menu_shell_append(GTK_MENU_SHELL(search_type), menuitem);
520
521         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
522                                         prefs_common.summary_quicksearch_dynamic);
523
524         g_signal_connect(G_OBJECT(menuitem), "activate",
525                          G_CALLBACK(searchtype_dynamic_changed),
526                          quicksearch);
527
528         gtk_option_menu_set_menu(GTK_OPTION_MENU(search_type_opt), search_type);
529
530         gtk_option_menu_set_history(GTK_OPTION_MENU(search_type_opt), prefs_common.summary_quicksearch_type);
531
532         gtk_widget_show(search_type);
533
534         search_string_entry = gtk_combo_new();
535         gtk_box_pack_start(GTK_BOX(hbox_search), search_string_entry, FALSE, FALSE, 2);
536         gtk_combo_set_value_in_list(GTK_COMBO(search_string_entry), FALSE, TRUE);
537         gtk_combo_set_case_sensitive(GTK_COMBO(search_string_entry), TRUE);
538         if (prefs_common.summary_quicksearch_history)
539                 gtk_combo_set_popdown_strings(GTK_COMBO(search_string_entry),
540                         prefs_common.summary_quicksearch_history);
541         gtk_entry_set_text(GTK_ENTRY(GTK_COMBO(search_string_entry)->entry), "");
542         gtk_widget_show(search_string_entry);
543
544         search_hbox = gtk_hbox_new(FALSE, 5);
545
546 #if GTK_CHECK_VERSION(2, 8, 0)
547         clear_search = gtk_button_new_from_stock(GTK_STOCK_CLEAR);
548 #else
549         clear_search = gtk_button_new_with_label(_(" Clear "));
550 #endif
551         gtk_box_pack_start(GTK_BOX(search_hbox), clear_search,
552                            FALSE, FALSE, 0);
553         g_signal_connect(G_OBJECT(clear_search), "clicked",
554                          G_CALLBACK(clear_search_cb), quicksearch);
555         gtk_widget_show(clear_search);
556
557 #if GTK_CHECK_VERSION(2, 8, 0)
558         search_condition_expression = gtk_button_new_from_stock(GTK_STOCK_EDIT);
559 #else
560         search_condition_expression = gtk_button_new_with_label(" ... ");
561 #endif
562         gtk_box_pack_start(GTK_BOX(search_hbox), search_condition_expression,
563                            FALSE, FALSE, 0);
564         g_signal_connect(G_OBJECT (search_condition_expression), "clicked",
565                          G_CALLBACK(search_condition_expr),
566                          quicksearch);
567         search_cond_expr_tip = gtk_tooltips_new();
568         gtk_tooltips_set_tip(GTK_TOOLTIPS(search_cond_expr_tip),
569                              search_condition_expression,
570                              _("Edit search criteria"), NULL);
571         gtk_widget_show(search_condition_expression);
572
573 #if GTK_CHECK_VERSION(2, 8, 0)
574         search_description = gtk_button_new_from_stock(GTK_STOCK_INFO);
575 #else
576         search_description = gtk_button_new_with_label(_(" Extended Symbols... "));
577 #endif
578         gtk_box_pack_start(GTK_BOX(search_hbox), search_description,
579                            FALSE, FALSE, 0);
580         g_signal_connect(G_OBJECT(search_description), "clicked",
581                          G_CALLBACK(search_description_cb), NULL);
582         gtk_widget_show(search_description);
583
584         gtk_box_pack_start(GTK_BOX(hbox_search), search_hbox, FALSE, FALSE, 2);
585         gtk_widget_show(search_hbox);
586
587         g_signal_connect(G_OBJECT(GTK_COMBO(search_string_entry)->entry),
588                            "key_press_event",
589                            G_CALLBACK(searchbar_pressed),
590                            quicksearch);
591
592         g_signal_connect(G_OBJECT(GTK_COMBO(search_string_entry)->entry),
593                          "changed",
594                          G_CALLBACK(searchbar_changed_cb),
595                          quicksearch);
596
597         g_signal_connect(G_OBJECT(GTK_COMBO(search_string_entry)->entry),
598                          "focus_in_event",
599                          G_CALLBACK(searchbar_focus_evt_in),
600                          quicksearch);
601         g_signal_connect(G_OBJECT(GTK_COMBO(search_string_entry)->entry),
602                          "focus_out_event",
603                          G_CALLBACK(searchbar_focus_evt_out),
604                          quicksearch);
605
606         quicksearch->hbox_search = hbox_search;
607         quicksearch->search_type = search_type;
608         quicksearch->search_type_opt = search_type_opt;
609         quicksearch->search_string_entry = search_string_entry;
610         quicksearch->search_condition_expression = search_condition_expression;
611         quicksearch->search_description = search_description;
612         quicksearch->matcher_list = NULL;
613         quicksearch->active = FALSE;
614         quicksearch->running = FALSE;
615         quicksearch->clear_search = clear_search;
616         quicksearch->in_typing = FALSE;
617         quicksearch->press_timeout_id = -1;
618
619         update_extended_buttons(quicksearch);
620
621         return quicksearch;
622 }
623
624 GtkWidget *quicksearch_get_widget(QuickSearch *quicksearch)
625 {
626         return quicksearch->hbox_search;
627 }
628
629 void quicksearch_show(QuickSearch *quicksearch)
630 {
631         prepare_matcher(quicksearch);
632         gtk_widget_show(quicksearch->hbox_search);
633         update_extended_buttons(quicksearch);
634         gtk_widget_grab_focus(
635                 GTK_WIDGET(GTK_COMBO(quicksearch->search_string_entry)->entry));
636 }
637
638 void quicksearch_hide(QuickSearch *quicksearch)
639 {
640         quicksearch_set(quicksearch, prefs_common.summary_quicksearch_type, "");
641         quicksearch_set_active(quicksearch, FALSE);
642         gtk_widget_hide(quicksearch->hbox_search);
643 }
644
645 void quicksearch_set(QuickSearch *quicksearch, QuickSearchType type,
646                      const gchar *matchstring)
647 {
648         gtk_option_menu_set_history(GTK_OPTION_MENU(quicksearch->search_type_opt),
649                                     type);
650
651         g_signal_handlers_block_by_func(G_OBJECT(GTK_COMBO(quicksearch->search_string_entry)->entry),
652                         G_CALLBACK(searchbar_changed_cb), quicksearch);
653         gtk_entry_set_text(GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry),
654                            matchstring);
655         g_signal_handlers_unblock_by_func(G_OBJECT(GTK_COMBO(quicksearch->search_string_entry)->entry),
656                         G_CALLBACK(searchbar_changed_cb), quicksearch);
657
658         prefs_common.summary_quicksearch_type = type;
659
660         prepare_matcher(quicksearch);
661
662         quicksearch_set_running(quicksearch, TRUE);
663         if (quicksearch->callback != NULL)
664                 quicksearch->callback(quicksearch, quicksearch->callback_data);
665         quicksearch_set_running(quicksearch, FALSE);
666 }
667
668 gboolean quicksearch_is_active(QuickSearch *quicksearch)
669 {
670         return quicksearch->active && 
671                 (prefs_common.summary_quicksearch_type != QUICK_SEARCH_EXTENDED
672                  || quicksearch->matcher_list != NULL);
673 }
674
675 static void quicksearch_set_active(QuickSearch *quicksearch, gboolean active)
676 {
677         static GdkColor yellow;
678         static GdkColor red;
679         static GdkColor black;
680         static gboolean colors_initialised = FALSE;
681         gboolean error = FALSE;
682
683         if (!colors_initialised) {
684                 gdk_color_parse("#f5f6be", &yellow);
685                 gdk_color_parse("#000000", &black);
686                 gdk_color_parse("#ff7070", &red);
687                 colors_initialised = gdk_colormap_alloc_color(
688                         gdk_colormap_get_system(), &yellow, FALSE, TRUE);
689                 colors_initialised &= gdk_colormap_alloc_color(
690                         gdk_colormap_get_system(), &black, FALSE, TRUE);
691                 colors_initialised &= gdk_colormap_alloc_color(
692                         gdk_colormap_get_system(), &red, FALSE, TRUE);
693         }
694
695         quicksearch->active = active;
696
697         if (active && 
698                 (prefs_common.summary_quicksearch_type == QUICK_SEARCH_EXTENDED
699                  && quicksearch->matcher_list == NULL))
700                 error = TRUE;
701
702         if (active) {
703                 gtk_widget_set_sensitive(quicksearch->clear_search, TRUE);
704                 if (colors_initialised) {
705                         gtk_widget_modify_base(
706                                 GTK_COMBO(quicksearch->search_string_entry)->entry,
707                                 GTK_STATE_NORMAL, error ? &red : &yellow);
708                         gtk_widget_modify_text(
709                                 GTK_COMBO(quicksearch->search_string_entry)->entry,
710                                 GTK_STATE_NORMAL, &black);
711                 }
712         } else {
713                 gtk_widget_set_sensitive(quicksearch->clear_search, FALSE);
714                 if (colors_initialised) {
715                         gtk_widget_modify_base(
716                                 GTK_COMBO(quicksearch->search_string_entry)->entry,
717                                 GTK_STATE_NORMAL, NULL);
718                         gtk_widget_modify_text(
719                                 GTK_COMBO(quicksearch->search_string_entry)->entry,
720                                 GTK_STATE_NORMAL, NULL);
721                 }
722         }
723
724         if (!active) {
725                 quicksearch_reset_cur_folder_item(quicksearch);
726         }
727 }
728
729 void quicksearch_set_execute_callback(QuickSearch *quicksearch,
730                                       QuickSearchExecuteCallback callback,
731                                       gpointer data)
732 {
733         quicksearch->callback = callback;
734         quicksearch->callback_data = data;
735 }
736
737 gboolean quicksearch_match(QuickSearch *quicksearch, MsgInfo *msginfo)
738 {
739         gchar *searched_header = NULL;
740         gboolean result = FALSE;
741
742         if (!quicksearch->active)
743                 return TRUE;
744
745         switch (prefs_common.summary_quicksearch_type) {
746         case QUICK_SEARCH_SUBJECT:
747                 searched_header = msginfo->subject;
748                 break;
749         case QUICK_SEARCH_FROM:
750                 searched_header = msginfo->from;
751                 break;
752         case QUICK_SEARCH_TO:
753                 searched_header = msginfo->to;
754                 break;
755         case QUICK_SEARCH_EXTENDED:
756                 break;
757         default:
758                 debug_print("unknown search type (%d)\n", prefs_common.summary_quicksearch_type);
759                 break;
760         }
761         quicksearch->matching = TRUE;
762         if (prefs_common.summary_quicksearch_type != QUICK_SEARCH_EXTENDED &&
763             quicksearch->search_string &&
764             searched_header && strcasestr(searched_header, quicksearch->search_string) != NULL)
765                 result = TRUE;
766         else if ((quicksearch->matcher_list != NULL) &&
767                  matcherlist_match(quicksearch->matcher_list, msginfo))
768                 result = TRUE;
769
770         quicksearch->matching = FALSE;
771         if (quicksearch->deferred_free) {
772                 prepare_matcher(quicksearch);
773         }
774
775         return result;
776 }
777
778 /* allow Mutt-like patterns in quick search */
779 gchar *expand_search_string(const gchar *search_string)
780 {
781         int i = 0;
782         gchar term_char, save_char;
783         gchar *cmd_start, *cmd_end;
784         GString *matcherstr;
785         gchar *returnstr = NULL;
786         gchar *copy_str;
787         gboolean casesens, dontmatch;
788         /* list of allowed pattern abbreviations */
789         struct {
790                 gchar           *abbreviated;   /* abbreviation */
791                 gchar           *command;       /* actual matcher command */
792                 gint            numparams;      /* number of params for cmd */
793                 gboolean        qualifier;      /* do we append regexpcase */
794                 gboolean        quotes;         /* do we need quotes */
795         }
796         cmds[] = {
797                 { "a",  "all",                          0,      FALSE,  FALSE },
798                 { "ag", "age_greater",                  1,      FALSE,  FALSE },
799                 { "al", "age_lower",                    1,      FALSE,  FALSE },
800                 { "b",  "body_part",                    1,      TRUE,   TRUE  },
801                 { "B",  "message",                      1,      TRUE,   TRUE  },
802                 { "c",  "cc",                           1,      TRUE,   TRUE  },
803                 { "C",  "to_or_cc",                     1,      TRUE,   TRUE  },
804                 { "D",  "deleted",                      0,      FALSE,  FALSE },
805                 { "e",  "header \"Sender\"",            1,      TRUE,   TRUE  },
806                 { "E",  "execute",                      1,      FALSE,  TRUE  },
807                 { "f",  "from",                         1,      TRUE,   TRUE  },
808                 { "F",  "forwarded",                    0,      FALSE,  FALSE },
809                 { "h",  "headers_part",                 1,      TRUE,   TRUE  },
810                 { "i",  "header \"Message-ID\"",        1,      TRUE,   TRUE  },
811                 { "I",  "inreplyto",                    1,      TRUE,   TRUE  },
812                 { "k",  "colorlabel",                   1,      FALSE,  FALSE },
813                 { "L",  "locked",                       0,      FALSE,  FALSE },
814                 { "n",  "newsgroups",                   1,      TRUE,   TRUE  },
815                 { "N",  "new",                          0,      FALSE,  FALSE },
816                 { "O",  "~new",                         0,      FALSE,  FALSE },
817                 { "r",  "replied",                      0,      FALSE,  FALSE },
818                 { "R",  "~unread",                      0,      FALSE,  FALSE },
819                 { "s",  "subject",                      1,      TRUE,   TRUE  },
820                 { "se", "score_equal",                  1,      FALSE,  FALSE },
821                 { "sg", "score_greater",                1,      FALSE,  FALSE },
822                 { "sl", "score_lower",                  1,      FALSE,  FALSE },
823                 { "Se", "size_equal",                   1,      FALSE,  FALSE },
824                 { "Sg", "size_greater",                 1,      FALSE,  FALSE },
825                 { "Ss", "size_smaller",                 1,      FALSE,  FALSE },
826                 { "t",  "to",                           1,      TRUE,   TRUE  },
827                 { "T",  "marked",                       0,      FALSE,  FALSE },
828                 { "U",  "unread",                       0,      FALSE,  FALSE },
829                 { "x",  "header \"References\"",        1,      TRUE,   TRUE  },
830                 { "X",  "test",                         1,      FALSE,  FALSE },
831                 { "y",  "header \"X-Label\"",           1,      TRUE,   TRUE  },
832                 { "&",  "&",                            0,      FALSE,  FALSE },
833                 { "|",  "|",                            0,      FALSE,  FALSE },
834                 { "p",  "partial",                      0,      FALSE,  FALSE },
835                 { NULL, NULL,                           0,      FALSE,  FALSE }
836         };
837
838         if (search_string == NULL)
839                 return NULL;
840
841         copy_str = g_strdup(search_string);
842
843         matcherstr = g_string_sized_new(16);
844         cmd_start = copy_str;
845         while (cmd_start && *cmd_start) {
846                 /* skip all white spaces */
847                 while (*cmd_start && isspace((guchar)*cmd_start))
848                         cmd_start++;
849                 cmd_end = cmd_start;
850
851                 /* extract a command */
852                 while (*cmd_end && !isspace((guchar)*cmd_end))
853                         cmd_end++;
854
855                 /* save character */
856                 save_char = *cmd_end;
857                 *cmd_end = '\0';
858
859                 dontmatch = FALSE;
860                 casesens = FALSE;
861
862                 /* ~ and ! mean logical NOT */
863                 if (*cmd_start == '~' || *cmd_start == '!')
864                 {
865                         dontmatch = TRUE;
866                         cmd_start++;
867                 }
868                 /* % means case sensitive match */
869                 if (*cmd_start == '%')
870                 {
871                         casesens = TRUE;
872                         cmd_start++;
873                 }
874
875                 /* find matching abbreviation */
876                 for (i = 0; cmds[i].command; i++) {
877                         if (!strcmp(cmd_start, cmds[i].abbreviated)) {
878                                 /* restore character */
879                                 *cmd_end = save_char;
880
881                                 /* copy command */
882                                 if (matcherstr->len > 0) {
883                                         g_string_append(matcherstr, " ");
884                                 }
885                                 if (dontmatch)
886                                         g_string_append(matcherstr, "~");
887                                 g_string_append(matcherstr, cmds[i].command);
888                                 g_string_append(matcherstr, " ");
889
890                                 /* stop if no params required */
891                                 if (cmds[i].numparams == 0)
892                                         break;
893
894                                 /* extract a parameter, allow quotes */
895                                 while (*cmd_end && isspace((guchar)*cmd_end))
896                                         cmd_end++;
897
898                                 cmd_start = cmd_end;
899                                 if (*cmd_start == '"') {
900                                         term_char = '"';
901                                         cmd_end++;
902                                 }
903                                 else
904                                         term_char = ' ';
905
906                                 /* extract actual parameter */
907                                 while ((*cmd_end) && (*cmd_end != term_char))
908                                         cmd_end++;
909
910                                 if (*cmd_end == '"')
911                                         cmd_end++;
912
913                                 save_char = *cmd_end;
914                                 *cmd_end = '\0';
915
916                                 if (cmds[i].qualifier) {
917                                         if (casesens)
918                                                 g_string_append(matcherstr, "regexp ");
919                                         else
920                                                 g_string_append(matcherstr, "regexpcase ");
921                                 }
922
923                                 /* do we need to add quotes ? */
924                                 if (cmds[i].quotes && term_char != '"')
925                                         g_string_append(matcherstr, "\"");
926
927                                 /* copy actual parameter */
928                                 g_string_append(matcherstr, cmd_start);
929
930                                 /* do we need to add quotes ? */
931                                 if (cmds[i].quotes && term_char != '"')
932                                         g_string_append(matcherstr, "\"");
933
934                                 /* restore original character */
935                                 *cmd_end = save_char;
936
937                                 break;
938                         }
939                 }
940
941                 if (*cmd_end)
942                         cmd_end++;
943                 cmd_start = cmd_end;
944         }
945
946         g_free(copy_str);
947
948         /* return search string if no match is found to allow
949            all available filtering expressions in quicksearch */
950         if (matcherstr->len > 0) returnstr = matcherstr->str;
951         else returnstr = g_strdup(search_string);
952
953         g_string_free(matcherstr, FALSE);
954         return returnstr;
955 }
956
957 static void quicksearch_set_running(QuickSearch *quicksearch, gboolean run)
958 {
959         quicksearch->running = run;
960 }
961
962 gboolean quicksearch_is_running(QuickSearch *quicksearch)
963 {
964         return quicksearch->running;
965 }
966
967 void quicksearch_pass_key(QuickSearch *quicksearch, guint val, GdkModifierType mod)
968 {
969         GtkEntry *entry = GTK_ENTRY(GTK_COMBO(quicksearch->search_string_entry)->entry);
970         glong curpos = gtk_editable_get_position(GTK_EDITABLE(entry));
971         guint32 c;
972         char *str = g_strdup(gtk_entry_get_text(entry));
973         char *begin = str;
974         char *end = NULL;
975         char *new = NULL;
976         char key[7] = "";
977         guint char_len = 0;
978
979         if (gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), NULL, NULL)) {
980                 /* remove selection */
981                 gtk_editable_delete_selection(GTK_EDITABLE(entry));
982                 curpos = gtk_editable_get_position(GTK_EDITABLE(entry));
983                 /* refresh string */
984                 g_free(str);
985                 str = g_strdup(gtk_entry_get_text(entry));
986                 begin = str;
987         }
988
989         if (!(c = gdk_keyval_to_unicode(val))) {
990                 g_free(str);
991                 return;
992         }
993         char_len = g_unichar_to_utf8(c, key);
994         if (char_len < 0)
995                 return;
996         key[char_len] = '\0';
997         if (curpos < g_utf8_strlen(str, -1)) {
998                 gchar *stop = g_utf8_offset_to_pointer(begin, curpos);
999                 end = g_strdup(g_utf8_offset_to_pointer(str, curpos));
1000                 *stop = '\0';
1001                 new = g_strdup_printf("%s%s%s", begin, key, end);
1002                 gtk_entry_set_text(entry, new);
1003                 g_free(end);
1004         } else {
1005                 new = g_strdup_printf("%s%s", begin, key);
1006                 gtk_entry_set_text(entry, new);
1007         }
1008         g_free(str);
1009         g_free(new);
1010         gtk_editable_set_position(GTK_EDITABLE(entry), curpos+1);
1011
1012 }
1013
1014 static gboolean quicksearch_match_subfolder(QuickSearch *quicksearch,
1015                                  FolderItem *src)
1016 {
1017         GSList *msglist = folder_item_get_msg_list(src);
1018         GSList *cur;
1019         gboolean result = FALSE;
1020         gint num = 0, total = src->total_msgs;
1021         gint interval = quicksearch_is_fast(quicksearch) ? 5000:100;
1022
1023         statusbar_print_all(_("Searching in %s... \n"),
1024                 src->path ? src->path : "(null)");
1025         folder_item_update_freeze();
1026         for (cur = msglist; cur != NULL; cur = cur->next) {
1027                 MsgInfo *msg = (MsgInfo *)cur->data;
1028                 statusbar_progress_all(num++,total, interval);
1029                 if (quicksearch_match(quicksearch, msg)) {
1030                         procmsg_msginfo_free(msg);
1031                         result = TRUE;
1032                         break;
1033                 }
1034                 procmsg_msginfo_free(msg);
1035                 if (num % interval == 0)
1036                         GTK_EVENTS_FLUSH();
1037                 if (!quicksearch_is_active(quicksearch))
1038                         break;
1039         }
1040         folder_item_update_thaw();
1041         statusbar_progress_all(0,0,0);
1042         statusbar_pop_all();
1043
1044         g_slist_free(msglist);
1045         return result;
1046 }
1047
1048 void quicksearch_search_subfolders(QuickSearch *quicksearch,
1049                                    FolderView *folderview,
1050                                    FolderItem *folder_item)
1051 {
1052         FolderItem *cur = NULL;
1053         GNode *node = folder_item->node->children;
1054
1055         if (!prefs_common.summary_quicksearch_recurse
1056         ||  quicksearch->in_typing == TRUE)
1057                 return;
1058
1059         for (; node != NULL; node = node->next) {
1060                 cur = FOLDER_ITEM(node->data);
1061                 if (quicksearch_match_subfolder(quicksearch, cur)) {
1062                         folderview_update_search_icon(cur, TRUE);
1063                 } else {
1064                         folderview_update_search_icon(cur, FALSE);
1065                 }
1066                 if (cur->node->children)
1067                         quicksearch_search_subfolders(quicksearch,
1068                                                       folderview,
1069                                                       cur);
1070         }
1071         quicksearch->root_folder_item = folder_item;
1072         if (!quicksearch_is_active(quicksearch))
1073                 quicksearch_reset_cur_folder_item(quicksearch);
1074 }
1075
1076 static void quicksearch_reset_folder_items(QuickSearch *quicksearch,
1077                                     FolderItem *folder_item)
1078 {
1079         FolderItem *cur = NULL;
1080         GNode *node = (folder_item && folder_item->node) ?
1081                         folder_item->node->children : NULL;
1082
1083         for (; node != NULL; node = node->next) {
1084                 cur = FOLDER_ITEM(node->data);
1085                 folderview_update_search_icon(cur, FALSE);
1086                 if (cur->node->children)
1087                         quicksearch_reset_folder_items(quicksearch,
1088                                                        cur);
1089         }
1090 }
1091
1092 void quicksearch_reset_cur_folder_item(QuickSearch *quicksearch)
1093 {
1094         if (quicksearch->root_folder_item)
1095                 quicksearch_reset_folder_items(quicksearch,
1096                                                quicksearch->root_folder_item);
1097
1098         quicksearch->root_folder_item = NULL;
1099 }
1100
1101 gboolean quicksearch_is_in_typing(QuickSearch *quicksearch)
1102 {
1103         return quicksearch->in_typing;
1104 }