[gssdp] sniffer: Port to a more modern GTK



commit ee0772817dd5729d42a2f3f3d3e1ea6a45a5fc66
Author: Jens Georg <mail jensge org>
Date:   Tue Oct 6 23:16:16 2020 +0200

    sniffer: Port to a more modern GTK
    
    Using appliation windows, actions, etc.

 tools/gssdp-device-sniffer.c             | 761 +++---------------------------
 tools/gssdp-device-sniffer.gresource.xml |   3 +-
 tools/gssdp-device-sniffer.ui            | 362 --------------
 tools/main-window.c                      | 779 +++++++++++++++++++++++++++++++
 tools/main-window.h                      |  18 +
 tools/main-window.ui                     | 395 ++++++++++++++++
 tools/meson.build                        |   2 +
 tools/window-menu.ui                     |  18 -
 8 files changed, 1269 insertions(+), 1069 deletions(-)
---
diff --git a/tools/gssdp-device-sniffer.c b/tools/gssdp-device-sniffer.c
index 4020130..725e667 100644
--- a/tools/gssdp-device-sniffer.c
+++ b/tools/gssdp-device-sniffer.c
@@ -17,6 +17,8 @@
  * Boston, MA 02110-1301, USA.
  */
 
+#include "main-window.h"
+
 #include <libgssdp/gssdp.h>
 #include <libgssdp/gssdp-client-private.h>
 #include <libsoup/soup.h>
@@ -24,716 +26,101 @@
 #include <string.h>
 #include <stdlib.h>
 
