[gnome-builder/wip/chergert/dspy: 2/2] dspy: add Dspy plugin using new libdspy code
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/dspy: 2/2] dspy: add Dspy plugin using new libdspy code
- Date: Tue, 23 Apr 2019 22:06:44 +0000 (UTC)
commit 9d2ae417558038d5b990f91f19cbac1dc45aa0f2
Author: Christian Hergert <chergert redhat com>
Date: Thu Apr 11 14:41:12 2019 -0700
dspy: add Dspy plugin using new libdspy code
data/org.gnome.Builder.desktop.in.in | 4 +
meson_options.txt | 1 +
src/plugins/dspy/dspy-plugin.c | 40 +
src/plugins/dspy/dspy.gresource.xml | 9 +
src/plugins/dspy/dspy.plugin | 10 +
src/plugins/dspy/gbp-dspy-application-addin.c | 163 ++++
src/plugins/dspy/gbp-dspy-application-addin.h | 31 +
src/plugins/dspy/gbp-dspy-surface.c | 60 ++
src/plugins/dspy/gbp-dspy-surface.h | 33 +
src/plugins/dspy/gbp-dspy-surface.ui | 10 +
src/plugins/dspy/gbp-dspy-workspace.c | 64 ++
src/plugins/dspy/gbp-dspy-workspace.h | 33 +
src/plugins/dspy/gbp-dspy-workspace.ui | 36 +
src/plugins/dspy/gtk/menus.ui | 25 +
src/plugins/dspy/libdspy/dspy-connection-button.c | 263 ++++++
src/plugins/dspy/libdspy/dspy-connection-button.h | 46 ++
src/plugins/dspy/libdspy/dspy-connection.c | 491 +++++++++++
src/plugins/dspy/libdspy/dspy-connection.h | 56 ++
.../dspy/libdspy/dspy-introspection-model.c | 919 +++++++++++++++++++++
.../dspy/libdspy/dspy-introspection-model.h | 35 +
src/plugins/dspy/libdspy/dspy-method-invocation.c | 585 +++++++++++++
src/plugins/dspy/libdspy/dspy-method-invocation.h | 74 ++
src/plugins/dspy/libdspy/dspy-method-view.c | 471 +++++++++++
src/plugins/dspy/libdspy/dspy-method-view.h | 46 ++
src/plugins/dspy/libdspy/dspy-method-view.ui | 282 +++++++
src/plugins/dspy/libdspy/dspy-name-marquee.c | 186 +++++
src/plugins/dspy/libdspy/dspy-name-marquee.h | 38 +
src/plugins/dspy/libdspy/dspy-name-marquee.ui | 115 +++
src/plugins/dspy/libdspy/dspy-name-row.c | 214 +++++
src/plugins/dspy/libdspy/dspy-name-row.h | 36 +
src/plugins/dspy/libdspy/dspy-name-row.ui | 52 ++
src/plugins/dspy/libdspy/dspy-name.c | 486 +++++++++++
src/plugins/dspy/libdspy/dspy-name.h | 53 ++
src/plugins/dspy/libdspy/dspy-names-model.c | 532 ++++++++++++
src/plugins/dspy/libdspy/dspy-names-model.h | 39 +
src/plugins/dspy/libdspy/dspy-node.c | 598 ++++++++++++++
src/plugins/dspy/libdspy/dspy-private.h | 202 +++++
src/plugins/dspy/libdspy/dspy-signature.c | 82 ++
src/plugins/dspy/libdspy/dspy-tree-view.c | 311 +++++++
src/plugins/dspy/libdspy/dspy-tree-view.h | 46 ++
src/plugins/dspy/libdspy/dspy-view.c | 610 ++++++++++++++
src/plugins/dspy/libdspy/dspy-view.h | 41 +
src/plugins/dspy/libdspy/dspy-view.ui | 218 +++++
src/plugins/dspy/libdspy/dspy.h | 37 +
src/plugins/dspy/libdspy/gtk/menus.ui | 9 +
.../symbolic/apps/org.gnome.dfeet-symbolic.svg | 81 ++
src/plugins/dspy/libdspy/libdspy.gresource.xml | 12 +
src/plugins/dspy/libdspy/meson.build | 35 +
src/plugins/dspy/libdspy/themes/shared.css | 16 +
src/plugins/dspy/meson.build | 21 +
src/plugins/meson.build | 2 +
51 files changed, 7859 insertions(+)
---
diff --git a/data/org.gnome.Builder.desktop.in.in b/data/org.gnome.Builder.desktop.in.in
index 87a592a31..caa25919c 100644
--- a/data/org.gnome.Builder.desktop.in.in
+++ b/data/org.gnome.Builder.desktop.in.in
@@ -30,3 +30,7 @@ Exec=gnome-builder --clone
[Desktop Action new-editor]
Name=New Editor Workspace
Exec=gnome-builder --editor
+
+[Desktop Action dspy]
+Name=DBus Inspector
+Exec=gnome-builder --dspy
diff --git a/meson_options.txt b/meson_options.txt
index 57fe9301c..3d6fc2e40 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -31,6 +31,7 @@ option('plugin_color_picker', type: 'boolean')
option('plugin_ctags', type: 'boolean')
option('plugin_devhelp', type: 'boolean')
option('plugin_deviced', type: 'boolean', value: false)
+option('plugin_dspy', type: 'boolean')
option('plugin_editorconfig', type: 'boolean')
option('plugin_eslint', type: 'boolean')
option('plugin_file_search', type: 'boolean')
diff --git a/src/plugins/dspy/dspy-plugin.c b/src/plugins/dspy/dspy-plugin.c
new file mode 100644
index 000000000..781afd2b4
--- /dev/null
+++ b/src/plugins/dspy/dspy-plugin.c
@@ -0,0 +1,40 @@
+/* dspy-plugin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include <libdspy-resources.h>
+
+#include "gbp-dspy-application-addin.h"
+
+_IDE_EXTERN void
+_gbp_dspy_register_types (PeasObjectModule *module)
+{
+ g_resources_register (libdspy_get_resource ());
+ dzl_application_add_resources (DZL_APPLICATION (IDE_APPLICATION_DEFAULT), "resource:///org/gnome/dspy");
+
+ peas_object_module_register_extension_type (module,
+ IDE_TYPE_APPLICATION_ADDIN,
+ GBP_TYPE_DSPY_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/dspy/dspy.gresource.xml b/src/plugins/dspy/dspy.gresource.xml
new file mode 100644
index 000000000..bb738591b
--- /dev/null
+++ b/src/plugins/dspy/dspy.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/plugins/dspy">
+ <file>dspy.plugin</file>
+ <file>gtk/menus.ui</file>
+ <file preprocess="xml-stripblanks">gbp-dspy-surface.ui</file>
+ <file preprocess="xml-stripblanks">gbp-dspy-workspace.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/plugins/dspy/dspy.plugin b/src/plugins/dspy/dspy.plugin
new file mode 100644
index 000000000..5f341dbe9
--- /dev/null
+++ b/src/plugins/dspy/dspy.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2019 Christian Hergert
+Description=Explore DBus session and system connections
+Embedded=_gbp_dspy_register_types
+Module=dspy
+Name=DBus Connection Explorer
+X-Workspace-Kind=primary;editor;
+X-At-Startup=true
diff --git a/src/plugins/dspy/gbp-dspy-application-addin.c b/src/plugins/dspy/gbp-dspy-application-addin.c
new file mode 100644
index 000000000..0f274e707
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-application-addin.c
@@ -0,0 +1,163 @@
+/* gbp-dspy-application-addin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-dspy-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-dspy-application-addin.h"
+#include "gbp-dspy-workspace.h"
+
+struct _GbpDspyApplicationAddin
+{
+ GObject parent_instance;
+};
+
+static void
+gbp_dspy_application_addin_add_option_entries (IdeApplicationAddin *addin,
+ IdeApplication *app)
+{
+ g_assert (GBP_IS_DSPY_APPLICATION_ADDIN (addin));
+ g_assert (G_IS_APPLICATION (app));
+
+ g_application_add_main_option (G_APPLICATION (app),
+ "dspy",
+ 0,
+ G_OPTION_FLAG_IN_MAIN,
+ G_OPTION_ARG_NONE,
+ _("Display DBus inspector"),
+ NULL);
+}
+
+static void
+gbp_dspy_application_addin_handle_command_line (IdeApplicationAddin *addin,
+ IdeApplication *application,
+ GApplicationCommandLine *cmdline)
+{
+ IdeApplication *app = (IdeApplication *)application;
+ GVariantDict *options;
+
+ g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+ g_assert (IDE_IS_APPLICATION (app));
+ g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+ if ((options = g_application_command_line_get_options_dict (cmdline)) &&
+ g_variant_dict_contains (options, "dspy"))
+ {
+ g_autoptr(IdeWorkbench) workbench = NULL;
+ g_autoptr(GFile) workdir = NULL;
+ GbpDspyWorkspace *workspace;
+ IdeContext *context;
+
+ workbench = ide_workbench_new ();
+ ide_application_add_workbench (app, workbench);
+
+ context = ide_workbench_get_context (workbench);
+
+ workdir = g_application_command_line_create_file_for_arg (cmdline, ".");
+ ide_context_set_workdir (context, workdir);
+
+ workspace = gbp_dspy_workspace_new (application);
+ ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+ ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+ ide_application_set_command_line_handled (application, cmdline, TRUE);
+ }
+}
+
+static void
+dspy_action_cb (GSimpleAction *action,
+ GVariant *param,
+ gpointer user_data)
+{
+ g_autoptr(IdeWorkbench) workbench = NULL;
+ g_autoptr(GFile) workdir = NULL;
+ GbpDspyWorkspace *workspace;
+ IdeContext *context;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (GBP_IS_DSPY_APPLICATION_ADDIN (user_data));
+
+ workbench = ide_workbench_new ();
+ ide_application_add_workbench (IDE_APPLICATION_DEFAULT, workbench);
+
+ context = ide_workbench_get_context (workbench);
+
+ workdir = g_file_new_for_path (ide_get_projects_dir ());
+ ide_context_set_workdir (context, workdir);
+
+ workspace = gbp_dspy_workspace_new (IDE_APPLICATION_DEFAULT);
+ ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+ ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+}
+
+static GActionEntry actions[] = {
+ { "dspy", dspy_action_cb },
+};
+
+static void
+gbp_dspy_application_addin_load (IdeApplicationAddin *addin,
+ IdeApplication *application)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+ g_assert (IDE_IS_APPLICATION (application));
+
+ g_action_map_add_action_entries (G_ACTION_MAP (application),
+ actions,
+ G_N_ELEMENTS (actions),
+ addin);
+}
+
+static void
+gbp_dspy_application_addin_unload (IdeApplicationAddin *addin,
+ IdeApplication *application)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+ g_assert (IDE_IS_APPLICATION (application));
+
+ for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+ g_action_map_remove_action (G_ACTION_MAP (application), actions[i].name);
+}
+
+static void
+app_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+ iface->load = gbp_dspy_application_addin_load;
+ iface->unload = gbp_dspy_application_addin_unload;
+ iface->add_option_entries = gbp_dspy_application_addin_add_option_entries;
+ iface->handle_command_line = gbp_dspy_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDspyApplicationAddin, gbp_dspy_application_addin, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, app_addin_iface_init))
+
+static void
+gbp_dspy_application_addin_class_init (GbpDspyApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_dspy_application_addin_init (GbpDspyApplicationAddin *self)
+{
+}
diff --git a/src/plugins/dspy/gbp-dspy-application-addin.h b/src/plugins/dspy/gbp-dspy-application-addin.h
new file mode 100644
index 000000000..6086bcfff
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-dspy-application-addin.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DSPY_APPLICATION_ADDIN (gbp_dspy_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspyApplicationAddin, gbp_dspy_application_addin, GBP, DSPY_APPLICATION_ADDIN,
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-surface.c b/src/plugins/dspy/gbp-dspy-surface.c
new file mode 100644
index 000000000..483eb9a25
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.c
@@ -0,0 +1,60 @@
+/* gbp-dspy-surface.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-dspy-surface"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <dspy.h>
+#include <glib/gi18n.h>
+
+#include "gbp-dspy-surface.h"
+
+struct _GbpDspySurface
+{
+ IdeSurface parent_instance;
+ DspyView *view;
+};
+
+G_DEFINE_TYPE (GbpDspySurface, gbp_dspy_surface, IDE_TYPE_SURFACE)
+
+static void
+gbp_dspy_surface_class_init (GbpDspySurfaceClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/gbp-dspy-surface.ui");
+ gtk_widget_class_bind_template_child (widget_class, GbpDspySurface, view);
+
+ g_type_ensure (DSPY_TYPE_VIEW);
+}
+
+static void
+gbp_dspy_surface_init (GbpDspySurface *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GbpDspySurface *
+gbp_dspy_surface_new (void)
+{
+ return g_object_new (GBP_TYPE_DSPY_SURFACE, NULL);
+}
diff --git a/src/plugins/dspy/gbp-dspy-surface.h b/src/plugins/dspy/gbp-dspy-surface.h
new file mode 100644
index 000000000..2ec6a35f7
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.h
@@ -0,0 +1,33 @@
+/* gbp-dspy-surface.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DSPY_SURFACE (gbp_dspy_surface_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspySurface, gbp_dspy_surface, GBP, DSPY_SURFACE, IdeSurface)
+
+GbpDspySurface *gbp_dspy_surface_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-surface.ui b/src/plugins/dspy/gbp-dspy-surface.ui
new file mode 100644
index 000000000..e6537eb80
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-surface.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GbpDspySurface" parent="IdeSurface">
+ <child>
+ <object class="DspyView" id="view">
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/dspy/gbp-dspy-workspace.c b/src/plugins/dspy/gbp-dspy-workspace.c
new file mode 100644
index 000000000..5be67a886
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.c
@@ -0,0 +1,64 @@
+/* gbp-dspy-workspace.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-dspy-workspace"
+
+#include "config.h"
+
+#include "gbp-dspy-surface.h"
+#include "gbp-dspy-workspace.h"
+
+struct _GbpDspyWorkspace
+{
+ IdeWorkspace parent_instance;
+ IdeHeaderBar *header_bar;
+ GbpDspySurface *surface;
+};
+
+G_DEFINE_TYPE (GbpDspyWorkspace, gbp_dspy_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+gbp_dspy_workspace_class_init (GbpDspyWorkspaceClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+ ide_workspace_class_set_kind (workspace_class, "dspy");
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/plugins/dspy/gbp-dspy-workspace.ui");
+ gtk_widget_class_bind_template_child (widget_class, GbpDspyWorkspace, header_bar);
+ gtk_widget_class_bind_template_child (widget_class, GbpDspyWorkspace, surface);
+}
+
+static void
+gbp_dspy_workspace_init (GbpDspyWorkspace *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GbpDspyWorkspace *
+gbp_dspy_workspace_new (IdeApplication *application)
+{
+ g_return_val_if_fail (IDE_IS_APPLICATION (application), NULL);
+
+ return g_object_new (GBP_TYPE_DSPY_WORKSPACE,
+ "application", application,
+ NULL);
+}
diff --git a/src/plugins/dspy/gbp-dspy-workspace.h b/src/plugins/dspy/gbp-dspy-workspace.h
new file mode 100644
index 000000000..ddf1e16a3
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.h
@@ -0,0 +1,33 @@
+/* gbp-dspy-workspace.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DSPY_WORKSPACE (gbp_dspy_workspace_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDspyWorkspace, gbp_dspy_workspace, GBP, DSPY_WORKSPACE, IdeWorkspace)
+
+GbpDspyWorkspace *gbp_dspy_workspace_new (IdeApplication *application);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/gbp-dspy-workspace.ui b/src/plugins/dspy/gbp-dspy-workspace.ui
new file mode 100644
index 000000000..3560e9fde
--- /dev/null
+++ b/src/plugins/dspy/gbp-dspy-workspace.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GbpDspyWorkspace" parent="IdeWorkspace">
+ <property name="default-width">1000</property>
+ <property name="default-height">700</property>
+ <child type="titlebar">
+ <object class="IdeHeaderBar" id="header_bar">
+ <property name="show-close-button">true</property>
+ <property name="show-fullscreen-button">true</property>
+ <property name="visible">true</property>
+ <child type="title">
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">DBus Inspector</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child internal-child="surfaces">
+ <object class="GtkStack" id="surfaces">
+ <property name="visible">true</property>
+ <child>
+ <object class="GbpDspySurface" id="surface">
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="name">dspy</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/dspy/gtk/menus.ui b/src/plugins/dspy/gtk/menus.ui
new file mode 100644
index 000000000..d39fe7b95
--- /dev/null
+++ b/src/plugins/dspy/gtk/menus.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <menu id="ide-primary-workspace-surfaces-menu">
+ <section id="ide-primary-workspace-surfaces-menu-utils-section">
+ <item>
+ <attribute name="id">surface-menu-dspy</attribute>
+ <attribute name="label" translatable="yes">DBus Inspector</attribute>
+ <attribute name="role">normal</attribute>
+ <attribute name="action">app.dspy</attribute>
+ <attribute name="verb-icon-name">org.gnome.dfeet-symbolic</attribute>
+ </item>
+ </section>
+ </menu>
+ <menu id="ide-editor-workspace-surfaces-menu">
+ <section id="ide-editor-workspace-surfaces-menu-utils-section">
+ <item>
+ <attribute name="id">surface-menu-dspy</attribute>
+ <attribute name="label" translatable="yes">DBus Inspector</attribute>
+ <attribute name="role">normal</attribute>
+ <attribute name="action">app.dspy</attribute>
+ <attribute name="verb-icon-name">org.gnome.dfeet-symbolic</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-connection-button.c
b/src/plugins/dspy/libdspy/dspy-connection-button.c
new file mode 100644
index 000000000..1f6b44450
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection-button.c
@@ -0,0 +1,263 @@
+/* dspy-connection-button.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection-button"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-connection-button.h"
+
+typedef struct
+{
+ DspyConnection *connection;
+
+ GtkImage *image;
+ GtkLabel *label;
+} DspyConnectionButtonPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyConnectionButton, dspy_connection_button, GTK_TYPE_RADIO_BUTTON)
+
+enum {
+ PROP_0,
+ PROP_BUS_TYPE,
+ PROP_CONNECTION,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_connection_button_new:
+ *
+ * Create a new #DspyConnectionButton.
+ *
+ * Returns: (transfer full): a newly created #DspyConnectionButton
+ */
+GtkWidget *
+dspy_connection_button_new (void)
+{
+ return g_object_new (DSPY_TYPE_CONNECTION_BUTTON, NULL);
+}
+
+static gboolean
+dspy_connection_button_query_tooltip (GtkWidget *widget,
+ gint x,
+ gint y,
+ gboolean keyboard,
+ GtkTooltip *tooltip)
+{
+ DspyConnectionButton *self = (DspyConnectionButton *)widget;
+ DspyConnection *connection;
+
+ g_assert (DSPY_IS_CONNECTION_BUTTON (self));
+
+ if ((connection = dspy_connection_button_get_connection (self)))
+ {
+ GDBusConnection *bus = dspy_connection_get_connection (connection);
+ const gchar *address = dspy_connection_get_address (connection);
+
+ if (bus != NULL && address != NULL)
+ {
+ /* translators: %s is replaced with the address of the DBus */
+ g_autofree gchar *text = g_strdup_printf (_("Connected to “%s”"), address);
+ gtk_tooltip_set_text (tooltip, text);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+dspy_connection_button_finalize (GObject *object)
+{
+ DspyConnectionButton *self = (DspyConnectionButton *)object;
+ DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+ g_clear_object (&priv->connection);
+
+ G_OBJECT_CLASS (dspy_connection_button_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyConnectionButton *self = DSPY_CONNECTION_BUTTON (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUS_TYPE:
+ {
+ DspyConnection *conn = dspy_connection_button_get_connection (self);
+
+ if (conn != NULL)
+ g_value_set_enum (value, dspy_connection_get_bus_type (conn));
+ }
+ break;
+
+ case PROP_CONNECTION:
+ g_value_set_object (value, dspy_connection_button_get_connection (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_connection_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyConnectionButton *self = DSPY_CONNECTION_BUTTON (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUS_TYPE:
+ {
+ GBusType bus_type = g_value_get_enum (value);
+
+ if (bus_type == G_BUS_TYPE_SESSION || bus_type == G_BUS_TYPE_SYSTEM)
+ {
+ g_autoptr(DspyConnection) conn = dspy_connection_new_for_bus (bus_type);
+ dspy_connection_button_set_connection (self, conn);
+ }
+ }
+ break;
+
+ case PROP_CONNECTION:
+ dspy_connection_button_set_connection (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_connection_button_class_init (DspyConnectionButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = dspy_connection_button_finalize;
+ object_class->get_property = dspy_connection_button_get_property;
+ object_class->set_property = dspy_connection_button_set_property;
+
+ widget_class->query_tooltip = dspy_connection_button_query_tooltip;
+
+ properties [PROP_BUS_TYPE] =
+ g_param_spec_enum ("bus-type",
+ "Bus Type",
+ "Bus Type",
+ G_TYPE_BUS_TYPE,
+ G_BUS_TYPE_SESSION,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_CONNECTION] =
+ g_param_spec_object ("connection",
+ "Connection",
+ "The connection underlying the button",
+ DSPY_TYPE_CONNECTION,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_connection_button_init (DspyConnectionButton *self)
+{
+ DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+ GtkBox *box;
+
+ g_object_set (self,
+ "has-tooltip", TRUE,
+ "draw-indicator", FALSE,
+ NULL);
+
+ box = g_object_new (GTK_TYPE_BOX,
+ "halign", GTK_ALIGN_CENTER,
+ "orientation", GTK_ORIENTATION_HORIZONTAL,
+ "visible", TRUE,
+ NULL);
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (box));
+
+ priv->image = g_object_new (GTK_TYPE_IMAGE,
+ "icon-name", "dialog-warning-symbolic",
+ "valign", GTK_ALIGN_CENTER,
+ "pixel-size", 16,
+ "margin-end", 6,
+ "visible", FALSE,
+ NULL);
+ gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->image));
+
+ priv->label = g_object_new (GTK_TYPE_LABEL,
+ "visible", TRUE,
+ NULL);
+ gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->label));
+}
+
+/**
+ * dspy_connection_button_get_connection:
+ * @self: a #DspyConnection
+ *
+ * Returns: (transfer none) (nullable): a #DspyConnection or %NULL
+ */
+DspyConnection *
+dspy_connection_button_get_connection (DspyConnectionButton *self)
+{
+ DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+ g_return_val_if_fail (DSPY_IS_CONNECTION_BUTTON (self), NULL);
+
+ return priv->connection;
+}
+
+void
+dspy_connection_button_set_connection (DspyConnectionButton *self,
+ DspyConnection *connection)
+{
+ DspyConnectionButtonPrivate *priv = dspy_connection_button_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_CONNECTION_BUTTON (self));
+ g_return_if_fail (DSPY_IS_CONNECTION (connection));
+
+ if (g_set_object (&priv->connection, connection))
+ {
+ GBusType bus_type = dspy_connection_get_bus_type (connection);
+
+ if (bus_type == G_BUS_TYPE_SYSTEM)
+ gtk_label_set_label (priv->label, _("System"));
+ else if (bus_type == G_BUS_TYPE_SESSION)
+ gtk_label_set_label (priv->label, _("Session"));
+ else
+ gtk_label_set_label (priv->label, _("Other"));
+
+ g_object_bind_property (connection, "has-error", priv->image, "visible", G_BINDING_SYNC_CREATE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONNECTION]);
+ }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-connection-button.h
b/src/plugins/dspy/libdspy/dspy-connection-button.h
new file mode 100644
index 000000000..c6632c04f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection-button.h
@@ -0,0 +1,46 @@
+/* dspy-connection-button.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-connection.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION_BUTTON (dspy_connection_button_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyConnectionButton, dspy_connection_button, DSPY, CONNECTION_BUTTON,
GtkRadioButton)
+
+struct _DspyConnectionButtonClass
+{
+ GtkRadioButtonClass parent_class;
+
+ /*< private >*/
+ gpointer _reserved[8];
+};
+
+GtkWidget *dspy_connection_button_new (void);
+DspyConnection *dspy_connection_button_get_connection (DspyConnectionButton *self);
+void dspy_connection_button_set_connection (DspyConnectionButton *self,
+ DspyConnection *connection);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-connection.c b/src/plugins/dspy/libdspy/dspy-connection.c
new file mode 100644
index 000000000..4684e4f6d
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection.c
@@ -0,0 +1,491 @@
+/* dspy-connection.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-connection"
+
+#include "config.h"
+
+#include "dspy-connection.h"
+#include "dspy-names-model.h"
+
+struct _DspyConnection
+{
+ GObject parent_instance;
+ GCancellable *cancellable;
+ GDBusConnection *connection;
+ gchar *address;
+ gchar *connected_address;
+ GPtrArray *errors;
+ GBusType bus_type;
+};
+
+G_DEFINE_TYPE (DspyConnection, dspy_connection, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_ADDRESS,
+ PROP_BUS_TYPE,
+ PROP_CONNECTION,
+ PROP_HAS_ERROR,
+ N_PROPS
+};
+
+enum {
+ ERROR,
+ N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * dspy_connection_new_for_address:
+ * @address: an address to connect to the bus
+ *
+ * Create a new #DspyConnection.
+ *
+ * Returns: (transfer full): a newly created #DspyConnection
+ */
+DspyConnection *
+dspy_connection_new_for_address (const gchar *address)
+{
+ return g_object_new (DSPY_TYPE_CONNECTION,
+ "address", address,
+ NULL);
+}
+
+/**
+ * dspy_connection_new_for_bus:
+ * @bus_type: the type of bus connection
+ *
+ * Create a new #DspyConnection.
+ *
+ * Returns: (transfer full): a newly created #DspyConnection
+ */
+DspyConnection *
+dspy_connection_new_for_bus (GBusType bus_type)
+{
+ return g_object_new (DSPY_TYPE_CONNECTION,
+ "bus-type", bus_type,
+ NULL);
+}
+
+static void
+dspy_connection_dispose (GObject *object)
+{
+ DspyConnection *self = (DspyConnection *)object;
+
+ g_assert (DSPY_IS_CONNECTION (self));
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ if (self->connection != NULL)
+ {
+ if (!g_dbus_connection_is_closed (self->connection))
+ g_dbus_connection_close (self->connection, NULL, NULL, NULL);
+ g_clear_object (&self->connection);
+ }
+
+ G_OBJECT_CLASS (dspy_connection_parent_class)->dispose (object);
+}
+
+static void
+dspy_connection_finalize (GObject *object)
+{
+ DspyConnection *self = (DspyConnection *)object;
+
+ g_clear_pointer (&self->address, g_free);
+ g_clear_pointer (&self->connected_address, g_free);
+
+ G_OBJECT_CLASS (dspy_connection_parent_class)->finalize (object);
+}
+
+static void
+dspy_connection_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyConnection *self = DSPY_CONNECTION (object);
+
+ switch (prop_id)
+ {
+ case PROP_ADDRESS:
+ g_value_set_string (value, dspy_connection_get_address (self));
+ break;
+
+ case PROP_BUS_TYPE:
+ g_value_set_enum (value, dspy_connection_get_bus_type (self));
+ break;
+
+ case PROP_CONNECTION:
+ g_value_set_object (value, dspy_connection_get_connection (self));
+ break;
+
+ case PROP_HAS_ERROR:
+ g_value_set_boolean (value, dspy_connection_get_has_error (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_connection_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyConnection *self = DSPY_CONNECTION (object);
+
+ switch (prop_id)
+ {
+ case PROP_ADDRESS:
+ if (g_value_get_string (value))
+ {
+ self->address = g_value_dup_string (value);
+ self->bus_type = G_BUS_TYPE_NONE;
+ }
+ break;
+
+ case PROP_BUS_TYPE:
+ if (g_value_get_enum (value))
+ {
+ self->bus_type = g_value_get_enum (value);
+ g_clear_pointer (&self->address, g_free);
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_connection_class_init (DspyConnectionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = dspy_connection_dispose;
+ object_class->finalize = dspy_connection_finalize;
+ object_class->get_property = dspy_connection_get_property;
+ object_class->set_property = dspy_connection_set_property;
+
+ properties [PROP_ADDRESS] =
+ g_param_spec_string ("address",
+ "Address",
+ "The bus address to connect",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_BUS_TYPE] =
+ g_param_spec_enum ("bus-type",
+ "Bus Type",
+ "The bus type to connect to, if no address is specified",
+ G_TYPE_BUS_TYPE,
+ G_BUS_TYPE_NONE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_CONNECTION] =
+ g_param_spec_object ("connection",
+ "Connection",
+ "The underlying GDBus connection",
+ G_TYPE_DBUS_CONNECTION,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_HAS_ERROR] =
+ g_param_spec_boolean ("has-error",
+ "Has Error",
+ "Has Error",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ signals [ERROR] =
+ g_signal_new ("error",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE, 1, G_TYPE_ERROR | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+dspy_connection_init (DspyConnection *self)
+{
+}
+
+/**
+ * dspy_connection_get_connection:
+ *
+ * Gets the #GDBusConnection, if one has been opened.
+ *
+ * Returns: (transfer none) (nullable): a #GDBusConnection or %NULL
+ */
+GDBusConnection *
+dspy_connection_get_connection (DspyConnection *self)
+{
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+
+ return self->connection;
+}
+
+const gchar *
+dspy_connection_get_address (DspyConnection *self)
+{
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+
+ if (self->address)
+ return self->address;
+
+ if (self->connected_address)
+ return self->connected_address;
+
+ return NULL;
+}
+
+GBusType
+dspy_connection_get_bus_type (DspyConnection *self)
+{
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), G_BUS_TYPE_NONE);
+
+ return self->bus_type;
+}
+
+static void
+dspy_connection_open_address_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GDBusConnection) bus = NULL;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if (!(bus = g_dbus_connection_new_for_address_finish (result, &error)))
+ g_task_return_error (task, g_steal_pointer (&error));
+ else
+ g_task_return_pointer (task, g_steal_pointer (&bus), g_object_unref);
+}
+
+void
+dspy_connection_open_async (DspyConnection *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_return_if_fail (DSPY_IS_CONNECTION (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, dspy_connection_open_async);
+
+ if (self->connection != NULL)
+ {
+ g_task_return_pointer (task, g_object_ref (self->connection), g_object_unref);
+ return;
+ }
+
+ g_clear_pointer (&self->connected_address, g_free);
+
+ if (self->address != NULL)
+ self->connected_address = g_strdup (self->address);
+ else
+ self->connected_address = g_dbus_address_get_for_bus_sync (self->bus_type,
+ cancellable,
+ &error);
+
+ if (error != NULL)
+ g_task_return_error (task, g_steal_pointer (&error));
+ else
+ g_dbus_connection_new_for_address (self->connected_address,
+ (G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION |
+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT),
+ NULL,
+ cancellable,
+ dspy_connection_open_address_cb,
+ g_steal_pointer (&task));
+}
+
+/**
+ * dspy_connection_open_finish:
+ *
+ * Completes an asynchronous request to dspy_connection_open_async().
+ *
+ * Returns: (transfer full): a #GDBusConnection if successful; otherwise
+ * %NULL and @error is set.
+ */
+GDBusConnection *
+dspy_connection_open_finish (DspyConnection *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ GDBusConnection *bus;
+
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+ g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+ if ((bus = g_task_propagate_pointer (G_TASK (result), error)))
+ {
+ g_dbus_connection_set_exit_on_close (bus, FALSE);
+
+ if (g_set_object (&self->connection, bus))
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONNECTION]);
+ }
+
+ return g_steal_pointer (&bus);
+}
+
+void
+dspy_connection_close (DspyConnection *self)
+{
+ g_return_if_fail (DSPY_IS_CONNECTION (self));
+
+ g_cancellable_cancel (self->cancellable);
+ g_dbus_connection_close (self->connection, NULL, NULL, NULL);
+
+ g_clear_object (&self->connection);
+ g_clear_object (&self->cancellable);
+}
+
+static void
+dspy_connection_list_names_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncInitable *initable = (GAsyncInitable *)object;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GTask) task = user_data;
+ DspyConnection *self;
+
+ g_assert (G_IS_ASYNC_INITABLE (initable));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ self = g_task_get_source_object (task);
+
+ if (!g_async_initable_init_finish (initable, result, &error))
+ {
+ dspy_connection_add_error (self, error);
+ g_task_return_error (task, g_steal_pointer (&error));
+ }
+ else
+ {
+ dspy_connection_clear_errors (self);
+ g_task_return_pointer (task, g_object_ref (initable), g_object_unref);
+ }
+}
+
+void
+dspy_connection_list_names_async (DspyConnection *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(DspyNamesModel) model = NULL;
+
+ g_return_if_fail (DSPY_IS_CONNECTION (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, dspy_connection_list_names_async);
+
+ model = dspy_names_model_new (self);
+
+ g_async_initable_init_async (G_ASYNC_INITABLE (model),
+ G_PRIORITY_DEFAULT,
+ cancellable,
+ dspy_connection_list_names_cb,
+ g_steal_pointer (&task));
+}
+
+GListModel *
+dspy_connection_list_names_finish (DspyConnection *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), NULL);
+ g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+/**
+ * dspy_connection_get_has_error:
+ *
+ * Checks if any errors have been registered with the connection, such
+ * as when listing peer names.
+ *
+ * This can be used to show extra information to the user about the
+ * connection issues.
+ *
+ * Returns: %TRUE if there are any errors
+ */
+gboolean
+dspy_connection_get_has_error (DspyConnection *self)
+{
+ g_return_val_if_fail (DSPY_IS_CONNECTION (self), FALSE);
+
+ return self->errors != NULL && self->errors->len > 0;
+}
+
+void
+dspy_connection_add_error (DspyConnection *self,
+ const GError *error)
+{
+ gboolean notify;
+
+ g_return_if_fail (DSPY_IS_CONNECTION (self));
+ g_return_if_fail (error != NULL);
+
+ if (self->errors == NULL)
+ self->errors = g_ptr_array_new_with_free_func ((GDestroyNotify)g_error_free);
+
+ notify = self->errors->len == 0;
+
+ g_ptr_array_add (self->errors, g_error_copy (error));
+
+ g_signal_emit (self, signals [ERROR], 0, error);
+
+ if (notify)
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
+}
+
+void
+dspy_connection_clear_errors (DspyConnection *self)
+{
+ g_return_if_fail (DSPY_IS_CONNECTION (self));
+
+ if (self->errors != NULL && self->errors->len > 0)
+ {
+ g_ptr_array_remove_range (self->errors, 0, self->errors->len);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERROR]);
+ }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-connection.h b/src/plugins/dspy/libdspy/dspy-connection.h
new file mode 100644
index 000000000..f39e7a60b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-connection.h
@@ -0,0 +1,56 @@
+/* dspy-connection.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_CONNECTION (dspy_connection_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyConnection, dspy_connection, DSPY, CONNECTION, GObject)
+
+DspyConnection *dspy_connection_new_for_address (const gchar *address);
+DspyConnection *dspy_connection_new_for_bus (GBusType bus_type);
+void dspy_connection_add_error (DspyConnection *self,
+ const GError *error);
+void dspy_connection_clear_errors (DspyConnection *self);
+GDBusConnection *dspy_connection_get_connection (DspyConnection *self);
+const gchar *dspy_connection_get_address (DspyConnection *self);
+GBusType dspy_connection_get_bus_type (DspyConnection *self);
+gboolean dspy_connection_get_has_error (DspyConnection *self);
+void dspy_connection_open_async (DspyConnection *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GDBusConnection *dspy_connection_open_finish (DspyConnection *self,
+ GAsyncResult *result,
+ GError **error);
+void dspy_connection_close (DspyConnection *self);
+void dspy_connection_list_names_async (DspyConnection *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GListModel *dspy_connection_list_names_finish (DspyConnection *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-introspection-model.c
b/src/plugins/dspy/libdspy/dspy-introspection-model.c
new file mode 100644
index 000000000..b2e749cb1
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-introspection-model.c
@@ -0,0 +1,919 @@
+/* dspy-introspection-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-introspection-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "dspy-introspection-model.h"
+#include "dspy-private.h"
+
+#if 0
+# define LOG_DEBUG(str) g_printerr ("%s\n", str);
+#else
+# define LOG_DEBUG(str)
+#endif
+
+struct _DspyIntrospectionModel
+{
+ GObject parent_instance;
+
+ GCancellable *cancellable;
+ DspyName *name;
+ DspyNodeInfo *root;
+
+ /* Synchronize chunks access in threaded workers */
+ GMutex chunks_mutex;
+ GStringChunk *chunks;
+};
+
+typedef struct
+{
+ GTask *task;
+ GDBusConnection *connection;
+ gchar *path;
+} Introspect;
+
+static void
+introspect_free (Introspect *state)
+{
+ g_clear_object (&state->task);
+ g_clear_object (&state->connection);
+ g_clear_pointer (&state->path, g_free);
+ g_slice_free (Introspect, state);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Introspect, introspect_free)
+
+static void dspy_introspection_model_introspect (GTask *task,
+ GDBusConnection *connection,
+ const gchar *path);
+
+static void
+parse_xml_worker (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ DspyIntrospectionModel *self = source_object;
+ GBytes *bytes = task_data;
+ g_autoptr(GError) error = NULL;
+ DspyNodeInfo *info;
+ const gchar *xml;
+
+ g_assert (G_IS_TASK (task));
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (source_object));
+ g_assert (bytes != NULL);
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ xml = (const gchar *)g_bytes_get_data (bytes, NULL);
+
+ g_mutex_lock (&self->chunks_mutex);
+ info = _dspy_node_parse (xml, self->chunks, &error);
+ g_mutex_unlock (&self->chunks_mutex);
+
+ if (info != NULL)
+ g_task_return_pointer (task, info, (GDestroyNotify) _dspy_node_free);
+ else
+ g_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+parse_xml_async (DspyIntrospectionModel *self,
+ GBytes *bytes,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (bytes != NULL);
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, parse_xml_async);
+ g_task_set_task_data (task, g_bytes_ref (bytes), (GDestroyNotify) g_bytes_unref);
+ g_task_run_in_thread (task, parse_xml_worker);
+}
+
+static DspyNodeInfo *
+parse_xml_finish (DspyIntrospectionModel *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (G_IS_TASK (result));
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+emit_row_inserted_for_tree_cb (gpointer item,
+ gpointer user_data)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ DspyIntrospectionModel *self = user_data;
+ GtkTreeIter iter = { .user_data = item, };
+
+ path = gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
+ gtk_tree_model_row_inserted (GTK_TREE_MODEL (self), path, &iter);
+}
+
+static void
+emit_row_inserted_for_tree (DspyIntrospectionModel *self,
+ DspyNode *tree)
+{
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (tree != NULL);
+
+ _dspy_node_walk (tree, emit_row_inserted_for_tree_cb, self);
+}
+
+static void
+dspy_introspection_model_init_parse_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+ g_autoptr(Introspect) state = user_data;
+ g_autoptr(GError) error = NULL;
+ DspyNodeInfo *info = NULL;
+ GCancellable *cancellable;
+ gint *n_active;
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (state != NULL);
+ g_assert (G_IS_TASK (state->task));
+ g_assert (state->path != NULL);
+
+ self = g_task_get_source_object (state->task);
+ n_active = g_task_get_task_data (state->task);
+ cancellable = g_task_get_cancellable (state->task);
+
+ g_assert (self != NULL);
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+ g_assert (n_active != NULL);
+ g_assert (*n_active > 0);
+
+ if ((info = parse_xml_finish (self, result, &error)))
+ {
+ g_assert (DSPY_IS_NODE (info));
+ g_assert (info->kind == DSPY_NODE_KIND_NODE);
+
+ /* First, queue a bunch of sub-path reads based on any discovered
+ * nodes from querying this specific node.
+ */
+ for (const GList *iter = info->nodes.head; iter; iter = iter->next)
+ {
+ DspyNodeInfo *child = iter->data;
+ g_autofree gchar *child_path = NULL;
+
+ g_assert (child != NULL);
+ g_assert (DSPY_IS_NODE (child));
+ g_assert (child->kind == DSPY_NODE_KIND_NODE);
+
+ child_path = g_build_path ("/", state->path, child->path, NULL);
+ dspy_introspection_model_introspect (state->task, state->connection, child_path);
+ }
+
+ /* Now add this node to our root if it contains any intefaces. */
+ if (info->interfaces->interfaces.length > 0)
+ {
+ g_autofree gchar *abs_path = g_build_path ("/", state->path, info->path, NULL);
+
+ g_mutex_lock (&self->chunks_mutex);
+ info->path = g_string_chunk_insert_const (self->chunks, abs_path);
+ g_mutex_unlock (&self->chunks_mutex);
+
+ g_queue_push_tail_link (&self->root->nodes, &info->link);
+ info->parent = (DspyNode *)self->root;
+
+ emit_row_inserted_for_tree (self, (DspyNode *)info);
+
+ /* Stolen */
+ info = NULL;
+ }
+
+ g_clear_pointer (&info, _dspy_node_free);
+ }
+
+ if (--(*n_active) == 0)
+ g_task_return_boolean (state->task, TRUE);
+}
+
+static void
+dspy_introspection_model_init_introspect_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *bus = (GDBusConnection *)object;
+ DspyIntrospectionModel *self;
+ g_autoptr(Introspect) state = user_data;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GError) error = NULL;
+ GCancellable *cancellable;
+ gint *n_active;
+
+ g_assert (G_IS_DBUS_CONNECTION (bus));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (state != NULL);
+ g_assert (G_IS_TASK (state->task));
+ g_assert (state->path != NULL);
+
+ self = g_task_get_source_object (state->task);
+ n_active = g_task_get_task_data (state->task);
+ cancellable = g_task_get_cancellable (state->task);
+
+ g_assert (self != NULL);
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (n_active != NULL);
+ g_assert (*n_active > 0);
+
+ if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+ {
+ g_autoptr(GBytes) bytes = NULL;
+ const gchar *str = NULL;
+
+ /* Get the XML contents, and wrap it in a new GBytes that will
+ * reference the original GVariant to avoid a copy as this might
+ * contain a large amount of text.
+ */
+ g_variant_get (reply, "(&s)", &str);
+
+ if (str[0] != 0)
+ {
+ bytes = g_bytes_new_with_free_func (str,
+ strlen (str),
+ (GDestroyNotify) g_variant_unref,
+ g_variant_ref (reply));
+ parse_xml_async (self,
+ bytes,
+ cancellable,
+ dspy_introspection_model_init_parse_cb,
+ g_steal_pointer (&state));
+ return;
+ }
+ }
+ else
+ {
+ DspyConnection *connection = dspy_name_get_connection (self->name);
+
+ dspy_connection_add_error (connection, error);
+ }
+
+ if (--(*n_active) == 0)
+ g_task_return_boolean (state->task, TRUE);
+}
+
+static gboolean
+has_node_with_path (DspyIntrospectionModel *self,
+ const gchar *path)
+{
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (path != NULL);
+
+ for (const GList *iter = self->root->nodes.head; iter; iter = iter->next)
+ {
+ const DspyNode *node = iter->data;
+
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+ g_assert (node->any.kind == DSPY_NODE_KIND_NODE);
+
+ if (g_strcmp0 (path, node->node.path) == 0)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+dspy_introspection_model_introspect (GTask *task,
+ GDBusConnection *connection,
+ const gchar *path)
+{
+ DspyIntrospectionModel *self;
+ Introspect *state;
+ gint *n_active;
+
+ g_assert (G_IS_TASK (task));
+ g_assert (G_IS_DBUS_CONNECTION (connection));
+ g_assert (path != NULL);
+
+ self = g_task_get_source_object (task);
+ n_active = g_task_get_task_data (task);
+
+ g_assert (G_IS_TASK (task));
+ g_assert (n_active != NULL);
+
+ /* If we already have this path, then ignore the suplimental query */
+ if (has_node_with_path (self, path))
+ return;
+
+ (*n_active)++;
+
+ state = g_slice_new0 (Introspect);
+ state->task = g_object_ref (task);
+ state->connection = g_object_ref (connection);
+ state->path = g_strdup (path);
+
+ g_dbus_connection_call (connection,
+ dspy_name_get_owner (self->name),
+ path,
+ "org.freedesktop.DBus.Introspectable",
+ "Introspect",
+ NULL, /* Params */
+ G_VARIANT_TYPE ("(s)"),
+ G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+ -1,
+ self->cancellable,
+ dspy_introspection_model_init_introspect_cb,
+ state);
+}
+
+static void
+dspy_introspection_model_init_async (GAsyncInitable *initiable,
+ gint io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)initiable;
+ GDBusConnection *bus = NULL;
+ DspyConnection *connection = NULL;
+ g_autoptr(GTask) task = NULL;
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, dspy_introspection_model_init_async);
+ g_task_set_task_data (task, g_new0 (gint, 1), g_free);
+ g_task_set_priority (task, io_priority);
+
+ if (self->name == NULL ||
+ !(connection = dspy_name_get_connection (self->name)) ||
+ !(bus = dspy_connection_get_connection (connection)))
+ {
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "%s has not been intialized with a name",
+ G_OBJECT_TYPE_NAME (self));
+ return;
+ }
+
+ dspy_introspection_model_introspect (task, bus, "/");
+}
+
+static gboolean
+dspy_introspection_model_init_finish (GAsyncInitable *initable,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (initable));
+ g_assert (G_IS_TASK (result));
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+ iface->init_async = dspy_introspection_model_init_async;
+ iface->init_finish = dspy_introspection_model_init_finish;
+}
+
+static gboolean
+dspy_introspection_model_iter_children (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *parent)
+{
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+ g_assert (iter != NULL);
+
+ return gtk_tree_model_iter_nth_child (model, iter, parent, 0);
+}
+
+static gboolean
+dspy_introspection_model_iter_next (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+ DspyNode *node;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (iter != NULL);
+
+ node = iter->user_data;
+
+ g_assert (node != NULL);
+ g_assert (node->any.kind > 0);
+ g_assert (node->any.kind < DSPY_NODE_KIND_LAST);
+
+ switch (node->any.kind)
+ {
+ case DSPY_NODE_KIND_NODE:
+ case DSPY_NODE_KIND_METHOD:
+ case DSPY_NODE_KIND_SIGNAL:
+ case DSPY_NODE_KIND_PROPERTY:
+ case DSPY_NODE_KIND_INTERFACE:
+ if (node->any.link.next != NULL)
+ {
+ iter->user_data = node->any.link.next->data;
+ return TRUE;
+ }
+ else
+ {
+ node->any.link.next = NULL;
+ return FALSE;
+ }
+
+ case DSPY_NODE_KIND_PROPERTIES:
+ iter->user_data = node->any.parent->interface.signals;
+ return TRUE;
+
+ case DSPY_NODE_KIND_SIGNALS:
+ iter->user_data = node->any.parent->interface.methods;
+ return TRUE;
+
+ case DSPY_NODE_KIND_INTERFACES:
+ case DSPY_NODE_KIND_METHODS:
+ case DSPY_NODE_KIND_ARG:
+ case DSPY_NODE_KIND_LAST:
+ default:
+ return FALSE;
+ }
+}
+
+static gint
+dspy_introspection_model_get_n_columns (GtkTreeModel *model)
+{
+ return 1;
+}
+
+static GtkTreePath *
+dspy_introspection_model_get_path (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+ GtkTreePath *path;
+ DspyNode *node;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (iter != NULL);
+
+ node = iter->user_data;
+
+ g_assert (node != NULL);
+ g_assert (node->any.parent != NULL);
+
+ path = gtk_tree_path_new_first ();
+
+ g_assert (gtk_tree_path_get_depth (path) == 1);
+
+ for (; node->any.parent != NULL; node = node->any.parent)
+ {
+ gint pos = 0;
+
+ for (const GList *list = &node->any.link; list->prev; list = list->prev)
+ pos++;
+
+ gtk_tree_path_prepend_index (path, pos);
+ }
+
+ gtk_tree_path_up (path);
+
+ return g_steal_pointer (&path);
+}
+
+static gboolean
+dspy_introspection_model_iter_parent (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *child)
+{
+ DspyNode *node;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+ g_assert (iter != NULL);
+ g_assert (child != NULL);
+
+ memset (iter, 0, sizeof *iter);
+
+ node = child->user_data;
+
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+ g_assert (node->any.parent != NULL);
+
+ /* Ignore root, we don't have a visual node for that */
+ if (node->any.parent->any.parent != NULL)
+ iter->user_data = node->node.parent;
+
+ return iter->user_data != NULL;
+}
+
+static GType
+dspy_introspection_model_get_column_type (GtkTreeModel *model,
+ gint column)
+{
+ if (column == 0)
+ return G_TYPE_STRING;
+
+ return G_TYPE_INVALID;
+}
+
+static gboolean
+dspy_introspection_model_iter_has_child (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ GtkTreeIter child;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (model));
+ g_assert (iter != NULL);
+
+ return gtk_tree_model_iter_nth_child (model, &child, iter, 0);
+}
+
+static GtkTreeModelFlags
+dspy_introspection_model_get_flags (GtkTreeModel *model)
+{
+ return 0;
+}
+
+static void
+dspy_introspection_model_get_value (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gint column,
+ GValue *value)
+{
+ LOG_DEBUG (G_STRFUNC);
+
+ if (column == 0)
+ {
+ DspyNode *node = iter->user_data;
+ g_autofree gchar *str = NULL;
+
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+
+ g_value_init (value, G_TYPE_STRING);
+
+ str = _dspy_node_get_text (node);
+
+ if (_dspy_node_is_group (node))
+ {
+ if (gtk_tree_model_iter_has_child (model, iter))
+ g_value_take_string (value, g_strdup_printf ("<b>%s</b>", str));
+ else
+ g_value_take_string (value, g_strdup_printf ("<span fgalpha='25000' weight='bold'>%s</span>",
str));
+ }
+ else
+ g_value_take_string (value, g_steal_pointer (&str));
+ }
+}
+
+static gboolean
+dspy_introspection_model_get_iter (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreePath *tree_path)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+ DspyNode *cur;
+ gint *indices;
+ gint depth;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (iter != NULL);
+ g_assert (tree_path != NULL);
+
+ memset (iter, 0, sizeof *iter);
+
+ cur = (DspyNode *)self->root;
+ indices = gtk_tree_path_get_indices_with_depth (tree_path, &depth);
+
+ for (guint i = 0; cur != NULL && i < depth; i++)
+ {
+ gint pos = indices[i];
+
+ if (cur->any.parent == NULL)
+ cur = g_queue_peek_nth (&cur->node.nodes, pos);
+ else if (cur->any.kind == DSPY_NODE_KIND_NODE)
+ cur = (DspyNode *)cur->node.interfaces;
+ else if (cur->any.kind == DSPY_NODE_KIND_INTERFACES)
+ cur = g_queue_peek_nth (&cur->interfaces.interfaces, pos);
+ else if (cur->any.kind == DSPY_NODE_KIND_INTERFACE)
+ {
+ if (pos == 0)
+ cur = (DspyNode *)cur->interface.properties;
+ else if (pos == 1)
+ cur = (DspyNode *)cur->interface.signals;
+ else if (pos == 2)
+ cur = (DspyNode *)cur->interface.methods;
+ else
+ cur = NULL;
+ }
+ else if (cur->any.kind == DSPY_NODE_KIND_PROPERTIES)
+ cur = g_queue_peek_nth (&cur->properties.properties, pos);
+ else if (cur->any.kind == DSPY_NODE_KIND_SIGNALS)
+ cur = g_queue_peek_nth (&cur->signals.signals, pos);
+ else if (cur->any.kind == DSPY_NODE_KIND_METHODS)
+ cur = g_queue_peek_nth (&cur->methods.methods, pos);
+ else
+ cur = NULL;
+ }
+
+ if (cur != NULL)
+ {
+ iter->user_data = cur;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gint
+dspy_introspection_model_iter_n_children (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+ DspyNode *node;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (iter != NULL);
+
+ node = iter ? iter->user_data : self->root;
+
+ if (node->any.kind == DSPY_NODE_KIND_NODE)
+ {
+ /* Root item is the list of paths */
+ if (node->any.parent == NULL)
+ return node->node.nodes.length;
+ else
+ return 1;
+ }
+
+ if (node->any.kind == DSPY_NODE_KIND_INTERFACES)
+ return node->interfaces.interfaces.length;
+
+ if (node->any.kind == DSPY_NODE_KIND_INTERFACE)
+ return 3;
+
+ if (node->any.kind == DSPY_NODE_KIND_METHODS)
+ return node->methods.methods.length;
+
+ if (node->any.kind == DSPY_NODE_KIND_SIGNALS)
+ return node->signals.signals.length;
+
+ if (node->any.kind == DSPY_NODE_KIND_PROPERTIES)
+ return node->properties.properties.length;
+
+ return 0;
+}
+
+static gboolean
+dspy_introspection_model_iter_nth_child (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *parent,
+ gint nth)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)model;
+ DspyNode *cur;
+
+ LOG_DEBUG (G_STRFUNC);
+
+ g_assert (DSPY_IS_INTROSPECTION_MODEL (self));
+ g_assert (iter != NULL);
+ g_assert (nth >= 0);
+
+ cur = parent ? parent->user_data : self->root;
+
+ g_assert (DSPY_IS_NODE (cur));
+
+ switch (cur->any.kind)
+ {
+ case DSPY_NODE_KIND_NODE:
+ if (cur->any.parent == NULL)
+ iter->user_data = g_queue_peek_nth (&cur->node.nodes, nth);
+ else
+ iter->user_data = cur->node.interfaces;
+ break;
+
+ case DSPY_NODE_KIND_METHODS:
+ iter->user_data = g_queue_peek_nth (&cur->methods.methods, nth);
+ break;
+
+ case DSPY_NODE_KIND_SIGNALS:
+ iter->user_data = g_queue_peek_nth (&cur->signals.signals, nth);
+ break;
+
+ case DSPY_NODE_KIND_PROPERTIES:
+ iter->user_data = g_queue_peek_nth (&cur->properties.properties, nth);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACES:
+ iter->user_data = g_queue_peek_nth (&cur->interfaces.interfaces, nth);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACE:
+ if (nth == 0)
+ iter->user_data = cur->interface.properties;
+ else if (nth == 1)
+ iter->user_data = cur->interface.signals;
+ else if (nth == 2)
+ iter->user_data = cur->interface.methods;
+ break;
+
+ case DSPY_NODE_KIND_ARG:
+ case DSPY_NODE_KIND_METHOD:
+ case DSPY_NODE_KIND_SIGNAL:
+ case DSPY_NODE_KIND_PROPERTY:
+ case DSPY_NODE_KIND_LAST:
+ default:
+ return FALSE;
+ }
+
+ return iter->user_data != NULL;
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+ iface->get_column_type = dspy_introspection_model_get_column_type;
+ iface->get_iter = dspy_introspection_model_get_iter;
+ iface->get_flags = dspy_introspection_model_get_flags;
+ iface->get_n_columns = dspy_introspection_model_get_n_columns;
+ iface->get_path = dspy_introspection_model_get_path;
+ iface->get_value = dspy_introspection_model_get_value;
+ iface->iter_children = dspy_introspection_model_iter_children;
+ iface->iter_has_child = dspy_introspection_model_iter_has_child;
+ iface->iter_n_children = dspy_introspection_model_iter_n_children;
+ iface->iter_nth_child = dspy_introspection_model_iter_nth_child;
+ iface->iter_next = dspy_introspection_model_iter_next;
+ iface->iter_parent = dspy_introspection_model_iter_parent;
+}
+
+G_DEFINE_TYPE_WITH_CODE (DspyIntrospectionModel, dspy_introspection_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init))
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+DspyIntrospectionModel *
+_dspy_introspection_model_new (DspyName *name)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+ return g_object_new (DSPY_TYPE_INTROSPECTION_MODEL,
+ "name", name,
+ NULL);
+}
+
+static void
+dspy_introspection_model_finalize (GObject *object)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->name);
+ g_clear_pointer (&self->chunks, g_string_chunk_free);
+ g_clear_pointer (&self->root, _dspy_node_free);
+ g_mutex_clear (&self->chunks_mutex);
+
+ G_OBJECT_CLASS (dspy_introspection_model_parent_class)->finalize (object);
+}
+
+static void
+dspy_introspection_model_dispose (GObject *object)
+{
+ DspyIntrospectionModel *self = (DspyIntrospectionModel *)object;
+
+ g_cancellable_cancel (self->cancellable);
+
+ G_OBJECT_CLASS (dspy_introspection_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_introspection_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyIntrospectionModel *self = DSPY_INTROSPECTION_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_object (value, dspy_introspection_model_get_name (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_introspection_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyIntrospectionModel *self = DSPY_INTROSPECTION_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ self->name = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_introspection_model_class_init (DspyIntrospectionModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = dspy_introspection_model_dispose;
+ object_class->finalize = dspy_introspection_model_finalize;
+ object_class->get_property = dspy_introspection_model_get_property;
+ object_class->set_property = dspy_introspection_model_set_property;
+
+ properties [PROP_NAME] =
+ g_param_spec_object ("name",
+ "Name",
+ "The DspyName to introspect",
+ DSPY_TYPE_NAME,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_introspection_model_init (DspyIntrospectionModel *self)
+{
+ self->cancellable = g_cancellable_new ();
+ self->chunks = g_string_chunk_new (4096L * 4);
+ self->root = _dspy_node_new_root ();
+ g_mutex_init (&self->chunks_mutex);
+}
+
+/**
+ * dspy_introspection_model_get_name:
+ *
+ * Gets the #DspyName that is being introspected.
+ *
+ * Returns: (transfer none): a #DspyName
+ */
+DspyName *
+dspy_introspection_model_get_name (DspyIntrospectionModel *self)
+{
+ g_return_val_if_fail (DSPY_IS_INTROSPECTION_MODEL (self), NULL);
+
+ return self->name;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-introspection-model.h
b/src/plugins/dspy/libdspy/dspy-introspection-model.h
new file mode 100644
index 000000000..0ffd00029
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-introspection-model.h
@@ -0,0 +1,35 @@
+/* dspy-introspection-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_INTROSPECTION_MODEL (dspy_introspection_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyIntrospectionModel, dspy_introspection_model, DSPY, INTROSPECTION_MODEL, GObject)
+
+DspyName *dspy_introspection_model_get_name (DspyIntrospectionModel *self);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-invocation.c
b/src/plugins/dspy/libdspy/dspy-method-invocation.c
new file mode 100644
index 000000000..034cde20f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-invocation.c
@@ -0,0 +1,585 @@
+/* dspy-method-invocation.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-method-invocation"
+
+#include "config.h"
+
+#include "dspy-method-invocation.h"
+
+typedef struct
+{
+ gchar *interface;
+ gchar *signature;
+ gchar *object_path;
+ gchar *method;
+ gchar *reply_signature;
+ DspyName *name;
+ GVariant *parameters;
+ gint timeout_msec;
+} DspyMethodInvocationPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyMethodInvocation, dspy_method_invocation, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_INTERFACE,
+ PROP_METHOD,
+ PROP_NAME,
+ PROP_OBJECT_PATH,
+ PROP_PARAMETERS,
+ PROP_REPLY_SIGNATURE,
+ PROP_SIGNATURE,
+ PROP_TIMEOUT,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_method_invocation_new:
+ *
+ * Create a new #DspyMethodInvocation.
+ *
+ * Returns: (transfer full): a newly created #DspyMethodInvocation
+ */
+DspyMethodInvocation *
+dspy_method_invocation_new (void)
+{
+ return g_object_new (DSPY_TYPE_METHOD_INVOCATION, NULL);
+}
+
+static void
+dspy_method_invocation_finalize (GObject *object)
+{
+ DspyMethodInvocation *self = (DspyMethodInvocation *)object;
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_clear_pointer (&priv->interface, g_free);
+ g_clear_pointer (&priv->signature, g_free);
+ g_clear_pointer (&priv->object_path, g_free);
+ g_clear_pointer (&priv->method, g_free);
+ g_clear_pointer (&priv->reply_signature, g_free);
+ g_clear_object (&priv->name);
+ g_clear_pointer (&priv->parameters, g_variant_unref);
+
+ G_OBJECT_CLASS (dspy_method_invocation_parent_class)->finalize (object);
+}
+
+static void
+dspy_method_invocation_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyMethodInvocation *self = DSPY_METHOD_INVOCATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_INTERFACE:
+ g_value_set_string (value, dspy_method_invocation_get_interface (self));
+ break;
+
+ case PROP_OBJECT_PATH:
+ g_value_set_string (value, dspy_method_invocation_get_object_path (self));
+ break;
+
+ case PROP_METHOD:
+ g_value_set_string (value, dspy_method_invocation_get_method (self));
+ break;
+
+ case PROP_SIGNATURE:
+ g_value_set_string (value, dspy_method_invocation_get_signature (self));
+ break;
+
+ case PROP_REPLY_SIGNATURE:
+ g_value_set_string (value, dspy_method_invocation_get_reply_signature (self));
+ break;
+
+ case PROP_NAME:
+ g_value_set_object (value, dspy_method_invocation_get_name (self));
+ break;
+
+ case PROP_PARAMETERS:
+ g_value_set_variant (value, dspy_method_invocation_get_parameters (self));
+ break;
+
+ case PROP_TIMEOUT:
+ g_value_set_int (value, dspy_method_invocation_get_timeout (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_method_invocation_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyMethodInvocation *self = DSPY_METHOD_INVOCATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_INTERFACE:
+ dspy_method_invocation_set_interface (self, g_value_get_string (value));
+ break;
+
+ case PROP_OBJECT_PATH:
+ dspy_method_invocation_set_object_path (self, g_value_get_string (value));
+ break;
+
+ case PROP_METHOD:
+ dspy_method_invocation_set_method (self, g_value_get_string (value));
+ break;
+
+ case PROP_SIGNATURE:
+ dspy_method_invocation_set_signature (self, g_value_get_string (value));
+ break;
+
+ case PROP_REPLY_SIGNATURE:
+ dspy_method_invocation_set_reply_signature (self, g_value_get_string (value));
+ break;
+
+ case PROP_NAME:
+ dspy_method_invocation_set_name (self, g_value_get_object (value));
+ break;
+
+ case PROP_PARAMETERS:
+ dspy_method_invocation_set_parameters (self, g_value_get_variant (value));
+ break;
+
+ case PROP_TIMEOUT:
+ dspy_method_invocation_set_timeout (self, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_method_invocation_class_init (DspyMethodInvocationClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = dspy_method_invocation_finalize;
+ object_class->get_property = dspy_method_invocation_get_property;
+ object_class->set_property = dspy_method_invocation_set_property;
+
+ properties [PROP_INTERFACE] =
+ g_param_spec_string ("interface",
+ "Interface",
+ "The interface containing the method",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_OBJECT_PATH] =
+ g_param_spec_string ("object-path",
+ "Object Path",
+ "The path containing the interface",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_METHOD] =
+ g_param_spec_string ("method",
+ "Method",
+ "The method of the interface to execute",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_SIGNATURE] =
+ g_param_spec_string ("signature",
+ "Signature",
+ "The signature of the method, used for display purposes",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_REPLY_SIGNATURE] =
+ g_param_spec_string ("reply-signature",
+ "Reply Signature",
+ "The reply signature of the method, used for display purposes",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_NAME] =
+ g_param_spec_object ("name",
+ "Name",
+ "The DspyName to communicate with",
+ DSPY_TYPE_NAME,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_PARAMETERS] =
+ g_param_spec_variant ("parameters",
+ "Parameters",
+ "The parameters for the invocation",
+ G_VARIANT_TYPE_ANY,
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_TIMEOUT] =
+ g_param_spec_int ("timeout",
+ "Timeout",
+ "The timeout for the operation",
+ -1, G_MAXINT, -1,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_method_invocation_init (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ priv->timeout_msec = -1;
+}
+
+static void
+dspy_method_invocation_execute_call_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *bus = (GDBusConnection *)object;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (G_IS_DBUS_CONNECTION (bus));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if (!(reply = g_dbus_connection_call_finish (bus, result, &error)))
+ g_task_return_error (task, g_steal_pointer (&error));
+ else
+ g_task_return_pointer (task, g_steal_pointer (&reply), (GDestroyNotify)g_variant_unref);
+}
+
+static void
+dspy_method_invocation_execute_open_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyMethodInvocationPrivate *priv;
+ DspyMethodInvocation *self;
+ DspyConnection *connection = (DspyConnection *)object;
+ g_autoptr(GDBusConnection) bus = NULL;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ GCancellable *cancellable;
+
+ g_assert (DSPY_IS_CONNECTION (connection));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if (!(bus = dspy_connection_open_finish (connection, result, &error)))
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ self = g_task_get_source_object (task);
+ priv = dspy_method_invocation_get_instance_private (self);
+ cancellable = g_task_get_cancellable (task);
+
+ if (priv->name == NULL ||
+ priv->object_path == NULL ||
+ priv->interface == NULL ||
+ priv->method == NULL ||
+ priv->parameters == NULL)
+ {
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "Method invocation contains uninitialized parameters");
+ return;
+ }
+
+ g_dbus_connection_call (bus,
+ dspy_name_get_owner (priv->name),
+ priv->object_path,
+ priv->interface,
+ priv->method,
+ priv->parameters,
+ NULL, /* Allow any reply type (even if invalid) */
+ G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+ priv->timeout_msec,
+ cancellable,
+ dspy_method_invocation_execute_call_cb,
+ g_steal_pointer (&task));
+}
+
+void
+dspy_method_invocation_execute_async (DspyMethodInvocation *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_autoptr(GTask) task = NULL;
+ DspyConnection *connection;
+
+ g_assert (DSPY_IS_METHOD_INVOCATION (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, dspy_method_invocation_execute_async);
+
+ if (priv->name == NULL)
+ {
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "No name set to communicate with");
+ return;
+ }
+
+ connection = dspy_name_get_connection (priv->name);
+
+ dspy_connection_open_async (connection,
+ cancellable,
+ dspy_method_invocation_execute_open_cb,
+ g_steal_pointer (&task));
+}
+
+/**
+ * dspy_method_invocation_execute_finish:
+ *
+ * Completes an asynchronous call to dspy_method_invocation_execute_async()
+ *
+ * Returns: (transfer full): a #GVariant if successful; otherwise %FALSE and
+ * @error is set.
+ */
+GVariant *
+dspy_method_invocation_execute_finish (DspyMethodInvocation *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+const gchar *
+dspy_method_invocation_get_interface (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ return priv->interface;
+}
+
+const gchar *
+dspy_method_invocation_get_object_path (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ return priv->object_path;
+}
+
+const gchar *
+dspy_method_invocation_get_method (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ return priv->method;
+}
+
+const gchar *
+dspy_method_invocation_get_signature (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ return priv->signature;
+}
+
+const gchar *
+dspy_method_invocation_get_reply_signature (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+ return priv->reply_signature;
+}
+
+/**
+ * dspy_method_invocation_get_parameters:
+ *
+ * Returns: (transfer none): a #GVariant if set; otherwise %NULL
+ */
+GVariant *
+dspy_method_invocation_get_parameters (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+
+ return priv->parameters;
+}
+
+/**
+ * dspy_method_invocation_get_name:
+ *
+ * Returns: (transfer none) (nullable): a #DspyName or %NULL if unset
+ */
+DspyName *
+dspy_method_invocation_get_name (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), NULL);
+
+ return priv->name;
+}
+
+void
+dspy_method_invocation_set_interface (DspyMethodInvocation *self,
+ const gchar *interface)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_strcmp0 (priv->interface, interface) != 0)
+ {
+ g_free (priv->interface);
+ priv->interface = g_strdup (interface);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INTERFACE]);
+ }
+}
+
+void
+dspy_method_invocation_set_method (DspyMethodInvocation *self,
+ const gchar *method)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_strcmp0 (priv->method, method) != 0)
+ {
+ g_free (priv->method);
+ priv->method = g_strdup (method);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_METHOD]);
+ }
+}
+
+void
+dspy_method_invocation_set_object_path (DspyMethodInvocation *self,
+ const gchar *object_path)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_strcmp0 (priv->object_path, object_path) != 0)
+ {
+ g_free (priv->object_path);
+ priv->object_path = g_strdup (object_path);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_OBJECT_PATH]);
+ }
+}
+
+void
+dspy_method_invocation_set_signature (DspyMethodInvocation *self,
+ const gchar *signature)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_strcmp0 (priv->signature, signature) != 0)
+ {
+ g_free (priv->signature);
+ priv->signature = g_strdup (signature);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SIGNATURE]);
+ }
+}
+
+void
+dspy_method_invocation_set_reply_signature (DspyMethodInvocation *self,
+ const gchar *reply_signature)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_strcmp0 (priv->reply_signature, reply_signature) != 0)
+ {
+ g_free (priv->reply_signature);
+ priv->reply_signature = g_strdup (reply_signature);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REPLY_SIGNATURE]);
+ }
+}
+
+void
+dspy_method_invocation_set_name (DspyMethodInvocation *self,
+ DspyName *name)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (g_set_object (&priv->name, name))
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+}
+
+void
+dspy_method_invocation_set_parameters (DspyMethodInvocation *self,
+ GVariant *parameters)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+
+ if (parameters != priv->parameters)
+ {
+ g_clear_pointer (&priv->parameters, g_variant_unref);
+ priv->parameters = parameters ? g_variant_ref_sink (parameters) : NULL;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PARAMETERS]);
+ }
+}
+
+gint
+dspy_method_invocation_get_timeout (DspyMethodInvocation *self)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_val_if_fail (DSPY_IS_METHOD_INVOCATION (self), -1);
+
+ return priv->timeout_msec;
+}
+
+void
+dspy_method_invocation_set_timeout (DspyMethodInvocation *self,
+ gint timeout)
+{
+ DspyMethodInvocationPrivate *priv = dspy_method_invocation_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_INVOCATION (self));
+ g_return_if_fail (timeout >= -1);
+
+ if (priv->timeout_msec != timeout)
+ {
+ priv->timeout_msec = timeout;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TIMEOUT]);
+ }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-method-invocation.h
b/src/plugins/dspy/libdspy/dspy-method-invocation.h
new file mode 100644
index 000000000..e327a4b8f
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-invocation.h
@@ -0,0 +1,74 @@
+/* dspy-method-invocation.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_METHOD_INVOCATION (dspy_method_invocation_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyMethodInvocation, dspy_method_invocation, DSPY, METHOD_INVOCATION, GObject)
+
+struct _DspyMethodInvocationClass
+{
+ GObjectClass parent_class;
+
+ /*< private >*/
+ gpointer _reserved[8];
+};
+
+DspyMethodInvocation *dspy_method_invocation_new (void);
+const gchar *dspy_method_invocation_get_interface (DspyMethodInvocation *self);
+const gchar *dspy_method_invocation_get_object_path (DspyMethodInvocation *self);
+const gchar *dspy_method_invocation_get_method (DspyMethodInvocation *self);
+const gchar *dspy_method_invocation_get_signature (DspyMethodInvocation *self);
+const gchar *dspy_method_invocation_get_reply_signature (DspyMethodInvocation *self);
+GVariant *dspy_method_invocation_get_parameters (DspyMethodInvocation *self);
+DspyName *dspy_method_invocation_get_name (DspyMethodInvocation *self);
+void dspy_method_invocation_set_interface (DspyMethodInvocation *self,
+ const gchar *interface);
+void dspy_method_invocation_set_method (DspyMethodInvocation *self,
+ const gchar *method);
+void dspy_method_invocation_set_object_path (DspyMethodInvocation *self,
+ const gchar *object_path);
+void dspy_method_invocation_set_signature (DspyMethodInvocation *self,
+ const gchar *signature);
+void dspy_method_invocation_set_reply_signature (DspyMethodInvocation *self,
+ const gchar *reply_signature);
+void dspy_method_invocation_set_name (DspyMethodInvocation *self,
+ DspyName *name);
+void dspy_method_invocation_set_parameters (DspyMethodInvocation *self,
+ GVariant *parameters);
+void dspy_method_invocation_execute_async (DspyMethodInvocation *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GVariant *dspy_method_invocation_execute_finish (DspyMethodInvocation *self,
+ GAsyncResult *result,
+ GError **error);
+gint dspy_method_invocation_get_timeout (DspyMethodInvocation *self);
+void dspy_method_invocation_set_timeout (DspyMethodInvocation *self,
+ gint timout);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.c b/src/plugins/dspy/libdspy/dspy-method-view.c
new file mode 100644
index 000000000..64e90fb23
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.c
@@ -0,0 +1,471 @@
+/* dspy-method-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-method-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "dspy-method-view.h"
+
+typedef struct
+{
+ DspyMethodInvocation *invocation;
+ DzlBindingGroup *bindings;
+ GCancellable *cancellable;
+ GArray *durations;
+
+ GtkLabel *label_interface;
+ GtkLabel *label_object_path;
+ GtkLabel *label_method;
+ GtkLabel *label_avg;
+ GtkLabel *label_min;
+ GtkLabel *label_max;
+ GtkButton *button;
+ GtkButton *copy_button;
+ GtkTextBuffer *buffer_params;
+ GtkTextBuffer *buffer_reply;
+ GtkTextView *textview_params;
+
+ guint busy : 1;
+} DspyMethodViewPrivate;
+
+typedef struct
+{
+ DspyMethodView *self;
+ GTimer *timer;
+} Execute;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyMethodView, dspy_method_view, DZL_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_INVOCATION,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+execute_free (Execute *state)
+{
+ if (state != NULL)
+ {
+ g_clear_pointer (&state->timer, g_timer_destroy);
+ g_clear_object (&state->self);
+ g_slice_free (Execute, state);
+ }
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Execute, execute_free)
+
+/**
+ * dspy_method_view_new:
+ *
+ * Create a new #DspyMethodView.
+ *
+ * Returns: (transfer full): a newly created #DspyMethodView
+ */
+GtkWidget *
+dspy_method_view_new (void)
+{
+ return g_object_new (DSPY_TYPE_METHOD_VIEW, NULL);
+}
+
+static void
+update_timings (DspyMethodView *self)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+ g_autofree gchar *mean_str = NULL;
+ g_autofree gchar *min_str = NULL;
+ g_autofree gchar *max_str = NULL;
+ gdouble min = G_MAXDOUBLE;
+ gdouble max = -G_MAXDOUBLE;
+ gdouble total = 0;
+ gdouble mean = 0;
+
+ g_assert (DSPY_IS_METHOD_VIEW (self));
+ g_assert (priv->durations != NULL);
+
+ if (priv->durations->len == 0)
+ {
+ gtk_label_set_label (priv->label_avg, NULL);
+ gtk_label_set_label (priv->label_min, NULL);
+ gtk_label_set_label (priv->label_max, NULL);
+ return;
+ }
+
+ for (guint i = 0; i < priv->durations->len; i++)
+ {
+ gdouble val = g_array_index (priv->durations, gdouble, i);
+
+ total += val;
+ min = MIN (min, val);
+ max = MAX (max, val);
+ }
+
+ mean = total / (gdouble)priv->durations->len;
+
+ mean_str = g_strdup_printf ("%0.4lf", mean);
+ min_str = g_strdup_printf ("%0.4lf", min);
+ max_str = g_strdup_printf ("%0.4lf", max);
+
+ gtk_label_set_label (priv->label_avg, mean_str);
+ gtk_label_set_label (priv->label_min, min_str);
+ gtk_label_set_label (priv->label_max, max_str);
+}
+
+static gboolean
+variant_to_string_transform (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ GVariant *v = g_value_get_variant (from_value);
+ if (v != NULL)
+ g_value_take_string (to_value, g_variant_print (v, FALSE));
+ else
+ g_value_set_string (to_value, "");
+ return TRUE;
+}
+
+static void
+dspy_method_view_execute_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyMethodInvocation *invocation = (DspyMethodInvocation *)object;
+ g_autoptr(Execute) state = user_data;
+ DspyMethodViewPrivate *priv;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GError) error = NULL;
+ gdouble elapsed;
+
+ g_assert (DSPY_IS_METHOD_INVOCATION (invocation));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (state != NULL);
+ g_assert (state->timer != NULL);
+ g_assert (DSPY_IS_METHOD_VIEW (state->self));
+
+ priv = dspy_method_view_get_instance_private (state->self);
+ priv->busy = FALSE;
+
+ g_timer_stop (state->timer);
+ elapsed = g_timer_elapsed (state->timer, NULL);
+ g_array_append_val (priv->durations, elapsed);
+
+ if (!(reply = dspy_method_invocation_execute_finish (invocation, result, &error)))
+ {
+ if (priv->invocation == invocation)
+ gtk_text_buffer_set_text (priv->buffer_reply, error->message, -1);
+ }
+ else
+ {
+ if (priv->invocation == invocation)
+ {
+ g_autofree gchar *replystr = g_variant_print (reply, TRUE);
+ gtk_text_buffer_set_text (priv->buffer_reply, replystr, -1);
+ }
+ }
+
+ update_timings (state->self);
+
+ gtk_button_set_label (priv->button, _("Execute"));
+}
+
+static GVariant *
+get_variant_for_text_buffer (GtkTextBuffer *buffer,
+ const GVariantType *type,
+ GError **error)
+{
+ g_autofree gchar *text = NULL;
+ GtkTextIter begin, end;
+
+ g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+ gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+ text = g_strstrip (gtk_text_buffer_get_text (buffer, &begin, &end, TRUE));
+
+ if (text[0] != '(')
+ {
+ g_autofree gchar *tmp = text;
+ text = g_strdup_printf ("(%s,)", tmp);
+ gtk_text_buffer_set_text (buffer, text, -1);
+ }
+
+ return g_variant_parse (type, text, NULL, NULL, error);
+}
+
+static void
+dspy_method_view_button_clicked_cb (DspyMethodView *self,
+ GtkButton *button)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+ g_autoptr(GVariant) params = NULL;
+ g_autoptr(GError) error = NULL;
+ const gchar *signature;
+ Execute *state;
+
+ g_assert (DSPY_IS_METHOD_VIEW (self));
+ g_assert (GTK_IS_BUTTON (button));
+
+ /* Always cancel anything in flight */
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+
+ if (priv->busy)
+ return;
+
+ if (priv->invocation == NULL)
+ return;
+
+ g_assert (priv->busy == FALSE);
+ g_assert (priv->cancellable == NULL);
+
+ signature = dspy_method_invocation_get_signature (priv->invocation);
+
+ if (!signature || !signature[0])
+ signature = NULL;
+
+ if (!(params = get_variant_for_text_buffer (priv->buffer_params,
+ (const GVariantType *)signature,
+ &error)))
+ {
+ gtk_text_buffer_set_text (priv->buffer_reply, error->message, -1);
+ return;
+ }
+
+ dspy_method_invocation_set_parameters (priv->invocation, params);
+
+ priv->busy = TRUE;
+ priv->cancellable = g_cancellable_new ();
+
+ gtk_text_buffer_set_text (priv->buffer_reply, "", -1);
+
+ state = g_slice_new0 (Execute);
+ state->self = g_object_ref (self);
+ state->timer = g_timer_new ();
+
+ dspy_method_invocation_execute_async (priv->invocation,
+ priv->cancellable,
+ dspy_method_view_execute_cb,
+ state);
+
+ gtk_button_set_label (priv->button, _("Cancel"));
+}
+
+static void
+dspy_method_view_invoke_method (GtkWidget *widget,
+ gpointer user_data)
+{
+ DspyMethodView *self = user_data;
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+ g_assert (DSPY_IS_METHOD_VIEW (self));
+
+ gtk_widget_activate (GTK_WIDGET (priv->button));
+}
+
+static void
+copy_button_clicked_cb (DspyMethodView *self,
+ GtkButton *button)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+ g_autofree gchar *text = NULL;
+ GtkClipboard *clipboard;
+ GtkTextIter begin;
+ GtkTextIter end;
+
+ g_assert (DSPY_IS_METHOD_VIEW (self));
+ g_assert (GTK_IS_BUTTON (button));
+
+ if (!gtk_text_buffer_get_selection_bounds (priv->buffer_reply, &begin, &end))
+ gtk_text_buffer_get_bounds (priv->buffer_reply, &begin, &end);
+
+ text = gtk_text_iter_get_slice (&begin, &end);
+ clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self), GDK_SELECTION_CLIPBOARD);
+ gtk_clipboard_set_text (clipboard, text, -1);
+}
+
+static void
+dspy_method_view_finalize (GObject *object)
+{
+ DspyMethodView *self = (DspyMethodView *)object;
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+ dzl_binding_group_set_source (priv->bindings, NULL);
+
+ g_clear_object (&priv->invocation);
+ g_clear_object (&priv->bindings);
+ g_clear_object (&priv->cancellable);
+ g_clear_pointer (&priv->durations, g_array_unref);
+
+ G_OBJECT_CLASS (dspy_method_view_parent_class)->finalize (object);
+}
+
+static void
+dspy_method_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyMethodView *self = DSPY_METHOD_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_INVOCATION:
+ g_value_set_object (value, dspy_method_view_get_invocation (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_method_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyMethodView *self = DSPY_METHOD_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_INVOCATION:
+ dspy_method_view_set_invocation (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_method_view_class_init (DspyMethodViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = dspy_method_view_finalize;
+ object_class->get_property = dspy_method_view_get_property;
+ object_class->set_property = dspy_method_view_set_property;
+
+ properties [PROP_INVOCATION] =
+ g_param_spec_object ("invocation",
+ "Invocation",
+ "The method invocation to view",
+ DSPY_TYPE_METHOD_INVOCATION,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-method-view.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, buffer_params);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, buffer_reply);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, button);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, copy_button);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_avg);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_interface);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_max);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_method);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_min);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, label_object_path);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyMethodView, textview_params);
+}
+
+static void
+dspy_method_view_init (DspyMethodView *self)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+ DzlShortcutController *controller;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ priv->durations = g_array_new (FALSE, FALSE, sizeof (gdouble));
+
+ priv->bindings = dzl_binding_group_new ();
+ dzl_binding_group_bind (priv->bindings, "interface", priv->label_interface, "label", 0);
+ dzl_binding_group_bind (priv->bindings, "method", priv->label_method, "label", 0);
+ dzl_binding_group_bind (priv->bindings, "object-path", priv->label_object_path, "label", 0);
+ dzl_binding_group_bind_full (priv->bindings, "parameters", priv->buffer_params, "text", 0,
+ variant_to_string_transform, NULL, NULL, NULL);
+
+ g_signal_connect_object (priv->button,
+ "clicked",
+ G_CALLBACK (dspy_method_view_button_clicked_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->copy_button,
+ "clicked",
+ G_CALLBACK (copy_button_clicked_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ controller = dzl_shortcut_controller_find (GTK_WIDGET (priv->textview_params));
+
+ dzl_shortcut_controller_add_command_callback (controller,
+ "org.gnome.dspy.invoke-method",
+ "<Primary>Return",
+ DZL_SHORTCUT_PHASE_DISPATCH,
+ dspy_method_view_invoke_method,
+ self,
+ NULL);
+}
+
+void
+dspy_method_view_set_invocation (DspyMethodView *self,
+ DspyMethodInvocation *invocation)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+ g_return_if_fail (DSPY_IS_METHOD_VIEW (self));
+ g_return_if_fail (!invocation || DSPY_IS_METHOD_INVOCATION (invocation));
+
+ if (g_set_object (&priv->invocation, invocation))
+ {
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+
+ dzl_binding_group_set_source (priv->bindings, invocation);
+ gtk_text_buffer_set_text (priv->buffer_reply, "", -1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INVOCATION]);
+ }
+}
+
+/**
+ * dspy_method_view_get_invocation:
+ *
+ * Returns: (transfer none) (nullable): a #DspyMethodInvocation or %NULL
+ */
+DspyMethodInvocation *
+dspy_method_view_get_invocation (DspyMethodView *self)
+{
+ DspyMethodViewPrivate *priv = dspy_method_view_get_instance_private (self);
+
+ g_return_val_if_fail (DSPY_IS_METHOD_VIEW (self), NULL);
+
+ return priv->invocation;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.h b/src/plugins/dspy/libdspy/dspy-method-view.h
new file mode 100644
index 000000000..8729b448c
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.h
@@ -0,0 +1,46 @@
+/* dspy-method-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+#include "dspy-method-invocation.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_METHOD_VIEW (dspy_method_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyMethodView, dspy_method_view, DSPY, METHOD_VIEW, DzlBin)
+
+struct _DspyMethodViewClass
+{
+ DzlBinClass parent_class;
+
+ /*< private >*/
+ gpointer _reserved[8];
+};
+
+GtkWidget *dspy_method_view_new (void);
+DspyMethodInvocation *dspy_method_view_get_invocation (DspyMethodView *self);
+void dspy_method_view_set_invocation (DspyMethodView *self,
+ DspyMethodInvocation *invocation);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-method-view.ui b/src/plugins/dspy/libdspy/dspy-method-view.ui
new file mode 100644
index 000000000..d5272ce51
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-method-view.ui
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.22"/>
+ <template class="DspyMethodView" parent="DzlBin">
+ <child>
+ <object class="GtkGrid">
+ <property name="column-spacing">12</property>
+ <property name="row-spacing">3</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Object Path</property>
+ <property name="visible">true</property>
+ <property name="xalign">1.0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_object_path">
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0.0</property>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Interface</property>
+ <property name="visible">true</property>
+ <property name="xalign">1.0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">1</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_interface">
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0.0</property>
+ </object>
+ <packing>
+ <property name="top-attach">1</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Method</property>
+ <property name="visible">true</property>
+ <property name="xalign">1.0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">2</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_method">
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0.0</property>
+ </object>
+ <packing>
+ <property name="top-attach">2</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Parameters</property>
+ <property name="visible">true</property>
+ <property name="xalign">1.0</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">3</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hexpand">true</property>
+ <property name="shadow-type">in</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">true</property>
+ <property name="max-content-height">100</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkTextView" id="textview_params">
+ <property name="buffer">buffer_params</property>
+ <property name="wrap-mode">char</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="button">
+ <property name="label" translatable="yes">Execute</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="top-attach">3</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Result</property>
+ <property name="visible">true</property>
+ <property name="xalign">1.0</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">4</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hexpand">true</property>
+ <property name="shadow-type">in</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">true</property>
+ <property name="max-content-height">100</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkTextView">
+ <property name="buffer">buffer_reply</property>
+ <property name="wrap-mode">char</property>
+ <property name="editable">false</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="copy_button">
+ <property name="label" translatable="yes">Copy</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="top-attach">4</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Elapsed Time</property>
+ <property name="halign">end</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">5</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="homogeneous">true</property>
+ <property name="spacing">6</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Ø:</property>
+ <property name="halign">end</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_avg">
+ <property name="halign">start</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Min:</property>
+ <property name="halign">end</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_min">
+ <property name="halign">start</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">Max:</property>
+ <property name="halign">end</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_max">
+ <property name="halign">start</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="top-attach">5</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkTextBuffer" id="buffer_params"/>
+ <object class="GtkTextBuffer" id="buffer_reply"/>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="button"/>
+ <widget name="copy_button"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.c b/src/plugins/dspy/libdspy/dspy-name-marquee.c
new file mode 100644
index 000000000..324877268
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.c
@@ -0,0 +1,186 @@
+/* dspy-name-marquee.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name-marquee"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "dspy-name-marquee.h"
+
+struct _DspyNameMarquee
+{
+ GtkBin parent_instance;
+
+ DspyName *name;
+ DzlBindingGroup *name_bindings;
+
+ GtkLabel *label_bus;
+ GtkLabel *label_name;
+ GtkLabel *label_owner;
+ GtkLabel *label_pid;
+};
+
+G_DEFINE_TYPE (DspyNameMarquee, dspy_name_marquee, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_name_marquee_new:
+ *
+ * Create a new #DspyNameMarquee.
+ *
+ * Returns: (transfer full): a newly created #DspyNameMarquee
+ */
+GtkWidget *
+dspy_name_marquee_new (void)
+{
+ return g_object_new (DSPY_TYPE_NAME_MARQUEE, NULL);
+}
+
+static void
+dspy_name_marquee_finalize (GObject *object)
+{
+ DspyNameMarquee *self = (DspyNameMarquee *)object;
+
+ dzl_binding_group_set_source (self->name_bindings, NULL);
+ g_clear_object (&self->name_bindings);
+ g_clear_object (&self->name);
+
+ G_OBJECT_CLASS (dspy_name_marquee_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_marquee_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNameMarquee *self = DSPY_NAME_MARQUEE (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_object (value, dspy_name_marquee_get_name (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_marquee_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNameMarquee *self = DSPY_NAME_MARQUEE (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ dspy_name_marquee_set_name (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_marquee_class_init (DspyNameMarqueeClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = dspy_name_marquee_finalize;
+ object_class->get_property = dspy_name_marquee_get_property;
+ object_class->set_property = dspy_name_marquee_set_property;
+
+ properties [PROP_NAME] =
+ g_param_spec_object ("name",
+ "Name",
+ "The DspyName to display on the marquee",
+ DSPY_TYPE_NAME,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-name-marquee.ui");
+ gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_bus);
+ gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_name);
+ gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_owner);
+ gtk_widget_class_bind_template_child (widget_class, DspyNameMarquee, label_pid);
+}
+
+static void
+dspy_name_marquee_init (DspyNameMarquee *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->name_bindings = dzl_binding_group_new ();
+
+ dzl_binding_group_bind (self->name_bindings, "pid", self->label_pid, "label", 0);
+ dzl_binding_group_bind (self->name_bindings, "name", self->label_name, "label", 0);
+ dzl_binding_group_bind (self->name_bindings, "owner", self->label_owner, "label", 0);
+}
+
+/**
+ * dspy_name_marquee_get_name:
+ *
+ * Gets the name on the marquee
+ *
+ * Returns: (nullable) (transfer none): a #DspyName or %NULL
+ */
+DspyName *
+dspy_name_marquee_get_name (DspyNameMarquee *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME_MARQUEE (self), NULL);
+
+ return self->name;
+}
+
+void
+dspy_name_marquee_set_name (DspyNameMarquee *self,
+ DspyName *name)
+{
+ g_return_if_fail (DSPY_IS_NAME_MARQUEE (self));
+ g_return_if_fail (!name || DSPY_IS_NAME (name));
+
+ if (g_set_object (&self->name, name))
+ {
+ const gchar *address = NULL;
+
+ if (name != NULL)
+ address = dspy_connection_get_address (dspy_name_get_connection (name));
+
+ dzl_binding_group_set_source (self->name_bindings, name);
+ gtk_label_set_label (self->label_bus, address);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+ }
+}
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.h b/src/plugins/dspy/libdspy/dspy-name-marquee.h
new file mode 100644
index 000000000..14cfa859b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.h
@@ -0,0 +1,38 @@
+/* dspy-name-marquee.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME_MARQUEE (dspy_name_marquee_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNameMarquee, dspy_name_marquee, DSPY, NAME_MARQUEE, GtkBin)
+
+GtkWidget *dspy_name_marquee_new (void);
+DspyName *dspy_name_marquee_get_name (DspyNameMarquee *self);
+void dspy_name_marquee_set_name (DspyNameMarquee *self,
+ DspyName *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-name-marquee.ui b/src/plugins/dspy/libdspy/dspy-name-marquee.ui
new file mode 100644
index 000000000..0c2087d12
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-marquee.ui
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="DspyNameMarquee" parent="GtkBin">
+ <child>
+ <object class="GtkGrid">
+ <property name="margin-start">12</property>
+ <property name="column-spacing">12</property>
+ <property name="row-spacing">3</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Bus Address</property>
+ <property name="xalign">1.0</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_bus">
+ <property name="xalign">0.0</property>
+ <property name="selectable">true</property>
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Name</property>
+ <property name="xalign">1.0</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">1</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_name">
+ <property name="xalign">0.0</property>
+ <property name="selectable">true</property>
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="top-attach">1</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Owner</property>
+ <property name="xalign">1.0</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">2</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_owner">
+ <property name="xalign">0.0</property>
+ <property name="selectable">true</property>
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="top-attach">2</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Process ID</property>
+ <property name="xalign">1.0</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">3</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label_pid">
+ <property name="xalign">0.0</property>
+ <property name="selectable">true</property>
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="top-attach">3</property>
+ <property name="left-attach">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name-row.c b/src/plugins/dspy/libdspy/dspy-name-row.c
new file mode 100644
index 000000000..987e9ec0b
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.c
@@ -0,0 +1,214 @@
+/* dspy-name-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name-row"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-name-row.h"
+
+struct _DspyNameRow
+{
+ GtkListBoxRow parent_instance;
+
+ DspyName *name;
+
+ GtkLabel *title;
+ GtkLabel *subtitle;
+};
+
+G_DEFINE_TYPE (DspyNameRow, dspy_name_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * dspy_name_row_new:
+ * @name: a #DspyName
+ *
+ * Create a new #DspyNameRow.
+ *
+ * Returns: (transfer full): a newly created #DspyNameRow
+ */
+GtkWidget *
+dspy_name_row_new (DspyName *name)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (name), NULL);
+
+ return g_object_new (DSPY_TYPE_NAME_ROW,
+ "name", name,
+ "visible", TRUE,
+ NULL);
+}
+
+static void
+dspy_name_row_update (DspyNameRow *self)
+{
+ g_autoptr(GString) str = NULL;
+ GPid pid;
+
+ g_assert (DSPY_IS_NAME_ROW (self));
+
+ pid = dspy_name_get_pid (self->name);
+ str = g_string_new (NULL);
+
+ if (dspy_name_get_activatable (self->name))
+ g_string_append_printf (str, _("%s: %s"), _("Activatable"), _("Yes"));
+ else
+ g_string_append_printf (str, _("%s: %s"), _("Activatable"), _("No"));
+
+ if (pid > -1)
+ {
+ g_string_append (str, ", ");
+ g_string_append_printf (str, _("%s: %u"), _("Pid"), pid);
+ }
+
+ gtk_label_set_label (self->subtitle, str->str);
+
+ gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+ dspy_name_get_owner (self->name));
+}
+
+static void
+dspy_name_row_set_name (DspyNameRow *self,
+ DspyName *name)
+{
+ g_assert (DSPY_IS_NAME_ROW (self));
+ g_assert (DSPY_IS_NAME (name));
+ g_assert (self->name == NULL);
+
+ g_set_object (&self->name, name);
+
+ g_signal_connect_object (self->name,
+ "notify::pid",
+ G_CALLBACK (dspy_name_row_update),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (self->name,
+ "notify::activatable",
+ G_CALLBACK (dspy_name_row_update),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gtk_label_set_label (self->title, dspy_name_get_name (self->name));
+
+ dspy_name_row_update (self);
+}
+
+static void
+dspy_name_row_finalize (GObject *object)
+{
+ DspyNameRow *self = (DspyNameRow *)object;
+
+ g_clear_object (&self->name);
+
+ G_OBJECT_CLASS (dspy_name_row_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNameRow *self = DSPY_NAME_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_object (value, dspy_name_row_get_name (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNameRow *self = DSPY_NAME_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ dspy_name_row_set_name (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_row_class_init (DspyNameRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = dspy_name_row_finalize;
+ object_class->get_property = dspy_name_row_get_property;
+ object_class->set_property = dspy_name_row_set_property;
+
+ properties [PROP_NAME] =
+ g_param_spec_object ("name",
+ "Name",
+ "The DspyName for the row",
+ DSPY_TYPE_NAME,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-name-row.ui");
+ gtk_widget_class_bind_template_child (widget_class, DspyNameRow, subtitle);
+ gtk_widget_class_bind_template_child (widget_class, DspyNameRow, title);
+}
+
+static void
+dspy_name_row_init (DspyNameRow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * dspy_name_row_get_name:
+ *
+ * Gets the #DspyName for the row.
+ *
+ * Returns: (transfer none): a #DspyName
+ */
+DspyName *
+dspy_name_row_get_name (DspyNameRow *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME_ROW (self), NULL);
+
+ return self->name;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-name-row.h b/src/plugins/dspy/libdspy/dspy-name-row.h
new file mode 100644
index 000000000..8b48f6ee4
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.h
@@ -0,0 +1,36 @@
+/* dspy-name-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME_ROW (dspy_name_row_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNameRow, dspy_name_row, DSPY, NAME_ROW, GtkListBoxRow)
+
+GtkWidget *dspy_name_row_new (DspyName *name);
+DspyName *dspy_name_row_get_name (DspyNameRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-name-row.ui b/src/plugins/dspy/libdspy/dspy-name-row.ui
new file mode 100644
index 000000000..eaa869d93
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name-row.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="DspyNameRow" parent="GtkListBoxRow">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">6</property>
+ <property name="margin_right">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="scale" value="0.83330000000000004"/>
+ </attributes>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy-name.c b/src/plugins/dspy/libdspy/dspy-name.c
new file mode 100644
index 000000000..2b0489dc6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name.c
@@ -0,0 +1,486 @@
+/* dspy-name.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-name"
+
+#include "config.h"
+
+#include "dspy-introspection-model.h"
+#include "dspy-name.h"
+#include "dspy-private.h"
+
+struct _DspyName
+{
+ GObject parent_instance;
+ DspyConnection *connection;
+ gchar *name;
+ gchar *owner;
+ gchar *search_text;
+ GPid pid;
+ guint activatable : 1;
+};
+
+enum {
+ PROP_0,
+ PROP_ACTIVATABLE,
+ PROP_CONNECTION,
+ PROP_NAME,
+ PROP_OWNER,
+ PROP_PID,
+ N_PROPS
+};
+
+G_DEFINE_TYPE (DspyName, dspy_name, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+dspy_name_finalize (GObject *object)
+{
+ DspyName *self = (DspyName *)object;
+
+ g_clear_object (&self->connection);
+ g_clear_pointer (&self->name, g_free);
+ g_clear_pointer (&self->owner, g_free);
+ g_clear_pointer (&self->search_text, g_free);
+
+ G_OBJECT_CLASS (dspy_name_parent_class)->finalize (object);
+}
+
+static void
+dspy_name_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyName *self = DSPY_NAME (object);
+
+ switch (prop_id)
+ {
+ case PROP_ACTIVATABLE:
+ g_value_set_boolean (value, dspy_name_get_activatable (self));
+ break;
+
+ case PROP_CONNECTION:
+ g_value_set_object (value, dspy_name_get_connection (self));
+ break;
+
+ case PROP_NAME:
+ g_value_set_string (value, dspy_name_get_name (self));
+ break;
+
+ case PROP_OWNER:
+ g_value_set_string (value, dspy_name_get_owner (self));
+ break;
+
+ case PROP_PID:
+ g_value_set_int (value, dspy_name_get_pid (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyName *self = DSPY_NAME (object);
+
+ switch (prop_id)
+ {
+ case PROP_ACTIVATABLE:
+ self->activatable = g_value_get_boolean (value);
+ break;
+
+ case PROP_CONNECTION:
+ self->connection = g_value_dup_object (value);
+ break;
+
+ case PROP_NAME:
+ self->name = g_value_dup_string (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_name_class_init (DspyNameClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = dspy_name_finalize;
+ object_class->get_property = dspy_name_get_property;
+ object_class->set_property = dspy_name_set_property;
+
+ properties [PROP_ACTIVATABLE] =
+ g_param_spec_boolean ("activatable",
+ "Activatable",
+ "Activatable",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_CONNECTION] =
+ g_param_spec_object ("connection",
+ "Connection",
+ "The connection where the name can be found",
+ DSPY_TYPE_CONNECTION,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_NAME] =
+ g_param_spec_string ("name",
+ "Name",
+ "The peer name",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_OWNER] =
+ g_param_spec_string ("owner",
+ "Owner",
+ "The owner of the DBus name",
+ NULL,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_PID] =
+ g_param_spec_int ("pid",
+ "Pid",
+ "The pid of the peer",
+ -1, G_MAXINT, -1,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_name_init (DspyName *self)
+{
+ self->pid = -1;
+}
+
+DspyName *
+dspy_name_new (DspyConnection *connection,
+ const gchar *name,
+ gboolean activatable)
+{
+ return g_object_new (DSPY_TYPE_NAME,
+ "activatable", activatable,
+ "connection", connection,
+ "name", name,
+ NULL);
+}
+
+gboolean
+dspy_name_get_activatable (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), FALSE);
+
+ return self->activatable;
+}
+
+void
+_dspy_name_set_activatable (DspyName *self,
+ gboolean activatable)
+{
+ g_return_if_fail (DSPY_IS_NAME (self));
+
+ activatable = !!activatable;
+
+ if (self->activatable != activatable)
+ {
+ self->activatable = activatable;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVATABLE]);
+ }
+}
+
+const gchar *
+dspy_name_get_name (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+ return self->name;
+}
+
+gint
+dspy_name_compare (gconstpointer a,
+ gconstpointer b)
+{
+ DspyName *item1 = DSPY_NAME ((gpointer)a);
+ DspyName *item2 = DSPY_NAME ((gpointer)b);
+ const gchar *name1 = dspy_name_get_name (item1);
+ const gchar *name2 = dspy_name_get_name (item2);
+
+ if (name1[0] != name2[0])
+ {
+ if (name1[0] == ':')
+ return 1;
+ if (name2[0] == ':')
+ return -1;
+ }
+
+ /* Sort numbers like :1.300 better */
+ if (g_str_has_prefix (name1, ":1.") &&
+ g_str_has_prefix (name2, ":1."))
+ {
+ gint i1 = g_ascii_strtoll (name1 + 3, NULL, 10);
+ gint i2 = g_ascii_strtoll (name2 + 3, NULL, 10);
+
+ return i1 - i2;
+ }
+
+ return g_strcmp0 (name1, name2);
+}
+
+GPid
+dspy_name_get_pid (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), 0);
+
+ return self->pid;
+}
+
+const gchar *
+dspy_name_get_owner (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+ return self->owner ? self->owner : self->name;
+}
+
+void
+_dspy_name_set_owner (DspyName *self,
+ const gchar *owner)
+{
+ g_return_if_fail (DSPY_IS_NAME (self));
+
+ if (g_strcmp0 (owner, self->owner) != 0)
+ {
+ g_free (self->owner);
+ self->owner = g_strdup (owner);
+ g_clear_pointer (&self->search_text, g_free);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_OWNER]);
+ }
+}
+
+void
+_dspy_name_clear_pid (DspyName *self)
+{
+ g_return_if_fail (DSPY_IS_NAME (self));
+
+ if (self->pid != -1)
+ {
+ self->pid = -1;
+ g_clear_pointer (&self->search_text, g_free);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PID]);
+ }
+}
+
+static void
+dspy_name_get_pid_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection = (GDBusConnection *)object;
+ g_autoptr(DspyName) self = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) reply = NULL;
+ guint pid;
+
+ g_assert (G_IS_DBUS_CONNECTION (connection));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (DSPY_IS_NAME (self));
+
+ if (!(reply = g_dbus_connection_call_finish (connection, result, &error)))
+ return;
+
+ g_variant_get (reply, "(u)", &pid);
+
+ if (self->pid != pid)
+ {
+ self->pid = pid;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PID]);
+ }
+}
+
+void
+_dspy_name_refresh_pid (DspyName *self,
+ GDBusConnection *connection)
+{
+ g_return_if_fail (DSPY_IS_NAME (self));
+ g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+ g_dbus_connection_call (connection,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "GetConnectionUnixProcessID",
+ g_variant_new ("(s)", self->name),
+ G_VARIANT_TYPE ("(u)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ dspy_name_get_pid_cb,
+ g_object_ref (self));
+}
+
+static void
+dspy_name_get_owner_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection = (GDBusConnection *)object;
+ g_autoptr(DspyName) self = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) reply = NULL;
+ const gchar *owner = NULL;
+
+ g_assert (G_IS_DBUS_CONNECTION (connection));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (DSPY_IS_NAME (self));
+
+ if (!(reply = g_dbus_connection_call_finish (connection, result, &error)))
+ return;
+
+ g_variant_get (reply, "(&s)", &owner);
+ _dspy_name_set_owner (self, owner);
+}
+
+void
+_dspy_name_refresh_owner (DspyName *self,
+ GDBusConnection *connection)
+{
+ g_return_if_fail (DSPY_IS_NAME (self));
+ g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+ g_clear_pointer (&self->owner, g_free);
+
+ /* If the name is already a :0.123 style name, that's the owner */
+ if (self->name[0] == ':')
+ return;
+
+ g_dbus_connection_call (connection,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "GetNameOwner",
+ g_variant_new ("(s)", self->name),
+ G_VARIANT_TYPE ("(s)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ dspy_name_get_owner_cb,
+ g_object_ref (self));
+}
+
+/**
+ * dspy_name_get_connection:
+ *
+ * Gets the connection that is to be used.
+ *
+ * Returns: (transer none): a #DspyConnection or %NULL
+ */
+DspyConnection *
+dspy_name_get_connection (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+
+ return self->connection;
+}
+
+static void
+dspy_name_introspection_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncInitable *initable = (GAsyncInitable *)object;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GTask) task = user_data;
+
+ g_assert (G_IS_ASYNC_INITABLE (initable));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if (!g_async_initable_init_finish (initable, result, &error))
+ g_task_return_error (task, g_steal_pointer (&error));
+ else
+ g_task_return_pointer (task, g_object_ref (initable), g_object_unref);
+}
+
+void
+dspy_name_introspect_async (DspyName *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(DspyIntrospectionModel) model = NULL;
+
+ g_return_if_fail (DSPY_IS_NAME (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, dspy_name_introspect_async);
+
+ model = _dspy_introspection_model_new (self);
+
+ g_async_initable_init_async (G_ASYNC_INITABLE (model),
+ G_PRIORITY_DEFAULT,
+ cancellable,
+ dspy_name_introspection_cb,
+ g_steal_pointer (&task));
+
+}
+
+/**
+ * dspy_name_introspect_finish:
+ *
+ * Completes a request to dspy_name_introspect_async().
+ *
+ * Returns: (transfer full): a #GtkTreeModel if successful; otherwise
+ * %NULL and @error is set.
+ */
+GtkTreeModel *
+dspy_name_introspect_finish (DspyName *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), NULL);
+ g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+const gchar *
+dspy_name_get_search_text (DspyName *self)
+{
+ g_return_val_if_fail (DSPY_IS_NAME (self), FALSE);
+
+ if (self->search_text == NULL)
+ {
+ const gchar *owner = dspy_name_get_owner (self);
+ self->search_text = g_strdup_printf ("%s %s %d", self->name, owner, self->pid);
+ }
+
+ return self->search_text;
+}
diff --git a/src/plugins/dspy/libdspy/dspy-name.h b/src/plugins/dspy/libdspy/dspy-name.h
new file mode 100644
index 000000000..c62ad9f73
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-name.h
@@ -0,0 +1,53 @@
+/* dspy-name.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "dspy-connection.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAME (dspy_name_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyName, dspy_name, DSPY, NAME, GObject)
+
+DspyName *dspy_name_new (DspyConnection *connection,
+ const gchar *name,
+ gboolean activatable);
+DspyConnection *dspy_name_get_connection (DspyName *self);
+gboolean dspy_name_get_activatable (DspyName *self);
+GPid dspy_name_get_pid (DspyName *self);
+const gchar *dspy_name_get_name (DspyName *self);
+const gchar *dspy_name_get_owner (DspyName *self);
+const gchar *dspy_name_get_search_text (DspyName *self);
+gint dspy_name_compare (gconstpointer a,
+ gconstpointer b);
+void dspy_name_introspect_async (DspyName *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GtkTreeModel *dspy_name_introspect_finish (DspyName *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-names-model.c b/src/plugins/dspy/libdspy/dspy-names-model.c
new file mode 100644
index 000000000..d1ea5605a
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-names-model.c
@@ -0,0 +1,532 @@
+/* dspy-names-model.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-names-model"
+
+#include "config.h"
+
+#include "dspy-names-model.h"
+#include "dspy-private.h"
+
+struct _DspyNamesModel
+{
+ GObject parent_instance;
+ DspyConnection *connection;
+ GSequence *items;
+ GDBusConnection *bus;
+ guint name_owner_changed_handler;
+};
+
+enum {
+ PROP_0,
+ PROP_CONNECTION,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+_g_weak_ref_free (GWeakRef *wr)
+{
+ g_weak_ref_clear (wr);
+ g_slice_free (GWeakRef, wr);
+}
+
+static GType
+dspy_names_model_get_item_type (GListModel *model)
+{
+ return DSPY_TYPE_NAME;
+}
+
+static guint
+dspy_names_model_get_n_items (GListModel *model)
+{
+ return g_sequence_get_length (DSPY_NAMES_MODEL (model)->items);
+}
+
+static gpointer
+dspy_names_model_get_item (GListModel *model,
+ guint position)
+{
+ DspyNamesModel *self = DSPY_NAMES_MODEL (model);
+ GSequenceIter *iter;
+
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+
+ if ((iter = g_sequence_get_iter_at_pos (self->items, position)) &&
+ !g_sequence_iter_is_end (iter))
+ return g_object_ref (g_sequence_get (iter));
+
+ return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = dspy_names_model_get_item_type;
+ iface->get_n_items = dspy_names_model_get_n_items;
+ iface->get_item = dspy_names_model_get_item;
+}
+
+/**
+ * dspy_names_model_get_by_name:
+ * @self: a #DspyNamesModel
+ * @name: the name to lookup, such as ":1.0" or "org.freedesktop.DBus"
+ *
+ * Looks for a #DspyName that matches @name.
+ *
+ * Returns: (transfer full) (nullable): a #DspyName or %NULL
+ */
+DspyName *
+dspy_names_model_get_by_name (DspyNamesModel *self,
+ const gchar *name)
+{
+ g_autoptr(DspyName) tmp = NULL;
+ GSequenceIter *iter;
+
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+ g_assert (name != NULL);
+
+ tmp = dspy_name_new (self->connection, name, FALSE);
+ iter = g_sequence_lookup (self->items, tmp, (GCompareDataFunc) dspy_name_compare, NULL);
+
+ if (!iter || g_sequence_iter_is_end (iter))
+ return NULL;
+
+ return g_object_ref (g_sequence_get (iter));
+}
+
+static void
+dspy_names_model_add_names (DspyNamesModel *self,
+ GDBusConnection *bus,
+ const gchar * const *names,
+ gboolean is_activatable)
+{
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+ g_assert (names != NULL);
+
+ for (guint i = 0; names[i] != NULL; i++)
+ {
+ g_autoptr(DspyName) name = NULL;
+ GSequenceIter *iter;
+
+ if ((name = dspy_names_model_get_by_name (self, names[i])))
+ {
+ if (is_activatable && !dspy_name_get_activatable (name))
+ _dspy_name_set_activatable (name, TRUE);
+ continue;
+ }
+
+ name = dspy_name_new (self->connection, names[i], is_activatable);
+
+ _dspy_name_refresh_pid (name, bus);
+ _dspy_name_refresh_owner (name, bus);
+
+ iter = g_sequence_insert_sorted (self->items,
+ g_steal_pointer (&name),
+ (GCompareDataFunc) dspy_name_compare,
+ NULL);
+ g_list_model_items_changed (G_LIST_MODEL (self),
+ g_sequence_iter_get_position (iter),
+ 0, 1);
+ }
+}
+
+static void
+dspy_names_model_name_owner_changed_cb (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *params,
+ gpointer user_data)
+{
+ GWeakRef *wr = user_data;
+ g_autoptr(DspyNamesModel) self = NULL;
+ g_autoptr(DspyName) name = NULL;
+ GSequenceIter *seq;
+ const gchar *vname;
+ const gchar *vold_name;
+ const gchar *vnew_name;
+
+ g_assert (G_IS_DBUS_CONNECTION (connection));
+ g_assert (params != NULL);
+ g_assert (g_variant_is_of_type (params, G_VARIANT_TYPE ("(sss)")));
+ g_assert (wr != NULL);
+
+ if (!(self = g_weak_ref_get (wr)))
+ return;
+
+ g_variant_get (params, "(&s&s&s)", &vname, &vold_name, &vnew_name);
+
+ name = dspy_name_new (self->connection, vname, FALSE);
+ seq = g_sequence_lookup (self->items,
+ name,
+ (GCompareDataFunc) dspy_name_compare,
+ NULL);
+
+ if (seq == NULL)
+ {
+ if (vnew_name[0])
+ {
+ const gchar *names[] = { vname, NULL };
+ dspy_names_model_add_names (self, connection, names, FALSE);
+ }
+ }
+ else if (!vnew_name[0])
+ {
+ DspyName *item = g_sequence_get (seq);
+
+ if (dspy_name_get_activatable (item) &&
+ dspy_name_get_name (item)[0] != ':')
+ {
+ _dspy_name_clear_pid (item);
+ }
+ else
+ {
+ guint position = g_sequence_iter_get_position (seq);
+ g_sequence_remove (seq);
+ g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+ }
+ }
+ else
+ {
+ DspyName *item = g_sequence_get (seq);
+
+ if (vnew_name[0] == ':')
+ _dspy_name_set_owner (item, vnew_name);
+
+ _dspy_name_refresh_pid (item, connection);
+ }
+}
+
+static void
+dspy_names_model_init_list_activatable_names_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *bus = (GDBusConnection *)object;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) reply = NULL;
+ gint *n_active;
+
+ g_assert (G_IS_DBUS_CONNECTION (bus));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+ {
+ g_autofree const gchar **names = NULL;
+ DspyNamesModel *self;
+
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+
+ self = g_task_get_source_object (task);
+ g_variant_get (reply, "(^as)", &names);
+ dspy_names_model_add_names (self, bus, (const gchar * const *)names, TRUE);
+ }
+
+ n_active = g_task_get_task_data (task);
+ g_assert (n_active != NULL);
+ g_assert (*n_active > 0);
+
+ if (--(*n_active) == 0)
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+dspy_names_model_init_list_names_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *bus = (GDBusConnection *)object;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) reply = NULL;
+ gint *n_active;
+
+ g_assert (G_IS_DBUS_CONNECTION (bus));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+ {
+ g_autofree const gchar **names = NULL;
+ DspyNamesModel *self;
+
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+
+ self = g_task_get_source_object (task);
+ g_variant_get (reply, "(^as)", &names);
+ dspy_names_model_add_names (self, bus, (const gchar * const *)names, FALSE);
+ }
+
+ n_active = g_task_get_task_data (task);
+ g_assert (n_active != NULL);
+ g_assert (*n_active > 0);
+
+ if (--(*n_active) == 0)
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+dspy_names_model_init_open_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyConnection *connection = (DspyConnection *)object;
+ g_autoptr(GDBusConnection) bus = NULL;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ DspyNamesModel *self;
+ GWeakRef *wr;
+
+ g_assert (DSPY_IS_CONNECTION (connection));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (G_IS_TASK (task));
+
+ if (!(bus = dspy_connection_open_finish (connection, result, &error)))
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ self = g_task_get_source_object (task);
+
+ g_assert (self != NULL);
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+
+ self->bus = g_object_ref (bus);
+
+ /* Because g_dbus_connection_signal_subscribe() is not guaranteed to
+ * call the cleanup function synchronously when unsubscribed, we need to
+ * use a weak ref in allocated state to ensure that we do not have a
+ * reference cycle. Otherwise, calling unsubscribe() from our finalize
+ * handler could result in a use-after-free. And we can't use a full
+ * reference because we'd never dispose/finalize without external
+ * intervention.
+ */
+ wr = g_slice_new0 (GWeakRef);
+ g_weak_ref_init (wr, self);
+ self->name_owner_changed_handler =
+ g_dbus_connection_signal_subscribe (bus,
+ NULL,
+ "org.freedesktop.DBus",
+ "NameOwnerChanged",
+ NULL,
+ NULL,
+ 0,
+ dspy_names_model_name_owner_changed_cb,
+ g_steal_pointer (&wr),
+ (GDestroyNotify)_g_weak_ref_free);
+
+ g_dbus_connection_call (bus,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "ListActivatableNames",
+ g_variant_new ("()"),
+ G_VARIANT_TYPE ("(as)"),
+ G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+ G_MAXINT,
+ g_task_get_cancellable (task),
+ dspy_names_model_init_list_activatable_names_cb,
+ g_object_ref (task));
+
+ g_dbus_connection_call (bus,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "ListNames",
+ g_variant_new ("()"),
+ G_VARIANT_TYPE ("(as)"),
+ G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+ G_MAXINT,
+ g_task_get_cancellable (task),
+ dspy_names_model_init_list_names_cb,
+ g_object_ref (task));
+}
+
+static void
+dspy_names_model_init_async (GAsyncInitable *initable,
+ gint io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ DspyNamesModel *self = (DspyNamesModel *)initable;
+ g_autoptr(GTask) task = NULL;
+ gint n_active = 2;
+
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, io_priority);
+ g_task_set_source_tag (task, dspy_names_model_init_async);
+ g_task_set_task_data (task, g_memdup (&n_active, sizeof n_active), g_free);
+
+ if (self->connection == NULL)
+ g_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "No connection to introspect");
+ else
+ dspy_connection_open_async (self->connection,
+ cancellable,
+ dspy_names_model_init_open_cb,
+ g_steal_pointer (&task));
+}
+
+static gboolean
+dspy_names_model_init_finish (GAsyncInitable *initable,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (DSPY_IS_NAMES_MODEL (initable));
+ g_assert (G_IS_TASK (result));
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+ iface->init_async = dspy_names_model_init_async;
+ iface->init_finish = dspy_names_model_init_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (DspyNamesModel, dspy_names_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+ G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init))
+
+/**
+ * dspy_names_model_new:
+ * @connection: a #DspyConnection
+ *
+ * Create a new #DspyNamesModel.
+ *
+ * Returns: (transfer full): a newly created #DspyNamesModel
+ */
+DspyNamesModel *
+dspy_names_model_new (DspyConnection *connection)
+{
+ return g_object_new (DSPY_TYPE_NAMES_MODEL,
+ "connection", connection,
+ NULL);
+}
+
+static void
+dspy_names_model_dispose (GObject *object)
+{
+ DspyNamesModel *self = (DspyNamesModel *)object;
+
+ g_assert (DSPY_IS_NAMES_MODEL (self));
+ g_assert (self->name_owner_changed_handler == 0 || self->bus != NULL);
+
+ if (self->name_owner_changed_handler > 0)
+ {
+ guint handler_id = self->name_owner_changed_handler;
+ self->name_owner_changed_handler = 0;
+ g_dbus_connection_signal_unsubscribe (self->bus, handler_id);
+ }
+
+ g_clear_object (&self->bus);
+
+ G_OBJECT_CLASS (dspy_names_model_parent_class)->dispose (object);
+}
+
+static void
+dspy_names_model_finalize (GObject *object)
+{
+ DspyNamesModel *self = (DspyNamesModel *)object;
+
+ g_clear_pointer (&self->items, g_sequence_free);
+ g_clear_object (&self->connection);
+
+ G_OBJECT_CLASS (dspy_names_model_parent_class)->finalize (object);
+}
+
+static void
+dspy_names_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNamesModel *self = DSPY_NAMES_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONNECTION:
+ g_value_set_object (value, self->connection);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_names_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DspyNamesModel *self = DSPY_NAMES_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONNECTION:
+ self->connection = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+dspy_names_model_class_init (DspyNamesModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = dspy_names_model_dispose;
+ object_class->finalize = dspy_names_model_finalize;
+ object_class->get_property = dspy_names_model_get_property;
+ object_class->set_property = dspy_names_model_set_property;
+
+ properties [PROP_CONNECTION] =
+ g_param_spec_object ("connection",
+ "Connection",
+ "The connection to introspect",
+ DSPY_TYPE_CONNECTION,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+dspy_names_model_init (DspyNamesModel *self)
+{
+ self->items = g_sequence_new (g_object_unref);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-names-model.h b/src/plugins/dspy/libdspy/dspy-names-model.h
new file mode 100644
index 000000000..70af82207
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-names-model.h
@@ -0,0 +1,39 @@
+/* dspy-names-model.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "dspy-connection.h"
+#include "dspy-name.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_NAMES_MODEL (dspy_names_model_get_type())
+
+G_DECLARE_FINAL_TYPE (DspyNamesModel, dspy_names_model, DSPY, NAMES_MODEL, GObject)
+
+DspyNamesModel *dspy_names_model_new (DspyConnection *connection);
+DspyConnection *dspy_names_model_get_connection (DspyNamesModel *self);
+DspyName *dspy_names_model_get_by_name (DspyNamesModel *self,
+ const gchar *name);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-node.c b/src/plugins/dspy/libdspy/dspy-node.c
new file mode 100644
index 000000000..e8389b8e0
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-node.c
@@ -0,0 +1,598 @@
+/* dspy-node.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-node"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+
+#define LPAREN "<span fgalpha='30000'>(</span>"
+#define RPAREN "<span fgalpha='30000'>)</span>"
+#define ARROW "<span fgalpha='20000'>↦</span>"
+#define BOLD(s) "<span weight='bold'>" s "</span>"
+#define DIM(s) "<span fgalpha='40000'>" s "</span>"
+
+/*
+ * This file contains an alternate GDBusNodeInfo hierarchy that we can use
+ * for a couple benefits over GDBusNodeInfo.
+ *
+ * First, it provides parent pointers so that we can navigate the structure
+ * like a tree. This is very useful when used as a GtkTreeModel.
+ *
+ * Second, we can use a GStringChunk and reduce a lot of duplicate strings.
+ */
+
+static gpointer
+dspy_node_new (DspyNodeKind kind,
+ DspyNode *parent)
+{
+ DspyNode *node;
+
+ g_assert (kind > 0);
+ g_assert (kind < DSPY_NODE_KIND_LAST);
+
+ node = g_slice_new0 (DspyNode);
+ node->any.kind = kind;
+ node->any.parent = parent;
+ node->any.link.data = node;
+
+ g_assert (DSPY_IS_NODE (node));
+
+ return g_steal_pointer (&node);
+}
+
+static void
+push_tail (GQueue *queue,
+ gpointer node)
+{
+ DspyNodeAny *any = node;
+
+ g_assert (DSPY_IS_NODE (any));
+
+ g_queue_push_tail_link (queue, &any->link);
+}
+
+static void
+clear_full (GQueue *queue)
+{
+ g_queue_foreach (queue, (GFunc) _dspy_node_free, NULL);
+ queue->length = 0;
+ queue->head = NULL;
+ queue->tail = NULL;
+}
+
+static DspyArgInfo *
+_dspy_arg_info_new (DspyNode *parent,
+ GDBusArgInfo *info,
+ GStringChunk *chunks)
+{
+ DspyArgInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_ARG, parent);
+ ret->name = g_string_chunk_insert_const (chunks, info->name);
+ ret->signature = g_string_chunk_insert_const (chunks, info->signature);
+
+ return ret;
+}
+
+static DspyMethodInfo *
+_dspy_method_info_new (DspyNode *parent,
+ GDBusMethodInfo *info,
+ GStringChunk *chunks)
+{
+ DspyMethodInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_METHOD, parent);
+ ret->name = g_string_chunk_insert_const (chunks, info->name);
+
+ for (guint i = 0; info->in_args[i] != NULL; i++)
+ push_tail (&ret->in_args,
+ _dspy_arg_info_new ((DspyNode *)ret, info->in_args[i], chunks));
+
+ for (guint i = 0; info->out_args[i] != NULL; i++)
+ push_tail (&ret->out_args,
+ _dspy_arg_info_new ((DspyNode *)ret, info->out_args[i], chunks));
+
+ return ret;
+}
+
+static DspySignalInfo *
+_dspy_signal_info_new (DspyNode *parent,
+ GDBusSignalInfo *info,
+ GStringChunk *chunks)
+{
+ DspySignalInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_SIGNAL, parent);
+ ret->name = g_string_chunk_insert_const (chunks, info->name);
+
+ for (guint i = 0; info->args[i] != NULL; i++)
+ push_tail (&ret->args,
+ _dspy_arg_info_new ((DspyNode *)ret, info->args[i], chunks));
+
+ return ret;
+}
+
+static DspyPropertyInfo *
+_dspy_property_info_new (DspyNode *parent,
+ GDBusPropertyInfo *info,
+ GStringChunk *chunks)
+{
+ DspyPropertyInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_PROPERTY, parent);
+ ret->name = g_string_chunk_insert_const (chunks, info->name);
+ ret->signature = g_string_chunk_insert_const (chunks, info->signature);
+ ret->flags = info->flags;
+
+ return ret;
+}
+
+static DspyInterfaceInfo *
+_dspy_interface_info_new (DspyNode *parent,
+ GDBusInterfaceInfo *info,
+ GStringChunk *chunks)
+{
+ DspyInterfaceInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_INTERFACE, parent);
+ ret->name = g_string_chunk_insert_const (chunks, info->name);
+ ret->properties = dspy_node_new (DSPY_NODE_KIND_PROPERTIES, (DspyNode *)ret);
+ ret->signals = dspy_node_new (DSPY_NODE_KIND_SIGNALS, (DspyNode *)ret);
+ ret->methods = dspy_node_new (DSPY_NODE_KIND_METHODS, (DspyNode *)ret);
+
+ for (guint i = 0; info->signals[i] != NULL; i++)
+ push_tail (&ret->signals->signals,
+ _dspy_signal_info_new ((DspyNode *)ret->signals, info->signals[i], chunks));
+
+ for (guint i = 0; info->methods[i] != NULL; i++)
+ push_tail (&ret->methods->methods,
+ _dspy_method_info_new ((DspyNode *)ret->methods, info->methods[i], chunks));
+
+ for (guint i = 0; info->properties[i] != NULL; i++)
+ push_tail (&ret->properties->properties,
+ _dspy_property_info_new ((DspyNode *)ret->properties,
+ info->properties[i],
+ chunks));
+
+ return ret;
+}
+
+static DspyNodeInfo *
+_dspy_node_info_new (DspyNode *parent,
+ GDBusNodeInfo *info,
+ GStringChunk *chunks)
+{
+ DspyNodeInfo *ret;
+
+ g_assert (!parent || DSPY_IS_NODE (parent));
+ g_assert (info != NULL);
+ g_assert (chunks != NULL);
+
+ ret = dspy_node_new (DSPY_NODE_KIND_NODE, parent);
+ ret->interfaces = dspy_node_new (DSPY_NODE_KIND_INTERFACES, (DspyNode *)ret);
+ ret->path = info->path ? g_string_chunk_insert_const (chunks, info->path) : NULL;
+
+ for (guint i = 0; info->nodes[i] != NULL; i++)
+ push_tail (&ret->nodes,
+ _dspy_node_info_new ((DspyNode *)ret, info->nodes[i], chunks));
+
+ if (info->interfaces[0])
+ {
+ for (guint i = 0; info->interfaces[i] != NULL; i++)
+ push_tail (&ret->interfaces->interfaces,
+ _dspy_interface_info_new ((DspyNode *)ret->interfaces,
+ info->interfaces[i],
+ chunks));
+ }
+
+ return ret;
+}
+
+DspyNodeInfo *
+_dspy_node_parse (const gchar *xml,
+ GStringChunk *chunks,
+ GError **error)
+{
+ g_autoptr(GDBusNodeInfo) info = NULL;
+
+ g_assert (xml != NULL);
+ g_assert (chunks != NULL);
+
+ if ((info = g_dbus_node_info_new_for_xml (xml, error)))
+ return _dspy_node_info_new (NULL, info, chunks);
+
+ return NULL;
+}
+
+void
+_dspy_node_free (gpointer data)
+{
+ DspyNode *node = data;
+
+ g_assert (!node || DSPY_IS_NODE (node));
+
+ if (node == NULL)
+ return;
+
+ node->any.parent = NULL;
+
+ switch (node->any.kind)
+ {
+ case DSPY_NODE_KIND_ARG:
+ break;
+
+ case DSPY_NODE_KIND_NODE:
+ _dspy_node_free ((DspyNode *)node->node.interfaces);
+ clear_full (&node->node.nodes);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACE:
+ _dspy_node_free ((DspyNode *)node->interface.properties);
+ _dspy_node_free ((DspyNode *)node->interface.signals);
+ _dspy_node_free ((DspyNode *)node->interface.methods);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACES:
+ clear_full (&node->interfaces.interfaces);
+ break;
+
+ case DSPY_NODE_KIND_METHODS:
+ clear_full (&node->methods.methods);
+ break;
+
+ case DSPY_NODE_KIND_METHOD:
+ clear_full (&node->method.in_args);
+ clear_full (&node->method.out_args);
+ break;
+
+ case DSPY_NODE_KIND_PROPERTIES:
+ clear_full (&node->properties.properties);
+ break;
+
+ case DSPY_NODE_KIND_PROPERTY:
+ g_clear_pointer (&node->property.value, g_free);
+ break;
+
+ case DSPY_NODE_KIND_SIGNALS:
+ clear_full (&node->signals.signals);
+ break;
+
+ case DSPY_NODE_KIND_SIGNAL:
+ clear_full (&node->signal.args);
+ break;
+
+ case DSPY_NODE_KIND_LAST:
+ default:
+ g_assert_not_reached ();
+ }
+
+ node->any.kind = 0;
+ node->any.parent = NULL;
+ node->any.link.prev = NULL;
+ node->any.link.next = NULL;
+ node->any.link.data = NULL;
+
+ g_slice_free (DspyNode, node);
+}
+
+gint
+_dspy_node_info_compare (const DspyNodeInfo *a,
+ const DspyNodeInfo *b)
+{
+ return g_strcmp0 (a->path, b->path);
+}
+
+gint
+_dspy_interface_info_compare (const DspyInterfaceInfo *a,
+ const DspyInterfaceInfo *b)
+{
+ return g_strcmp0 (a->name, b->name);
+}
+
+DspyNodeInfo *
+_dspy_node_new_root (void)
+{
+ return dspy_node_new (DSPY_NODE_KIND_NODE, NULL);
+}
+
+void
+_dspy_node_walk (DspyNode *node,
+ GFunc func,
+ gpointer user_data)
+{
+ g_assert (DSPY_IS_NODE (node));
+ g_assert (func != NULL);
+
+ func (node, user_data);
+
+ switch (node->any.kind)
+ {
+ case DSPY_NODE_KIND_ARG:
+ break;
+
+ case DSPY_NODE_KIND_NODE:
+ if (node->node.interfaces != NULL)
+ _dspy_node_walk ((DspyNode *)node->node.interfaces, func, user_data);
+ for (const GList *iter = node->node.nodes.head; iter; iter = iter->next)
+ _dspy_node_walk (iter->data, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACE:
+ _dspy_node_walk ((DspyNode *)node->interface.properties, func, user_data);
+ _dspy_node_walk ((DspyNode *)node->interface.signals, func, user_data);
+ _dspy_node_walk ((DspyNode *)node->interface.methods, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_INTERFACES:
+ for (const GList *iter = node->interfaces.interfaces.head; iter; iter = iter->next)
+ _dspy_node_walk (iter->data, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_METHODS:
+ for (const GList *iter = node->methods.methods.head; iter; iter = iter->next)
+ _dspy_node_walk (iter->data, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_METHOD:
+ case DSPY_NODE_KIND_PROPERTY:
+ case DSPY_NODE_KIND_SIGNAL:
+ break;
+
+ case DSPY_NODE_KIND_PROPERTIES:
+ for (const GList *iter = node->properties.properties.head; iter; iter = iter->next)
+ _dspy_node_walk (iter->data, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_SIGNALS:
+ for (const GList *iter = node->signals.signals.head; iter; iter = iter->next)
+ _dspy_node_walk (iter->data, func, user_data);
+ break;
+
+ case DSPY_NODE_KIND_LAST:
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static gchar *
+_dspy_property_info_to_string (DspyPropertyInfo *info)
+{
+ g_autofree gchar *sig = NULL;
+ const gchar *rw;
+
+ g_assert (DSPY_IS_NODE (info));
+ g_assert (info->kind == DSPY_NODE_KIND_PROPERTY);
+
+ sig = _dspy_signature_humanize (info->signature);
+
+ if (info->flags == (G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE))
+ rw = _("read/write");
+ else if (info->flags & G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE)
+ rw = _("write-only");
+ else if (info->flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+ rw = _("read-only");
+ else
+ rw = "";
+
+ return g_strdup_printf ("%s "ARROW" "BOLD(DIM("%s"))" "LPAREN DIM("%s") RPAREN,
+ info->name, sig, rw);
+}
+
+static gboolean
+arg_name_is_generated (const gchar *str)
+{
+ if (str == NULL)
+ return TRUE;
+
+ if (g_str_has_prefix (str, "arg_"))
+ {
+ gchar *endptr = NULL;
+ gint64 val;
+
+ str += strlen ("arg_");
+ errno = 0;
+ val = g_ascii_strtoll (str, &endptr, 10);
+
+ if (val >= 0 && errno == 0 && *endptr == 0)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gchar *
+_dspy_method_info_to_string (DspyMethodInfo *info)
+{
+ GString *str;
+
+ g_assert (DSPY_IS_NODE (info));
+ g_assert (info->kind == DSPY_NODE_KIND_METHOD);
+
+ str = g_string_new (info->name);
+ g_string_append (str, " "LPAREN);
+
+ for (const GList *iter = info->in_args.head; iter; iter = iter->next)
+ {
+ DspyArgInfo *arg = iter->data;
+ g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+ if (iter->prev != NULL)
+ g_string_append (str, ", ");
+ g_string_append_printf (str, BOLD(DIM("%s")), sig);
+ if (!arg_name_is_generated (arg->name))
+ g_string_append_printf (str, DIM(" %s"), arg->name);
+ }
+
+ g_string_append (str, RPAREN" "ARROW" "LPAREN);
+
+ for (const GList *iter = info->out_args.head; iter; iter = iter->next)
+ {
+ DspyArgInfo *arg = iter->data;
+ g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+ if (iter->prev != NULL)
+ g_string_append (str, ", ");
+ g_string_append_printf (str, BOLD(DIM("%s")), sig);
+ if (!arg_name_is_generated (arg->name))
+ g_string_append_printf (str, DIM(" %s"), arg->name);
+ }
+
+ g_string_append (str, RPAREN);
+
+ return g_string_free (str, FALSE);
+}
+
+static gchar *
+_dspy_signal_info_to_string (DspySignalInfo *info)
+{
+ GString *str;
+
+ g_assert (DSPY_IS_NODE (info));
+ g_assert (info->kind == DSPY_NODE_KIND_SIGNAL);
+
+ str = g_string_new (info->name);
+ g_string_append (str, " "LPAREN);
+
+ for (const GList *iter = info->args.head; iter; iter = iter->next)
+ {
+ DspyArgInfo *arg = iter->data;
+ g_autofree gchar *sig = _dspy_signature_humanize (arg->signature);
+
+ if (iter->prev != NULL)
+ g_string_append (str, ", ");
+ g_string_append_printf (str, BOLD(DIM("%s")), sig);
+ if (!arg_name_is_generated (arg->name))
+ g_string_append_printf (str, DIM(" %s"), arg->name);
+ }
+
+ g_string_append (str, RPAREN);
+
+ return g_string_free (str, FALSE);
+}
+
+gchar *
+_dspy_node_get_text (DspyNode *node)
+{
+ switch (node->any.kind)
+ {
+ case DSPY_NODE_KIND_ARG:
+ return g_strdup (node->arg.name);
+
+ case DSPY_NODE_KIND_NODE:
+ return g_strdup (node->node.path);
+
+ case DSPY_NODE_KIND_INTERFACE:
+ return g_strdup (node->interface.name);
+
+ case DSPY_NODE_KIND_INTERFACES:
+ return g_strdup (_("Interfaces"));
+
+ case DSPY_NODE_KIND_METHODS:
+ return g_strdup (_("Methods"));
+
+ case DSPY_NODE_KIND_METHOD:
+ return _dspy_method_info_to_string (&node->method);
+
+ case DSPY_NODE_KIND_PROPERTIES:
+ return g_strdup (_("Properties"));
+
+ case DSPY_NODE_KIND_PROPERTY:
+ {
+ g_autofree gchar *str = _dspy_property_info_to_string (&node->property);
+
+ if (node->property.value != NULL)
+ {
+ g_autofree gchar *escaped = g_markup_escape_text (node->property.value, -1);
+ return g_strdup_printf ("%s = %s", str, escaped);
+ }
+
+ return g_steal_pointer (&str);
+ }
+
+ case DSPY_NODE_KIND_SIGNALS:
+ return g_strdup (_("Signals"));
+
+ case DSPY_NODE_KIND_SIGNAL:
+ return _dspy_signal_info_to_string (&node->signal);
+
+ case DSPY_NODE_KIND_LAST:
+ default:
+ g_return_val_if_reached (NULL);
+ }
+}
+
+gboolean
+_dspy_node_is_group (DspyNode *node)
+{
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+
+ return node->any.kind == DSPY_NODE_KIND_INTERFACES ||
+ node->any.kind == DSPY_NODE_KIND_PROPERTIES ||
+ node->any.kind == DSPY_NODE_KIND_SIGNALS ||
+ node->any.kind == DSPY_NODE_KIND_METHODS;
+}
+
+const gchar *
+_dspy_node_get_object_path (DspyNode *node)
+{
+ if (node == NULL)
+ return NULL;
+
+ if (node->any.kind == DSPY_NODE_KIND_NODE)
+ return node->node.path;
+
+ return _dspy_node_get_object_path (node->any.parent);
+}
+
+const gchar *
+_dspy_node_get_interface (DspyNode *node)
+{
+ if (node == NULL)
+ return NULL;
+
+ if (node->any.kind == DSPY_NODE_KIND_INTERFACE)
+ return node->interface.name;
+
+ return _dspy_node_get_interface (node->any.parent);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-private.h b/src/plugins/dspy/libdspy/dspy-private.h
new file mode 100644
index 000000000..0094a0ad6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-private.h
@@ -0,0 +1,202 @@
+/* dspy-private.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "dspy-name.h"
+#include "dspy-introspection-model.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ DSPY_NODE_KIND_NODE = 1,
+ DSPY_NODE_KIND_INTERFACES,
+ DSPY_NODE_KIND_INTERFACE,
+ DSPY_NODE_KIND_METHOD,
+ DSPY_NODE_KIND_METHODS,
+ DSPY_NODE_KIND_SIGNAL,
+ DSPY_NODE_KIND_SIGNALS,
+ DSPY_NODE_KIND_PROPERTY,
+ DSPY_NODE_KIND_PROPERTIES,
+ DSPY_NODE_KIND_ARG,
+ DSPY_NODE_KIND_LAST
+} DspyNodeKind;
+
+typedef union _DspyNode DspyNode;
+typedef struct _DspyArgInfo DspyArgInfo;
+typedef struct _DspyInterfaces DspyInterfaces;
+typedef struct _DspyInterfaceInfo DspyInterfaceInfo;
+typedef struct _DspyMethodInfo DspyMethodInfo;
+typedef struct _DspyMethods DspyMethods;
+typedef struct _DspyNodeAny DspyNodeAny;
+typedef struct _DspyNodeInfo DspyNodeInfo;
+typedef struct _DspyProperties DspyProperties;
+typedef struct _DspyPropertyInfo DspyPropertyInfo;
+typedef struct _DspySignalInfo DspySignalInfo;
+typedef struct _DspySignals DspySignals;
+
+struct _DspyNodeAny
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+};
+
+struct _DspyNodeInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *path;
+ GQueue nodes;
+ DspyInterfaces *interfaces;
+};
+
+struct _DspyInterfaceInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *name;
+ DspyProperties *properties;
+ DspySignals *signals;
+ DspyMethods *methods;
+};
+
+struct _DspyMethodInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *name;
+ GQueue in_args;
+ GQueue out_args;
+};
+
+struct _DspySignalInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *name;
+ const gchar *signature;
+ GQueue args;
+};
+
+struct _DspyPropertyInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *name;
+ const gchar *signature;
+ GDBusPropertyInfoFlags flags;
+ gchar *value;
+};
+
+struct _DspyArgInfo
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ const gchar *name;
+ const gchar *signature;
+};
+
+struct _DspyMethods
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ GQueue methods;
+};
+
+struct _DspySignals
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ GQueue signals;
+};
+
+struct _DspyProperties
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ GQueue properties;
+};
+
+struct _DspyInterfaces
+{
+ DspyNodeKind kind;
+ DspyNode *parent;
+ GList link;
+ GQueue interfaces;
+};
+
+union _DspyNode
+{
+ DspyNodeAny any;
+ DspyNodeInfo node;
+ DspyInterfaceInfo interface;
+ DspyInterfaces interfaces;
+ DspyMethodInfo method;
+ DspyMethods methods;
+ DspySignalInfo signal;
+ DspySignals signals;
+ DspyPropertyInfo property;
+ DspyProperties properties;
+ DspyArgInfo arg;
+};
+
+#define DSPY_IS_NODE(n) \
+ (((DspyNode*)n)->any.kind > 0 && ((DspyNode*)n)->any.kind < DSPY_NODE_KIND_LAST)
+
+DspyNodeInfo *_dspy_node_parse (const gchar *xml,
+ GStringChunk *chunks,
+ GError **error);
+void _dspy_node_free (gpointer data);
+void _dspy_node_walk (DspyNode *node,
+ GFunc func,
+ gpointer user_data);
+gchar *_dspy_node_get_text (DspyNode *node);
+DspyNodeInfo *_dspy_node_new_root (void);
+gboolean _dspy_node_is_group (DspyNode *node);
+gint _dspy_node_info_compare (const DspyNodeInfo *a,
+ const DspyNodeInfo *b);
+const gchar *_dspy_node_get_object_path (DspyNode *node);
+const gchar *_dspy_node_get_interface (DspyNode *node);
+gint _dspy_interface_info_compare (const DspyInterfaceInfo *a,
+ const DspyInterfaceInfo *b);
+void _dspy_name_clear_pid (DspyName *name);
+void _dspy_name_refresh_pid (DspyName *name,
+ GDBusConnection *connection);
+void _dspy_name_refresh_owner (DspyName *name,
+ GDBusConnection *connection);
+void _dspy_name_set_owner (DspyName *self,
+ const gchar *owner);
+void _dspy_name_set_activatable (DspyName *name,
+ gboolean is_activatable);
+DspyIntrospectionModel *_dspy_introspection_model_new (DspyName *name);
+gchar *_dspy_signature_humanize (const gchar *signature);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-signature.c b/src/plugins/dspy/libdspy/dspy-signature.c
new file mode 100644
index 000000000..174a1b998
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-signature.c
@@ -0,0 +1,82 @@
+/* dspy-signature.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-signature"
+
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+
+static GHashTable *
+get_common_signatures (void)
+{
+ static GHashTable *common;
+
+ if (g_once_init_enter (&common))
+ {
+ GHashTable *ht = g_hash_table_new (g_str_hash, g_str_equal);
+
+#define INSERT_COMMON(type,word) g_hash_table_insert(ht, (gchar *)type, (gchar *)word)
+ INSERT_COMMON ("n", "int16");
+ INSERT_COMMON ("q", "uint16");
+ INSERT_COMMON ("i", "int32");
+ INSERT_COMMON ("u", "uint32");
+ INSERT_COMMON ("x", "int64");
+ INSERT_COMMON ("t", "uint64");
+ INSERT_COMMON ("s", "string");
+ INSERT_COMMON ("b", "boolean");
+ INSERT_COMMON ("y", "byte");
+ INSERT_COMMON ("o", "Object Path");
+ INSERT_COMMON ("g", "Signature");
+ INSERT_COMMON ("d", "double");
+ INSERT_COMMON ("v", "Variant");
+ INSERT_COMMON ("h", "File Descriptor");
+ INSERT_COMMON ("as", "string[]");
+ INSERT_COMMON ("a{sv}", "Vardict");
+ INSERT_COMMON ("ay", "Byte Array");
+#undef INSERT_COMMON
+
+ g_once_init_leave (&common, ht);
+ }
+
+ return common;
+}
+
+gchar *
+_dspy_signature_humanize (const gchar *signature)
+{
+ GHashTable *common;
+ const gchar *found;
+
+ if (signature == NULL)
+ return NULL;
+
+ common = get_common_signatures ();
+
+ if ((found = g_hash_table_lookup (common, signature)))
+ return g_strdup (found);
+
+ /* If this is a simple array of something else ... */
+ if ((found = g_hash_table_lookup (common, signature + 1)))
+ /* translators: %s is replaced with the simple DBus type string */
+ return g_strdup_printf (_("Array of [%s]"), found);
+
+ return g_strdup (signature);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-tree-view.c b/src/plugins/dspy/libdspy/dspy-tree-view.c
new file mode 100644
index 000000000..5db96b566
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-tree-view.c
@@ -0,0 +1,311 @@
+/* dspy-tree-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-tree-view"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "dspy-private.h"
+#include "dspy-tree-view.h"
+
+G_DEFINE_TYPE (DspyTreeView, dspy_tree_view, GTK_TYPE_TREE_VIEW)
+
+typedef struct
+{
+ DspyTreeView *self;
+ GtkTreePath *path;
+} GetProperty;
+
+enum {
+ METHOD_ACTIVATED,
+ N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+get_property_free (GetProperty *state)
+{
+ g_clear_object (&state->self);
+ g_clear_pointer (&state->path, gtk_tree_path_free);
+ g_slice_free (GetProperty, state);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetProperty, get_property_free)
+
+GtkWidget *
+dspy_tree_view_new (void)
+{
+ return g_object_new (DSPY_TYPE_TREE_VIEW, NULL);
+}
+
+static void
+dspy_tree_view_selection_changed (DspyTreeView *self,
+ GtkTreeSelection *selection)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ g_assert (DSPY_IS_TREE_VIEW (self));
+ g_assert (GTK_IS_TREE_SELECTION (selection));
+
+ if (gtk_tree_selection_get_selected (selection, &model, &iter) &&
+ DSPY_IS_INTROSPECTION_MODEL (model))
+ {
+ DspyName *name = dspy_introspection_model_get_name (DSPY_INTROSPECTION_MODEL (model));
+ g_autoptr(DspyMethodInvocation) invocation = NULL;
+ DspyNode *node = iter.user_data;
+
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+
+ if (node->any.kind == DSPY_NODE_KIND_METHOD)
+ {
+ invocation = dspy_method_invocation_new ();
+ dspy_method_invocation_set_interface (invocation, _dspy_node_get_interface (node));
+ dspy_method_invocation_set_method (invocation, node->method.name);
+
+ if (node->method.in_args.length == 0)
+ dspy_method_invocation_set_parameters (invocation, g_variant_new ("()"));
+ }
+ else if (node->any.kind == DSPY_NODE_KIND_PROPERTY)
+ {
+ const gchar *iface = _dspy_node_get_interface (node);
+
+ invocation = dspy_method_invocation_new ();
+ dspy_method_invocation_set_interface (invocation, "org.freedesktop.DBus.Properties");
+ dspy_method_invocation_set_method (invocation, "Get");
+ dspy_method_invocation_set_signature (invocation, "(ss)");
+ dspy_method_invocation_set_reply_signature (invocation, "v");
+ dspy_method_invocation_set_parameters (invocation,
+ g_variant_new ("(ss)", iface, node->property.name));
+ }
+
+ if (invocation != NULL)
+ {
+ dspy_method_invocation_set_object_path (invocation, _dspy_node_get_object_path (node));
+ dspy_method_invocation_set_name (invocation, name);
+ g_signal_emit (self, signals [METHOD_ACTIVATED], 0, invocation);
+ }
+ }
+
+}
+
+static void
+dspy_tree_view_get_property_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *bus = (GDBusConnection *)object;
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GetProperty) state = user_data;
+
+ g_assert (G_IS_DBUS_CONNECTION (bus));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (state != NULL);
+
+ if ((reply = g_dbus_connection_call_finish (bus, result, &error)))
+ {
+ GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (state->self));
+ DspyNode *node;
+ GtkTreeIter iter;
+
+ if (gtk_tree_model_get_iter (model, &iter, state->path) &&
+ (node = iter.user_data) &&
+ DSPY_IS_NODE (node) &&
+ node->any.kind == DSPY_NODE_KIND_PROPERTY)
+ {
+ g_autoptr(GVariant) box = g_variant_get_child_value (reply, 0);
+ g_autoptr(GVariant) child = g_variant_get_child_value (box, 0);
+
+ g_clear_pointer (&node->property.value, g_free);
+
+ if (g_variant_is_of_type (child, G_VARIANT_TYPE_STRING) ||
+ g_variant_is_of_type (child, G_VARIANT_TYPE_OBJECT_PATH))
+ node->property.value = g_strdup (g_variant_get_string (child, NULL));
+ else if (g_variant_is_of_type (child, G_VARIANT_TYPE_BYTESTRING))
+ node->property.value = g_strdup (g_variant_get_bytestring (child));
+ else
+ node->property.value = g_variant_print (child, FALSE);
+
+ if (strlen (node->property.value) > 64)
+ {
+ g_autofree gchar *tmp = g_steal_pointer (&node->property.value);
+ tmp[64] = 0;
+ node->property.value = g_strdup_printf ("%s…", tmp);
+ }
+
+ gtk_tree_model_row_changed (model, state->path, &iter);
+ }
+ }
+ else
+ {
+ g_warning ("Failed to get property: %s", error->message);
+ }
+}
+
+static void
+dspy_tree_view_row_activated (GtkTreeView *view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ g_assert (DSPY_IS_TREE_VIEW (view));
+ g_assert (path != NULL);
+ g_assert (!column || GTK_IS_TREE_VIEW_COLUMN (column));
+
+ model = gtk_tree_view_get_model (view);
+
+ if (DSPY_IS_INTROSPECTION_MODEL (model) &&
+ gtk_tree_model_get_iter (model, &iter, path))
+ {
+ DspyName *name = dspy_introspection_model_get_name (DSPY_INTROSPECTION_MODEL (model));
+ DspyConnection *connection = dspy_name_get_connection (name);
+ GDBusConnection *bus = dspy_connection_get_connection (connection);
+ DspyNode *node = iter.user_data;
+
+ g_assert (!node || DSPY_IS_NODE (node));
+
+ if (node != NULL &&
+ node->any.kind == DSPY_NODE_KIND_PROPERTY &&
+ node->property.flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+ {
+ GetProperty *state;
+
+ state = g_slice_new0 (GetProperty);
+ state->path = gtk_tree_path_copy (path);
+ state->self = g_object_ref (DSPY_TREE_VIEW (view));
+
+ g_dbus_connection_call (bus,
+ dspy_name_get_owner (name),
+ _dspy_node_get_object_path (node),
+ "org.freedesktop.DBus.Properties",
+ "Get",
+ g_variant_new ("(ss)",
+ _dspy_node_get_interface (node),
+ node->property.name),
+ G_VARIANT_TYPE ("(v)"),
+ G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
+ -1,
+ NULL,
+ dspy_tree_view_get_property_cb,
+ state);
+ return;
+ }
+ }
+
+ if (gtk_tree_view_row_expanded (view, path))
+ gtk_tree_view_collapse_row (view, path);
+ else
+ gtk_tree_view_expand_row (view, path, FALSE);
+}
+
+static void
+dspy_tree_view_row_expanded (GtkTreeView *view,
+ GtkTreeIter *iter,
+ GtkTreePath *path)
+{
+ DspyNode *node = NULL;
+ GtkTreeModel *model;
+
+ g_assert (GTK_IS_TREE_VIEW (view));
+ g_assert (iter != NULL);
+ g_assert (path != NULL);
+
+ if (GTK_TREE_VIEW_CLASS (dspy_tree_view_parent_class)->row_expanded)
+ GTK_TREE_VIEW_CLASS (dspy_tree_view_parent_class)->row_expanded (view, iter, path);
+
+ if (!(model = gtk_tree_view_get_model (view)) ||
+ !DSPY_IS_INTROSPECTION_MODEL (model))
+ return;
+
+ node = iter->user_data;
+
+ g_assert (node != NULL);
+ g_assert (DSPY_IS_NODE (node));
+
+ /* Expand children too if there are fixed number of children */
+ if (node->any.kind == DSPY_NODE_KIND_NODE || /* path node */
+ node->any.kind == DSPY_NODE_KIND_INTERFACE) /* iface node */
+ {
+ GtkTreeIter child;
+
+ if (gtk_tree_model_iter_children (model, &child, iter))
+ {
+ g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (path);
+
+ gtk_tree_path_down (copy);
+
+ do
+ {
+ gtk_tree_view_expand_row (view, copy, FALSE);
+ gtk_tree_path_next (copy);
+ }
+ while (gtk_tree_model_iter_next (model, &child));
+ }
+ }
+}
+
+static void
+dspy_tree_view_class_init (DspyTreeViewClass *klass)
+{
+ GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+ tree_view_class->row_activated = dspy_tree_view_row_activated;
+ tree_view_class->row_expanded = dspy_tree_view_row_expanded;
+
+ signals [METHOD_ACTIVATED] =
+ g_signal_new ("method-activated",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (DspyTreeViewClass, method_activated),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE, 1, DSPY_TYPE_METHOD_INVOCATION);
+}
+
+static void
+dspy_tree_view_init (DspyTreeView *self)
+{
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *cell;
+
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self), TRUE);
+
+ column = gtk_tree_view_column_new ();
+ gtk_tree_view_column_set_title (column, _("Object Path"));
+ gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+ cell = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (column), cell, "markup", 0);
+
+ g_signal_connect_object (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)),
+ "changed",
+ G_CALLBACK (dspy_tree_view_selection_changed),
+ self,
+ G_CONNECT_SWAPPED);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-tree-view.h b/src/plugins/dspy/libdspy/dspy-tree-view.h
new file mode 100644
index 000000000..db4fc9a1d
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-tree-view.h
@@ -0,0 +1,46 @@
+/* dspy-tree-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "dspy-method-invocation.h"
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_TREE_VIEW (dspy_tree_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyTreeView, dspy_tree_view, DSPY, TREE_VIEW, GtkTreeView)
+
+struct _DspyTreeViewClass
+{
+ GtkTreeViewClass parent_class;
+
+ void (*method_activated) (DspyTreeView *self,
+ DspyMethodInvocation *invocation);
+
+ /*< private >*/
+ gpointer _reserved[8];
+};
+
+GtkWidget *dspy_tree_view_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-view.c b/src/plugins/dspy/libdspy/dspy-view.c
new file mode 100644
index 000000000..c420d7ec2
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.c
@@ -0,0 +1,610 @@
+/* dspy-view.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "dspy-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "dspy-connection-button.h"
+#include "dspy-name-marquee.h"
+#include "dspy-method-view.h"
+#include "dspy-name-row.h"
+#include "dspy-tree-view.h"
+#include "dspy-view.h"
+
+#include "libdspy-resources.h"
+
+typedef struct
+{
+ GCancellable *cancellable;
+ DzlListModelFilter *filter_model;
+ GListModel *model;
+
+ /* Template widgets */
+ GtkTreeView *introspection_tree_view;
+ GtkListBox *names_list_box;
+ GtkButton *refresh_button;
+ DspyNameMarquee *name_marquee;
+ GtkScrolledWindow *names_scroller;
+ DspyMethodView *method_view;
+ GtkRevealer *method_revealer;
+ DspyConnectionButton *session_button;
+ DspyConnectionButton *system_button;
+ GtkSearchEntry *search_entry;
+ GtkMenuButton *menu_button;
+ GtkBox *radio_buttons;
+ GtkStack *stack;
+
+ guint destroyed : 1;
+} DspyViewPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DspyView, dspy_view, GTK_TYPE_BIN)
+
+static void dspy_view_set_model (DspyView *self,
+ GListModel *model);
+
+/**
+ * dspy_view_new:
+ *
+ * Create a new #DspyView.
+ *
+ * This widget contains the window contents beneath the headerbar.
+ *
+ * Returns: (transfer full): a newly created #DspyView
+ */
+GtkWidget *
+dspy_view_new (void)
+{
+ return g_object_new (DSPY_TYPE_VIEW, NULL);
+}
+
+static void
+dspy_view_list_names_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyConnection *conn = (DspyConnection *)object;
+ g_autoptr(DspyView) self = user_data;
+ g_autoptr(GListModel) model = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (DSPY_IS_CONNECTION (conn));
+
+ if (!(model = dspy_connection_list_names_finish (conn, result, &error)))
+ g_warning ("Failed to list names: %s", error->message);
+
+ dspy_view_set_model (self, model);
+}
+
+static void
+radio_button_toggled_cb (DspyView *self,
+ DspyConnectionButton *button)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ DspyConnection *connection;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (DSPY_IS_CONNECTION_BUTTON (button));
+
+ if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
+ return;
+
+ gtk_stack_set_visible_child_name (priv->stack, "empty-state");
+
+ connection = dspy_connection_button_get_connection (button);
+ dspy_connection_list_names_async (connection,
+ NULL,
+ dspy_view_list_names_cb,
+ g_object_ref (self));
+}
+
+static void
+connect_address_changed_cb (DspyView *self,
+ DzlSimplePopover *popover)
+{
+ const gchar *text;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+ text = dzl_simple_popover_get_text (popover);
+ dzl_simple_popover_set_ready (popover, text && *text);
+}
+
+static void
+connection_got_error_cb (DspyView *self,
+ const GError *error,
+ DspyConnection *connection)
+{
+ const gchar *title;
+ GtkWidget *dialog;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (error != NULL);
+ g_assert (DSPY_IS_CONNECTION (connection));
+
+ if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED))
+ title = _("Access Denied by Peer");
+ else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_AUTH_FAILED))
+ title = _("Authentication Failed");
+ else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_TIMEOUT))
+ title = _("Operation Timed Out");
+ else if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_DISCONNECTED))
+ title = _("Lost Connection to Bus");
+ else
+ title = _("DBus Connection Failed");
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
+ GTK_MESSAGE_WARNING,
+ GTK_BUTTONS_CLOSE,
+ "%s", title);
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", error->message);
+ g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+connect_address_activate_cb (DspyView *self,
+ const gchar *text,
+ DzlSimplePopover *popover)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ g_autoptr(DspyConnection) connection = NULL;
+ DspyConnectionButton *button;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+ connection = dspy_connection_new_for_address (text);
+
+ button = g_object_new (DSPY_TYPE_CONNECTION_BUTTON,
+ "group", priv->session_button,
+ "connection", connection,
+ "visible", TRUE,
+ NULL);
+ g_signal_connect_object (button,
+ "toggled",
+ G_CALLBACK (radio_button_toggled_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (dspy_connection_button_get_connection (button),
+ "error",
+ G_CALLBACK (connection_got_error_cb),
+ self,
+ G_CONNECT_SWAPPED);
+ gtk_container_add (GTK_CONTAINER (priv->radio_buttons), GTK_WIDGET (button));
+
+ gtk_widget_activate (GTK_WIDGET (button));
+}
+
+static void
+clear_search (DspyView *self)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+ g_assert (DSPY_IS_VIEW (self));
+
+ if (priv->filter_model != NULL)
+ dzl_list_model_filter_set_filter_func (priv->filter_model, NULL, NULL, NULL);
+}
+
+static gboolean
+search_filter_func (DspyName *name,
+ DzlPatternSpec *spec)
+{
+ g_assert (DSPY_IS_NAME (name));
+ g_assert (spec != NULL);
+
+ return dzl_pattern_spec_match (spec, dspy_name_get_search_text (name));
+}
+
+static void
+apply_search (DspyView *self,
+ const gchar *text)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (text != NULL);
+ g_assert (text[0] != 0);
+
+ if (priv->filter_model != NULL)
+ dzl_list_model_filter_set_filter_func (priv->filter_model,
+ (DzlListModelFilterFunc) search_filter_func,
+ dzl_pattern_spec_new (text),
+ (GDestroyNotify) dzl_pattern_spec_unref);
+}
+
+static GtkWidget *
+create_name_row_cb (gpointer item,
+ gpointer user_data)
+{
+ DspyName *name = item;
+
+ g_assert (DSPY_IS_NAME (name));
+ g_assert (user_data == NULL);
+
+ return dspy_name_row_new (name);
+}
+
+static void
+dspy_view_set_model (DspyView *self,
+ GListModel *model)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ const gchar *text;
+ GtkAdjustment *adj;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (!model || G_IS_LIST_MODEL (model));
+
+ /* Asynchronous completion implies that we might get here after
+ * the widget has been destroyed.
+ */
+ if (priv->destroyed)
+ return;
+
+ gtk_list_box_bind_model (priv->names_list_box, NULL, NULL, NULL, NULL);
+
+ g_clear_object (&priv->filter_model);
+ g_clear_object (&priv->model);
+
+ if (model != NULL)
+ {
+ priv->model = g_object_ref (model);
+ priv->filter_model = dzl_list_model_filter_new (model);
+ }
+
+ text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
+
+ if (text && *text)
+ apply_search (self, text);
+ else
+ clear_search (self);
+
+ gtk_list_box_bind_model (priv->names_list_box,
+ G_LIST_MODEL (priv->filter_model),
+ create_name_row_cb,
+ NULL,
+ NULL);
+
+ adj = gtk_scrolled_window_get_vadjustment (priv->names_scroller);
+ gtk_adjustment_set_value (adj, 0.0);
+}
+
+static void
+dspy_view_introspect_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DspyName *name = (DspyName *)object;
+ g_autoptr(GtkTreeModel) model = NULL;
+ g_autoptr(DspyView) self = user_data;
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ g_autoptr(GError) error = NULL;
+
+ g_assert (DSPY_IS_NAME (name));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (DSPY_IS_VIEW (self));
+
+ if (!(model = dspy_name_introspect_finish (name, result, &error)))
+ {
+ DspyConnection *connection = dspy_name_get_connection (name);
+ dspy_connection_add_error (connection, error);
+ }
+
+ gtk_tree_view_set_model (priv->introspection_tree_view, model);
+}
+
+static void
+name_row_activated_cb (DspyView *self,
+ DspyNameRow *row,
+ GtkListBox *list_box)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ DspyName *name;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (DSPY_IS_NAME_ROW (row));
+ g_assert (GTK_IS_LIST_BOX (list_box));
+
+ name = dspy_name_row_get_name (row);
+
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+ priv->cancellable = g_cancellable_new ();
+
+ gtk_tree_view_set_model (priv->introspection_tree_view, NULL);
+ dspy_name_marquee_set_name (priv->name_marquee, name);
+
+ gtk_revealer_set_reveal_child (priv->method_revealer, FALSE);
+
+ dspy_name_introspect_async (name,
+ priv->cancellable,
+ dspy_view_introspect_cb,
+ g_object_ref (self));
+
+ gtk_stack_set_visible_child_name (priv->stack, "introspect");
+}
+
+static void
+refresh_button_clicked_cb (DspyView *self,
+ GtkButton *button)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ GtkListBoxRow *row;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (GTK_IS_BUTTON (button));
+
+ if ((row = gtk_list_box_get_selected_row (priv->names_list_box)))
+ name_row_activated_cb (self, DSPY_NAME_ROW (row), priv->names_list_box);
+}
+
+static void
+method_activated_cb (DspyView *self,
+ DspyMethodInvocation *invocation,
+ DspyTreeView *tree_view)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (!invocation || DSPY_IS_METHOD_INVOCATION (invocation));
+ g_assert (DSPY_IS_TREE_VIEW (tree_view));
+
+ if (DSPY_IS_METHOD_INVOCATION (invocation))
+ {
+ dspy_method_view_set_invocation (priv->method_view, invocation);
+ gtk_revealer_set_reveal_child (priv->method_revealer, TRUE);
+ }
+}
+
+static void
+notify_child_revealed_cb (DspyView *self,
+ GParamSpec *pspec,
+ GtkRevealer *revealer)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (GTK_IS_REVEALER (revealer));
+
+ if (!gtk_revealer_get_child_revealed (revealer))
+ {
+ dspy_method_view_set_invocation (priv->method_view, NULL);
+ }
+ else
+ {
+ GtkTreeSelection *selection;
+ GtkTreeModel *model = NULL;
+ GtkTreeIter iter;
+
+ selection = gtk_tree_view_get_selection (priv->introspection_tree_view);
+
+ if (gtk_tree_selection_get_selected (selection, &model, &iter))
+ {
+ g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (model, &iter);
+ GtkTreeViewColumn *column = gtk_tree_view_get_column (priv->introspection_tree_view, 0);
+
+ /* Move the selected row as far up as we can so that the revealer
+ * for the method invocation does not cover the selected area.
+ */
+ gtk_tree_view_scroll_to_cell (priv->introspection_tree_view,
+ path,
+ column,
+ TRUE,
+ 0.0,
+ 0.0);
+ }
+ }
+}
+
+static void
+search_entry_changed_cb (DspyView *self,
+ GtkSearchEntry *search_entry)
+{
+ const gchar *text;
+
+ g_assert (DSPY_IS_VIEW (self));
+ g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+ text = gtk_entry_get_text (GTK_ENTRY (search_entry));
+
+ if (text == NULL || *text == 0)
+ clear_search (self);
+ else
+ apply_search (self, text);
+}
+
+static void
+connect_to_bus_action (GSimpleAction *action,
+ GVariant *params,
+ gpointer user_data)
+{
+ DspyView *self = user_data;
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ GtkPopover *popover;
+
+ g_assert (G_IS_SIMPLE_ACTION (action));
+ g_assert (DSPY_IS_VIEW (self));
+
+ popover = g_object_new (DZL_TYPE_SIMPLE_POPOVER,
+ "button-text", _("Connect"),
+ "message", _("Provide the address of the message bus"),
+ "position", GTK_POS_RIGHT,
+ "title", _("Connect to Other Bus"),
+ "relative-to", priv->system_button,
+ NULL);
+
+ g_signal_connect_object (popover,
+ "changed",
+ G_CALLBACK (connect_address_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (popover,
+ "activate",
+ G_CALLBACK (connect_address_activate_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect (popover,
+ "closed",
+ G_CALLBACK (gtk_widget_destroy),
+ NULL);
+
+ gtk_popover_popup (popover);
+}
+
+static GActionEntry action_entries[] = {
+ { "connect-to-bus", connect_to_bus_action },
+};
+
+static void
+dspy_view_destroy (GtkWidget *widget)
+{
+ DspyView *self = (DspyView *)widget;
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+
+ priv->destroyed = TRUE;
+
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+ g_clear_object (&priv->filter_model);
+ g_clear_object (&priv->model);
+
+ GTK_WIDGET_CLASS (dspy_view_parent_class)->destroy (widget);
+}
+
+static void
+dspy_view_class_init (DspyViewClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ widget_class->destroy = dspy_view_destroy;
+
+ gtk_widget_class_set_css_name (widget_class, "dspyview");
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dspy/dspy-view.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, introspection_tree_view);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, menu_button);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_revealer);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, method_view);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, name_marquee);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_list_box);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, names_scroller);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, radio_buttons);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, refresh_button);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, search_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, session_button);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, stack);
+ gtk_widget_class_bind_template_child_private (widget_class, DspyView, system_button);
+
+ g_type_ensure (DSPY_TYPE_METHOD_VIEW);
+ g_type_ensure (DSPY_TYPE_NAME_MARQUEE);
+ g_type_ensure (DSPY_TYPE_TREE_VIEW);
+}
+
+static void
+dspy_view_init (DspyView *self)
+{
+ DspyViewPrivate *priv = dspy_view_get_instance_private (self);
+ g_autoptr(GSimpleActionGroup) actions = g_simple_action_group_new ();
+ GMenu *menu;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ g_action_map_add_action_entries (G_ACTION_MAP (actions),
+ action_entries,
+ G_N_ELEMENTS (action_entries),
+ self);
+ gtk_widget_insert_action_group (GTK_WIDGET (self), "dspy", G_ACTION_GROUP (actions));
+
+ menu = dzl_application_get_menu_by_id (DZL_APPLICATION (g_application_get_default ()),
+ "dspy-connections-menu");
+ gtk_menu_button_set_menu_model (priv->menu_button, G_MENU_MODEL (menu));
+
+ g_signal_connect_object (self,
+ "key-press-event",
+ G_CALLBACK (dzl_shortcut_manager_handle_event),
+ NULL,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->names_list_box,
+ "row-activated",
+ G_CALLBACK (name_row_activated_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->refresh_button,
+ "clicked",
+ G_CALLBACK (refresh_button_clicked_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->method_revealer,
+ "notify::child-revealed",
+ G_CALLBACK (notify_child_revealed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->introspection_tree_view,
+ "method-activated",
+ G_CALLBACK (method_activated_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->session_button,
+ "toggled",
+ G_CALLBACK (radio_button_toggled_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (dspy_connection_button_get_connection (priv->session_button),
+ "error",
+ G_CALLBACK (connection_got_error_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->system_button,
+ "toggled",
+ G_CALLBACK (radio_button_toggled_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (dspy_connection_button_get_connection (priv->system_button),
+ "error",
+ G_CALLBACK (connection_got_error_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (priv->search_entry,
+ "changed",
+ G_CALLBACK (search_entry_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ radio_button_toggled_cb (self, priv->session_button);
+}
diff --git a/src/plugins/dspy/libdspy/dspy-view.h b/src/plugins/dspy/libdspy/dspy-view.h
new file mode 100644
index 000000000..4b05ce9f2
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.h
@@ -0,0 +1,41 @@
+/* dspy-view.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DSPY_TYPE_VIEW (dspy_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DspyView, dspy_view, DSPY, VIEW, GtkBin)
+
+struct _DspyViewClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer _reserved[16];
+};
+
+GtkWidget *dspy_view_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/dspy/libdspy/dspy-view.ui b/src/plugins/dspy/libdspy/dspy-view.ui
new file mode 100644
index 000000000..f720ab976
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy-view.ui
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <template class="DspyView" parent="GtkBin">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="DzlMultiPaned">
+ <property name="orientation">horizontal</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="sidebar"/>
+ <class name="view"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="radio_buttons">
+ <property name="margin">6</property>
+ <property name="homogeneous">true</property>
+ <property name="orientation">horizontal</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="DspyConnectionButton" id="session_button">
+ <property name="bus-type">session</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="DspyConnectionButton" id="system_button">
+ <property name="bus-type">system</property>
+ <property name="group">session_button</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="placeholder-text" translatable="yes">Search Bus Names</property>
+ <property name="margin-top">6</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-bottom">12</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Bus Names</property>
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <property name="xalign">0.0</property>
+ <property name="margin-start">6</property>
+ <property name="margin-bottom">1</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value=".833333"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="focus-on-click">false</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">pan-down-symbolic</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="names_scroller">
+ <property name="propagate-natural-width">True</property>
+ <property name="max-content-width">300</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkListBox" id="names_list_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">300</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="transition-duration">300</property>
+ <property name="transition-type">crossfade</property>
+ <property name="hexpand">true</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="DzlEmptyState">
+ <property name="icon-name">org.gnome.dfeet-symbolic</property>
+ <property name="title" translatable="yes">Select a Bus Name</property>
+ <property name="subtitle" translatable="yes">Select a bus name to introspect the
peer.</property>
+ <property name="visible">true</property>
+ </object>
+ <packing>
+ <property name="name">empty-state</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="margin">12</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="DspyNameMarquee" id="name_marquee">
+ <property name="visible">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">true</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="shadow-type">in</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="DspyTreeView" id="introspection_tree_view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="DzlBin">
+ <property name="visible">true</property>
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkButton" id="refresh_button">
+ <property name="visible">true</property>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">view-refresh-symbolic</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="method_revealer">
+ <property name="reveal-child">false</property>
+ <property name="visible">true</property>
+ <child>
+ <object class="DspyMethodView" id="method_view">
+ <property name="margin-start">12</property>
+ <property name="visible">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">introspect</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/dspy/libdspy/dspy.h b/src/plugins/dspy/libdspy/dspy.h
new file mode 100644
index 000000000..7f5a06247
--- /dev/null
+++ b/src/plugins/dspy/libdspy/dspy.h
@@ -0,0 +1,37 @@
+/* dspy.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#define DSPY_INSIDE
+
+# include "dspy-connection.h"
+# include "dspy-connection-button.h"
+# include "dspy-introspection-model.h"
+# include "dspy-method-invocation.h"
+# include "dspy-method-view.h"
+# include "dspy-name.h"
+# include "dspy-name-marquee.h"
+# include "dspy-name-row.h"
+# include "dspy-names-model.h"
+# include "dspy-tree-view.h"
+# include "dspy-view.h"
+
+#undef DSPY_INSIDE
diff --git a/src/plugins/dspy/libdspy/gtk/menus.ui b/src/plugins/dspy/libdspy/gtk/menus.ui
new file mode 100644
index 000000000..9b844307c
--- /dev/null
+++ b/src/plugins/dspy/libdspy/gtk/menus.ui
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <menu id="dspy-connections-menu">
+ <item>
+ <attribute name="label" translatable="yes">Connect to Other Bus</attribute>
+ <attribute name="action">dspy.connect-to-bus</attribute>
+ </item>
+ </menu>
+</interface>
diff --git a/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg
b/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg
new file mode 100644
index 000000000..71fc85382
--- /dev/null
+++ b/src/plugins/dspy/libdspy/icons/symbolic/apps/org.gnome.dfeet-symbolic.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="16"
+ height="16"
+ viewBox="0 0 16 16.000001"
+ version="1.1"
+ id="svg2008"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"
+ sodipodi:docname="org.gnome.dfeet-symbolic.svg">
+ <defs
+ id="defs2002" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="45.254834"
+ inkscape:cx="4.6852043"
+ inkscape:cy="0.94029949"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ units="px"
+ borderlayer="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2553" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata2005">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1106.5197)">
+ <path
+
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ d="m 10,1109.5197 c -1.108,0 -2,0.892 -2,2 v 3 h 4 v -3 c 0,-1.108 -0.892,-2 -2,-2 z"
+ id="rect1969"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccss" />
+ <path
+ sodipodi:nodetypes="ssccss"
+ inkscape:connector-curvature="0"
+ id="path1972"
+ d="m 10,1118.5197 c -1.108,0 -2,-0.892 -2,-2 v -1 h 4 v 1 c 0,1.108 -0.892,2 -2,2 z"
+
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
/>
+ <path
+
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ d="m 4,1112.5197 c 1.108,0 2,0.892 2,2 v 3 H 5.27734 c -0.94474,-0.8696 -1.54492,-2.1073
-1.54492,-3.5 0,-0.5283 0.10678,-1.026 0.26367,-1.5 10e-4,0 0.003,0 0.004,0 z m -1.98242,1.834 c
0.0597,1.1613 0.42327,2.2424 1.01758,3.166 H 2 v -3 c 0,-0.057 0.013,-0.1103 0.0176,-0.166 z"
+ id="path1974"
+ inkscape:connector-curvature="0" />
+ <path
+
style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ d="M 8.5 0 C 4.369709 0 1 3.3697 1 7.5 C 1 9.0628162 1.4830998 10.515066 2.3066406 11.71875 L 0 14 L
0 16 L 2 16 L 4.296875 13.703125 C 5.4974125 14.519964 6.9441527 15 8.5 15 C 12.630291 15 16 11.6303 16 7.5 C
16 3.3697 12.630291 0 8.5 0 z M 8.5 2 C 11.549411 2 14 4.4506 14 7.5 C 14 10.5494 11.549411 13 8.5 13 C
5.4505892 13 3 10.5494 3 7.5 C 3 4.4506 5.4505892 2 8.5 2 z "
+ transform="translate(0,1106.5197)"
+ id="path1987" />
+ </g>
+</svg>
diff --git a/src/plugins/dspy/libdspy/libdspy.gresource.xml b/src/plugins/dspy/libdspy/libdspy.gresource.xml
new file mode 100644
index 000000000..d6ecfa90e
--- /dev/null
+++ b/src/plugins/dspy/libdspy/libdspy.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/dspy">
+ <file preprocess="xml-stripblanks">dspy-method-view.ui</file>
+ <file preprocess="xml-stripblanks">dspy-name-marquee.ui</file>
+ <file preprocess="xml-stripblanks">dspy-name-row.ui</file>
+ <file preprocess="xml-stripblanks">dspy-view.ui</file>
+ <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+ <file preprocess="xml-stripblanks">icons/symbolic/apps/org.gnome.dfeet-symbolic.svg</file>
+ <file>themes/shared.css</file>
+ </gresource>
+</gresources>
diff --git a/src/plugins/dspy/libdspy/meson.build b/src/plugins/dspy/libdspy/meson.build
new file mode 100644
index 000000000..e87b2bab6
--- /dev/null
+++ b/src/plugins/dspy/libdspy/meson.build
@@ -0,0 +1,35 @@
+libdspy_sources = [
+ 'dspy-connection.c',
+ 'dspy-connection-button.c',
+ 'dspy-introspection-model.c',
+ 'dspy-method-invocation.c',
+ 'dspy-method-view.c',
+ 'dspy-name.c',
+ 'dspy-name-marquee.c',
+ 'dspy-name-row.c',
+ 'dspy-names-model.c',
+ 'dspy-node.c',
+ 'dspy-signature.c',
+ 'dspy-tree-view.c',
+ 'dspy-view.c',
+]
+
+libdspy_deps = [
+ libgio_dep,
+ libgtk_dep,
+ libdazzle_dep,
+]
+
+libdspy_sources += gnome.compile_resources('libdspy-resources', 'libdspy.gresource.xml',
+ c_name: 'libdspy'
+)
+
+libdspy = static_library('libdspy', libdspy_sources,
+ dependencies: libdspy_deps,
+)
+
+libdspy_dep = declare_dependency(
+ dependencies: libdspy_deps,
+ link_with: [ libdspy ],
+ include_directories: include_directories('.'),
+)
diff --git a/src/plugins/dspy/libdspy/themes/shared.css b/src/plugins/dspy/libdspy/themes/shared.css
new file mode 100644
index 000000000..7498c3cd9
--- /dev/null
+++ b/src/plugins/dspy/libdspy/themes/shared.css
@@ -0,0 +1,16 @@
+dspyview dzlmultipaned > :first-child {
+ border-right: 1px solid @borders;
+}
+dspyview box.linked.horizontal > scrolledwindow:first-child {
+ border-right: none;
+}
+dspyview .sidebar button.popup.toggle {
+ border: none;
+ border-radius: 0px;
+ background: none;
+ box-shadow: none;
+ padding: 0px 6px;
+}
+dspyview .sidebar button:not(:hover) image {
+ color: alpha(currentColor, 0.6);
+}
diff --git a/src/plugins/dspy/meson.build b/src/plugins/dspy/meson.build
new file mode 100644
index 000000000..d238594d8
--- /dev/null
+++ b/src/plugins/dspy/meson.build
@@ -0,0 +1,21 @@
+if get_option('plugin_dspy')
+
+subdir('libdspy')
+
+plugins_sources += files([
+ 'dspy-plugin.c',
+ 'gbp-dspy-application-addin.c',
+ 'gbp-dspy-surface.c',
+ 'gbp-dspy-workspace.c',
+])
+
+plugin_dspy_resources = gnome.compile_resources(
+ 'dspy-resources',
+ 'dspy.gresource.xml',
+ c_name: 'gbp_dspy',
+)
+
+plugins_sources += plugin_dspy_resources
+plugins_deps += libdspy_dep
+
+endif
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 0c5fd6aa4..4feeb4a41 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -56,6 +56,7 @@ subdir('devhelp')
subdir('deviceui')
subdir('deviced')
subdir('doap')
+subdir('dspy')
subdir('editor')
subdir('editorconfig')
subdir('emacs')
@@ -143,6 +144,7 @@ status += [
'CTags ................. : @0@'.format(get_option('plugin_ctags')),
'Devhelp ............... : @0@'.format(get_option('plugin_devhelp')),
'Deviced ............... : @0@'.format(get_option('plugin_deviced')),
+ 'DBus Spy .............. : @0@'.format(get_option('plugin_dspy')),
'Editorconfig .......... : @0@'.format(get_option('plugin_editorconfig')),
'ESLint ................ : @0@'.format(get_option('plugin_eslint')),
'File Search ........... : @0@'.format(get_option('plugin_file_search')),
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]