1 /*
2  * Copyright (C) 2016 Jens Georg <mail@jensge.org>
3  *
4  * Authors: Jens Georg <mail@jensge.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  */
20 
21 #include <glib/gi18n.h>
22 #include <libgupnp-av/gupnp-av.h>
23 
24 #include <string.h>
25 
26 #include "entry-completion.h"
27 #include "search-dialog.h"
28 #include "server-device.h"
29 #include "didl-dialog.h"
30 #include "icons.h"
31 
32 /* DLNA recommends something between 10 and 30, let's just use 30
33  * cf. 7.4.1.4.10.9 in the 2014 version of the architecture and protocols
34  * guideline
35  * dlna-guidelines-march-2014---part-1-1-architectures-and-protocols.pdf
36  */
37 #define SEARCH_DIALOG_DEFAULT_SLICE 30
38 
39 #define DEFAULT_SEARCH_FILTER "upnp:class,dc:title"
40 
41 #define DIALOG_RESOURCE_PATH "/org/gupnp/Tools/AV-CP/search-dialog.ui"
42 
43 typedef struct _SearchTask SearchTask;
44 
45 struct _SearchDialog {
46         GtkDialog parent;
47 };
48 
49 struct _SearchDialogClass {
50         GtkDialogClass parent_class;
51 };
52 
53 struct _SearchDialogPrivate {
54         GtkListStore *search_dialog_liststore;
55         GtkEntry *search_dialog_entry;
56         GtkTreeView *search_dialog_treeview;
57         char *id;
58         char *title;
59         AVCPMediaServer *server;
60         guint pulse_timer;
61         SearchTask *task;
62         GUPnPSearchCriteriaParser *parser;
63         GRegex *position_re;
64         GtkWidget *popup_menu;
65 };
66 
67 typedef struct _SearchDialogPrivate SearchDialogPrivate;
68 G_DEFINE_TYPE_WITH_PRIVATE (SearchDialog, search_dialog, GTK_TYPE_DIALOG)
69 
70 static void
71 search_dialog_on_search_activate (SearchDialog *self, GtkEntry *entry);
72 
73 static gboolean
74 search_dialog_on_listview_button_release (GtkWidget      *widget,
75                                           GdkEventButton *event,
76                                           gpointer        user_data);
77 
78 static void
79 search_dialog_on_didl_popup_activate (SearchDialog *self, GVariant *parameter, gpointer user_data);
80 
81 static void
82 search_dialog_on_icon_release (SearchDialog *self, GtkEntryIconPosition icon_pos, GdkEvent *event, gpointer user_data);
83 
84 static void
85 search_dialog_finalize (GObject *object);
86 
87 static void
88 search_dialog_dispose (GObject *object);
89 
90 struct _SearchTask {
91         AVCPMediaServer *server;
92         char *search_expression;
93         char *container_id;
94         GtkListStore *target;
95         int start;
96         int count;
97         int total;
98         gboolean running;
99         GUPnPDIDLLiteParser *parser;
100         GError *error;
101         GSourceFunc callback;
102         gpointer user_data;
103         GCancellable *cancellable;
104 };
105 
106 static void
107 search_task_on_didl_object_available (GUPnPDIDLLiteParser *parser,
108                                       GUPnPDIDLLiteObject *object,
109                                       gpointer             user_data);
110 
111 static SearchTask *
search_task_new(AVCPMediaServer * server,GtkListStore * target,const char * container_id,const char * search_expression,int count,GSourceFunc callback,gpointer user_data)112 search_task_new (AVCPMediaServer *server,
113                  GtkListStore *target,
114                  const char *container_id,
115                  const char *search_expression,
116                  int count,
117                  GSourceFunc callback,
118                  gpointer user_data)
119 {
120         SearchTask *task = g_new0 (SearchTask, 1);
121 
122         task->search_expression = g_strdup (search_expression);
123         task->target = g_object_ref (target);
124         task->server = g_object_ref (server);
125         task->container_id = g_strdup (container_id);
126         task->start = 0;
127         task->count = count;
128         task->total = -1;
129         task->parser = gupnp_didl_lite_parser_new ();
130         task->error = NULL;
131         task->callback = callback;
132         task->user_data = user_data;
133         task->cancellable = g_cancellable_new ();
134 
135         g_signal_connect (G_OBJECT (task->parser),
136                           "object-available",
137                           G_CALLBACK (search_task_on_didl_object_available),
138                           task);
139 
140         return task;
141 }
142 
143 static void
search_task_free(SearchTask * task)144 search_task_free (SearchTask *task) {
145         g_clear_object (&task->target);
146         g_clear_object (&task->server);
147         g_clear_object (&task->parser);
148         g_free (task->search_expression);
149         g_free (task->container_id);
150         if (task->error != NULL) {
151                 g_error_free (task->error);
152         }
153         g_free (task);
154 }
155 
156 static void
search_task_cancel(SearchTask * task)157 search_task_cancel (SearchTask *task) {
158         g_cancellable_cancel (task->cancellable);
159 }
160 
161 static gboolean
search_task_idle_callback(gpointer user_data)162 search_task_idle_callback (gpointer user_data)
163 {
164         SearchTask *task = (SearchTask *)user_data;
165         if (task->callback != NULL) {
166             task->callback (task->user_data);
167         }
168 
169         return FALSE;
170 }
171 
172 static void
search_task_set_finished(SearchTask * task,GError * error)173 search_task_set_finished (SearchTask *task, GError *error)
174 {
175         task->running = FALSE;
176         task->error = error;
177 
178         g_idle_add (search_task_idle_callback, task);
179 }
180 
181 static void
search_task_on_search_ready(GObject * source,GAsyncResult * res,gpointer user_data)182 search_task_on_search_ready (GObject *source, GAsyncResult *res, gpointer user_data)
183 {
184         SearchTask *task  = (SearchTask *)user_data;
185         GError *error = NULL;
186         char *didl_xml = NULL;
187         guint32 total = 0;
188         guint32 returned = 0;
189         gboolean result;
190         gboolean finished = FALSE;
191 
192         result = av_cp_media_server_search_finish (AV_CP_MEDIA_SERVER (source),
193                                                    res,
194                                                    &didl_xml,
195                                                    &total,
196                                                    &returned,
197                                                    &error);
198 
199         g_debug ("Received search slice result for %s with expression %s, result is %s",
200                  task->container_id,
201                  task->search_expression,
202                  result ? "TRUE" : "FALSE");
203 
204 
205         if (!result) {
206                 finished = TRUE;
207 
208                 goto out;
209         }
210 
211         if (g_cancellable_is_cancelled (task->cancellable)) {
212                 finished = TRUE;
213 
214                 goto out;
215         }
216 
217         /* Nothing returned by the server */
218         if (returned == 0) {
219                 finished = TRUE;
220 
221                 goto out;
222         }
223 
224         if (didl_xml == NULL) {
225                 finished = TRUE;
226                 goto out;
227         }
228 
229         gupnp_didl_lite_parser_parse_didl (task->parser, didl_xml, &error);
230         if (error != NULL) {
231                 finished = TRUE;
232 
233                 goto out;
234         }
235 
236         if (total != 0) {
237                 task->total = total;
238         }
239 
240         task->start += returned;
241 
242 out:
243         g_clear_pointer (&didl_xml, g_free);
244         if (finished) {
245                 g_debug ("Finished search, error: %s",
246                          error ? error->message : "none");
247                 search_task_set_finished (task, error);
248         } else {
249                 g_debug ("Starting new slice %u/%u (total %u)",
250                          task->start,
251                          task->count,
252                          task->total == -1 ? 0 : task->total);
253 
254                 av_cp_media_server_search_async (task->server,
255                                                  task->cancellable,
256                                                  search_task_on_search_ready,
257                                                  task->container_id,
258                                                  task->search_expression,
259                                                  DEFAULT_SEARCH_FILTER,
260                                                  task->start,
261                                                  task->count,
262                                                  task);
263         }
264 }
265 
266 static void
search_task_run(SearchTask * task)267 search_task_run (SearchTask *task) {
268         if (task->running) {
269                 g_debug ("Search task is already running, not doing anything.");
270 
271                 return;
272         }
273 
274         g_debug ("Starting search task for %s with expression %s",
275                  task->container_id,
276                  task->search_expression);
277 
278         task->running = TRUE;
279 
280         av_cp_media_server_search_async (task->server,
281                                          NULL,
282                                          search_task_on_search_ready,
283                                          task->container_id,
284                                          task->search_expression,
285                                          DEFAULT_SEARCH_FILTER,
286                                          task->start,
287                                          task->count,
288                                          task);
289 }
290 
291 #define ITEM_CLASS_IMAGE "object.item.imageItem"
292 #define ITEM_CLASS_AUDIO "object.item.audioItem"
293 #define ITEM_CLASS_VIDEO "object.item.videoItem"
294 #define ITEM_CLASS_TEXT  "object.item.textItem"
295 #define CONTAINER_CLASS "object.container"
296 
297 static GdkPixbuf *
get_item_icon(GUPnPDIDLLiteObject * object)298 get_item_icon (GUPnPDIDLLiteObject *object)
299 {
300         GdkPixbuf  *icon;
301         const char *class_name;
302 
303         class_name = gupnp_didl_lite_object_get_upnp_class (object);
304         if (G_UNLIKELY (class_name == NULL)) {
305                 return get_icon_by_id (ICON_FILE);
306         }
307 
308         if (g_str_has_prefix (class_name, ITEM_CLASS_IMAGE)) {
309                 icon = get_icon_by_id (ICON_IMAGE_ITEM);
310         } else if (g_str_has_prefix (class_name, ITEM_CLASS_AUDIO)) {
311                 icon = get_icon_by_id (ICON_AUDIO_ITEM);
312         } else if (g_str_has_prefix (class_name, ITEM_CLASS_VIDEO)) {
313                 icon = get_icon_by_id (ICON_VIDEO_ITEM);
314         } else if (g_str_has_prefix (class_name, ITEM_CLASS_TEXT)) {
315                 icon = get_icon_by_id (ICON_TEXT_ITEM);
316         } else if (g_str_has_prefix (class_name, CONTAINER_CLASS) ){
317                 icon = get_icon_by_id (ICON_CONTAINER);
318         } else {
319                 icon = get_icon_by_id (ICON_FILE);
320         }
321 
322         return icon;
323 }
324 
325 static void
search_task_on_didl_object_available(GUPnPDIDLLiteParser * parser,GUPnPDIDLLiteObject * object,gpointer user_data)326 search_task_on_didl_object_available (GUPnPDIDLLiteParser *parser,
327                                       GUPnPDIDLLiteObject *object,
328                                       gpointer             user_data)
329 {
330         SearchTask *task = (SearchTask *)user_data;
331         GtkTreeIter iter;
332 
333         gtk_list_store_insert_with_values (task->target,
334                                            &iter,
335                                            -1,
336                                            0, get_item_icon (object),
337                                            1, gupnp_didl_lite_object_get_title (object),
338                                            3, gupnp_didl_lite_object_get_id (object),
339                                            -1);
340 }
341 
342 static void
search_dialog_class_init(SearchDialogClass * klass)343 search_dialog_class_init (SearchDialogClass *klass)
344 {
345         GObjectClass *object_class = G_OBJECT_CLASS (klass);
346         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
347 
348         gtk_widget_class_set_template_from_resource (widget_class,
349                                                      DIALOG_RESOURCE_PATH);
350         gtk_widget_class_bind_template_child_private (widget_class,
351                                                       SearchDialog,
352                                                       search_dialog_liststore);
353         gtk_widget_class_bind_template_child_private (widget_class,
354                                                       SearchDialog,
355                                                       search_dialog_entry);
356         gtk_widget_class_bind_template_child_private (widget_class,
357                                                       SearchDialog,
358                                                       search_dialog_treeview);
359         gtk_widget_class_bind_template_callback (widget_class,
360                                                  search_dialog_on_search_activate);
361         gtk_widget_class_bind_template_callback (widget_class,
362                                                  search_dialog_on_listview_button_release);
363         gtk_widget_class_bind_template_callback (widget_class, gtk_widget_hide);
364         gtk_widget_class_bind_template_callback (widget_class, search_dialog_on_icon_release);
365 
366 
367         object_class->finalize = search_dialog_finalize;
368         object_class->dispose = search_dialog_dispose;
369 }
370 
371 static void
search_dialog_init(SearchDialog * self)372 search_dialog_init (SearchDialog *self)
373 {
374         SearchDialogPrivate *priv = NULL;
375 
376         gtk_widget_init_template (GTK_WIDGET (self));
377         priv = search_dialog_get_instance_private (self);
378 
379         priv->parser = gupnp_search_criteria_parser_new ();
380         gtk_entry_set_completion (priv->search_dialog_entry,
381                                   entry_completion_new ());
382 
383         GMenu *menu = g_menu_new ();
384         g_menu_insert (menu, 0, _("Show _DIDL…"), "search.show-didl");
385         priv->popup_menu = gtk_menu_new_from_model (G_MENU_MODEL (menu));
386         g_object_unref (menu);
387 
388         gtk_menu_attach_to_widget (GTK_MENU (priv->popup_menu),
389                                    GTK_WIDGET (self),
390                                    NULL);
391         GSimpleActionGroup *group = g_simple_action_group_new ();
392         GSimpleAction *action = g_simple_action_new ("show-didl", NULL);
393         g_signal_connect_swapped (G_OBJECT (action),
394                                   "activate",
395                                   G_CALLBACK (search_dialog_on_didl_popup_activate),
396                                   self);
397         g_action_map_add_action (G_ACTION_MAP (group), G_ACTION (action));
398 
399         gtk_widget_insert_action_group (GTK_WIDGET (self),
400                                         "search",
401                                         G_ACTION_GROUP (group));
402         g_object_unref (group);
403         g_object_unref (action);
404 }
405 
406 static void
search_dialog_dispose(GObject * object)407 search_dialog_dispose (GObject *object)
408 {
409         SearchDialog *self = SEARCH_DIALOG (object);
410         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
411         GObjectClass *parent_class =
412                               G_OBJECT_CLASS (search_dialog_parent_class);
413 
414         if (priv->task != NULL) {
415                 search_task_cancel (priv->task);
416         }
417 
418         if (priv->pulse_timer != 0) {
419                 g_source_remove (priv->pulse_timer);
420                 priv->pulse_timer = 0;
421         }
422 
423         g_clear_object (&priv->parser);
424         g_clear_object (&priv->popup_menu);
425 
426         if (parent_class->dispose != NULL) {
427                 parent_class->dispose (object);
428         }
429 }
430 
431 static void
search_dialog_finalize(GObject * object)432 search_dialog_finalize (GObject *object)
433 {
434         SearchDialog *self = SEARCH_DIALOG (object);
435         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
436         GObjectClass *parent_class =
437                               G_OBJECT_CLASS (search_dialog_parent_class);
438 
439         g_clear_pointer (&priv->id, g_free);
440         g_clear_pointer (&priv->title, g_free);
441         g_clear_pointer (&priv->task, search_task_free);
442         g_clear_pointer (&priv->position_re, g_regex_unref);
443 
444         if (parent_class->finalize != NULL) {
445                 parent_class->finalize (object);
446         }
447 }
448 
449 static gboolean
search_dialog_on_search_task_done(gpointer user_data)450 search_dialog_on_search_task_done (gpointer user_data)
451 {
452         SearchDialog *self = SEARCH_DIALOG (user_data);
453         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
454 
455         g_source_remove (priv->pulse_timer);
456         gtk_entry_set_progress_fraction (priv->search_dialog_entry, 0);
457         gtk_editable_set_editable (GTK_EDITABLE (priv->search_dialog_entry), TRUE);
458         g_object_set (G_OBJECT (priv->search_dialog_entry),
459                         "primary-icon-name",
460                         "edit-find-symbolic",
461                         "secondary-icon-activatable",
462                         TRUE,
463                         NULL);
464 
465         /* Only show visible error if dialog is visible. */
466         if (priv->task->error != NULL &&
467             !g_error_matches (priv->task->error,
468                               G_IO_ERROR,
469                               G_IO_ERROR_CANCELLED) &&
470             gtk_widget_is_visible (GTK_WIDGET (self))) {
471                 GtkWidget *dialog = NULL;
472 
473                 dialog = gtk_message_dialog_new (GTK_WINDOW (self),
474                                                  GTK_DIALOG_DESTROY_WITH_PARENT,
475                                                  GTK_MESSAGE_WARNING,
476                                                  GTK_BUTTONS_CLOSE,
477                                                  "%s",
478                                                  _("Search failed"));
479 
480                 gtk_message_dialog_format_secondary_text
481                                     (GTK_MESSAGE_DIALOG (dialog),
482                                      _("Error message was: %s"),
483                                      priv->task->error->message);
484                 gtk_dialog_run (GTK_DIALOG (dialog));
485                 gtk_widget_destroy (dialog);
486 
487                 g_critical ("Failed to search: %s", priv->task->error->message);
488         }
489 
490         g_clear_pointer (&priv->task, search_task_free);
491 
492         return FALSE;
493 }
494 
495 void
search_dialog_set_server(SearchDialog * self,AVCPMediaServer * server)496 search_dialog_set_server (SearchDialog *self, AVCPMediaServer *server)
497 {
498         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
499         GtkEntryCompletion *completion = gtk_entry_get_completion (priv->search_dialog_entry);
500 
501         priv->server = server;
502 
503         entry_completion_set_search_criteria (ENTRY_COMPLETION (completion),
504                                               av_cp_media_server_get_search_caps (server));
505 }
506 
507 void
search_dialog_set_container_id(SearchDialog * self,char * id)508 search_dialog_set_container_id (SearchDialog *self, char *id)
509 {
510         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
511         g_free (priv->id);
512 
513         priv->id = id;
514 }
515 
516 void
search_dialog_set_container_title(SearchDialog * self,char * title)517 search_dialog_set_container_title (SearchDialog *self, char *title)
518 {
519         char *name = NULL;
520         char *window_title = NULL;
521 
522         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
523         g_free (priv->title);
524 
525         priv->title = title;
526         name = gupnp_device_info_get_friendly_name
527                                 (GUPNP_DEVICE_INFO (priv->server));
528 
529         if (g_str_equal (priv->id, "0")) {
530                 window_title = g_strdup_printf (_("Searching on %s"),
531                                          name);
532         } else {
533                 window_title = g_strdup_printf (_("Searching in %s on %s"),
534                                          title,
535                                          name);
536         }
537 
538         gtk_window_set_title (GTK_WINDOW (self), window_title);
539 
540         g_free (name);
541         g_free (window_title);
542 }
543 
544 void
search_dialog_run(SearchDialog * self)545 search_dialog_run (SearchDialog *self)
546 {
547         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
548         gtk_dialog_run (GTK_DIALOG (self));
549         gtk_widget_hide (GTK_WIDGET (self));
550 
551         if (priv->task != NULL &&
552             priv->task->running) {
553                 search_task_cancel (priv->task);
554         }
555 }
556 
557 static gboolean
pulse_timer(gpointer user_data)558 pulse_timer (gpointer user_data)
559 {
560         SearchDialog *self = SEARCH_DIALOG (user_data);
561         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
562         if (priv->task->total == -1) {
563                 gtk_entry_progress_pulse (GTK_ENTRY (priv->search_dialog_entry));
564         } else {
565                 gdouble progress = (gdouble) priv->task->start /
566                                    (gdouble) priv->task->total;
567                 gtk_entry_set_progress_fraction
568                                     (GTK_ENTRY (priv->search_dialog_entry),
569                                      progress);
570         }
571 
572         return TRUE;
573 }
574 
575 static void
search_dialog_on_search_activate(SearchDialog * self,GtkEntry * entry)576 search_dialog_on_search_activate (SearchDialog *self, GtkEntry *entry)
577 {
578         GError *error = NULL;
579         const char *text = gtk_entry_get_text (entry);
580         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
581 
582         gupnp_search_criteria_parser_parse_text (priv->parser, text, &error);
583         if (error == NULL) {
584 
585                 g_object_set (G_OBJECT (entry),
586                               "primary-icon-name",
587                               "media-playback-stop-symbolic",
588                               "secondary-icon-activatable",
589                               FALSE,
590                               NULL);
591                 gtk_list_store_clear (priv->search_dialog_liststore);
592                 gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
593                 priv->pulse_timer = g_timeout_add_seconds (1, pulse_timer, self);
594 
595                 g_clear_pointer (&priv->task, search_task_free);
596 
597                 priv->task = search_task_new (priv->server,
598                                               priv->search_dialog_liststore,
599                                               priv->id,
600                                               gtk_entry_get_text (entry),
601                                               SEARCH_DIALOG_DEFAULT_SLICE,
602                                               search_dialog_on_search_task_done,
603                                               self);
604                 search_task_run (priv->task);
605         } else {
606                 GtkWidget *dialog = NULL;
607                 GMatchInfo *info = NULL;
608                 char *position = NULL;
609 
610                 if (priv->position_re == NULL) {
611                         priv->position_re = g_regex_new ("([0-9]+)$", 0, 0, NULL);
612                 }
613 
614                 if (!g_regex_match (priv->position_re, error->message, 0, &info)) {
615                         position = g_strdup ("-1");
616                 } else {
617                         position = g_match_info_fetch (info, 0);
618                 }
619 
620                 g_match_info_free (info);
621 
622                 dialog = gtk_message_dialog_new (GTK_WINDOW (self),
623                                                  GTK_DIALOG_DESTROY_WITH_PARENT,
624                                                  GTK_MESSAGE_WARNING,
625                                                  GTK_BUTTONS_CLOSE,
626                                                  "%s",
627                                                  _("Search failed"));
628 
629                 gtk_message_dialog_format_secondary_text
630                                     (GTK_MESSAGE_DIALOG (dialog),
631                                      _("Search criteria invalid: %s"),
632                                      error->message);
633                 gtk_dialog_run (GTK_DIALOG (dialog));
634                 gtk_widget_destroy (dialog);
635 
636                 g_error_free (error);
637                 error = NULL;
638 
639                 gtk_editable_set_position (GTK_EDITABLE (entry),
640                                            atoi (position));
641                 g_free (position);
642 
643                 return;
644         }
645 
646 }
647 
648 static void
do_popup_menu(GtkMenu * menu,GtkWidget * widget,GdkEventButton * event)649 do_popup_menu (GtkMenu *menu, GtkWidget *widget, GdkEventButton *event)
650 {
651 #if GTK_CHECK_VERSION(3,22,0)
652     gtk_menu_popup_at_pointer (menu,
653                                event != NULL ? (GdkEvent *) event
654                                              : gtk_get_current_event ());
655 #else
656         int button = 0;
657         int event_time;
658         if (event) {
659                 button = event->button;
660                 event_time = event->time;
661         } else {
662                 event_time = gtk_get_current_event_time ();
663         }
664 
665         gtk_menu_popup (menu, NULL, NULL, NULL, NULL, button, event_time);
666 #endif
667 }
668 
669 static gboolean
search_dialog_on_listview_button_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)670 search_dialog_on_listview_button_release (GtkWidget      *widget,
671                                           GdkEventButton *event,
672                                           gpointer        user_data)
673 {
674         SearchDialog *self = SEARCH_DIALOG (user_data);
675         SearchDialogPrivate *priv = search_dialog_get_instance_private (self);
676         GtkTreeSelection *selection = NULL;
677         GtkTreeModel *model = NULL;
678         GtkTreeIter iter;
679         GtkTreeView  *treeview = priv->search_dialog_treeview;
680 
681         if (event->type != GDK_BUTTON_RELEASE || event->button != 3) {
682                 return FALSE;
683         }
684 
685         selection = gtk_tree_view_get_selection (treeview);
686         g_assert (selection != NULL);
687 
688         /* Only show the popup menu when a row is selected */
689         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
690                 return FALSE;
691         }
692 
693         do_popup_menu (GTK_MENU (priv->popup_menu),
694                        GTK_WIDGET (treeview),
695                        event);
696 
697         return TRUE;
698 }
699 
700 static void
on_object(GUPnPDIDLLiteParser * parser,GUPnPDIDLLiteObject * object,gpointer user_data)701 on_object (GUPnPDIDLLiteParser *parser,
702            GUPnPDIDLLiteObject *object,
703            gpointer user_data)
704 {
705         GUPnPDIDLLiteObject **result = (GUPnPDIDLLiteObject **)user_data;
706         if (*result == NULL) {
707                 *result = g_object_ref (object);
708         }
709 }
710 
711 static void
search_dialog_on_metadata_ready(GObject * source,GAsyncResult * res,gpointer user_data)712 search_dialog_on_metadata_ready (GObject *source,
713                                  GAsyncResult *res,
714                                  gpointer user_data)
715 {
716         SearchDialog *self = SEARCH_DIALOG (user_data);
717         SearchDialogPrivate *priv  = search_dialog_get_instance_private (self);
718         AVCPMediaServer *server = AV_CP_MEDIA_SERVER (source);
719         GtkTreeView  *treeview = priv->search_dialog_treeview;
720         GtkTreeModel *model = NULL;
721         GtkTreeSelection *selection = NULL;
722         char *xml;
723         GError *error = NULL;
724         GtkTreeIter iter;
725 
726         if (!av_cp_media_server_browse_metadata_finish (server,
727                                                         res,
728                                                         &xml,
729                                                         &error)) {
730                 GtkWidget *message = NULL;
731 
732                 message = gtk_message_dialog_new (GTK_WINDOW (self),
733                                                   GTK_DIALOG_MODAL,
734                                                   GTK_MESSAGE_WARNING,
735                                                   GTK_BUTTONS_CLOSE,
736                                                   _("Error fetching detailed information: %s"),
737                                                   error->message);
738                 gtk_dialog_run (GTK_DIALOG (message));
739                 gtk_widget_destroy (message);
740         } else {
741                 selection = gtk_tree_view_get_selection (treeview);
742                 if (gtk_tree_selection_get_selected (selection,
743                                                      &model,
744                                                      &iter)) {
745                         GUPnPDIDLLiteParser *parser = NULL;
746                         GUPnPDIDLLiteObject *didl_object = NULL;
747 
748                         parser = gupnp_didl_lite_parser_new ();
749                         g_signal_connect (G_OBJECT (parser),
750                                           "object-available",
751                                           G_CALLBACK (on_object),
752                                           &didl_object);
753                         gupnp_didl_lite_parser_parse_didl (parser, xml, NULL);
754                         gtk_list_store_set (GTK_LIST_STORE (model),
755                                             &iter,
756                                             2, didl_object,
757                                             -1);
758                         g_object_unref (parser);
759                         g_object_unref (didl_object);
760                         {
761                             AVCPDidlDialog *dialog = av_cp_didl_dialog_new ();
762 
763                             av_cp_didl_dialog_set_xml (dialog, xml);
764                             gtk_window_set_transient_for (GTK_WINDOW (dialog),
765                                                           GTK_WINDOW (self));
766                             gtk_dialog_run (GTK_DIALOG (dialog));
767                             gtk_widget_destroy (GTK_WIDGET (dialog));
768                         }
769                 }
770                 g_free (xml);
771         }
772 }
773 
774 static void
search_dialog_on_didl_popup_activate(SearchDialog * self,GVariant * parameter,gpointer user_data)775 search_dialog_on_didl_popup_activate (SearchDialog *self, GVariant *parameter, gpointer user_data)
776 {
777         SearchDialogPrivate *priv  = search_dialog_get_instance_private (self);
778         GtkTreeView  *treeview = priv->search_dialog_treeview;
779         GtkTreeModel *model = NULL;
780         GUPnPDIDLLiteObject *didl_object = NULL;
781         GtkTreeSelection *selection = NULL;
782         GtkTreeIter iter;
783         char *id = NULL;
784 
785         selection = gtk_tree_view_get_selection (treeview);
786 
787         /* Only show the popup menu when a row is selected */
788         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
789                 return;
790         }
791 
792         gtk_tree_model_get (model,
793                             &iter,
794                             2, &didl_object,
795                             3, &id,
796                             -1);
797 
798         if (didl_object != NULL) {
799                 AVCPDidlDialog *dialog = av_cp_didl_dialog_new ();
800                 char *xml = NULL;
801 
802                 g_free (id);
803                 xml = gupnp_didl_lite_object_get_xml_string (didl_object);
804                 av_cp_didl_dialog_set_xml (dialog, xml);
805                 gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (self));
806                 gtk_dialog_run (GTK_DIALOG (dialog));
807                 gtk_widget_destroy (GTK_WIDGET (dialog));
808                 g_free (xml);
809                 g_object_unref (didl_object);
810         } else {
811                 av_cp_media_server_browse_metadata_async (priv->server,
812                                                           NULL,
813                                                           search_dialog_on_metadata_ready,
814                                                           id,
815                                                           self);
816                 g_free (id);
817         }
818 }
819 
820 GtkDialog *
search_dialog_new(void)821 search_dialog_new (void)
822 {
823         GtkSettings *settings = gtk_settings_get_default ();
824         int use_header;
825 
826         g_object_get (G_OBJECT (settings),
827                                 "gtk-dialogs-use-header",
828                                 &use_header,
829                                 NULL);
830 
831         return (GtkDialog *) g_object_new (SEARCH_DIALOG_TYPE,
832                                            "use-header-bar",
833                                            use_header == 1 ? TRUE : FALSE,
834                                            NULL);
835 }
836 
837 static void
search_dialog_on_icon_release(SearchDialog * self,GtkEntryIconPosition icon_pos,GdkEvent * event,gpointer user_data)838 search_dialog_on_icon_release (SearchDialog *self, GtkEntryIconPosition icon_pos, GdkEvent *event, gpointer user_data)
839 {
840         SearchDialogPrivate *priv  = search_dialog_get_instance_private (self);
841 
842         if (icon_pos == GTK_ENTRY_ICON_PRIMARY) {
843                 if (priv->task != NULL && priv->task->running) {
844                         search_task_cancel (priv->task);
845                 } else {
846                         search_dialog_on_search_activate (self, user_data);
847                 }
848         } else {
849                 gtk_entry_set_text (GTK_ENTRY (user_data), "");
850         }
851 }
852