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