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