1 /*
2     DeaDBeeF -- the music player
3     Copyright (C) 2009-2015 Alexey Yakovenko and other contributors
4 
5     This software is provided 'as-is', without any express or implied
6     warranty.  In no event will the authors be held liable for any damages
7     arising from the use of this software.
8 
9     Permission is granted to anyone to use this software for any purpose,
10     including commercial applications, and to alter it and redistribute it
11     freely, subject to the following restrictions:
12 
13     1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17 
18     2. Altered source versions must be plainly marked as such, and must not be
19      misrepresented as being the original software.
20 
21     3. This notice may not be removed or altered from any source distribution.
22 */
23 
24 #ifdef HAVE_CONFIG_H
25 #  include <config.h>
26 #endif
27 
28 #include <gtk/gtk.h>
29 #include <gdk/gdkkeysyms.h>
30 #include <math.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include "../../gettext.h"
34 
35 #include "callbacks.h"
36 #include "interface.h"
37 #include "support.h"
38 
39 #include "search.h"
40 #include "ddblistview.h"
41 #include "plcommon.h"
42 #include "../../deadbeef.h"
43 #include "mainplaylist.h"
44 
45 #include "gtkui.h"
46 
47 #include "wingeom.h"
48 
49 #define min(x,y) ((x)<(y)?(x):(y))
50 #define max(x,y) ((x)>(y)?(x):(y))
51 
52 extern DB_functions_t *deadbeef; // defined in gtkui.c
53 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
54 #define trace(fmt,...)
55 
56 extern GtkWidget *searchwin;
57 extern GtkWidget *mainwin;
58 
59 static char *window_title_bytecode = NULL;
60 
61 static gboolean
unlock_search_columns_cb(void * ctx)62 unlock_search_columns_cb (void *ctx) {
63     ddb_listview_lock_columns (DDB_LISTVIEW (lookup_widget (searchwin, "searchlist")), 0);
64     return FALSE;
65 }
66 
67 void
search_start(void)68 search_start (void) {
69     ddb_listview_lock_columns (DDB_LISTVIEW (lookup_widget (searchwin, "searchlist")), 1);
70     wingeom_restore (searchwin, "searchwin", -1, -1, 450, 150, 0);
71     gtk_entry_set_text (GTK_ENTRY (lookup_widget (searchwin, "searchentry")), "");
72     gtk_widget_grab_focus (lookup_widget (searchwin, "searchentry"));
73     gtk_widget_show (searchwin);
74     gtk_window_present (GTK_WINDOW (searchwin));
75     g_idle_add (unlock_search_columns_cb, NULL);
76     search_refresh ();
77     main_refresh ();
78 }
79 
80 void
search_destroy(void)81 search_destroy (void) {
82     gtk_widget_destroy (searchwin);
83     searchwin = NULL;
84     if (window_title_bytecode) {
85         deadbeef->tf_free (window_title_bytecode);
86         window_title_bytecode = NULL;
87     }
88 }
89 
90 static void
search_process(const char * text)91 search_process (const char *text) {
92     ddb_playlist_t *plt = deadbeef->plt_get_curr ();
93     deadbeef->plt_search_process (plt, text);
94     deadbeef->plt_unref (plt);
95 
96     int row = deadbeef->pl_get_cursor (PL_SEARCH);
97     if (row >= deadbeef->pl_getcount (PL_SEARCH)) {
98         deadbeef->pl_set_cursor (PL_SEARCH, deadbeef->pl_getcount (PL_SEARCH) - 1);
99     }
100 }
101 
102 void
on_searchentry_changed(GtkEditable * editable,gpointer user_data)103 on_searchentry_changed                 (GtkEditable     *editable,
104                                         gpointer         user_data)
105 {
106     search_refresh ();
107     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_SELECTION, 0);
108     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_SEARCHRESULT, 0);
109 }
110 
111 void
search_refresh(void)112 search_refresh (void) {
113     if (searchwin && gtk_widget_get_visible (searchwin)) {
114         GtkEntry *entry = GTK_ENTRY (lookup_widget (searchwin, "searchentry"));
115         const gchar *text = gtk_entry_get_text (entry);
116         search_process (text);
117         GtkWidget *pl = lookup_widget (searchwin, "searchlist");
118         ddb_listview_refresh (DDB_LISTVIEW (pl), DDB_REFRESH_VSCROLL | DDB_REFRESH_LIST | DDB_LIST_CHANGED);
119         deadbeef->sendmessage (DB_EV_FOCUS_SELECTION, (uintptr_t)pl, PL_MAIN, 0);
120 
121         char title[1024] = "";
122         ddb_tf_context_t ctx = {
123             ._size = sizeof (ddb_tf_context_t),
124             .plt = deadbeef->plt_get_curr (),
125             .iter = PL_SEARCH
126         };
127         deadbeef->tf_eval (&ctx, window_title_bytecode, title, sizeof (title));
128         gtk_window_set_title (GTK_WINDOW (searchwin), title);
129     }
130 }
131 
132 void
search_redraw(void)133 search_redraw (void) {
134     if (searchwin && gtk_widget_get_visible (searchwin)) {
135         GtkWidget *pl = lookup_widget (searchwin, "searchlist");
136         ddb_listview_refresh (DDB_LISTVIEW (pl), DDB_REFRESH_VSCROLL | DDB_REFRESH_LIST | DDB_LIST_CHANGED);
137     }
138 }
139 
140 ///////// searchwin header handlers
141 
142 gboolean
on_searchheader_button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)143 on_searchheader_button_press_event     (GtkWidget       *widget,
144                                         GdkEventButton  *event,
145                                         gpointer         user_data)
146 {
147 
148   return FALSE;
149 }
150 
151 
152 gboolean
on_searchheader_button_release_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)153 on_searchheader_button_release_event   (GtkWidget       *widget,
154                                         GdkEventButton  *event,
155                                         gpointer         user_data)
156 {
157 
158   return FALSE;
159 }
160 
161 
162 gboolean
on_searchheader_configure_event(GtkWidget * widget,GdkEventConfigure * event,gpointer user_data)163 on_searchheader_configure_event        (GtkWidget       *widget,
164                                         GdkEventConfigure *event,
165                                         gpointer         user_data)
166 {
167     return FALSE;
168 }
169 
170 
171 gboolean
on_searchheader_expose_event(GtkWidget * widget,GdkEventExpose * event,gpointer user_data)172 on_searchheader_expose_event           (GtkWidget       *widget,
173                                         GdkEventExpose  *event,
174                                         gpointer         user_data)
175 {
176 
177   return FALSE;
178 }
179 
180 
181 gboolean
on_searchheader_motion_notify_event(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)182 on_searchheader_motion_notify_event    (GtkWidget       *widget,
183                                         GdkEventMotion  *event,
184                                         gpointer         user_data)
185 {
186 
187   return FALSE;
188 }
189 
190 
191 ///////// searchwin playlist navigation and rendering
192 
193 void
on_searchentry_activate(GtkEntry * entry,gpointer user_data)194 on_searchentry_activate                (GtkEntry        *entry,
195                                         gpointer         user_data)
196 {
197     if (deadbeef->pl_getcount (PL_SEARCH) > 0) {
198         int row = deadbeef->pl_get_cursor (PL_SEARCH);
199         DB_playItem_t *it = deadbeef->pl_get_for_idx_and_iter (max (row, 0), PL_SEARCH);
200         if (it) {
201             deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, deadbeef->pl_get_idx_of (it), 0);
202             deadbeef->pl_item_unref (it);
203         }
204     }
205 }
206 
207 
208 gboolean
on_searchwin_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)209 on_searchwin_key_press_event           (GtkWidget       *widget,
210                                         GdkEventKey     *event,
211                                         gpointer         user_data)
212 {
213     // that's for when user attempts to navigate list while entry has focus
214     if (event->keyval == GDK_Escape) {
215         gtk_widget_hide (searchwin);
216         return TRUE;
217     }
218     else if (event->keyval == GDK_Return) {
219         on_searchentry_activate (NULL, 0);
220         return TRUE;
221     }
222     return FALSE;
223 }
224 
225 gboolean
on_searchwin_configure_event(GtkWidget * widget,GdkEventConfigure * event,gpointer user_data)226 on_searchwin_configure_event           (GtkWidget       *widget,
227                                         GdkEventConfigure *event,
228                                         gpointer         user_data)
229 {
230     wingeom_save (widget, "searchwin");
231     return FALSE;
232 }
233 
234 gboolean
on_searchwin_window_state_event(GtkWidget * widget,GdkEventWindowState * event,gpointer user_data)235 on_searchwin_window_state_event        (GtkWidget       *widget,
236                                         GdkEventWindowState *event,
237                                         gpointer         user_data)
238 {
239     wingeom_save_max (event, widget, "searchwin");
240     return FALSE;
241 }
242 
243 static int
search_get_count(void)244 search_get_count (void) {
245     return deadbeef->pl_getcount (PL_SEARCH);
246 }
247 
248 static int
search_get_sel_count(void)249 search_get_sel_count (void) {
250     int cnt = 0;
251     DB_playItem_t *it = deadbeef->pl_get_first (PL_SEARCH);
252     while (it) {
253         if (deadbeef->pl_is_selected (it)) {
254             cnt++;
255         }
256         DB_playItem_t *next = deadbeef->pl_get_next (it, PL_SEARCH);
257         deadbeef->pl_item_unref (it);
258         it = next;
259     }
260     return cnt;
261 }
262 
263 static int
search_get_cursor(void)264 search_get_cursor (void) {
265     return deadbeef->pl_get_cursor (PL_SEARCH);
266 }
267 
268 static void
search_set_cursor(int cursor)269 search_set_cursor (int cursor) {
270     return deadbeef->pl_set_cursor (PL_SEARCH, cursor);
271 }
272 
search_head(void)273 static DdbListviewIter search_head (void) {
274     return (DdbListviewIter)deadbeef->pl_get_first (PL_SEARCH);
275 }
276 
search_tail(void)277 static DdbListviewIter search_tail (void) {
278     return (DdbListviewIter)deadbeef->pl_get_last(PL_SEARCH);
279 }
280 
search_next(DdbListviewIter it)281 static DdbListviewIter search_next (DdbListviewIter it) {
282     return (DdbListviewIter)deadbeef->pl_get_next(it, PL_SEARCH);
283 }
284 
search_prev(DdbListviewIter it)285 static DdbListviewIter search_prev (DdbListviewIter it) {
286     return (DdbListviewIter)deadbeef->pl_get_prev(it, PL_SEARCH);
287 }
288 
search_get_for_idx(int idx)289 static DdbListviewIter search_get_for_idx (int idx) {
290     return deadbeef->pl_get_for_idx_and_iter (idx, PL_SEARCH);
291 }
292 
search_get_idx(DdbListviewIter it)293 int search_get_idx (DdbListviewIter it) {
294     DB_playItem_t *c = deadbeef->pl_get_first (PL_SEARCH);
295     int idx = 0;
296     while (c && c != it) {
297         DB_playItem_t *next = deadbeef->pl_get_next (c, PL_SEARCH);
298         deadbeef->pl_item_unref (c);
299         c = next;
300         idx++;
301     }
302     if (!c) {
303         return -1;
304     }
305     deadbeef->pl_item_unref (c);
306     return idx;
307 }
308 
309 static int
search_is_selected(DdbListviewIter it)310 search_is_selected (DdbListviewIter it) {
311     return deadbeef->pl_is_selected ((DB_playItem_t *)it);
312 }
313 
314 static void
search_select(DdbListviewIter it,int sel)315 search_select (DdbListviewIter it, int sel) {
316     deadbeef->pl_set_selected ((DB_playItem_t *)it, sel);
317     deadbeef->sendmessage (DB_EV_SELCHANGED, 0, deadbeef->plt_get_curr_idx (), PL_SEARCH);
318 }
319 
320 static void
search_col_sort(int col,int sort_order,void * user_data)321 search_col_sort (int col, int sort_order, void *user_data) {
322     col_info_t *c = (col_info_t*)user_data;
323     ddb_playlist_t *plt = deadbeef->plt_get_curr ();
324     deadbeef->plt_sort_v2 (plt, PL_SEARCH, c->id, c->format, sort_order-1);
325     deadbeef->plt_unref (plt);
326 }
327 
328 static void
search_groups_changed(DdbListview * listview,const char * format)329 search_groups_changed (DdbListview *listview, const char *format) {
330     if (!format) {
331         return;
332     }
333     if (listview->group_format) {
334         free (listview->group_format);
335     }
336     if (listview->group_title_bytecode) {
337         free (listview->group_title_bytecode);
338     }
339     deadbeef->conf_set_str ("gtkui.search.group_by_tf", format);
340     listview->group_format = strdup (format);
341     listview->group_title_bytecode = deadbeef->tf_compile (listview->group_format);
342 }
343 
344 static int lock_column_config = 0;
345 
346 static void
search_columns_changed(DdbListview * listview)347 search_columns_changed (DdbListview *listview) {
348     if (!lock_column_config) {
349         rewrite_column_config (listview, "gtkui.columns.search");
350     }
351 }
352 
353 static void
search_col_free_user_data(void * data)354 search_col_free_user_data (void *data) {
355     if (data) {
356         col_info_t *inf = data;
357         if (inf->format) {
358             free (inf->format);
359         }
360         free (data);
361     }
362 }
363 
364 static void
search_handle_doubleclick(DdbListview * listview,DdbListviewIter iter,int idx)365 search_handle_doubleclick (DdbListview *listview, DdbListviewIter iter, int idx) {
366     deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, deadbeef->pl_get_idx_of ((DB_playItem_t *)iter), 0);
367 }
368 
369 static void
search_selection_changed(DdbListview * ps,DdbListviewIter it,int idx)370 search_selection_changed (DdbListview *ps, DdbListviewIter it, int idx) {
371     deadbeef->sendmessage (DB_EV_SELCHANGED, (uintptr_t)ps, -1, -1);
372     deadbeef->sendmessage (DB_EV_FOCUS_SELECTION, (uintptr_t)ps, PL_MAIN, 0);
373 }
374 
375 static void
search_delete_selected(void)376 search_delete_selected (void) {
377     deadbeef->pl_delete_selected ();
378     main_refresh ();
379     search_refresh ();
380 }
381 
382 static void
search_header_context_menu(DdbListview * ps,int column)383 search_header_context_menu (DdbListview *ps, int column) {
384     GtkWidget *menu = create_headermenu (1);
385     set_last_playlist_cm (ps); // playlist ptr for context menu
386     set_active_column_cm (column);
387     gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, ps, 3, gtk_get_current_event_time());
388 }
389 
390 static void
search_draw_column_data(DdbListview * listview,cairo_t * cr,DdbListviewIter it,int idx,int column,int iter,int x,int y,int width,int height)391 search_draw_column_data (DdbListview *listview, cairo_t *cr, DdbListviewIter it, int idx, int column, int iter, int x, int y, int width, int height)
392 {
393     draw_column_data (listview, cr, it, idx, column, PL_SEARCH, x, y, width, height);
394 }
395 
396 static void
search_draw_group_title(DdbListview * listview,cairo_t * drawable,DdbListviewIter it,int iter,int x,int y,int width,int height)397 search_draw_group_title (DdbListview *listview, cairo_t *drawable, DdbListviewIter it, int iter, int x, int y, int width, int height)
398 {
399     pl_common_draw_group_title (listview, drawable, it, PL_SEARCH, x, y, width, height);
400 }
401 
402 static DdbListviewBinding search_binding = {
403     // rows
404     .count = search_get_count,
405     .sel_count = search_get_sel_count,
406 
407     .cursor = search_get_cursor,
408     .set_cursor = search_set_cursor,
409 
410     .head = search_head,
411     .tail = search_tail,
412     .next = search_next,
413     .prev = search_prev,
414 
415     .get_for_idx = search_get_for_idx,
416     .get_idx = search_get_idx,
417 
418     .is_selected = search_is_selected,
419     .select = search_select,
420 
421     .get_group = pl_common_get_group,
422     .groups_changed = search_groups_changed,
423 
424     .drag_n_drop = NULL,
425     .external_drag_n_drop = NULL,
426 
427     .draw_column_data = search_draw_column_data,
428     .draw_album_art = draw_album_art,
429     .draw_group_title = search_draw_group_title,
430 
431     // columns
432     .col_sort = search_col_sort,
433     .columns_changed = search_columns_changed,
434     .col_free_user_data = search_col_free_user_data,
435 
436     // callbacks
437     .handle_doubleclick = search_handle_doubleclick,
438     .selection_changed = search_selection_changed,
439     .header_context_menu = search_header_context_menu,
440     .list_context_menu = list_context_menu,
441     .delete_selected = search_delete_selected,
442     .modification_idx = gtkui_get_curr_playlist_mod,
443 };
444 
445 void
search_playlist_init(GtkWidget * widget)446 search_playlist_init (GtkWidget *widget) {
447     DdbListview *listview = DDB_LISTVIEW(widget);
448     g_signal_connect ((gpointer)listview->list, "key_press_event", G_CALLBACK (on_searchwin_key_press_event), listview);
449     search_binding.ref = (void (*) (DdbListviewIter))deadbeef->pl_item_ref;
450     search_binding.unref = (void (*) (DdbListviewIter))deadbeef->pl_item_unref;
451     search_binding.is_selected = (int (*) (DdbListviewIter))deadbeef->pl_is_selected;
452     ddb_listview_set_binding (listview, &search_binding);
453     lock_column_config = 1;
454 
455     deadbeef->conf_lock ();
456     if (!deadbeef->conf_get_str_fast ("gtkui.columns.search", NULL)) {
457         import_column_config_0_6 ("search.column.", "gtkui.columns.search");
458     }
459     deadbeef->conf_unlock ();
460     // create default set of columns
461     if (load_column_config (listview, "gtkui.columns.search") < 0) {
462         add_column_helper (listview, _("Artist / Album"), 150, -1, "%artist% - %album%", 0);
463         add_column_helper (listview, _("Track No"), 50, -1, "%tracknumber%", 1);
464         add_column_helper (listview, _("Title"), 150, -1, "%title%", 0);
465         add_column_helper (listview, _("Duration"), 50, -1, "%length%", 0);
466     }
467     lock_column_config = 0;
468 
469     deadbeef->conf_lock ();
470     listview->group_format = strdup (deadbeef->conf_get_str_fast ("gtkui.search.group_by_tf", ""));
471     deadbeef->conf_unlock ();
472     listview->group_title_bytecode = deadbeef->tf_compile (listview->group_format);
473     window_title_bytecode = deadbeef->tf_compile (_("Search [(%list_total% results)]"));
474 }
475