1 /*
2     Quick Search Plugin for DeaDBeeF audio player
3     Copyright (C) 2014 Christian Boxdörfer <christian.boxdoerfer@posteo.de>
4 
5     This program is free software; you can redistribute it and/or
6     modify it under the terms of the GNU General Public License
7     as published by the Free Software Foundation; either version 2
8     of the License, or (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 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <stdlib.h>
23 #include <stdio.h>
24 #include <string.h>
25 #include <gtk/gtk.h>
26 #include <gdk/gdkkeysyms.h>
27 #include <deadbeef/deadbeef.h>
28 #include <deadbeef/gtkui_api.h>
29 
30 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
31 #define trace(fmt,...)
32 
33 #define CONFSTR_APPEND_SEARCH_STRING "quick_search.append_search_string"
34 #define CONFSTR_SEARCH_IN "quick_search.search_in"
35 #define CONFSTR_AUTOSEARCH "quick_search.autosearch"
36 #define CONFSTR_HISTORY_SIZE "quick_search.history_size"
37 
38 static DB_misc_t plugin;
39 static DB_functions_t *deadbeef = NULL;
40 static ddb_gtkui_t *gtkui_plugin = NULL;
41 static GtkWidget *searchentry = NULL;
42 static int search_delay_timer = 0;
43 static ddb_playlist_t *last_active_plt = NULL;
44 
45 static gboolean new_plt_button_state = FALSE;
46 static ddb_playlist_t *added_plt = NULL;
47 static int history_entries = 0;
48 static const char *uuid = "779e2992-3e6e-40d4-9f2e-de06466142a0";
49 static char cache_path[PATH_MAX];
50 static int cache_path_size;
51 
52 // search in modes
53 enum search_in_mode_t {
54     SEARCH_INLINE = 0,
55     SEARCH_PLAYLIST = 1,
56     SEARCH_ALL_PLAYLISTS = 2,
57 };
58 
59 static int config_search_in = SEARCH_INLINE;
60 static int config_autosearch = TRUE;
61 static int config_append_search_string = FALSE;
62 static int config_history_size = 10;
63 
64 typedef struct {
65     ddb_gtkui_widget_t base;
66     GtkWidget *popup;
67     GtkWidget *combo;
68     GtkWidget *clear_history;
69     char *prev_query;
70 } w_quick_search_t;
71 
72 static void
73 add_history_query_to_combo (gpointer user_data, const char *text, int prepend);
74 
75 static int
check_dir(const char * dir,mode_t mode)76 check_dir (const char *dir, mode_t mode)
77 {
78     char *tmp = strdup (dir);
79     char *slash = tmp;
80     struct stat stat_buf;
81     do
82     {
83         slash = strstr (slash+1, "/");
84         if (slash)
85             *slash = 0;
86         if (-1 == stat (tmp, &stat_buf))
87         {
88             if (0 != mkdir (tmp, mode))
89             {
90                 free (tmp);
91                 return 0;
92             }
93         }
94         if (slash)
95             *slash = '/';
96     } while (slash);
97     free (tmp);
98     return 1;
99 }
100 
101 static int
make_cache_dir(char * path,int size)102 make_cache_dir (char *path, int size)
103 {
104 #if (DDB_API_LEVEL >= 8)
105     const char *cache_dir = deadbeef->get_system_dir (DDB_SYS_DIR_CACHE);
106 #else
107     const char *cache_dir = getenv ("HOME");
108 #endif
109     if (cache_dir) {
110 #if (DDB_API_LEVEL >= 8)
111         const int sz = snprintf (path, size, "%s/quick_search/", cache_dir);
112 #else
113         const int sz = snprintf (path, size, "%s/.cache/deadbeef/quick_search/", cache_dir);
114 #endif
115         if (!check_dir (path, 0755)) {
116             return 0;
117         }
118         return sz;
119     }
120     return 0;
121 }
122 
123 static FILE *
get_file_descriptor(const char * path,const char * fname,const char * mode)124 get_file_descriptor (const char *path, const char *fname, const char *mode)
125 {
126     char full_path[PATH_MAX];
127     snprintf (full_path, sizeof (full_path), "%s%s", path, fname);
128     FILE *fp = fopen (full_path, mode);
129     if (!fp) {
130         return NULL;
131     }
132     return fp;
133 }
134 
135 static void
load_history_entries(gpointer user_data)136 load_history_entries (gpointer user_data)
137 {
138     w_quick_search_t *w = (w_quick_search_t *)user_data;
139     FILE *fp = get_file_descriptor (cache_path, "history", "r");
140     if (!fp) {
141         return;
142     }
143     char history[1024];
144     while (fgets (history, sizeof (history), fp)) {
145         char *ptr;
146         if ((ptr = strchr(history, '\n')) != NULL) {
147             *ptr = '\0';
148         }
149         if (strcmp (history, "")) {
150             add_history_query_to_combo (w, history, 0);
151         }
152     }
153     fclose (fp);
154 }
155 
156 static void
save_history_entries(gpointer user_data)157 save_history_entries (gpointer user_data)
158 {
159     w_quick_search_t *w = (w_quick_search_t *)user_data;
160     FILE *fp = get_file_descriptor (cache_path, "history", "w");
161     if (!fp) {
162         return;
163     }
164     GtkTreeModel *tree = gtk_combo_box_get_model (GTK_COMBO_BOX (w->combo));
165     if (tree) {
166         GtkTreeIter iter;
167         gboolean valid = gtk_tree_model_get_iter_first (tree, &iter);
168         while (valid) {
169             /* Walk through the list, reading each row */
170             gchar *str_data;
171             gtk_tree_model_get (tree, &iter, 0, &str_data, -1);
172             if (strcmp (str_data, "")) {
173                 fprintf (fp, "%s\n", str_data);
174             }
175             g_free (str_data);
176             valid = gtk_tree_model_iter_next (tree, &iter);
177         }
178     }
179     fclose (fp);
180 }
181 
182 static gboolean
is_quick_search_playlist(ddb_playlist_t * plt)183 is_quick_search_playlist (ddb_playlist_t *plt)
184 {
185     deadbeef->pl_lock ();
186     if (plt && deadbeef->plt_find_meta (plt, "quick_search") != NULL
187             && !strcmp (deadbeef->plt_find_meta (plt, "quick_search"), uuid)) {
188         deadbeef->pl_unlock ();
189         return TRUE;
190     }
191     deadbeef->pl_unlock ();
192     return FALSE;
193 }
194 
195 static ddb_playlist_t *
get_last_active_playlist()196 get_last_active_playlist ()
197 {
198     deadbeef->pl_lock ();
199     ddb_playlist_t *plt = NULL;
200     if (!last_active_plt) {
201         plt = deadbeef->plt_get_curr ();
202     }
203     else {
204         // check if playlist got removed
205         int valid = 0;
206         int plt_count = deadbeef->plt_get_count();
207         for (int i = 0; i < plt_count; i++) {
208             ddb_playlist_t *plt_temp = deadbeef->plt_get_for_idx (i);
209             if (!plt_temp) {
210                 continue;
211             }
212             if (plt_temp == last_active_plt) {
213                 valid = 1;
214             }
215             deadbeef->plt_unref (plt_temp);
216         }
217         if (valid) {
218             plt = last_active_plt;
219             deadbeef->plt_ref (plt);
220         }
221         else {
222             deadbeef->plt_unref (last_active_plt);
223             last_active_plt = NULL;
224             plt = deadbeef->plt_get_curr ();
225         }
226     }
227     deadbeef->pl_unlock ();
228     return plt;
229 }
230 
231 static void
set_last_active_playlist(ddb_playlist_t * plt)232 set_last_active_playlist (ddb_playlist_t *plt)
233 {
234     deadbeef->pl_lock ();
235     if (!is_quick_search_playlist (plt) && plt != last_active_plt) {
236         if (last_active_plt) {
237             deadbeef->plt_unref (last_active_plt);
238         }
239         last_active_plt = plt;
240         deadbeef->plt_ref (last_active_plt);
241     }
242     deadbeef->pl_unlock ();
243 }
244 
245 static int
add_new_playlist(const char * title)246 add_new_playlist (const char *title) {
247     if (!title) {
248         return -1;
249     }
250     int cnt = deadbeef->plt_get_count ();
251     return deadbeef->plt_add (cnt, title);
252 }
253 
254 static int
get_quick_search_playlist()255 get_quick_search_playlist () {
256     // find existing one
257     deadbeef->pl_lock ();
258     int plt_count = deadbeef->plt_get_count();
259     for (int i = 0; i < plt_count; i++) {
260         ddb_playlist_t *plt = deadbeef->plt_get_for_idx (i);
261         if (plt) {
262             if (is_quick_search_playlist (plt)) {
263                 deadbeef->plt_unref (plt);
264                 deadbeef->pl_unlock ();
265                 return i;
266             }
267             deadbeef->plt_unref (plt);
268         }
269     }
270 
271     // add new playlist
272     int idx = deadbeef->plt_add (plt_count, "Quick Search");
273     ddb_playlist_t *plt = deadbeef->plt_get_for_idx (idx);
274     // use a uuid to identify quick_search list
275     deadbeef->plt_add_meta (plt, "quick_search", uuid);
276     deadbeef->plt_unref (plt);
277     deadbeef->pl_unlock ();
278     return idx;
279 }
280 
281 static void
set_default_quick_search_playlist_title()282 set_default_quick_search_playlist_title ()
283 {
284     deadbeef->pl_lock ();
285     int plt_idx = get_quick_search_playlist ();
286     if (plt_idx >= 0) {
287         ddb_playlist_t *plt = deadbeef->plt_get_for_idx (plt_idx);
288         if (plt) {
289             char new_title[1024] = "";
290             snprintf (new_title, sizeof (new_title), "%s", "Quick Search");
291             deadbeef->plt_set_title (plt, new_title);
292             deadbeef->plt_unref (plt);
293         }
294     }
295     deadbeef->pl_unlock ();
296 }
297 
298 static void
append_search_string_to_plt_title(ddb_playlist_t * plt,const char * search_string)299 append_search_string_to_plt_title (ddb_playlist_t *plt, const char *search_string)
300 {
301     if (!search_string || !plt) {
302         return;
303     }
304     deadbeef->pl_lock ();
305     if (plt) {
306         char new_title[1024] = "";
307         if (!strcmp (search_string, "")) {
308             if (new_plt_button_state) {
309                 snprintf (new_title, sizeof (new_title), "%s", "New Playlist");
310             }
311             else {
312                 snprintf (new_title, sizeof (new_title), "%s", "Quick Search");
313             }
314         }
315         else {
316             if (new_plt_button_state) {
317                 snprintf (new_title, sizeof (new_title), "[%s]", search_string);
318             }
319             else {
320                 snprintf (new_title, sizeof (new_title), "%s [%s]", "Quick Search", search_string);
321             }
322         }
323         deadbeef->plt_set_title (plt, new_title);
324     }
325     deadbeef->pl_unlock ();
326 }
327 
328 static void
copy_selected_tracks(ddb_playlist_t * from,ddb_playlist_t * to)329 copy_selected_tracks (ddb_playlist_t *from, ddb_playlist_t *to)
330 {
331     if (!from || !to) {
332         return;
333     }
334     deadbeef->pl_lock ();
335     deadbeef->plt_set_curr (to);
336 
337     int sel_count = deadbeef->plt_get_sel_count (deadbeef->plt_get_idx (from));
338     uint32_t *track_list = malloc ((sel_count) * sizeof (uint32_t));
339     if (track_list) {
340         int track_idx = 0;
341         int i = 0;
342         DB_playItem_t *it = deadbeef->plt_get_first (from, PL_MAIN);
343         for (; it; track_idx++) {
344             if (deadbeef->pl_is_selected (it)) {
345                 track_list[i] = track_idx;
346                 i++;
347             }
348             DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
349             deadbeef->pl_item_unref (it);
350             it = next;
351         }
352         DB_playItem_t *after = deadbeef->plt_get_first (to, PL_MAIN);
353         deadbeef->plt_copy_items (to, PL_MAIN, from, after, track_list, sel_count);
354         if (after) {
355             deadbeef->pl_item_unref (after);
356         }
357         free (track_list);
358     }
359     deadbeef->pl_unlock ();
360 }
361 
362 static void
on_add_quick_search_list()363 on_add_quick_search_list ()
364 {
365     deadbeef->pl_lock ();
366     int new_plt_idx = -1;
367     if (new_plt_button_state) {
368         if (added_plt == NULL) {
369             new_plt_idx = add_new_playlist ("Quick Search*");
370             added_plt = deadbeef->plt_get_for_idx (new_plt_idx);
371         }
372         else {
373             new_plt_idx = deadbeef->plt_get_idx (added_plt);
374         }
375     }
376     else {
377         new_plt_idx = get_quick_search_playlist ();
378         if (added_plt) {
379             deadbeef->plt_unref (added_plt);
380             added_plt = NULL;
381         }
382     }
383 
384     ddb_playlist_t *plt_to = deadbeef->plt_get_for_idx (new_plt_idx);
385 
386     if (plt_to) {
387         if (config_search_in != SEARCH_ALL_PLAYLISTS) {
388             ddb_playlist_t *plt_from = get_last_active_playlist ();
389             if (plt_from) {
390                 if (is_quick_search_playlist (plt_from)) {
391                     deadbeef->plt_unref (plt_from);
392                     deadbeef->plt_unref (plt_to);
393                     deadbeef->pl_unlock ();
394                     return;
395                 }
396                 deadbeef->plt_set_scroll (plt_to, 0);
397                 deadbeef->plt_clear (plt_to);
398                 copy_selected_tracks (plt_from, plt_to);
399                 deadbeef->plt_unref (plt_from);
400             }
401         }
402         else if (config_search_in == SEARCH_ALL_PLAYLISTS) {
403             deadbeef->plt_set_scroll (plt_to, 0);
404             deadbeef->plt_clear (plt_to);
405             int plt_count = deadbeef->plt_get_count ();
406             for (int i = 0; i < plt_count; i++) {
407                 ddb_playlist_t *plt_from = deadbeef->plt_get_for_idx (i);
408                 if (!plt_from) {
409                     continue;
410                 }
411                 if (!is_quick_search_playlist (plt_from)) {
412                     copy_selected_tracks (plt_from, plt_to);
413                 }
414                 deadbeef->plt_unref (plt_from);
415             }
416         }
417         if (config_append_search_string && config_search_in != SEARCH_INLINE) {
418             const gchar *text = gtk_entry_get_text (GTK_ENTRY (searchentry));
419             append_search_string_to_plt_title (plt_to, text);
420         }
421 
422         deadbeef->plt_unref (plt_to);
423     }
424     deadbeef->pl_unlock ();
425 
426 #if (DDB_API_LEVEL >= 8)
427     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_CONTENT, 0);
428 #else
429     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0);
430 #endif
431 }
432 
433 static void
on_searchentry_activate(GtkEntry * entry,gpointer user_data)434 on_searchentry_activate                (GtkEntry        *entry,
435                                         gpointer         user_data)
436 {
437     deadbeef->pl_lock ();
438     ddb_playlist_t *plt = get_last_active_playlist ();
439     if (plt) {
440         DB_playItem_t *it = deadbeef->plt_get_first (plt, PL_MAIN);
441         while (it) {
442             if (deadbeef->pl_is_selected (it)) {
443                 int idx = deadbeef->plt_get_item_idx(plt, it, PL_MAIN);
444                 deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, idx, 0);
445                 break;
446             }
447             DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
448             deadbeef->pl_item_unref (it);
449             it = next;
450         }
451         if (it) {
452             deadbeef->pl_item_unref (it);
453         }
454         deadbeef->plt_unref (plt);
455     }
456     deadbeef->pl_unlock ();
457 }
458 
459 static void
on_searchentry_icon_press(GtkEntry * entry,GtkEntryIconPosition icon_pos,GdkEvent * event,gpointer user_data)460 on_searchentry_icon_press (GtkEntry            *entry,
461                            GtkEntryIconPosition icon_pos,
462                            GdkEvent            *event,
463                            gpointer             user_data)
464 {
465     w_quick_search_t *w = user_data;
466     if (icon_pos == GTK_ENTRY_ICON_PRIMARY) {
467         gtk_menu_popup (GTK_MENU (w->popup), NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time ());
468     }
469     else {
470         gtk_entry_set_text (entry, "");
471     }
472 }
473 
474 static void
add_history_query_to_combo(gpointer user_data,const char * text,int prepend)475 add_history_query_to_combo (gpointer user_data, const char *text, int prepend)
476 {
477     if (!user_data || !text) {
478         return;
479     }
480     w_quick_search_t *w = user_data;
481 
482     if (history_entries >= config_history_size) {
483         gtk_combo_box_text_remove (GTK_COMBO_BOX_TEXT (w->combo), config_history_size);
484     }
485 
486     if (prepend) {
487         gtk_combo_box_text_prepend_text (GTK_COMBO_BOX_TEXT (w->combo), text);
488     }
489     else {
490         gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (w->combo), text);
491     }
492 
493     if (!history_entries) {
494         gtk_widget_set_sensitive (GTK_WIDGET (w->clear_history), TRUE);
495     }
496     history_entries++;
497 }
498 
499 static void
add_history_entry(gpointer user_data)500 add_history_entry (gpointer user_data)
501 {
502     if (!user_data) {
503         return;
504     }
505     w_quick_search_t *w = user_data;
506     const gchar *text = gtk_entry_get_text (GTK_ENTRY (searchentry));
507     if (text) {
508         if (!strcmp (text, "")) {
509             return;
510         }
511         if (w->prev_query) {
512             if (strcmp (w->prev_query, text)) {
513                 free (w->prev_query);
514                 w->prev_query = NULL;
515                 w->prev_query = strdup (text);
516                 add_history_query_to_combo (user_data, text, 1);
517             }
518         }
519         else {
520             w->prev_query = strdup (text);
521             add_history_query_to_combo (user_data, text, 1);
522         }
523     }
524 }
525 
526 static void
searchentry_perform_autosearch()527 searchentry_perform_autosearch ()
528 {
529     switch (config_search_in) {
530 #if (DDB_API_LEVEL >= 8)
531         case SEARCH_INLINE:
532             deadbeef->sendmessage (DB_EV_FOCUS_SELECTION, 0, PL_MAIN, 0);
533             break;
534 #endif
535         case SEARCH_PLAYLIST:
536         case SEARCH_ALL_PLAYLISTS:
537             on_add_quick_search_list ();
538             break;
539     }
540 }
541 
542 static gboolean
543 search_process (gpointer userdata);
544 
545 static gboolean
on_searchentry_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)546 on_searchentry_key_press_event           (GtkWidget       *widget,
547                                         GdkEventKey     *event,
548                                         gpointer         user_data)
549 {
550 #if GTK_CHECK_VERSION(3,0,0)
551     if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) {
552 #else
553     if (event->keyval == GDK_Return || event->keyval == GDK_KP_Enter) {
554 #endif
555         if (!config_autosearch) {
556             GtkEntry *entry = GTK_ENTRY (widget);
557             const gchar *text = gtk_entry_get_text (entry);
558             if (search_delay_timer) {
559                 g_source_remove (search_delay_timer);
560                 search_delay_timer = 0;
561             }
562             search_delay_timer = g_timeout_add (100, search_process, (void *)text);
563         }
564         else {
565             on_searchentry_activate (NULL, 0);
566         }
567         add_history_entry (user_data);
568         if (added_plt) {
569             deadbeef->plt_unref (added_plt);
570             added_plt = NULL;
571         }
572         return TRUE;
573     }
574     return FALSE;
575 }
576 
577 static void
578 update_list ()
579 {
580 #if (DDB_API_LEVEL >= 8)
581     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_SELECTION, 0);
582     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_SEARCHRESULT, 0);
583 #else
584     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0);
585 #endif
586 }
587 
588 static gboolean
589 search_process (gpointer userdata) {
590     if (search_delay_timer) {
591         g_source_remove (search_delay_timer);
592         search_delay_timer = 0;
593     }
594     g_return_val_if_fail (userdata != NULL, FALSE);
595 
596     const char *text = userdata;
597     deadbeef->pl_lock ();
598     if (config_search_in != SEARCH_ALL_PLAYLISTS) {
599         ddb_playlist_t *plt = deadbeef->plt_get_curr ();
600         if (plt) {
601             if (is_quick_search_playlist (plt)) {
602                 deadbeef->plt_unref (plt);
603                 plt = get_last_active_playlist ();
604             }
605             else {
606                 set_last_active_playlist (plt);
607             }
608             if (plt) {
609                 deadbeef->plt_search_process (plt, text);
610                 deadbeef->plt_unref (plt);
611             }
612         }
613     }
614     else {
615         ddb_playlist_t *plt_curr = deadbeef->plt_get_curr ();
616         if (plt_curr) {
617             set_last_active_playlist (plt_curr);
618             deadbeef->plt_unref (plt_curr);
619         }
620         int plt_count = deadbeef->plt_get_count ();
621         for (int i = 0; i < plt_count; i++) {
622             ddb_playlist_t *plt = deadbeef->plt_get_for_idx (i);
623             if (!plt) {
624                 continue;
625             }
626             if (!is_quick_search_playlist (plt)) {
627                 deadbeef->plt_deselect_all (plt);
628                 deadbeef->plt_search_process (plt, text);
629             }
630             deadbeef->plt_unref (plt);
631         }
632     }
633     deadbeef->pl_unlock ();
634 
635     update_list ();
636     searchentry_perform_autosearch ();
637     if (config_autosearch && !strcmp (text, "")){
638         ddb_playlist_t *plt = get_last_active_playlist ();
639         if (plt) {
640             deadbeef->plt_set_curr (plt);
641             deadbeef->plt_unref (plt);
642         }
643     }
644 
645     return FALSE;
646 }
647 
648 static void
649 on_searchentry_changed                 (GtkEditable     *editable,
650                                         gpointer         user_data)
651 {
652     if (config_autosearch) {
653         GtkEntry *entry = GTK_ENTRY (editable);
654         const gchar *text = gtk_entry_get_text (entry);
655         if (search_delay_timer) {
656             g_source_remove (search_delay_timer);
657             search_delay_timer = 0;
658         }
659         search_delay_timer = g_timeout_add (100, search_process, (void *)text);
660     }
661 }
662 
663 static gboolean
664 on_searchentry_focus_out_event (GtkWidget *widget,
665                                GdkEvent  *event,
666                                gpointer   user_data)
667 {
668     if (added_plt) {
669         deadbeef->plt_unref (added_plt);
670         added_plt = NULL;
671     }
672     add_history_entry (user_data);
673     return FALSE;
674 }
675 
676 static gboolean
677 on_searchentry_focus_in_event (GtkWidget *widget,
678                                GdkEvent  *event,
679                                gpointer   user_data)
680 {
681     on_searchentry_changed (GTK_EDITABLE (widget), user_data);
682     return FALSE;
683 }
684 
685 static int initialized = 0;
686 
687 static int
688 quick_search_on_action (DB_plugin_action_t *action, int ctx)
689 {
690     if (initialized && searchentry) {
691         gtk_widget_grab_focus (searchentry);
692     }
693     return 0;
694 }
695 
696 static void
697 quick_search_set_placeholder_text ()
698 {
699 #if GTK_CHECK_VERSION(3,0,0)
700     switch (config_search_in) {
701         case SEARCH_INLINE:
702             gtk_entry_set_placeholder_text (GTK_ENTRY (searchentry), "Search in playlist (inline)...");
703             break;
704         case SEARCH_PLAYLIST:
705             gtk_entry_set_placeholder_text (GTK_ENTRY (searchentry), "Search in playlist...");
706             break;
707         case SEARCH_ALL_PLAYLISTS:
708             gtk_entry_set_placeholder_text (GTK_ENTRY (searchentry), "Search in all playlists...");
709             break;
710         default:
711             gtk_entry_set_placeholder_text (GTK_ENTRY (searchentry), "Search...");
712             break;
713     }
714 #endif
715 }
716 
717 static void
718 on_search_playlist_inline_activate     (GtkMenuItem     *menuitem,
719                                         gpointer         user_data)
720 {
721     deadbeef->conf_set_int (CONFSTR_SEARCH_IN, SEARCH_INLINE);
722     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
723     config_search_in = SEARCH_INLINE;
724     quick_search_set_placeholder_text ();
725 }
726 
727 static void
728 on_search_playlist_activate            (GtkMenuItem     *menuitem,
729                                         gpointer         user_data)
730 {
731     deadbeef->conf_set_int (CONFSTR_SEARCH_IN, SEARCH_PLAYLIST);
732     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
733     config_search_in = SEARCH_PLAYLIST;
734     quick_search_set_placeholder_text ();
735 }
736 
737 static void
738 on_search_all_playlists_activate       (GtkMenuItem     *menuitem,
739                                         gpointer         user_data)
740 {
741     deadbeef->conf_set_int (CONFSTR_SEARCH_IN, SEARCH_ALL_PLAYLISTS);
742     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
743     config_search_in = SEARCH_ALL_PLAYLISTS;
744     quick_search_set_placeholder_text ();
745 }
746 
747 static void
748 on_autosearch_activate       (GtkMenuItem     *menuitem,
749                                         gpointer         user_data)
750 {
751     config_autosearch = config_autosearch ? FALSE : TRUE;
752     deadbeef->conf_set_int (CONFSTR_AUTOSEARCH, config_autosearch);
753     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
754 }
755 
756 static void
757 on_clear_history_activate       (GtkMenuItem     *menuitem,
758                                         gpointer         user_data)
759 {
760     w_quick_search_t *w = user_data;
761     if (history_entries) {
762         for (int i = 0; i < history_entries; i++) {
763             gtk_combo_box_text_remove (GTK_COMBO_BOX_TEXT (w->combo), 0);
764         }
765         history_entries = 0;
766         gtk_widget_set_sensitive (GTK_WIDGET (w->clear_history), FALSE);
767     }
768 }
769 
770 static void
771 quick_search_create_popup_menu (gpointer user_data)
772 {
773     w_quick_search_t *w = user_data;
774     w->popup = gtk_menu_new ();
775     gtk_widget_show (w->popup);
776 
777     GtkWidget *search_in = gtk_menu_item_new_with_mnemonic ("Search in");
778     gtk_container_add (GTK_CONTAINER (w->popup), search_in);
779     gtk_widget_show (search_in);
780 
781     GtkWidget *search_in_menu = gtk_menu_new ();
782     gtk_menu_item_set_submenu (GTK_MENU_ITEM (search_in), search_in_menu);
783 
784 
785     GSList *search_in_group = NULL;
786     GtkWidget *search_playlist_inline = gtk_radio_menu_item_new_with_mnemonic (search_in_group, "Playlist (inline)");
787     search_in_group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (search_playlist_inline));
788     gtk_widget_show (search_playlist_inline);
789     gtk_container_add (GTK_CONTAINER (search_in_menu), search_playlist_inline);
790     g_signal_connect ((gpointer) search_playlist_inline, "activate",
791             G_CALLBACK (on_search_playlist_inline_activate),
792             NULL);
793 
794     GtkWidget *search_playlist = gtk_radio_menu_item_new_with_mnemonic (search_in_group, "Playlist");
795     search_in_group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (search_playlist));
796     gtk_widget_show (search_playlist);
797     gtk_container_add (GTK_CONTAINER (search_in_menu), search_playlist);
798     g_signal_connect ((gpointer) search_playlist, "activate",
799             G_CALLBACK (on_search_playlist_activate),
800             NULL);
801 
802     GtkWidget *search_all_playlists = gtk_radio_menu_item_new_with_mnemonic (search_in_group, "All Playlists");
803     search_in_group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (search_all_playlists));
804     gtk_widget_show (search_all_playlists);
805     gtk_container_add (GTK_CONTAINER (search_in_menu), search_all_playlists);
806     g_signal_connect ((gpointer) search_all_playlists, "activate",
807             G_CALLBACK (on_search_all_playlists_activate),
808             NULL);
809 
810     GtkWidget *autosearch = gtk_check_menu_item_new_with_mnemonic ("Autosearch");
811     gtk_widget_show (autosearch);
812     gtk_container_add (GTK_CONTAINER (w->popup), autosearch);
813     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (autosearch), config_autosearch);
814     g_signal_connect ((gpointer) autosearch, "activate",
815             G_CALLBACK (on_autosearch_activate),
816             NULL);
817 
818     GtkWidget *separator = gtk_separator_menu_item_new ();
819     gtk_widget_show (separator);
820     gtk_container_add (GTK_CONTAINER (w->popup), separator);
821 
822     w->clear_history = gtk_menu_item_new_with_mnemonic ("Clear history");
823     gtk_widget_show (w->clear_history);
824     gtk_container_add (GTK_CONTAINER (w->popup), w->clear_history);
825     gtk_widget_set_sensitive (GTK_WIDGET (w->clear_history), history_entries);
826     g_signal_connect ((gpointer) w->clear_history, "activate",
827             G_CALLBACK (on_clear_history_activate),
828             user_data);
829 
830     if (config_search_in == SEARCH_INLINE) {
831         gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (search_playlist_inline), TRUE);
832     }
833     else if (config_search_in == SEARCH_PLAYLIST) {
834         gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (search_playlist), TRUE);
835     }
836     else if (config_search_in == SEARCH_ALL_PLAYLISTS) {
837         gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (search_all_playlists), TRUE);
838     }
839 }
840 
841 static int
842 quick_search_message (ddb_gtkui_widget_t *widget, uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2)
843 {
844     switch (id) {
845         case DB_EV_CONFIGCHANGED:
846             config_search_in = deadbeef->conf_get_int (CONFSTR_SEARCH_IN, FALSE);
847             config_autosearch = deadbeef->conf_get_int (CONFSTR_AUTOSEARCH, TRUE);
848             config_append_search_string = deadbeef->conf_get_int (CONFSTR_APPEND_SEARCH_STRING, FALSE);
849 
850             if (!config_append_search_string) {
851                 set_default_quick_search_playlist_title ();
852             }
853             break;
854     }
855     return 0;
856 }
857 
858 
859 #if GTK_CHECK_VERSION(3,0,0)
860 static DB_plugin_action_t
861 quick_search_action_gtk3 = {
862     .title = "Quick search (GTK3)",
863     .name = "quick_search_gtk3",
864     .flags = DB_ACTION_COMMON,
865     .callback2 = quick_search_on_action,
866     .next = NULL
867 };
868 #else
869 static DB_plugin_action_t
870 quick_search_action = {
871     .title = "Quick search",
872     .name = "quick_search",
873     .flags = DB_ACTION_COMMON,
874     .callback2 = quick_search_on_action,
875     .next = NULL
876 };
877 #endif
878 
879 static DB_plugin_action_t *
880 quick_search_get_actions (DB_playItem_t *it)
881 {
882 #if GTK_CHECK_VERSION(3,0,0)
883     return &quick_search_action_gtk3;
884 #else
885     return &quick_search_action;
886 #endif
887 }
888 
889 static void
890 quick_search_init (ddb_gtkui_widget_t *ww) {
891     w_quick_search_t *w = (w_quick_search_t *)ww;
892 
893     cache_path_size = make_cache_dir (cache_path, sizeof (cache_path));
894 
895     GtkWidget *hbox = gtk_hbox_new (FALSE, 3);
896     gtk_widget_show (hbox);
897     gtk_container_add (GTK_CONTAINER (w->base.widget), hbox);
898 
899     searchentry = gtk_entry_new ();
900 #if GTK_CHECK_VERSION(3,0,0)
901     gtk_entry_set_icon_from_icon_name (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_PRIMARY, "edit-find-symbolic");
902     gtk_entry_set_icon_from_icon_name (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_SECONDARY, "edit-clear-symbolic");
903 #else
904     gtk_entry_set_icon_from_icon_name (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_PRIMARY, "edit-find");
905     gtk_entry_set_icon_from_icon_name (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_SECONDARY, "edit-clear");
906 #endif
907     gtk_entry_set_invisible_char (GTK_ENTRY (searchentry), 8226);
908     gtk_entry_set_activates_default (GTK_ENTRY (searchentry), TRUE);
909     gtk_entry_set_icon_tooltip_text (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_PRIMARY, "Preferences");
910     gtk_entry_set_icon_tooltip_text (GTK_ENTRY (searchentry), GTK_ENTRY_ICON_SECONDARY, "Clear the search text");
911     gtk_widget_show (searchentry);
912 
913     w->combo = gtk_combo_box_text_new_with_entry ();
914     gtk_container_add (GTK_CONTAINER (hbox), w->combo);
915     gtk_container_remove (GTK_CONTAINER (w->combo), gtk_bin_get_child (GTK_BIN (w->combo)));
916     gtk_container_add (GTK_CONTAINER (w->combo), searchentry);
917     gtk_widget_show (w->combo);
918 
919     GtkEntryCompletion *completion = gtk_entry_completion_new ();
920     gtk_entry_set_completion (GTK_ENTRY (searchentry), completion);
921     g_object_unref (completion);
922     gtk_entry_completion_set_model (completion, gtk_combo_box_get_model (GTK_COMBO_BOX (w->combo)));
923     gtk_entry_completion_set_text_column (completion, 0);
924 
925     g_signal_connect ((gpointer) searchentry, "changed",
926             G_CALLBACK (on_searchentry_changed),
927             NULL);
928     g_signal_connect ((gpointer) searchentry, "key_press_event",
929             G_CALLBACK (on_searchentry_key_press_event),
930             w);
931     g_signal_connect ((gpointer) searchentry, "focus_in_event",
932             G_CALLBACK (on_searchentry_focus_in_event),
933             NULL);
934     g_signal_connect ((gpointer) searchentry, "focus_out_event",
935             G_CALLBACK (on_searchentry_focus_out_event),
936             w);
937     g_signal_connect ((gpointer) searchentry, "icon_release",
938             G_CALLBACK (on_searchentry_icon_press),
939             w);
940 
941     config_search_in = deadbeef->conf_get_int (CONFSTR_SEARCH_IN, FALSE);
942     config_autosearch = deadbeef->conf_get_int (CONFSTR_AUTOSEARCH, TRUE);
943     config_append_search_string = deadbeef->conf_get_int (CONFSTR_APPEND_SEARCH_STRING, FALSE);
944     quick_search_set_placeholder_text ();
945     quick_search_create_popup_menu (w);
946     load_history_entries (w);
947 
948     initialized = 1;
949 }
950 
951 static void
952 quick_search_destroy (ddb_gtkui_widget_t *w) {
953     w_quick_search_t *ww = (w_quick_search_t *)w;
954     if (last_active_plt) {
955         deadbeef->plt_unref (last_active_plt);
956         last_active_plt = NULL;
957     }
958     if (ww->prev_query) {
959         free (ww->prev_query);
960         ww->prev_query = NULL;
961     }
962     if (search_delay_timer) {
963         g_source_remove (search_delay_timer);
964         search_delay_timer = 0;
965     }
966 }
967 
968 static void
969 quick_search_save (struct ddb_gtkui_widget_s *w, char *s, int sz)
970 {
971     save_history_entries (w);
972 }
973 
974 static ddb_gtkui_widget_t *
975 w_quick_search_create (void) {
976     w_quick_search_t *w = malloc (sizeof (w_quick_search_t));
977     memset (w, 0, sizeof (w_quick_search_t));
978 
979     w->base.widget = gtk_event_box_new ();
980     w->base.destroy  = quick_search_destroy;
981     w->base.init = quick_search_init;
982     w->base.save = quick_search_save;
983     w->base.message = quick_search_message;
984     gtkui_plugin->w_override_signals (w->base.widget, w);
985 
986     return (ddb_gtkui_widget_t *)w;
987 }
988 
989 static int
990 quick_search_connect (void)
991 {
992     gtkui_plugin = (ddb_gtkui_t *) deadbeef->plug_get_for_id (DDB_GTKUI_PLUGIN_ID);
993     if (gtkui_plugin) {
994         //trace("using '%s' plugin %d.%d\n", DDB_GTKUI_PLUGIN_ID, gtkui_plugin->gui.plugin.version_major, gtkui_plugin->gui.plugin.version_minor );
995         if (gtkui_plugin->gui.plugin.version_major == 2) {
996             //printf ("fb api2\n");
997             // 0.6+, use the new widget API
998             gtkui_plugin->w_reg_widget ("Quick search", DDB_WF_SINGLE_INSTANCE, w_quick_search_create, "quick_search", NULL);
999             return 0;
1000         }
1001     }
1002     return -1;
1003 }
1004 
1005 static const char settings_dlg[] =
1006     "property \"Append search string to playlist name \" checkbox " CONFSTR_APPEND_SEARCH_STRING " 0 ;\n"
1007     "property \"History size: \" spinbtn[0,20,1] " CONFSTR_HISTORY_SIZE " 10 ;\n"
1008 ;
1009 
1010 static int
1011 quick_search_disconnect (void)
1012 {
1013     gtkui_plugin = NULL;
1014     return 0;
1015 }
1016 
1017 // define plugin interface
1018 static DB_misc_t plugin = {
1019     .plugin.api_vmajor = 1,
1020     .plugin.api_vminor = 5,
1021     .plugin.version_major = 0,
1022     .plugin.version_minor = 1,
1023 #if GTK_CHECK_VERSION(3,0,0)
1024     .plugin.id              = "quick_search-gtk3",
1025 #else
1026     .plugin.id              = "quick_search",
1027 #endif
1028     .plugin.type = DB_PLUGIN_MISC,
1029     .plugin.name = "Quick search",
1030     .plugin.descr = "A widget to perform a quick search",
1031     .plugin.copyright =
1032         "Copyright (C) 2015 Christian Boxdörfer <christian.boxdoerfer@posteo.de>\n"
1033         "\n"
1034         "This program is free software; you can redistribute it and/or\n"
1035         "modify it under the terms of the GNU General Public License\n"
1036         "as published by the Free Software Foundation; either version 2\n"
1037         "of the License, or (at your option) any later version.\n"
1038         "\n"
1039         "This program is distributed in the hope that it will be useful,\n"
1040         "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
1041         "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
1042         "GNU General Public License for more details.\n"
1043         "\n"
1044         "You should have received a copy of the GNU General Public License\n"
1045         "along with this program; if not, write to the Free Software\n"
1046         "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
1047     ,
1048     .plugin.website = "http://www.github.com/cboxdoerfer/ddb_quick_search",
1049     .plugin.connect  = quick_search_connect,
1050     .plugin.disconnect  = quick_search_disconnect,
1051     .plugin.get_actions     = quick_search_get_actions,
1052     .plugin.configdialog    = settings_dlg,
1053 };
1054 
1055 #if !GTK_CHECK_VERSION(3,0,0)
1056 DB_plugin_t *
1057 ddb_misc_quick_search_GTK2_load (DB_functions_t *ddb) {
1058     deadbeef = ddb;
1059     return &plugin.plugin;
1060 }
1061 #else
1062 DB_plugin_t *
1063 ddb_misc_quick_search_GTK3_load (DB_functions_t *ddb) {
1064     deadbeef = ddb;
1065     return &plugin.plugin;
1066 }
1067 #endif
1068