-#define UI_RESOURCE "/org/gupnp/GSSDP/DeviceSniffer.ui"
-#define MENU_RESOURCE "/org/gupnp/GSSDP/WindowMenu.ui"
-
-static char *interface = NULL;
-
-typedef enum {
-        PACKET_STORE_COLUMN_TIME,
-        PACKET_STORE_COLUMN_IP,
-        PACKET_STORE_COLUMN_INTERFACE,
-        PACKET_STORE_COLUMN_PACKET_TYPE,
-        PACKET_STORE_COLUMN_TARGET,
-        PACKET_STORE_COLUMN_HEADERS,
-        PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME
-} PACKET_STORE_COLUMNS;
-
-typedef enum {
-        DEVICE_STORE_COLUMN_UUID,
-        DEVICE_STORE_COLUMN_FIRST_SEEN,
-        DEVICE_STORE_COLUMN_TYPE,
-        DEVICE_STORE_COLUMN_LOCATION
-} DEVICE_STORE_COLUMNS;
-
-
-GtkBuilder *builder;
-GSSDPResourceBrowser *resource_browser;
-GSSDPClient *client;
-char *ip_filter = NULL;
-gboolean capture_packets = TRUE;
-gboolean prefer_v6 = FALSE;
-
-GOptionEntry entries[] =
-{
-        {"interface", 'i', 0, G_OPTION_ARG_STRING, &interface, "Network interface to listen on", NULL },
-        { "prefer-v6", '6', 0, G_OPTION_ARG_NONE, &prefer_v6, "Prefer IPv6 for the client", NULL },
-        { "prefer-v4", '4', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &prefer_v6, "Prefer IPv4 for the 
client", NULL },
-        { NULL }
-};
-
-void
-on_enable_packet_capture_activate (GtkToggleButton  *menuitem,
-                                   gpointer          user_data);
-
-void
-on_address_filter_dialog_response (GtkDialog *dialog,
-                                   gint       response,
-                                   gpointer   user_data);
-
-gboolean
-on_delete_event (GtkWidget *widget, GdkEvent  *event, gpointer   user_data);
-
-G_MODULE_EXPORT void
-on_enable_packet_capture_activate (GtkToggleButton *menuitem,
-                                   gpointer         user_data)
-{
-        const gchar *icon_name = NULL;
-
-        capture_packets = gtk_toggle_button_get_active (menuitem);
-        icon_name = capture_packets
-                ? "media-playback-stop-symbolic"
-                : "media-playback-start-symbolic";
-
-        gtk_image_set_from_icon_name (GTK_IMAGE (user_data),
-                                      icon_name,
-                                      GTK_ICON_SIZE_BUTTON);
-}
-
-static void
-clear_packet_treeview (void)
-{
-        GtkWidget *treeview;
-        GtkTreeModel *model;
-
-        treeview = GTK_WIDGET(gtk_builder_get_object (builder, "packet-treeview"));
-        g_assert (treeview != NULL);
-        model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
-        gtk_list_store_clear (GTK_LIST_STORE (model));
-}
-
-static void
-on_details_activate (GSimpleAction *action,
-                     GVariant *parameter,
-                     gpointer user_data)
-{
-        GtkWidget *scrolled_window = NULL;
-
-        scrolled_window = GTK_WIDGET (gtk_builder_get_object (builder,
-                                                              "packet-details-scrolledwindow"));
-
-        g_object_set (G_OBJECT (scrolled_window), "visible", g_variant_get_boolean (parameter), NULL);
-        g_simple_action_set_state (action, parameter);
-}
-
-static void
-packet_header_to_string (const char *header_name,
-                 const char *header_val,
-                 GString **text)
-{
-        g_string_append_printf (*text, "%s: %s\n",
-                         header_name,
-                         header_val);
-}
- 
-static void
-clear_textbuffer (GtkTextBuffer *textbuffer)
-{
-        GtkTextIter start, end;
-
-        gtk_text_buffer_get_bounds (textbuffer, &start, &end);
-        gtk_text_buffer_delete (textbuffer, &start, &end);
-}
-
-static void
-update_packet_details (const char *text, unsigned int len)
-{
-        GtkWidget *textview;
-        GtkTextBuffer *textbuffer;
-        
-        textview = GTK_WIDGET(gtk_builder_get_object (builder, "packet-details-textview"));
-        g_assert (textview != NULL);
-        textbuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
-        
-        clear_textbuffer (textbuffer);
-        gtk_text_buffer_insert_at_cursor (textbuffer, text, len);
-}
-
-static void
-display_packet (GDateTime *arrival_time, SoupMessageHeaders *packet_headers)
-{
-        GString *text;
-        char *time = NULL;
-
-        time = g_date_time_format_iso8601 (arrival_time);
-        text = g_string_new ("Received on: ");
-        g_string_append (text, time);
-        g_string_append (text, "\nHeaders:\n\n");
-        g_free (time);
-
-        soup_message_headers_foreach (packet_headers,
-                        (SoupMessageHeadersForeachFunc)
-                        packet_header_to_string,
-                        &text);
-
-        update_packet_details (text->str, text->len);
-        g_string_free (text, TRUE);
-}
 
 static void
-on_packet_selected (GtkTreeSelection      *selection,
-                    G_GNUC_UNUSED gpointer user_data)
+on_activate (GtkApplication *app)
 {
-        GtkTreeModel *model;
-        GtkTreeIter iter;
-        GDateTime *arrival_time;
-
-        if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
-                SoupMessageHeaders *packet_headers;
+        GtkWindow *window;
 
-                gtk_tree_model_get (model,
-                                    &iter,
-                                    PACKET_STORE_COLUMN_HEADERS,
-                                        &packet_headers,
-                                    PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME,
-                                        &arrival_time,
-                                    -1);
-                display_packet (arrival_time, packet_headers);
-                g_boxed_free (SOUP_TYPE_MESSAGE_HEADERS, packet_headers);
-                g_date_time_unref (arrival_time);
+        window = gtk_application_get_active_window (app);
+        if (window == NULL) {
+                window = g_object_new (GSSDP_DEVICE_SNIFFER_TYPE_MAIN_WINDOW,
+                                       "application",
+                                       app,
+                                       NULL);
         }
 
-        else
-                update_packet_details ("", 0);
+        gtk_window_present (window);
 }
 
-static const char *message_types[] = {"M-SEARCH", "RESPONSE", "NOTIFY"};
-
-static char **
-packet_to_treeview_data (const gchar        *from_ip,
-                         GDateTime          *arrival_time,
-                         _GSSDPMessageType   type,
-                         SoupMessageHeaders *headers)
+static int
+on_command_line (GtkApplication *app,
+                 GApplicationCommandLine *cmdline,
+                 gpointer user_data)
 {
-        char **packet_data;
-        const char *target;
+        char *iface = NULL;
+        GSocketFamily family = G_SOCKET_FAMILY_INVALID;
+        gboolean six = FALSE;
 
-        packet_data = g_new0 (char *, 6);
 
-        /* Set the Time */
-        packet_data[0] = g_date_time_format (arrival_time, "%R");
+        GVariantDict *args = g_application_command_line_get_options_dict (cmdline);
 
-        /* Now the Source Address */
-        packet_data[1] = g_strdup (from_ip);
-        
-        packet_data[2] = g_strdup (gssdp_client_get_interface (client));
+        GOptionContext *context = g_option_context_new (NULL);
+        g_option_context_set_help_enabled (context, FALSE);
 
-        /* Now the Packet Type */
-        packet_data[3] = g_strdup (message_types[type]);
-        
-        /* Now the Packet Information */
-        if (type == _GSSDP_DISCOVERY_RESPONSE)
-                target = soup_message_headers_get_one (headers, "ST");
-        else
-                target = soup_message_headers_get_one (headers, "NT");
-        
-        packet_data[4] = g_strdup (target);
-        packet_data[5] = NULL;
+        g_variant_dict_lookup (args, "interface" ,"s", &iface);
+        g_variant_dict_lookup (args, "prefer-v6", "b", &six);
 
-        return packet_data;
-}
-
-static void
-append_packet (const gchar *from_ip,
-               GDateTime *arrival_time,
-               _GSSDPMessageType type,
-               SoupMessageHeaders *headers)
-{
-        GtkWidget *treeview;
-        GtkListStore *liststore;
-        GtkTreeIter iter;
-        char **packet_data;
-        
-        treeview = GTK_WIDGET(gtk_builder_get_object (builder, "packet-treeview"));
-        g_assert (treeview != NULL);
-        liststore = GTK_LIST_STORE (
-                        gtk_tree_view_get_model (GTK_TREE_VIEW (treeview)));
-        g_assert (liststore != NULL);
-       
-        packet_data = packet_to_treeview_data (from_ip,
-                                               arrival_time,
-                                               type,
-                                               headers);
-
-        gtk_list_store_insert_with_values (liststore, &iter, 0,
-                        PACKET_STORE_COLUMN_TIME, packet_data[0],
-                        PACKET_STORE_COLUMN_IP, packet_data[1],
-                        PACKET_STORE_COLUMN_INTERFACE, packet_data[2],
-                        PACKET_STORE_COLUMN_PACKET_TYPE, packet_data[3],
-                        PACKET_STORE_COLUMN_TARGET, packet_data[4],
-                        PACKET_STORE_COLUMN_HEADERS, headers,
-                        PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME, arrival_time,
-                        -1);
-        g_strfreev (packet_data);
-}
-
-static void
-on_ssdp_message (G_GNUC_UNUSED GSSDPClient *ssdp_client,
-                 G_GNUC_UNUSED const gchar *from_ip,
-                 G_GNUC_UNUSED gushort      from_port,
-                 _GSSDPMessageType          type,
-                 SoupMessageHeaders        *headers,
-                 G_GNUC_UNUSED gpointer     user_data)
-{
-        GDateTime *arrival_time;
-     
-        if (type == _GSSDP_DISCOVERY_REQUEST)
-                return;
-        if (ip_filter != NULL && strcmp (ip_filter, from_ip) != 0)
-                return;
-        if (!capture_packets) 
-                return;
-
-        arrival_time = g_date_time_new_now_local ();
-        append_packet (from_ip, arrival_time, type, headers);
-        g_date_time_unref (arrival_time);
-}
-
-static gboolean 
-find_device (GtkTreeModel *model, const char *uuid, GtkTreeIter *iter)
-{
-        gboolean found = FALSE;
-        gboolean more;
-       
-        more = gtk_tree_model_get_iter_first (model, iter);
-        while (more) {
-                char *device_uuid;
-                gtk_tree_model_get (model,
-                                    iter, 
-                                    0, &device_uuid, -1);
-                found = g_strcmp0 (device_uuid, uuid) == 0;
-                g_free (device_uuid);
-
-                if (found)
-                        break;
-                more = gtk_tree_model_iter_next (model, iter);
-        }
 
-        return found;
-}
-
-static void
-append_device (const char *uuid,
-               const char *first_notify,
-               const char *device_type,
-               const char *location)
-{
-        GtkWidget *treeview;
-        GtkTreeModel *model;
-        GtkTreeIter iter;
-       
-        treeview = GTK_WIDGET(gtk_builder_get_object (builder, "device-details-treeview"));
-        g_assert (treeview != NULL);
-        model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
-        g_assert (model != NULL);
-       
-        if (!find_device (model, uuid, &iter)) {
-                gtk_list_store_insert_with_values (GTK_LIST_STORE (model),
-                                &iter, 0,
-                                DEVICE_STORE_COLUMN_UUID, uuid,
-                                DEVICE_STORE_COLUMN_FIRST_SEEN, first_notify,
-                                DEVICE_STORE_COLUMN_LOCATION, location, -1);
-        }
-                
-        if (device_type) {
-                gtk_list_store_set (GTK_LIST_STORE (model), &iter,
-                                DEVICE_STORE_COLUMN_TYPE, device_type, -1);
+        if (six) {
+                family = G_SOCKET_FAMILY_IPV6;
+        } else {
+                family = G_SOCKET_FAMILY_IPV4;
         }
-}
-
-static void
-resource_available_cb (G_GNUC_UNUSED GSSDPResourceBrowser *ssdp_resource_browser,
-                       const char                         *usn,
-                       GList                              *locations)
-{
 
-        char **usn_tokens;
-        char *uuid;
-        char *device_type = NULL;
-        GDateTime *current_time = NULL;
-        char *first_notify;
-                
-        current_time = g_date_time_new_now_local ();
-        first_notify = g_date_time_format (current_time, "%R");
-        g_date_time_unref (current_time);
-
-        usn_tokens = g_strsplit (usn, "::", -1);
-        g_assert (usn_tokens != NULL && usn_tokens[0] != NULL);
-
-        uuid = usn_tokens[0] + 5; /* skip the prefix 'uuid:' */
-
-        if (usn_tokens[1] && strlen(usn_tokens[1]) != 0) {
-                char **urn_tokens;
-
-                urn_tokens = g_strsplit (usn_tokens[1], ":device:", -1);
-                        
-                if (urn_tokens[1])
-                        device_type = g_strdup (urn_tokens[1]);
-                g_strfreev (urn_tokens);
-        }
-
-        /* Device Announcement */
-        append_device (uuid,
-                       first_notify,
-                       device_type,
-                       (char *) locations->data);
-        
-        g_free (device_type);
-        g_free (first_notify);
-        g_strfreev (usn_tokens);
-}
-
-static void
-remove_device (const char *uuid)
-{
-        GtkWidget *treeview;
-        GtkTreeModel *model;
-        GtkTreeIter iter;
-       
-        treeview = GTK_WIDGET(gtk_builder_get_object (builder, "device-details-treeview"));
-        g_assert (treeview != NULL);
-        model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
-        g_assert (model != NULL);
-      
-        if (find_device (model, uuid, &iter)) {
-                gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
-        }
-}
-
-static void
-resource_unavailable_cb (G_GNUC_UNUSED GSSDPResourceBrowser *ssdp_resource_browser,
-                         const char                         *usn)
-{
-        char **usn_tokens;
-        char *uuid;
-
-        usn_tokens = g_strsplit (usn, "::", -1);
-        g_assert (usn_tokens != NULL && usn_tokens[0] != NULL);
-        uuid = usn_tokens[0] + 5; /* skip the prefix 'uuid:' */
-        
-        remove_device (uuid);
-        
-        g_strfreev (usn_tokens);
-}
-
-G_MODULE_EXPORT
-void
-on_use_filter_radiobutton_toggled (GtkToggleButton       *togglebutton,
-                                   G_GNUC_UNUSED gpointer user_data)
-{
-        GtkWidget *filter_hbox;
-        gboolean use_filter;
-
-        filter_hbox = GTK_WIDGET(gtk_builder_get_object (builder, "address-entry0"));
-        g_assert (filter_hbox != NULL);
-        
-        use_filter = gtk_toggle_button_get_active (togglebutton);
-        gtk_widget_set_sensitive (filter_hbox, use_filter);
-}
-
-static char *
-get_ip_filter (void)
-{
-        GtkEntry *entry;
-        GInetAddress *addr;
+        GtkWindow *window;
+        window = g_object_new (GSSDP_DEVICE_SNIFFER_TYPE_MAIN_WINDOW,
+                               "application",
+                               app,
+                               "address-family",
+                               family,
+                               "interface",
+                               iface,
+                               NULL);
+
+        gtk_window_present (window);
+
+        return EXIT_SUCCESS;
+}
+
+GOptionEntry entries[] = { { "interface",
+                             'i',
+                             0,
+                             G_OPTION_ARG_STRING,
+                             NULL,
+                             "Network interface to listen on",
+                             NULL },
+                           { "prefer-v6",
+                             '6',
+                             0,
+                             G_OPTION_ARG_NONE,
+                             NULL,
+                             "Prefer IPv6 for the client",
+                             NULL },
+                           { NULL , 0, 0, 0, NULL, NULL, NULL} };
 
-        entry = GTK_ENTRY (gtk_builder_get_object (builder, "address-entry0"));
-        addr = g_inet_address_new_from_string (gtk_entry_get_text (entry));
-
-        if (addr == NULL) {
-                g_warning ("Filter not a valid IP address");
-
-                return NULL;
-        }
-        g_object_unref (addr);
-
-        return g_strdup (gtk_entry_get_text (entry));
-}
-
-G_MODULE_EXPORT
-void
-on_address_filter_dialog_response (GtkDialog             *dialog,
-                                   G_GNUC_UNUSED gint     response,
-                                   G_GNUC_UNUSED gpointer user_data)
-{
-        GtkWidget *use_filter_radio;
-
-        gtk_widget_hide (GTK_WIDGET (dialog));
-        
-        use_filter_radio = GTK_WIDGET(gtk_builder_get_object (builder, "use-filter-radiobutton"));
-        g_assert (use_filter_radio != NULL);
-        
-        if (response != GTK_RESPONSE_OK)
-                return;
-        
-        g_free (ip_filter);
-        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (use_filter_radio))) {
-                ip_filter = get_ip_filter ();
-        }
-       
-        else
-                ip_filter = NULL;
-}
-
-static gboolean
-on_treeview_popup_menu (GtkWidget *tv, GdkEventButton *event, gpointer user_data)
-{
-        if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
-                gtk_menu_popup_at_pointer (GTK_MENU (user_data), (GdkEvent *)event);
-        }
-
-        return FALSE;
-}
-
-static void
-setup_treeviews (void)
-{
-        GtkWidget *treeview;
-        GtkTreeSelection *selection;
-        GtkWidget *menu = NULL;
-
-        treeview = GTK_WIDGET(gtk_builder_get_object (builder,
-                                                      "packet-treeview"));
-        
-        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
-        g_signal_connect (selection,
-                          "changed",
-                          G_CALLBACK (on_packet_selected),
-                          (gpointer *) treeview);
-        menu = gtk_menu_new_from_model (G_MENU_MODEL (gtk_builder_get_object (builder,
-                                                                              "sniffer-context-menu")));
-        gtk_menu_attach_to_widget (GTK_MENU (menu), treeview, NULL);
-        g_signal_connect (G_OBJECT (treeview),
-                          "button-press-event",
-                          G_CALLBACK (on_treeview_popup_menu),
-                          menu);
-}
-
-G_MODULE_EXPORT
-gboolean
-on_delete_event (G_GNUC_UNUSED GtkWidget *widget,
-                 G_GNUC_UNUSED GdkEvent  *event,
-                 G_GNUC_UNUSED gpointer   user_data)
-{
-        gtk_main_quit ();
-
-        return TRUE;
-}
-
-static void
-on_show_address_filter (GSimpleAction *action,
-                        GVariant *parameter,
-                        gpointer user_data)
-{
-        GtkWidget *dialog = NULL;
-
-        dialog = GTK_WIDGET (gtk_builder_get_object (builder,
-                                                     "address-filter-dialog"));
-        gtk_widget_show (dialog);
-}
-
-static void
-on_set_address_filter (GSimpleAction *action,
-                       GVariant      *parameter,
-                       gpointer       user_data)
-{
-        GtkWidget *treeview = NULL;
-        GtkTreeSelection *selection = NULL;
-        GtkTreeModel *model = NULL;
-        GtkTreeIter iter;
-        GtkWidget *use_filter_radio;
-
-        treeview = GTK_WIDGET (gtk_builder_get_object (builder, "packet-treeview"));
-        use_filter_radio = GTK_WIDGET(gtk_builder_get_object (builder, "use-filter-radiobutton"));
-
-        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
-        gtk_tree_selection_get_selected (selection, &model, &iter);
-        g_free (ip_filter);
-        gtk_tree_model_get (model, &iter,
-                        PACKET_STORE_COLUMN_IP, &ip_filter, -1);
-        gtk_entry_set_text (GTK_ENTRY (gtk_builder_get_object (builder, "address-entry0")),
-                            ip_filter);
-
-        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (use_filter_radio), TRUE);
-}
-
-static void
-on_about (GSimpleAction *action, GVariant *parameter, gpointer user_data)
-{
-        GtkWidget *dialog = NULL;
-
-        dialog = GTK_WIDGET (gtk_builder_get_object (builder,
-                                                     "about-dialog"));
-        gtk_widget_show (dialog);
-}
-
-
-G_MODULE_EXPORT
-void
-on_clear_packet_capture_activate (G_GNUC_UNUSED GtkMenuItem *menuitem,
-                                  G_GNUC_UNUSED gpointer     user_data)
-{
-        clear_packet_treeview ();
-}
-
-static void
-on_trigger_rescan (GSimpleAction *action,
-                   GVariant *parameter,
-                   gpointer user_data)
-{
-        gssdp_resource_browser_rescan (resource_browser);
-}
-
-static GActionEntry actions[] = {
-        { "trigger-rescan", on_trigger_rescan },
-        { "set-address-filter", on_set_address_filter },
-        { "show-packet-details", NULL, NULL, "true", on_details_activate },
-        { "show-address-filter", on_show_address_filter },
-        { "about", on_about }
-};
-
-static gboolean
-init_ui (gint *argc, gchar **argv[])
-{
-        GtkWidget *main_window;
-        gint window_width, window_height;
-        GError *error = NULL;
-        GOptionContext *context;
-        double w, h;
-        GSimpleActionGroup *group = NULL;
-
-        context = g_option_context_new ("- graphical SSDP debug tool");
-        g_option_context_add_main_entries (context, entries, NULL);
-        g_option_context_add_group (context, gtk_get_option_group (TRUE));
-        if (!g_option_context_parse (context, argc, argv, &error)) {
-                g_print ("Failed to parse options: %s\n", error->message);
-                g_error_free (error);
-
-                return FALSE;
-        }
-
-        builder = gtk_builder_new();
-        if (gtk_builder_add_from_resource(builder, UI_RESOURCE, NULL) == 0)
-                return FALSE;
-
-        if (gtk_builder_add_from_resource (builder, MENU_RESOURCE, NULL) == 0)
-                return FALSE;
-
-        gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (gtk_builder_get_object (builder, "window-menu")),
-                        G_MENU_MODEL (gtk_builder_get_object (builder, "sniffer-window-menu")));
-
-        main_window = GTK_WIDGET(gtk_builder_get_object (builder, "main-window"));
-        g_assert (main_window != NULL);
-
-        group = g_simple_action_group_new ();
-        gtk_widget_insert_action_group (GTK_WIDGET (main_window), "win", G_ACTION_GROUP (group));
-        g_action_map_add_action_entries (G_ACTION_MAP (group), actions, G_N_ELEMENTS (actions), NULL);
-
-
-#if GTK_CHECK_VERSION(3,22,0)
-        gtk_widget_realize (main_window);
-        {
-            GdkWindow *window = gtk_widget_get_window (main_window);
-            GdkDisplay *display = gdk_display_get_default ();
-            GdkMonitor *monitor = gdk_display_get_monitor_at_window (display,
-                                                                     window);
-            GdkRectangle rectangle;
-
-            gdk_monitor_get_geometry (monitor, &rectangle);
-            w = rectangle.width * 0.75;
-            h = rectangle.height * 0.75;
-        }
-
-#else
-        w = gdk_screen_width () * 0.75;
-        h = gdk_screen_height () * 0.75;
-#endif
-
-        window_width = CLAMP ((int) w, 10, 1000);
-        window_height = CLAMP ((int) h, 10, 800);
-        gtk_window_set_default_size (GTK_WINDOW (main_window),
-                                     window_width,
-                                     window_height);
-
-        gtk_builder_connect_signals (builder, NULL);
-        setup_treeviews ();
-        gtk_widget_show_all (main_window);
-
-        return TRUE;
-}
-
-static void
-deinit_ui (void)
-{
-        g_object_unref (builder);
-}
-
-static gboolean
-init_upnp (void)
+gint
+main (gint argc, gchar *argv[])
 {
-        GError *error;
-        
-        error = NULL;
-        client = g_initable_new (GSSDP_TYPE_CLIENT,
-                                 NULL,
-                                 &error,
-                                 "address-family", prefer_v6 ? G_SOCKET_FAMILY_IPV6 : G_SOCKET_FAMILY_IPV4,
-                                 "interface", interface,
-                                 NULL);
-        if (error) {
-                g_printerr ("Error creating the GSSDP client: %s\n",
-                            error->message);
+        g_type_ensure (G_TYPE_DATE_TIME);
 
-                g_error_free (error);
+        GtkApplication *app =
+                gtk_application_new ("org.gupnp.GSSDP.DeviceSniffer",
+                                     G_APPLICATION_HANDLES_COMMAND_LINE);
 
-                return FALSE;
-        }
+        g_application_add_main_option_entries (G_APPLICATION (app), entries);
+        g_application_set_option_context_parameter_string (
+                G_APPLICATION (app),
+                "- graphical SSDP debug tool");
 
-        resource_browser = gssdp_resource_browser_new (client,
-                                                       GSSDP_ALL_RESOURCES);
-        
-        g_signal_connect (client,
-                          "message-received",
-                          G_CALLBACK (on_ssdp_message),
-                          NULL);
-        g_signal_connect (resource_browser,
-                          "resource-available",
-                          G_CALLBACK (resource_available_cb),
+        g_signal_connect (G_OBJECT (app),
+                          "command-line",
+                          G_CALLBACK (on_command_line),
                           NULL);
-        g_signal_connect (resource_browser,
-                          "resource-unavailable",
-                          G_CALLBACK (resource_unavailable_cb),
+        g_signal_connect (G_OBJECT (app),
+                          "activate",
+                          G_CALLBACK (on_activate),
                           NULL);
 
-        gssdp_resource_browser_set_active (resource_browser, TRUE);
-
-        return TRUE;
-}
-
-static void
-deinit_upnp (void)
-{
-        g_object_unref (resource_browser);
-        g_object_unref (client);
-}
-
-gint
-main (gint argc, gchar *argv[])
-{
-        g_type_ensure (G_TYPE_DATE_TIME);
-
-        if (!init_ui (&argc, &argv)) {
-           return -2;
-        }
-
-        if (!init_upnp ()) {
-           return -3;
-        }
-        
-        gtk_main ();
-       
-        deinit_upnp ();
-        deinit_ui ();
-        
-        return 0;
+        return g_application_run (G_APPLICATION (app), argc, argv);
 }
diff --git a/tools/gssdp-device-sniffer.gresource.xml b/tools/gssdp-device-sniffer.gresource.xml
index ec8dc93..6a9d0a3 100644
--- a/tools/gssdp-device-sniffer.gresource.xml
+++ b/tools/gssdp-device-sniffer.gresource.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gupnp/GSSDP">
-    <file preprocess="xml-stripblanks" alias="DeviceSniffer.ui">gssdp-device-sniffer.ui</file>
-    <file alias="WindowMenu.ui">window-menu.ui</file>
+    <file preprocess="xml-stripblanks" alias="MainWindow.ui">main-window.ui</file>
   </gresource>
 </gresources>
diff --git a/tools/gssdp-device-sniffer.ui b/tools/gssdp-device-sniffer.ui
index 95faf76..a18d6b1 100644
--- a/tools/gssdp-device-sniffer.ui
+++ b/tools/gssdp-device-sniffer.ui
@@ -2,368 +2,6 @@
 <!-- Generated with glade 3.22.2 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
-  <object class="GtkListStore" id="device_store">
-    <columns>
-      <!-- column-name uid -->
-      <column type="gchararray"/>
-      <!-- column-name first-seen -->
-      <column type="gchararray"/>
-      <!-- column-name device-type -->
-      <column type="gchararray"/>
-      <!-- column-name location -->
-      <column type="gchararray"/>
-    </columns>
-  </object>
-  <object class="GtkListStore" id="packet_store">
-    <columns>
-      <!-- column-name time -->
-      <column type="gchararray"/>
-      <!-- column-name source-address -->
-      <column type="gchararray"/>
-      <!-- column-name interface -->
-      <column type="gchararray"/>
-      <!-- column-name type -->
-      <column type="gchararray"/>
-      <!-- column-name information -->
-      <column type="gchararray"/>
-      <!-- column-name SoupMessageHeader1 -->
-      <column type="SoupMessageHeaders"/>
-      <!-- column-name GDateTime1 -->
-      <column type="GDateTime"/>
-    </columns>
-  </object>
-  <object class="GtkWindow" id="main-window">
-    <property name="can_focus">False</property>
-    <signal name="delete-event" handler="on_delete_event" swapped="no"/>
-    <child type="titlebar">
-      <object class="GtkHeaderBar">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="show_close_button">True</property>
-        <child>
-          <object class="GtkBox">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="spacing">6</property>
-            <child>
-              <object class="GtkButton">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Clear packet capture</property>
-                <signal name="clicked" handler="on_clear_packet_capture_activate" swapped="no"/>
-                <child>
-                  <object class="GtkImage">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">edit-clear-all-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkToggleButton">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Toggle packet capture</property>
-                <property name="active">True</property>
-                <signal name="toggled" handler="on_enable_packet_capture_activate" object="capture-image" 
swapped="no"/>
-                <child>
-                  <object class="GtkImage" id="capture-image">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">media-playback-stop-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="tooltip_text" translatable="yes">Trigger a rescan of the network</property>
-                <property name="action_name">win.trigger-rescan</property>
-                <child>
-                  <object class="GtkImage">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="icon_name">view-refresh-symbolic</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">2</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="position">1</property>
-          </packing>
-        </child>
-        <child type="title">
-          <object class="GtkStackSwitcher">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="halign">center</property>
-            <property name="icon_size">0</property>
-            <property name="stack">stack1</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkMenuButton" id="window-menu">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <child>
-              <object class="GtkImage">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="icon_name">open-menu-symbolic</property>
-              </object>
-            </child>
-          </object>
-          <packing>
-            <property name="pack_type">end</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-    <child>
-      <object class="GtkStack" id="stack1">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <child>
-          <object class="GtkBox" id="vbox">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="border_width">6</property>
-            <property name="orientation">vertical</property>
-            <property name="spacing">6</property>
-            <child>
-              <object class="GtkScrolledWindow" id="packet-scrolledwindow">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="shadow_type">in</property>
-                <child>
-                  <object class="GtkTreeView" id="packet-treeview">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="model">packet_store</property>
-                    <child internal-child="selection">
-                      <object class="GtkTreeSelection"/>
-                    </child>
-                    <child>
-                      <object class="GtkTreeViewColumn">
-                        <property name="title" translatable="yes">Time</property>
-                        <child>
-                          <object class="GtkCellRendererText"/>
-                          <attributes>
-                            <attribute name="markup">0</attribute>
-                          </attributes>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkTreeViewColumn">
-                        <property name="title" translatable="yes">Source Address</property>
-                        <child>
-                          <object class="GtkCellRendererText"/>
-                          <attributes>
-                            <attribute name="markup">1</attribute>
-                          </attributes>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkTreeViewColumn">
-                        <property name="title" translatable="yes">Interface</property>
-                        <child>
-                          <object class="GtkCellRendererText"/>
-                          <attributes>
-                            <attribute name="markup">2</attribute>
-                          </attributes>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkTreeViewColumn">
-                        <property name="title" translatable="yes">Packet Type</property>
-                        <child>
-                          <object class="GtkCellRendererText"/>
-                          <attributes>
-                            <attribute name="markup">3</attribute>
-                          </attributes>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkTreeViewColumn">
-                        <property name="title" translatable="yes">Packet Information</property>
-                        <child>
-                          <object class="GtkCellRendererText"/>
-                          <attributes>
-                            <attribute name="markup">4</attribute>
-                          </attributes>
-                        </child>
-                      </object>
-                    </child>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkScrolledWindow" id="packet-details-scrolledwindow">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="shadow_type">in</property>
-                <child>
-                  <object class="GtkTextView" id="packet-details-textview">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="editable">False</property>
-                    <property name="monospace">True</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="name">page0</property>
-            <property name="title" translatable="yes">Packets</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkScrolledWindow" id="device-details-scrolledwindow">
-            <property name="name">Bar</property>
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="margin_top">6</property>
-            <property name="margin_bottom">6</property>
-            <property name="shadow_type">in</property>
-            <child>
-              <object class="GtkTreeView" id="device-details-treeview">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="model">device_store</property>
-                <child internal-child="selection">
-                  <object class="GtkTreeSelection"/>
-                </child>
-                <child>
-                  <object class="GtkTreeViewColumn">
-                    <property name="title" translatable="yes">Unique Identifier</property>
-                    <child>
-                      <object class="GtkCellRendererText"/>
-                      <attributes>
-                        <attribute name="text">0</attribute>
-                      </attributes>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkTreeViewColumn">
-                    <property name="title" translatable="yes">First Notify</property>
-                    <child>
-                      <object class="GtkCellRendererText"/>
-                      <attributes>
-                        <attribute name="text">1</attribute>
-                      </attributes>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkTreeViewColumn">
-                    <property name="title" translatable="yes">Device Type</property>
-                    <child>
-                      <object class="GtkCellRendererText"/>
-                      <attributes>
-                        <attribute name="text">2</attribute>
-                      </attributes>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkTreeViewColumn">
-                    <property name="title" translatable="yes">Location</property>
-                    <child>
-                      <object class="GtkCellRendererText"/>
-                      <attributes>
-                        <attribute name="text">3</attribute>
-                      </attributes>
-                    </child>
-                  </object>
-                </child>
-              </object>
-            </child>
-          </object>
-          <packing>
-            <property name="name">page1</property>
-            <property name="title" translatable="yes">Device history</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-  </object>
-  <object class="GtkAboutDialog" id="about-dialog">
-    <property name="can_focus">False</property>
-    <property name="resizable">False</property>
-    <property name="type_hint">normal</property>
-    <property name="transient_for">main-window</property>
-    <property name="copyright" translatable="yes">Copyright © 2007 Zeeshan Ali (Khattak)</property>
-    <property name="comments" translatable="yes">A Device Sniffer tool based on GSSDP framework.
-Inspired by Intel Tools for UPnP.</property>
-    <property name="authors">Zeeshan Ali (Khattak) &lt;zeeshanak gnome org&gt;</property>
-    <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with 
your names, one name per line.">translator-credits</property>
-    <property name="logo_icon_name"/>
-    <property name="license_type">lgpl-2-1</property>
-    <signal name="delete-event" handler="gtk_widget_hide" object="about-dialog" swapped="yes"/>
-    <signal name="response" handler="gtk_widget_hide" object="about-dialog" swapped="yes"/>
-    <child type="titlebar">
-      <placeholder/>
-    </child>
-    <child internal-child="vbox">
-      <object class="GtkBox" id="aboutdialog-vbox1">
-        <property name="can_focus">False</property>
-        <property name="orientation">vertical</property>
-        <child internal-child="action_area">
-          <object class="GtkButtonBox" id="aboutdialog-action_area1">
-            <property name="can_focus">False</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="pack_type">end</property>
-            <property name="position">0</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-  </object>
   <object class="GtkDialog" id="address-filter-dialog">
     <property name="can_focus">False</property>
     <property name="border_width">7</property>
diff --git a/tools/main-window.c b/tools/main-window.c
new file mode 100644
index 0000000..99a22b7
--- /dev/null
+++ b/tools/main-window.c
@@ -0,0 +1,779 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "main-window.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <glib/gi18n.h>
+
+#include <libgssdp/gssdp-client-private.h>
+#include <libgssdp/gssdp.h>
+
+#include <libsoup/soup.h>
+
+typedef enum
+{
+        PACKET_STORE_COLUMN_TIME,
+        PACKET_STORE_COLUMN_IP,
+        PACKET_STORE_COLUMN_INTERFACE,
+        PACKET_STORE_COLUMN_PACKET_TYPE,
+        PACKET_STORE_COLUMN_TARGET,
+        PACKET_STORE_COLUMN_HEADERS,
+        PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME
+} PACKET_STORE_COLUMNS;
+
+typedef enum {
+        DEVICE_STORE_COLUMN_UUID,
+        DEVICE_STORE_COLUMN_FIRST_SEEN,
+        DEVICE_STORE_COLUMN_TYPE,
+        DEVICE_STORE_COLUMN_LOCATION
+} DEVICE_STORE_COLUMNS;
+
+enum
+{
+        PROP_0,
+        PROP_INTERFACE,
+        PROP_ADDRESS_FAMILY,
+        PROP_CAPTURE,
+};
+
+struct _GSSDPDeviceSnifferMainWindow {
+        GtkApplicationWindow parent_instance;
+
+        // SSDP related parameters
+        char *interface;
+        GSocketFamily family;
+        GSSDPClient *client;
+        GSSDPResourceBrowser *browser;
+
+        // Bound child widgets
+        GtkWidget *packet_treeview;
+        GtkWidget *packet_textview;
+        GtkWidget *capture_button_image;
+        GtkWidget *device_treeview;
+        GtkWidget *details_scrolled;
+        GMenuModel *sniffer_context_menu;
+        GtkWidget *searchbar;
+        GtkWidget *address_filter;
+        GtkWidget *searchbutton;
+
+        // Other widgets
+        GtkWidget *context_menu;
+
+        // Options
+        gboolean capture;
+};
+
+G_DEFINE_TYPE (GSSDPDeviceSnifferMainWindow,
+               gssdp_device_sniffer_main_window,
+               GTK_TYPE_APPLICATION_WINDOW)
+
+
+static void
+main_window_get_property (GObject *object,
+                          guint property_id,
+                          GValue *value,
+                          GParamSpec *pspec)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (object);
+
+        switch (property_id) {
+        case PROP_CAPTURE:
+                g_value_set_boolean (value, self->capture);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        }
+}
+
+static void
+main_window_set_property (GObject *object,
+                          guint property_id,
+                          const GValue *value,
+                          GParamSpec *pspec)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (object);
+
+        switch (property_id) {
+        case PROP_INTERFACE:
+                self->interface = g_value_dup_string (value);
+                break;
+        case PROP_ADDRESS_FAMILY:
+                self->family = g_value_get_enum (value);
+                break;
+        case PROP_CAPTURE:
+                self->capture = g_value_get_boolean (value);
+                const char *icon_name = self->capture
+                                    ? "media-playback-stop-symbolic"
+                                    : "media-playback-start-symbolic";
+
+                gtk_image_set_from_icon_name (GTK_IMAGE (self->capture_button_image),
+                                              icon_name,
+                                              GTK_ICON_SIZE_BUTTON);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        }
+}
+
+static const char *message_types[] = { "M-SEARCH", "RESPONSE", "NOTIFY" };
+
+static char **
+packet_to_treeview_data (const gchar *from_ip,
+                         char *client_interface,
+                         GDateTime *arrival_time,
+                         _GSSDPMessageType type,
+                         SoupMessageHeaders *headers)
+{
+        char **packet_data;
+        const char *target;
+
+        packet_data = g_new0 (char *, 6);
+
+        /* Set the Time */
+        packet_data[0] = g_date_time_format (arrival_time, "%R");
+
+        /* Now the Source Address */
+        packet_data[1] = g_strdup (from_ip);
+
+        packet_data[2] = client_interface;
+
+        /* Now the Packet Type */
+        packet_data[3] = g_strdup (message_types[type]);
+
+        /* Now the Packet Information */
+        if (type == _GSSDP_DISCOVERY_RESPONSE)
+                target = soup_message_headers_get_one (headers, "ST");
+        else
+                target = soup_message_headers_get_one (headers, "NT");
+
+        packet_data[4] = g_strdup (target);
+        packet_data[5] = NULL;
+
+        return packet_data;
+}
+
+static void
+append_packet (GSSDPDeviceSnifferMainWindow *self,
+               const gchar *from_ip,
+               GDateTime *arrival_time,
+               _GSSDPMessageType type,
+               SoupMessageHeaders *headers)
+{
+        GtkListStore *liststore;
+        GtkTreeIter iter;
+        char **packet_data;
+
+        liststore = GTK_LIST_STORE (gtk_tree_view_get_model (
+                GTK_TREE_VIEW (self->packet_treeview)));
+        g_assert (liststore != NULL);
+
+        char *client_interface =
+                g_strdup (gssdp_client_get_interface (self->client));
+
+        packet_data = packet_to_treeview_data (from_ip,
+                                               client_interface,
+                                               arrival_time,
+                                               type,
+                                               headers);
+
+        gtk_list_store_insert_with_values (liststore,
+                                           &iter,
+                                           0,
+                                           PACKET_STORE_COLUMN_TIME,
+                                           packet_data[0],
+                                           PACKET_STORE_COLUMN_IP,
+                                           packet_data[1],
+                                           PACKET_STORE_COLUMN_INTERFACE,
+                                           packet_data[2],
+                                           PACKET_STORE_COLUMN_PACKET_TYPE,
+                                           packet_data[3],
+                                           PACKET_STORE_COLUMN_TARGET,
+                                           packet_data[4],
+                                           PACKET_STORE_COLUMN_HEADERS,
+                                           headers,
+                                           PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME,
+                                           arrival_time,
+                                           -1);
+        g_strfreev (packet_data);
+}
+
+static void
+on_ssdp_message (GSSDPClient *ssdp_client,
+                 const gchar *from_ip,
+                 gushort from_port,
+                 _GSSDPMessageType type,
+                 SoupMessageHeaders *headers,
+                 gpointer user_data)
+{
+        GDateTime *arrival_time;
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        if (type == _GSSDP_DISCOVERY_REQUEST)
+                return;
+
+        if (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (self->searchbar))) {
+                const char *filter =
+                        gtk_entry_get_text (GTK_ENTRY (self->address_filter));
+                if (filter != NULL && strcmp (filter, from_ip) != 0)
+                        return;
+        }
+
+        if (!self->capture)
+                return;
+
+        arrival_time = g_date_time_new_now_local ();
+        append_packet (self, from_ip, arrival_time, type, headers);
+        g_date_time_unref (arrival_time);
+}
+
+static gboolean
+find_device (GtkTreeModel *model, const char *uuid, GtkTreeIter *iter)
+{
+        gboolean found = FALSE;
+        gboolean more;
+
+        more = gtk_tree_model_get_iter_first (model, iter);
+        while (more) {
+                char *device_uuid;
+                gtk_tree_model_get (model,
+                                    iter,
+                                    0, &device_uuid, -1);
+                found = g_strcmp0 (device_uuid, uuid) == 0;
+                g_free (device_uuid);
+
+                if (found)
+                        break;
+                more = gtk_tree_model_iter_next (model, iter);
+        }
+
+        return found;
+}
+
+static void
+append_device (GtkWidget *treeview,
+               const char *uuid,
+               const char *first_notify,
+               const char *device_type,
+               const char *location)
+{
+        GtkTreeModel *model;
+        GtkTreeIter iter;
+
+        model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
+
+        if (!find_device (model, uuid, &iter)) {
+                gtk_list_store_insert_with_values (GTK_LIST_STORE (model),
+                                                   &iter, 0,
+                                                   DEVICE_STORE_COLUMN_UUID, uuid,
+                                                   DEVICE_STORE_COLUMN_FIRST_SEEN, first_notify,
+                                                   DEVICE_STORE_COLUMN_LOCATION, location, -1);
+        }
+
+        if (device_type) {
+                gtk_list_store_set (GTK_LIST_STORE (model), &iter,
+                                    DEVICE_STORE_COLUMN_TYPE, device_type, -1);
+        }
+}
+
+static void
+resource_available_cb (GSSDPDeviceSnifferMainWindow *self,
+                       const char *usn,
+                       GList *locations,
+                       gpointer user_data)
+{
+
+        char **usn_tokens;
+        char *uuid;
+        char *device_type = NULL;
+        GDateTime *current_time = NULL;
+        char *first_notify;
+
+        current_time = g_date_time_new_now_local ();
+        first_notify = g_date_time_format (current_time, "%R");
+        g_date_time_unref (current_time);
+
+        usn_tokens = g_strsplit (usn, "::", -1);
+        g_assert (usn_tokens != NULL && usn_tokens[0] != NULL);
+
+        uuid = usn_tokens[0] + 5; /* skip the prefix 'uuid:' */
+
+        if (usn_tokens[1] && strlen(usn_tokens[1]) != 0) {
+                char **urn_tokens;
+
+                urn_tokens = g_strsplit (usn_tokens[1], ":device:", -1);
+
+                if (urn_tokens[1])
+                        device_type = g_strdup (urn_tokens[1]);
+                g_strfreev (urn_tokens);
+        }
+
+        /* Device Announcement */
+        append_device (self->device_treeview,
+                       uuid,
+                       first_notify,
+                       device_type,
+                       (char *) locations->data);
+
+        g_free (device_type);
+        g_free (first_notify);
+        g_strfreev (usn_tokens);
+}
+
+static void
+remove_device (GtkWidget *treeview, const char *uuid)
+{
+        GtkTreeModel *model;
+        GtkTreeIter iter;
+
+        model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
+
+        if (find_device (model, uuid, &iter)) {
+                gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
+        }
+}
+
+static void
+resource_unavailable_cb (GSSDPDeviceSnifferMainWindow *self,
+                         const char *usn,
+                         gpointer user_data)
+{
+        char **usn_tokens;
+        char *uuid;
+
+        usn_tokens = g_strsplit (usn, "::", -1);
+        g_assert (usn_tokens != NULL && usn_tokens[0] != NULL);
+        uuid = usn_tokens[0] + 5; /* skip the prefix 'uuid:' */
+
+        remove_device (self->device_treeview, uuid);
+
+        g_strfreev (usn_tokens);
+}
+
+static void
+main_window_constructed (GObject *object)
+{
+        if (G_OBJECT_CLASS (gssdp_device_sniffer_main_window_parent_class) != NULL)
+                G_OBJECT_CLASS (gssdp_device_sniffer_main_window_parent_class)
+                        ->constructed (object);
+
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (object);
+        GError *error = NULL;
+
+        self->client = GSSDP_CLIENT (g_initable_new (GSSDP_TYPE_CLIENT,
+                                                     NULL,
+                                                     &error,
+                                                     "address-family",
+                                                     self->family,
+                                                     "interface",
+                                                     self->interface,
+                                                     NULL));
+
+        if (error != NULL) {
+                g_critical ("Failed to create client: %s", error->message);
+                g_clear_error (&error);
+                // TODO: Show error dialog
+                return;
+        }
+
+        self->browser =
+                gssdp_resource_browser_new (self->client, GSSDP_ALL_RESOURCES);
+
+        g_signal_connect (self->client,
+                          "message-received",
+                          G_CALLBACK (on_ssdp_message),
+                          self);
+        g_signal_connect_swapped (self->browser,
+                                  "resource-available",
+                                  G_CALLBACK (resource_available_cb),
+                                  self);
+        g_signal_connect_swapped (self->browser,
+                                  "resource-unavailable",
+                                  G_CALLBACK (resource_unavailable_cb),
+                                  self);
+
+        gssdp_resource_browser_set_active (self->browser, TRUE);        
+}
+
+static void
+gssdp_device_sniffer_main_window_class_init (
+        GSSDPDeviceSnifferMainWindowClass *klass)
+{
+        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+        gtk_widget_class_set_template_from_resource (
+                widget_class,
+                "/org/gupnp/GSSDP/MainWindow.ui");
+        gtk_widget_class_bind_template_child (widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              packet_treeview);
+        gtk_widget_class_bind_template_child (widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              packet_textview);
+
+        gtk_widget_class_bind_template_child (widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              capture_button_image);
+
+        gtk_widget_class_bind_template_child (widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              device_treeview);
+
+        gtk_widget_class_bind_template_child (widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              details_scrolled);
+
+        gtk_widget_class_bind_template_child(widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              sniffer_context_menu);
+
+        gtk_widget_class_bind_template_child(widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              searchbar);
+
+        gtk_widget_class_bind_template_child(widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              address_filter);
+
+        gtk_widget_class_bind_template_child(widget_class,
+                                              GSSDPDeviceSnifferMainWindow,
+                                              searchbutton);
+
+
+        GObjectClass *object_class = G_OBJECT_CLASS (klass);
+        object_class->set_property = main_window_set_property;
+        object_class->get_property = main_window_get_property;
+        object_class->constructed = main_window_constructed;
+
+        g_object_class_install_property (
+                object_class,
+                PROP_ADDRESS_FAMILY,
+                g_param_spec_enum (
+                        "address-family",
+                        "Socket address familiy",
+                        "The socket address familiy of the SSDP client",
+                        G_TYPE_SOCKET_FAMILY,
+                        G_SOCKET_FAMILY_INVALID,
+                        G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+                                G_PARAM_STATIC_STRINGS));
+
+        g_object_class_install_property (
+                object_class,
+                PROP_INTERFACE,
+                g_param_spec_string ("interface",
+                                     "Network interface",
+                                     "The network interface of the SSDP client",
+                                     NULL,
+                                     G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+                                             G_PARAM_STATIC_STRINGS));
+
+        g_object_class_install_property (
+                object_class,
+                PROP_CAPTURE,
+                g_param_spec_boolean (
+                        "capture",
+                        "Packet capture state",
+                        "Whether or not packet capturing is enabled",
+                        TRUE,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
+                                G_PARAM_STATIC_STRINGS));
+}
+
+static void
+on_realize (GtkWidget *self, gpointer user_data)
+{
+        double w;
+        double h;
+#if GTK_CHECK_VERSION(3, 22, 0)
+        {
+                GdkWindow *window = gtk_widget_get_window (self);
+                GdkDisplay *display = gdk_display_get_default ();
+                GdkMonitor *monitor =
+                        gdk_display_get_monitor_at_window (display, window);
+                GdkRectangle rectangle;
+
+                gdk_monitor_get_geometry (monitor, &rectangle);
+                w = rectangle.width * 0.75;
+                h = rectangle.height * 0.75;
+        }
+
+#else
+        w = gdk_screen_width () * 0.75;
+        h = gdk_screen_height () * 0.75;
+#endif
+
+        int window_width = CLAMP ((int) w, 10, 1000);
+        int window_height = CLAMP ((int) h, 10, 800);
+        gtk_window_set_default_size (GTK_WINDOW (self),
+                                     window_width,
+                                     window_height);
+}
+
+static void
+packet_header_to_string (const char *header_name,
+                         const char *header_val,
+                         GString **text)
+{
+        g_string_append_printf (*text, "%s: %s\n", header_name, header_val);
+}
+
+static void
+clear_textbuffer (GtkTextBuffer *textbuffer)
+{
+        GtkTextIter start, end;
+
+        gtk_text_buffer_get_bounds (textbuffer, &start, &end);
+        gtk_text_buffer_delete (textbuffer, &start, &end);
+}
+
+static void
+update_packet_details (GSSDPDeviceSnifferMainWindow *self,
+                       const char *text,
+                       int len)
+{
+        GtkTextBuffer *textbuffer;
+
+        textbuffer = gtk_text_view_get_buffer (
+                GTK_TEXT_VIEW (self->packet_textview));
+
+        clear_textbuffer (textbuffer);
+        gtk_text_buffer_insert_at_cursor (textbuffer, text, len);
+}
+
+static void
+display_packet (GSSDPDeviceSnifferMainWindow *self,
+                GDateTime *arrival_time,
+                SoupMessageHeaders *packet_headers)
+{
+        GString *text;
+        char *time = NULL;
+
+        time = g_date_time_format_iso8601 (arrival_time);
+        text = g_string_new ("Received on: ");
+        g_string_append (text, time);
+        g_string_append (text, "\nHeaders:\n\n");
+        g_free (time);
+
+        soup_message_headers_foreach (
+                packet_headers,
+                (SoupMessageHeadersForeachFunc) packet_header_to_string,
+                &text);
+
+        update_packet_details (self, text->str, text->len);
+        g_string_free (text, TRUE);
+}
+
+static void
+on_packet_selected (GtkTreeSelection *selection, gpointer user_data)
+{
+        GtkTreeModel *model;
+        GtkTreeIter iter;
+        GDateTime *arrival_time;
+
+        if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
+                SoupMessageHeaders *packet_headers;
+
+                gtk_tree_model_get (model,
+                                    &iter,
+                                    PACKET_STORE_COLUMN_HEADERS,
+                                    &packet_headers,
+                                    PACKET_STORE_COLUMN_RAW_ARRIVAL_TIME,
+                                    &arrival_time,
+                                    -1);
+                display_packet (GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data),
+                                arrival_time,
+                                packet_headers);
+                g_boxed_free (SOUP_TYPE_MESSAGE_HEADERS, packet_headers);
+                g_date_time_unref (arrival_time);
+        }
+
+        else
+                update_packet_details (
+                        GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data),
+                        "",
+                        0);
+}
+
+static void
+on_clear_capture (GSimpleAction *action,
+                  GVariant *parameter,
+                  gpointer user_data)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        GtkTreeModel *model;
+
+        model = gtk_tree_view_get_model (GTK_TREE_VIEW (self->packet_treeview));
+        gtk_list_store_clear (GTK_LIST_STORE (model));
+}
+
+static void
+on_trigger_rescan (GSimpleAction *action,
+                   GVariant *parameter,
+                   gpointer user_data)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        gssdp_resource_browser_rescan (self->browser);
+}
+
+static void
+on_about (GSimpleAction *action, GVariant *parameter, gpointer user_data)
+{
+        const char *AUTHORS[] = { "Zeeshan Ali (Khattak) <zeeshanak gnome org>",
+                                  NULL };
+        gtk_show_about_dialog (
+                GTK_WINDOW (user_data),
+                "copyright",
+                _ ("Copyright © 2007 Zeeshan Ali (Khattak)"),
+                "comments",
+                _ ("A Device Sniffer tool based on GSSDP framework. Inspired "
+                   "by Intel Tools for UPnP."),
+                "authors",
+                AUTHORS,
+                // TRANSLATORS: Replace this string with your names, one per
+                // line
+                "translator_credits",
+                "translator-credits",
+                "license-type",
+                GTK_LICENSE_LGPL_2_1,
+                NULL);
+}
+
+static void
+on_set_address_filter (GSimpleAction *action,
+                       GVariant *parameter,
+                       gpointer user_data)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        GtkTreeSelection *selection = NULL;
+        GtkTreeModel *model = NULL;
+        GtkTreeIter iter;
+        char *ip_filter;
+
+        selection = gtk_tree_view_get_selection (
+                GTK_TREE_VIEW (self->packet_treeview));
+        gtk_tree_selection_get_selected (selection, &model, &iter);
+        gtk_tree_model_get (model,
+                            &iter,
+                            PACKET_STORE_COLUMN_IP,
+                            &ip_filter,
+                            -1);
+        gtk_entry_set_text (GTK_ENTRY (self->address_filter), ip_filter);
+        g_free (ip_filter);
+
+        gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (self->searchbar), TRUE);
+}
+
+
+static GActionEntry actions[] = { { "clear-capture", on_clear_capture },
+                                  { "trigger-rescan", on_trigger_rescan },
+                                  { "about", on_about },
+                                  { "set-address-filter",
+                                    on_set_address_filter } };
+
+static void
+do_popup_menu (GSSDPDeviceSnifferMainWindow *self, GdkEventButton *event)
+{
+        gtk_menu_popup_at_pointer (GTK_MENU (self->context_menu),
+                                   (event != NULL) ? (GdkEvent *) event
+                                                   : gtk_get_current_event ());
+}
+
+static gboolean
+on_popup_menu (GtkWidget *widget, gpointer user_data)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        do_popup_menu (self, NULL);
+
+        return TRUE;
+}
+
+static gboolean
+on_button_release (GtkWidget *widget, GdkEventButton *event, gpointer user_data)
+{
+        GSSDPDeviceSnifferMainWindow *self =
+                GSSDP_DEVICE_SNIFFER_MAIN_WINDOW (user_data);
+
+        GtkTreeSelection *selection;
+        GtkTreeModel *model;
+        GtkTreeIter iter;
+        if (event->type != GDK_BUTTON_RELEASE || event->button != 3) {
+                return FALSE;
+        }
+
+        selection = gtk_tree_view_get_selection (
+                GTK_TREE_VIEW (self->packet_treeview));
+        if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
+                return FALSE;
+        }
+
+        do_popup_menu (self, event);
+
+        return TRUE;
+}
+
+static void
+gssdp_device_sniffer_main_window_init (GSSDPDeviceSnifferMainWindow *self)
+{
+        gtk_widget_init_template (GTK_WIDGET (self));
+
+        self->context_menu =
+                gtk_menu_new_from_model (self->sniffer_context_menu);
+
+        gtk_menu_attach_to_widget(GTK_MENU (self->context_menu), self->packet_treeview, NULL);
+
+        g_signal_connect (G_OBJECT (self),
+                          "realize",
+                          G_CALLBACK (on_realize),
+                          NULL);
+
+        GtkTreeSelection *selection = gtk_tree_view_get_selection (
+                GTK_TREE_VIEW (self->packet_treeview));
+        g_signal_connect (G_OBJECT (selection),
+                          "changed",
+                          G_CALLBACK (on_packet_selected),
+                          self);
+
+        g_signal_connect (G_OBJECT (self->packet_treeview),
+                          "popup-menu",
+                          G_CALLBACK (on_popup_menu),
+                          self);
+
+        g_signal_connect (G_OBJECT (self->packet_treeview),
+                          "button-release-event",
+                          G_CALLBACK (on_button_release),
+                          self);
+
+
+        GPropertyAction *action =
+                g_property_action_new ("toggle-capture", self, "capture");        
+        g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
+        action = g_property_action_new ("show-packet-details",
+                                        self->details_scrolled,
+                                        "visible");
+        g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
+
+        g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                         actions,
+                                         G_N_ELEMENTS (actions),
+                                         self);
+
+        g_object_bind_property (self->searchbutton,
+                                "active",
+                                self->searchbar,
+                                "search-mode-enabled",
+                                G_BINDING_BIDIRECTIONAL |
+                                G_BINDING_SYNC_CREATE);
+}
diff --git a/tools/main-window.h b/tools/main-window.h
new file mode 100644
index 0000000..ba8877f
--- /dev/null
+++ b/tools/main-window.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSSDP_DEVICE_SNIFFER_TYPE_MAIN_WINDOW                                  \
+        (gssdp_device_sniffer_main_window_get_type ())
+
+G_DECLARE_FINAL_TYPE (GSSDPDeviceSnifferMainWindow,
+                      gssdp_device_sniffer_main_window,
+                      GSSDP_DEVICE_SNIFFER,
+                      MAIN_WINDOW,
+                      GtkApplicationWindow)
+
+G_END_DECLS
diff --git a/tools/main-window.ui b/tools/main-window.ui
new file mode 100644
index 0000000..13d5ed9
--- /dev/null
+++ b/tools/main-window.ui
@@ -0,0 +1,395 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.2 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkListStore" id="device_store">
+    <columns>
+      <!-- column-name uid -->
+      <column type="gchararray"/>
+      <!-- column-name first-seen -->
+      <column type="gchararray"/>
+      <!-- column-name device-type -->
+      <column type="gchararray"/>
+      <!-- column-name location -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkListStore" id="packet_store">
+    <columns>
+      <!-- column-name time -->
+      <column type="gchararray"/>
+      <!-- column-name source-address -->
+      <column type="gchararray"/>
+      <!-- column-name interface -->
+      <column type="gchararray"/>
+      <!-- column-name type -->
+      <column type="gchararray"/>
+      <!-- column-name information -->
+      <column type="gchararray"/>
+      <!-- column-name SoupMessageHeader1 -->
+      <column type="SoupMessageHeaders"/>
+      <!-- column-name GDateTime1 -->
+      <column type="GDateTime"/>
+    </columns>
+  </object>
+  <menu id="sniffer-window-menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Show packet details</attribute>
+        <attribute name="action">win.show-packet-details</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_About</attribute>
+        <attribute name="action">win.about</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="GSSDPDeviceSnifferMainWindow" parent="GtkApplicationWindow">
+    <property name="can_focus">False</property>
+    <property name="can_focus">False</property>
+    <child>
+      <menu id="sniffer_context_menu">
+        <section>
+          <item>
+            <attribute name="label" translatable="yes">Set as filter</attribute>
+            <attribute name="action">win.set-address-filter</attribute>
+          </item>
+        </section>
+      </menu>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="show_close_button">True</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="tooltip_text" translatable="yes">Clear packet capture</property>
+                <property name="action_name">win.clear-capture</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="icon_name">edit-clear-all-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkToggleButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="tooltip_text" translatable="yes">Toggle packet capture</property>
+                <property name="active">True</property>
+                <property name="action_name">win.toggle-capture</property>
+                <child>
+                  <object class="GtkImage" id="capture_button_image">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="icon_name">media-playback-stop-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="tooltip_text" translatable="yes">Trigger a rescan of the network</property>
+                <property name="action_name">win.trigger-rescan</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="icon_name">view-refresh-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child type="title">
+          <object class="GtkStackSwitcher">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="icon_size">0</property>
+            <property name="stack">stack1</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="searchbutton">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="tooltip_text" translatable="yes">Enable content filter</property>
+            <property name="active">False</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">system-search-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkMenuButton" id="window-menu">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="menu-model">sniffer-window-menu</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">open-menu-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkStack" id="stack1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkBox" id="vbox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="border_width">6</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkSearchBar" id="searchbar">
+                <property name="visible">True</property>
+                <property name="show_close_button">True</property>
+                <property name="search-mode-enabled">True</property>
+                <child>
+                 <object class="GtkSearchEntry" id="address_filter">
+                   <property name="visible">True</property>
+                   <property name="max-width-chars">40</property>
+                 </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="packet-scrolledwindow">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkTreeView" id="packet_treeview">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="model">packet_store</property>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection"/>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Time</property>
+                        <child>
+                          <object class="GtkCellRendererText"/>
+                          <attributes>
+                            <attribute name="markup">0</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Source Address</property>
+                        <child>
+                          <object class="GtkCellRendererText"/>
+                          <attributes>
+                            <attribute name="markup">1</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Interface</property>
+                        <child>
+                          <object class="GtkCellRendererText"/>
+                          <attributes>
+                            <attribute name="markup">2</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Packet Type</property>
+                        <child>
+                          <object class="GtkCellRendererText"/>
+                          <attributes>
+                            <attribute name="markup">3</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkTreeViewColumn">
+                        <property name="title" translatable="yes">Packet Information</property>
+                        <child>
+                          <object class="GtkCellRendererText"/>
+                          <attributes>
+                            <attribute name="markup">4</attribute>
+                          </attributes>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="details_scrolled">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <object class="GtkTextView" id="packet_textview">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="editable">False</property>
+                    <property name="monospace">True</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">page0</property>
+            <property name="title" translatable="yes">Packets</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="device-details-scrolledwindow">
+            <property name="name">Bar</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="margin_top">6</property>
+            <property name="margin_bottom">6</property>
+            <property name="shadow_type">in</property>
+            <child>
+              <object class="GtkTreeView" id="device_treeview">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="model">device_store</property>
+                <child internal-child="selection">
+                  <object class="GtkTreeSelection"/>
+                </child>
+                <child>
+                  <object class="GtkTreeViewColumn">
+                    <property name="title" translatable="yes">Unique Identifier</property>
+                    <child>
+                      <object class="GtkCellRendererText"/>
+                      <attributes>
+                        <attribute name="text">0</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkTreeViewColumn">
+                    <property name="title" translatable="yes">First Notify</property>
+                    <child>
+                      <object class="GtkCellRendererText"/>
+                      <attributes>
+                        <attribute name="text">1</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkTreeViewColumn">
+                    <property name="title" translatable="yes">Device Type</property>
+                    <child>
+                      <object class="GtkCellRendererText"/>
+                      <attributes>
+                        <attribute name="text">2</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkTreeViewColumn">
+                    <property name="title" translatable="yes">Location</property>
+                    <child>
+                      <object class="GtkCellRendererText"/>
+                      <attributes>
+                        <attribute name="text">3</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">page1</property>
+            <property name="title" translatable="yes">Device history</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+   </template>
+</interface>
diff --git a/tools/meson.build b/tools/meson.build
index bfd44d2..59c5e74 100644
--- a/tools/meson.build
+++ b/tools/meson.build
@@ -7,6 +7,8 @@ sniffer = executable(
     'gssdp-device-sniffer',
     [
         'gssdp-device-sniffer.c',
+        'main-window.c',
+        'main-window.h',
         resource
     ],
     dependencies : [gssdp, gtk],
diff --git a/tools/window-menu.ui b/tools/window-menu.ui
index eae5745..a6bd31c 100644
--- a/tools/window-menu.ui
+++ b/tools/window-menu.ui
@@ -2,24 +2,6 @@
 <!-- Generated with glade 3.22.0 -->
 <interface>
   <requires lib="gtk+" version="3.12"/>
-  <menu id="sniffer-window-menu">
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">Show packet details</attribute>
-        <attribute name="action">win.show-packet-details</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Address Filter…</attribute>
-        <attribute name="action">win.show-address-filter</attribute>
-      </item>
-    </section>
-    <section>
-      <item>
-        <attribute name="label" translatable="yes">_About</attribute>
-        <attribute name="action">win.about</attribute>
-      </item>
-    </section>
-  </menu>
   <menu id="sniffer-context-menu">
     <section>
       <item>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]