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