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