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