1 /* dspy-view.c
2  *
3  * Copyright 2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This file is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as
7  * published by the Free Software Foundation; either version 3 of the
8  * License, or (at your option) any later version.
9  *
10  * This file is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: LGPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "dspy-view"
22 
23 #include "config.h"
24 
25 #include <dazzle.h>
26 #include <glib/gi18n.h>
27 
28 #include "dspy-connection-button.h"
29 #include "dspy-name-marquee.h"
30 #include "dspy-method-view.h"
31 #include "dspy-name-row.h"
32 #include "dspy-tree-view.h"
33 #include "dspy-view.h"
34 
35 #include "libdspy-resources.h"
36 
37 typedef struct
38 {
39   GCancellable          *cancellable;
40   DzlListModelFilter    *filter_model;
41   GListModel            *model;
42 
43   /* Template widgets */
44   GtkTreeView           *introspection_tree_view;
45   GtkListBox            *names_list_box;
46   GtkButton             *refresh_button;
47   DspyNameMarquee       *name_marquee;
48   GtkScrolledWindow     *names_scroller;
49   DspyMethodView        *method_view;
50   GtkRevealer           *method_revealer;
51   DspyConnectionButton  *session_button;
52   DspyConnectionButton  *system_button;
53   GtkSearchEntry        *search_entry;
54   GtkMenuButton         *menu_button;
55   GtkBox                *radio_buttons;
56   GtkStack              *stack;
57 
58   guint                  destroyed : 1;
59 } DspyViewPrivate;
60 
61 G_DEFINE_TYPE_WITH_PRIVATE (DspyView, dspy_view, GTK_TYPE_BIN)
62 
63 static void dspy_view_set_model (DspyView   *self,
64                                  GListModel *model);
65 
66 /**
67  * dspy_view_new:
68  *
69  * Create a new #DspyView.
70  *
71  * This widget contains the window contents beneath the headerbar.
72  *
73  * Returns: (transfer full): a newly created #DspyView
74  */
75 GtkWidget *
dspy_view_new(void)76 dspy_view_new (void)
77 {
78   return g_object_new (DSPY_TYPE_VIEW, NULL);
79 }
80 
81 static void
dspy_view_list_names_cb(GObject * object,GAsyncResult * result,gpointer user_data)82 dspy_view_list_names_cb (GObject      *object,
83                          GAsyncResult *result,
84                          gpointer      user_data)
85 {
86   DspyConnection *conn = (DspyConnection *)object;
87   g_autoptr(DspyView) self = user_data;
88   g_autoptr(GListModel) model = NULL;
89   g_autoptr(GError) error = NULL;
90 
91   g_assert (DSPY_IS_VIEW (self));
92   g_assert (G_IS_ASYNC_RESULT (result));
93   g_assert (DSPY_IS_CONNECTION (conn));
94 
95   if (!(model = dspy_connection_list_names_finish (conn, result, &error)))
96     g_warning ("Failed to list names: %s", error->message);
97 
98   dspy_view_set_model (self, model);
99 }
100 
101 static void
radio_button_toggled_cb(DspyView * self,DspyConnectionButton * button)102 radio_button_toggled_cb (DspyView             *self,
103                          DspyConnectionButton *button)
104 {
105   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
106   DspyConnection *connection;
107 
108   g_assert (DSPY_IS_VIEW (self));
109   g_assert (DSPY_IS_CONNECTION_BUTTON (button));
110 
111   if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
112     return;
113 
114   gtk_stack_set_visible_child_name (priv->stack, "empty-state");
115 
116   connection = dspy_connection_button_get_connection (button);
117   dspy_connection_list_names_async (connection,
118                                     NULL,
119                                     dspy_view_list_names_cb,
120                                     g_object_ref (self));
121 }
122 
123 static void
connect_address_changed_cb(DspyView * self,DzlSimplePopover * popover)124 connect_address_changed_cb (DspyView       *self,
125                             DzlSimplePopover *popover)
126 {
127   const gchar *text;
128 
129   g_assert (DSPY_IS_VIEW (self));
130   g_assert (DZL_IS_SIMPLE_POPOVER (popover));
131 
132   text = dzl_simple_popover_get_text (popover);
133   dzl_simple_popover_set_ready (popover, text && *text);
134 }
135 
136 static void
connection_got_error_cb(DspyView * self,const GError * error,DspyConnection * connection)137 connection_got_error_cb (DspyView       *self,
138                          const GError   *error,
139                          DspyConnection *connection)
140 {
141   static GtkWidget *dialog;
142   const gchar *title;
143 
144   g_assert (DSPY_IS_VIEW (self));
145   g_assert (error != NULL);
146   g_assert (DSPY_IS_CONNECTION (connection));
147 
148   /* Only show one dialog at a time */
149   if (dialog != NULL)
150     return;
151 
152   if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED))
153     title = _("Access Denied by Peer");
154   else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED))
155     title = _("Authentication Failed");
156   else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_TIMEOUT))
157     title = _("Operation Timed Out");
158   else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_DISCONNECTED))
159     title = _("Lost Connection to Bus");
160   else
161     title = _("D-Bus Connection Failed");
162 
163   dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
164                                    GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
165                                    GTK_MESSAGE_WARNING,
166                                    GTK_BUTTONS_CLOSE,
167                                    "%s", title);
168   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", error->message);
169   g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
170   g_signal_connect (dialog, "destroy", G_CALLBACK (gtk_widget_destroyed), &dialog);
171   gtk_window_present (GTK_WINDOW (dialog));
172 }
173 
174 static void
connect_address_activate_cb(DspyView * self,const gchar * text,DzlSimplePopover * popover)175 connect_address_activate_cb (DspyView         *self,
176                              const gchar      *text,
177                              DzlSimplePopover *popover)
178 {
179   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
180   g_autoptr(DspyConnection) connection = NULL;
181   DspyConnectionButton *button;
182 
183   g_assert (DSPY_IS_VIEW (self));
184   g_assert (DZL_IS_SIMPLE_POPOVER (popover));
185 
186   connection = dspy_connection_new_for_address (text);
187 
188   button = g_object_new (DSPY_TYPE_CONNECTION_BUTTON,
189                          "group", priv->session_button,
190                          "connection", connection,
191                          "visible", TRUE,
192                          NULL);
193   g_signal_connect_object (button,
194                            "toggled",
195                            G_CALLBACK (radio_button_toggled_cb),
196                            self,
197                            G_CONNECT_SWAPPED);
198   g_signal_connect_object (dspy_connection_button_get_connection (button),
199                            "error",
200                            G_CALLBACK (connection_got_error_cb),
201                            self,
202                            G_CONNECT_SWAPPED);
203   gtk_container_add (GTK_CONTAINER (priv->radio_buttons), GTK_WIDGET (button));
204 
205   gtk_widget_activate (GTK_WIDGET (button));
206 }
207 
208 static void
clear_search(DspyView * self)209 clear_search (DspyView *self)
210 {
211   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
212 
213   g_assert (DSPY_IS_VIEW (self));
214 
215   if (priv->filter_model != NULL)
216     dzl_list_model_filter_set_filter_func (priv->filter_model, NULL, NULL, NULL);
217 }
218 
219 static gboolean
search_filter_func(DspyName * name,DzlPatternSpec * spec)220 search_filter_func (DspyName       *name,
221                     DzlPatternSpec *spec)
222 {
223   g_assert (DSPY_IS_NAME (name));
224   g_assert (spec != NULL);
225 
226   return dzl_pattern_spec_match (spec, dspy_name_get_search_text (name));
227 }
228 
229 static void
apply_search(DspyView * self,const gchar * text)230 apply_search (DspyView    *self,
231               const gchar *text)
232 {
233   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
234 
235   g_assert (DSPY_IS_VIEW (self));
236   g_assert (text != NULL);
237   g_assert (text[0] != 0);
238 
239   if (priv->filter_model != NULL)
240     dzl_list_model_filter_set_filter_func (priv->filter_model,
241                                            (DzlListModelFilterFunc) search_filter_func,
242                                            dzl_pattern_spec_new (text),
243                                            (GDestroyNotify) dzl_pattern_spec_unref);
244 }
245 
246 static GtkWidget *
create_name_row_cb(gpointer item,gpointer user_data)247 create_name_row_cb (gpointer item,
248                     gpointer user_data)
249 {
250   DspyName *name = item;
251 
252   g_assert (DSPY_IS_NAME (name));
253   g_assert (user_data == NULL);
254 
255   return dspy_name_row_new (name);
256 }
257 
258 static void
dspy_view_set_model(DspyView * self,GListModel * model)259 dspy_view_set_model (DspyView   *self,
260                      GListModel *model)
261 {
262   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
263   const gchar *text;
264   GtkAdjustment *adj;
265 
266   g_assert (DSPY_IS_VIEW (self));
267   g_assert (!model || G_IS_LIST_MODEL (model));
268 
269   /* Asynchronous completion implies that we might get here after
270    * the widget has been destroyed.
271    */
272   if (priv->destroyed)
273     return;
274 
275   gtk_list_box_bind_model (priv->names_list_box, NULL, NULL, NULL, NULL);
276 
277   g_clear_object (&priv->filter_model);
278   g_clear_object (&priv->model);
279 
280   if (model != NULL)
281     {
282       priv->model = g_object_ref (model);
283       priv->filter_model = dzl_list_model_filter_new (model);
284     }
285 
286   text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
287 
288   if (text && *text)
289     apply_search (self, text);
290   else
291     clear_search (self);
292 
293   gtk_list_box_bind_model (priv->names_list_box,
294                            G_LIST_MODEL (priv->filter_model),
295                            create_name_row_cb,
296                            NULL,
297                            NULL);
298 
299   adj = gtk_scrolled_window_get_vadjustment (priv->names_scroller);
300   gtk_adjustment_set_value (adj, 0.0);
301 }
302 
303 static void
dspy_view_introspect_cb(GObject * object,GAsyncResult * result,gpointer user_data)304 dspy_view_introspect_cb (GObject      *object,
305                          GAsyncResult *result,
306                          gpointer      user_data)
307 {
308   DspyName *name = (DspyName *)object;
309   g_autoptr(GtkTreeModel) model = NULL;
310   g_autoptr(DspyView) self = user_data;
311   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
312   g_autoptr(GError) error = NULL;
313 
314   g_assert (DSPY_IS_NAME (name));
315   g_assert (G_IS_ASYNC_RESULT (result));
316   g_assert (DSPY_IS_VIEW (self));
317 
318   if (!(model = dspy_name_introspect_finish (name, result, &error)))
319     {
320       DspyConnection *connection = dspy_name_get_connection (name);
321       dspy_connection_add_error (connection, error);
322     }
323 
324   gtk_tree_view_set_model (priv->introspection_tree_view, model);
325 }
326 
327 static void
name_row_activated_cb(DspyView * self,DspyNameRow * row,GtkListBox * list_box)328 name_row_activated_cb (DspyView    *self,
329                        DspyNameRow *row,
330                        GtkListBox  *list_box)
331 {
332   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
333   DspyName *name;
334 
335   g_assert (DSPY_IS_VIEW (self));
336   g_assert (DSPY_IS_NAME_ROW (row));
337   g_assert (GTK_IS_LIST_BOX (list_box));
338 
339   name = dspy_name_row_get_name (row);
340 
341   g_cancellable_cancel (priv->cancellable);
342   g_clear_object (&priv->cancellable);
343   priv->cancellable = g_cancellable_new ();
344 
345   gtk_tree_view_set_model (priv->introspection_tree_view, NULL);
346   dspy_name_marquee_set_name (priv->name_marquee, name);
347 
348   gtk_revealer_set_reveal_child (priv->method_revealer, FALSE);
349 
350   dspy_name_introspect_async (name,
351                               priv->cancellable,
352                               dspy_view_introspect_cb,
353                               g_object_ref (self));
354 
355   gtk_stack_set_visible_child_name (priv->stack, "introspect");
356 }
357 
358 static void
refresh_button_clicked_cb(DspyView * self,GtkButton * button)359 refresh_button_clicked_cb (DspyView  *self,
360                            GtkButton *button)
361 {
362   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
363   GtkListBoxRow *row;
364 
365   g_assert (DSPY_IS_VIEW (self));
366   g_assert (GTK_IS_BUTTON (button));
367 
368   if ((row = gtk_list_box_get_selected_row (priv->names_list_box)))
369     name_row_activated_cb (self, DSPY_NAME_ROW (row), priv->names_list_box);
370 }
371 
372 static void
method_activated_cb(DspyView * self,DspyMethodInvocation * invocation,DspyTreeView * tree_view)373 method_activated_cb (DspyView             *self,
374                      DspyMethodInvocation *invocation,
375                      DspyTreeView         *tree_view)
376 {
377   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
378 
379   g_assert (DSPY_IS_VIEW (self));
380   g_assert (!invocation || DSPY_IS_METHOD_INVOCATION (invocation));
381   g_assert (DSPY_IS_TREE_VIEW (tree_view));
382 
383   if (DSPY_IS_METHOD_INVOCATION (invocation))
384     {
385       dspy_method_view_set_invocation (priv->method_view, invocation);
386       gtk_revealer_set_reveal_child (priv->method_revealer, TRUE);
387     }
388 }
389 
390 static void
notify_child_revealed_cb(DspyView * self,GParamSpec * pspec,GtkRevealer * revealer)391 notify_child_revealed_cb (DspyView    *self,
392                           GParamSpec  *pspec,
393                           GtkRevealer *revealer)
394 {
395   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
396 
397   g_assert (DSPY_IS_VIEW (self));
398   g_assert (GTK_IS_REVEALER (revealer));
399 
400   if (!gtk_revealer_get_child_revealed (revealer))
401     {
402       dspy_method_view_set_invocation (priv->method_view, NULL);
403     }
404   else
405     {
406       GtkTreeSelection *selection;
407       GtkTreeModel *model = NULL;
408       GtkTreeIter iter;
409 
410       selection = gtk_tree_view_get_selection (priv->introspection_tree_view);
411 
412       if (gtk_tree_selection_get_selected (selection, &model, &iter))
413         {
414           g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (model, &iter);
415           GtkTreeViewColumn *column = gtk_tree_view_get_column (priv->introspection_tree_view, 0);
416 
417           /* Move the selected row as far up as we can so that the revealer
418            * for the method invocation does not cover the selected area.
419            */
420           gtk_tree_view_scroll_to_cell (priv->introspection_tree_view,
421                                         path,
422                                         column,
423                                         TRUE,
424                                         0.0,
425                                         0.0);
426         }
427     }
428 }
429 
430 static void
search_entry_changed_cb(DspyView * self,GtkSearchEntry * search_entry)431 search_entry_changed_cb (DspyView       *self,
432                          GtkSearchEntry *search_entry)
433 {
434   const gchar *text;
435 
436   g_assert (DSPY_IS_VIEW (self));
437   g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
438 
439   text = gtk_entry_get_text (GTK_ENTRY (search_entry));
440 
441   if (text == NULL || *text == 0)
442     clear_search (self);
443   else
444     apply_search (self, text);
445 }
446 
447 static void
connect_to_bus_action(GSimpleAction * action,GVariant * params,gpointer user_data)448 connect_to_bus_action (GSimpleAction *action,
449                        GVariant      *params,
450                        gpointer       user_data)
451 {
452   DspyView *self = user_data;
453   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
454   GtkPopover *popover;
455 
456   g_assert (G_IS_SIMPLE_ACTION (action));
457   g_assert (DSPY_IS_VIEW (self));
458 
459   popover = g_object_new (DZL_TYPE_SIMPLE_POPOVER,
460                           "button-text", _("Connect"),
461                           "message", _("Provide the address of the message bus"),
462                           "position", GTK_POS_RIGHT,
463                           "title", _("Connect to Other Bus"),
464                           "relative-to", priv->system_button,
465                           NULL);
466 
467   g_signal_connect_object (popover,
468                            "changed",
469                            G_CALLBACK (connect_address_changed_cb),
470                            self,
471                            G_CONNECT_SWAPPED);
472 
473   g_signal_connect_object (popover,
474                            "activate",
475                            G_CALLBACK (connect_address_activate_cb),
476                            self,
477                            G_CONNECT_SWAPPED);
478 
479   g_signal_connect (popover,
480                     "closed",
481                     G_CALLBACK (gtk_widget_destroy),
482                     NULL);
483 
484   gtk_popover_popup (popover);
485 }
486 
487 static GActionEntry action_entries[] = {
488   { "connect-to-bus", connect_to_bus_action },
489 };
490 
491 static void
dspy_view_destroy(GtkWidget * widget)492 dspy_view_destroy (GtkWidget *widget)
493 {
494   DspyView *self = (DspyView *)widget;
495   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
496 
497   priv->destroyed = TRUE;
498 
499   g_cancellable_cancel (priv->cancellable);
500   g_clear_object (&priv->cancellable);
501   g_clear_object (&priv->filter_model);
502   g_clear_object (&priv->model);
503 
504   GTK_WIDGET_CLASS (dspy_view_parent_class)->destroy (widget);
505 }
506 
507 static void
dspy_view_class_init(DspyViewClass * klass)508 dspy_view_class_init (DspyViewClass *klass)
509 {
510   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
511 
512   widget_class->destroy = dspy_view_destroy;
513 
514   gtk_widget_class_set_css_name (widget_class, "dspyview");
515   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-view.ui");
516   gtk_widget_class_bind_template_child_private (widget_class, DspyView, introspection_tree_view);
517   gtk_widget_class_bind_template_child_private (widget_class, DspyView, menu_button);
518   gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_revealer);
519   gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_view);
520   gtk_widget_class_bind_template_child_private (widget_class, DspyView, name_marquee);
521   gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_list_box);
522   gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_scroller);
523   gtk_widget_class_bind_template_child_private (widget_class, DspyView, radio_buttons);
524   gtk_widget_class_bind_template_child_private (widget_class, DspyView, refresh_button);
525   gtk_widget_class_bind_template_child_private (widget_class, DspyView, search_entry);
526   gtk_widget_class_bind_template_child_private (widget_class, DspyView, session_button);
527   gtk_widget_class_bind_template_child_private (widget_class, DspyView, stack);
528   gtk_widget_class_bind_template_child_private (widget_class, DspyView, system_button);
529 
530   g_type_ensure (DSPY_TYPE_METHOD_VIEW);
531   g_type_ensure (DSPY_TYPE_NAME_MARQUEE);
532   g_type_ensure (DSPY_TYPE_TREE_VIEW);
533 }
534 
535 static void
dspy_view_init(DspyView * self)536 dspy_view_init (DspyView *self)
537 {
538   DspyViewPrivate *priv = dspy_view_get_instance_private (self);
539   g_autoptr(GSimpleActionGroup) actions = g_simple_action_group_new ();
540   GMenu *menu;
541 
542   gtk_widget_init_template (GTK_WIDGET (self));
543 
544   g_action_map_add_action_entries (G_ACTION_MAP (actions),
545                                    action_entries,
546                                    G_N_ELEMENTS (action_entries),
547                                    self);
548   gtk_widget_insert_action_group (GTK_WIDGET (self), "dspy", G_ACTION_GROUP (actions));
549 
550   menu = dzl_application_get_menu_by_id (DZL_APPLICATION (g_application_get_default ()),
551                                          "dspy-connections-menu");
552   gtk_menu_button_set_menu_model (priv->menu_button, G_MENU_MODEL (menu));
553 
554   g_signal_connect_object (self,
555                            "key-press-event",
556                            G_CALLBACK (dzl_shortcut_manager_handle_event),
557                            NULL,
558                            G_CONNECT_SWAPPED);
559 
560   g_signal_connect_object (priv->names_list_box,
561                            "row-activated",
562                            G_CALLBACK (name_row_activated_cb),
563                            self,
564                            G_CONNECT_SWAPPED);
565 
566   g_signal_connect_object (priv->refresh_button,
567                            "clicked",
568                            G_CALLBACK (refresh_button_clicked_cb),
569                            self,
570                            G_CONNECT_SWAPPED);
571 
572   g_signal_connect_object (priv->method_revealer,
573                            "notify::child-revealed",
574                            G_CALLBACK (notify_child_revealed_cb),
575                            self,
576                            G_CONNECT_SWAPPED);
577 
578   g_signal_connect_object (priv->introspection_tree_view,
579                            "method-activated",
580                            G_CALLBACK (method_activated_cb),
581                            self,
582                            G_CONNECT_SWAPPED);
583 
584   g_signal_connect_object (priv->session_button,
585                            "toggled",
586                            G_CALLBACK (radio_button_toggled_cb),
587                            self,
588                            G_CONNECT_SWAPPED);
589 
590   g_signal_connect_object (dspy_connection_button_get_connection (priv->session_button),
591                            "error",
592                            G_CALLBACK (connection_got_error_cb),
593                            self,
594                            G_CONNECT_SWAPPED);
595 
596   g_signal_connect_object (priv->system_button,
597                            "toggled",
598                            G_CALLBACK (radio_button_toggled_cb),
599                            self,
600                            G_CONNECT_SWAPPED);
601 
602   g_signal_connect_object (dspy_connection_button_get_connection (priv->system_button),
603                            "error",
604                            G_CALLBACK (connection_got_error_cb),
605                            self,
606                            G_CONNECT_SWAPPED);
607 
608   g_signal_connect_object (priv->search_entry,
609                            "changed",
610                            G_CALLBACK (search_entry_changed_cb),
611                            self,
612                            G_CONNECT_SWAPPED);
613 
614   radio_button_toggled_cb (self, priv->session_button);
615 }
616