[librest] demo: add authentication methods



commit 110734aaee4675535f4f28bd8fc60d7aba2e5645
Author: Günther Wagner <info gunibert de>
Date:   Tue Feb 1 22:05:39 2022 +0100

    demo: add authentication methods

 examples/demo/demo-main.c                      |   2 +
 examples/demo/demo-rest-page.c                 | 437 ++++++++++++++++++++++++-
 examples/demo/demo-rest-page.ui                | 333 +++++++++++++++++++
 examples/demo/demo-table-row.c                 | 149 +++++++++
 examples/demo/demo-table-row.h                 |  35 ++
 examples/demo/demo-table-row.ui                |  36 ++
 examples/demo/demo-table.c                     | 210 ++++++++++++
 examples/demo/demo-table.h                     |  34 ++
 examples/demo/demo-window.c                    |  10 -
 examples/demo/meson.build                      |   2 +
 examples/demo/org.gnome.RestDemo.gresource.xml |   2 +
 examples/demo/org.gnome.RestDemo.json          |  14 +-
 examples/demo/style.css                        |   3 +
 13 files changed, 1255 insertions(+), 12 deletions(-)
---
diff --git a/examples/demo/demo-main.c b/examples/demo/demo-main.c
index 8f12404..f4f6029 100644
--- a/examples/demo/demo-main.c
+++ b/examples/demo/demo-main.c
@@ -23,6 +23,7 @@
 #include <adwaita.h>
 #include <gtksourceview/gtksource.h>
 #include "demo-window.h"
+#include "demo-table.h"
 
 static void
 on_activate (GApplication *app, gpointer user_data)
@@ -76,6 +77,7 @@ main (gint   argc,
 {
   adw_init ();
   gtk_source_init ();
+  g_type_ensure (DEMO_TYPE_TABLE);
 
   g_set_prgname ("librest-demo");
   g_set_application_name ("librest-demo");
diff --git a/examples/demo/demo-rest-page.c b/examples/demo/demo-rest-page.c
index 05290c0..46ebfec 100644
--- a/examples/demo/demo-rest-page.c
+++ b/examples/demo/demo-rest-page.c
@@ -20,8 +20,11 @@
 
 #include "demo-rest-page.h"
 #include <rest/rest.h>
+#include <libsoup/soup.h>
 #include <gtksourceview/gtksource.h>
 #include <json-glib/json-glib.h>
+#include "demo-table.h"
+#include "demo-table-row.h"
 
 struct _DemoRestPage
 {
@@ -33,8 +36,45 @@ struct _DemoRestPage
   GtkWidget *sourceview;
   GtkWidget *body;
   GtkWidget *notebook;
+  GtkWidget *authentication;
+  GtkWidget *authmethods;
+  GtkWidget *authentication_stack;
+  GtkWidget *headers;
+  GtkWidget *headerslb;
+  GtkWidget *params;
+  GtkWidget *paramslb;
+
+  /* basic auth */
+  GtkWidget *basic_username;
+  GtkWidget *basic_password;
+
+  /* digest auth */
+  GtkWidget *digest_username;
+  GtkWidget *digest_password;
+
+  /* oauth 1 auth */
+  GtkWidget *oauth1_client_identifier;
+  GtkWidget *oauth1_client_secret;
+  RestProxy *oauth1_proxy;
+
+  /* oauth 2 auth */
+  GtkWidget *oauth2_client_identifier;
+  GtkWidget *oauth2_client_secret;
+  GtkWidget *oauth2_auth_url;
+  GtkWidget *oauth2_token_url;
+  GtkWidget *oauth2_redirect_url;
+  RestProxy *oauth2_proxy;
+  RestPkceCodeChallenge *pkce;
 };
 
+typedef enum {
+  AUTHMODE_NO,
+  AUTHMODE_BASIC,
+  AUTHMODE_DIGEST,
+  AUTHMODE_OAUTH1,
+  AUTHMODE_OAUTH2
+} AuthMode;
+
 G_DEFINE_FINAL_TYPE (DemoRestPage, demo_rest_page, GTK_TYPE_BOX)
 
 DemoRestPage *
@@ -48,9 +88,35 @@ demo_rest_page_finalize (GObject *object)
 {
   DemoRestPage *self = (DemoRestPage *)object;
 
+  g_clear_object (&self->oauth1_proxy);
+  g_clear_object (&self->oauth2_proxy);
+  g_clear_pointer (&self->pkce, rest_pkce_code_challenge_free);
+
   G_OBJECT_CLASS (demo_rest_page_parent_class)->finalize (object);
 }
 
+static AuthMode
+get_current_auth_mode (DemoRestPage *self)
+{
+  const gchar *stack_name;
+
+  g_return_val_if_fail (DEMO_IS_REST_PAGE (self), 0);
+
+  stack_name = gtk_stack_get_visible_child_name (GTK_STACK (self->authentication_stack));
+  if (g_strcmp0 (stack_name, "no_auth") == 0)
+    return AUTHMODE_NO;
+  else if (g_strcmp0 (stack_name, "basic") == 0)
+    return AUTHMODE_BASIC;
+  else if (g_strcmp0 (stack_name, "digest") == 0)
+    return AUTHMODE_DIGEST;
+  else if (g_strcmp0 (stack_name, "oauth1") == 0)
+    return AUTHMODE_OAUTH1;
+  else if (g_strcmp0 (stack_name, "oauth2") == 0)
+    return AUTHMODE_OAUTH2;
+
+  return AUTHMODE_NO;
+}
+
 static void
 populate_http_method (DemoRestPage *self)
 {
@@ -82,6 +148,265 @@ set_json_response (DemoRestPage  *self,
   GtkSourceLanguageManager *manager = gtk_source_language_manager_get_default ();
   GtkSourceLanguage *lang = gtk_source_language_manager_guess_language (manager, NULL, "application/json");
   gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), lang);
+  gtk_notebook_set_current_page (GTK_NOTEBOOK (self->notebook), 0);
+}
+
+static void
+set_text_response (DemoRestPage  *self,
+                   RestProxyCall *call)
+{
+  g_autoptr(GHashTable) response_headers = NULL;
+
+  const gchar *payload = rest_proxy_call_get_payload (call);
+  goffset payload_length = rest_proxy_call_get_payload_length (call);
+  response_headers = rest_proxy_call_get_response_headers (call);
+
+  GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->sourceview));
+  gtk_text_buffer_set_text (buffer, payload, payload_length);
+
+  GtkSourceLanguageManager *manager = gtk_source_language_manager_get_default ();
+  GtkSourceLanguage *lang = gtk_source_language_manager_guess_language (manager, NULL, g_hash_table_lookup 
(response_headers, "Content-Type"));
+  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), lang);
+  gtk_notebook_set_current_page (GTK_NOTEBOOK (self->notebook), 0);
+}
+
+static void
+demo_rest_page_fetched_oauth1_access_token (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  RestProxy *proxy = (RestProxy *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  oauth_proxy_access_token_finish (OAUTH_PROXY (proxy), result, &error);
+}
+
+static void
+demo_rest_page_fetched_oauth2_access_token (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  RestProxy *proxy = (RestProxy *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  rest_oauth2_proxy_fetch_access_token_finish (REST_OAUTH2_PROXY (proxy), result, &error);
+}
+
+static void
+oauth1_dialog_response (GtkDialog    *dialog,
+                        gint          response_id,
+                        DemoRestPage *self)
+{
+  switch (response_id)
+    {
+    case GTK_RESPONSE_OK:
+      {
+        const gchar *verifier = NULL;
+        GtkWidget *content_area = gtk_dialog_get_content_area (dialog);
+        GtkWidget *box = gtk_widget_get_first_child (content_area);
+        GtkWidget *entry = gtk_widget_get_last_child (box);
+
+        verifier = gtk_editable_get_text (GTK_EDITABLE (entry));
+        oauth_proxy_access_token_async (OAUTH_PROXY (self->oauth1_proxy),
+                                        "access_token",
+                                        verifier,
+                                        NULL,
+                                        demo_rest_page_fetched_oauth1_access_token,
+                                        self);
+        break;
+      }
+    case GTK_RESPONSE_CANCEL:
+      g_clear_object (&self->oauth1_proxy);
+      break;
+    }
+}
+
+static void
+oauth2_dialog_response (GtkDialog    *dialog,
+                        gint          response_id,
+                        DemoRestPage *self)
+{
+  switch (response_id)
+    {
+    case GTK_RESPONSE_OK:
+      {
+        const gchar *code = NULL;
+        GtkWidget *content_area = gtk_dialog_get_content_area (dialog);
+        GtkWidget *box = gtk_widget_get_first_child (content_area);
+        GtkWidget *entry = gtk_widget_get_last_child (box);
+
+        code = gtk_editable_get_text (GTK_EDITABLE (entry));
+        rest_oauth2_proxy_fetch_access_token_async (REST_OAUTH2_PROXY (self->oauth2_proxy),
+                                                    code,
+                                                    rest_pkce_code_challenge_get_verifier (self->pkce),
+                                                    NULL,
+                                                    demo_rest_page_fetched_oauth2_access_token,
+                                                    self);
+        break;
+      }
+    case GTK_RESPONSE_CANCEL:
+      g_clear_object (&self->oauth2_proxy);
+      break;
+    }
+}
+
+static GtkWidget *
+demo_rest_page_create_oauth1_dialog (DemoRestPage *self,
+                                     RestProxy    *proxy)
+{
+  GtkWidget *dialog = NULL;
+  GtkWidget *content_area;
+  GtkWidget *box, *lbl, *token_lbl, *verifier_entry;
+  g_autofree char *token_str = NULL;
+
+  dialog = gtk_dialog_new_with_buttons ("Get Verifier...",
+                                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
+                                        GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | 
GTK_DIALOG_USE_HEADER_BAR,
+                                        "Ok",
+                                        GTK_RESPONSE_OK,
+                                        "Cancel",
+                                        GTK_RESPONSE_CANCEL,
+                                        NULL);
+
+  content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+  gtk_widget_set_margin_top (content_area, 6);
+  gtk_widget_set_margin_start (content_area, 6);
+  gtk_widget_set_margin_bottom (content_area, 6);
+  gtk_widget_set_margin_end (content_area, 6);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+  lbl = gtk_label_new ("Open a browser and authorize this application...");
+  gtk_box_append (GTK_BOX (box), lbl);
+  token_str = g_strdup_printf ("Use this token: %s", oauth_proxy_get_token (OAUTH_PROXY (proxy)));
+  token_lbl = gtk_label_new (token_str);
+  gtk_label_set_selectable (GTK_LABEL (token_lbl), TRUE);
+  gtk_box_append (GTK_BOX (box), token_lbl);
+  verifier_entry = gtk_entry_new ();
+  gtk_box_append (GTK_BOX (box), verifier_entry);
+
+  gtk_box_append (GTK_BOX (content_area), box);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (oauth1_dialog_response), self);
+  g_signal_connect_swapped (dialog, "response", G_CALLBACK (gtk_window_destroy), dialog);
+
+  return dialog;
+}
+
+static GtkWidget *
+demo_rest_page_create_oauth2_dialog (DemoRestPage *self,
+                                     RestProxy    *proxy)
+{
+  GtkWidget *dialog = NULL;
+  GtkWidget *content_area;
+  GtkWidget *box, *lbl, *token_lbl, *verifier_entry;
+  g_autofree char *token_str = NULL;
+
+  dialog = gtk_dialog_new_with_buttons ("Get Code...",
+                                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
+                                        GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | 
GTK_DIALOG_USE_HEADER_BAR,
+                                        "Ok",
+                                        GTK_RESPONSE_OK,
+                                        "Cancel",
+                                        GTK_RESPONSE_CANCEL,
+                                        NULL);
+
+  content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+  gtk_widget_set_margin_top (content_area, 6);
+  gtk_widget_set_margin_start (content_area, 6);
+  gtk_widget_set_margin_bottom (content_area, 6);
+  gtk_widget_set_margin_end (content_area, 6);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+  lbl = gtk_label_new ("Open a browser and authorize this application:");
+  gtk_box_append (GTK_BOX (box), lbl);
+  token_str = g_markup_printf_escaped ("Use this <a href=\"%s\">link</a>", 
rest_oauth2_proxy_build_authorization_url (REST_OAUTH2_PROXY (self->oauth2_proxy),
+                                                                                               
rest_pkce_code_challenge_get_challenge (self->pkce),
+                                                                                               NULL,
+                                                                                               NULL));
+  token_lbl = gtk_label_new (NULL);
+  gtk_label_set_markup (GTK_LABEL (token_lbl), token_str);
+  gtk_label_set_selectable (GTK_LABEL (token_lbl), TRUE);
+  gtk_box_append (GTK_BOX (box), token_lbl);
+  verifier_entry = gtk_entry_new ();
+  gtk_box_append (GTK_BOX (box), verifier_entry);
+
+  gtk_box_append (GTK_BOX (content_area), box);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (oauth2_dialog_response), self);
+  g_signal_connect_swapped (dialog, "response", G_CALLBACK (gtk_window_destroy), dialog);
+
+  return dialog;
+}
+
+static void
+demo_rest_page_fetched_oauth1_request_token (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data)
+{
+  DemoRestPage *self = (DemoRestPage *)user_data;
+  RestProxy *proxy = (RestProxy *)object;
+  GtkWidget *dialog = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  oauth_proxy_request_token_finish (OAUTH_PROXY (proxy), result, &error);
+
+  /* here we show a dialog requesting the user to a browser for authentication */
+  /* g_print ("%s\n", oauth_proxy_get_token (proxy)); */
+  dialog = demo_rest_page_create_oauth1_dialog (self, proxy);
+
+  gtk_widget_show (dialog);
+}
+
+static void
+on_oauth1_get_access_token_clicked (GtkButton *btn,
+                                    gpointer   user_data)
+{
+  DemoRestPage *self = (DemoRestPage *)user_data;
+  const char *url = NULL;
+  const char *consumer_key = NULL, *consumer_secret = NULL;
+  const char *function = NULL;
+
+  url = gtk_editable_get_text (GTK_EDITABLE (self->host));
+  consumer_key = gtk_editable_get_text (GTK_EDITABLE (self->oauth1_client_identifier));
+  consumer_secret = gtk_editable_get_text (GTK_EDITABLE (self->oauth1_client_secret));
+  function = gtk_editable_get_text (GTK_EDITABLE (self->function));
+
+  self->oauth1_proxy = oauth_proxy_new (consumer_key, consumer_secret, url, FALSE);
+  oauth_proxy_request_token_async (OAUTH_PROXY (self->oauth1_proxy), function, "https://www.gnome.org";, 
NULL, demo_rest_page_fetched_oauth1_request_token, self);
+}
+
+static void
+on_oauth2_get_access_token_clicked (GtkButton *btn,
+                                    gpointer   user_data)
+{
+  DemoRestPage *self = (DemoRestPage *)user_data;
+  GtkWidget *dialog = NULL;
+  const char *url = NULL;
+  const char *client_id = NULL, *client_secret = NULL;
+  const char *authurl = NULL, *tokenurl = NULL, *redirecturl = NULL;
+
+  url = gtk_editable_get_text (GTK_EDITABLE (self->host));
+  client_id = gtk_editable_get_text (GTK_EDITABLE (self->oauth2_client_identifier));
+  client_secret = gtk_editable_get_text (GTK_EDITABLE (self->oauth2_client_secret));
+  authurl = gtk_editable_get_text (GTK_EDITABLE (self->oauth2_auth_url));
+  tokenurl = gtk_editable_get_text (GTK_EDITABLE (self->oauth2_token_url));
+  redirecturl = gtk_editable_get_text (GTK_EDITABLE (self->oauth2_redirect_url));
+
+  self->oauth2_proxy = REST_PROXY (rest_oauth2_proxy_new (authurl, tokenurl, redirecturl, client_id, 
client_secret, url));
+  self->pkce = rest_pkce_code_challenge_new_random ();
+
+  dialog = demo_rest_page_create_oauth2_dialog (self, self->oauth2_proxy);
+
+  gtk_widget_show (dialog);
 }
 
 static void
@@ -103,6 +428,8 @@ call_returned (GObject      *object,
     {
       if (g_strcmp0 (content_type, "application/json") == 0)
         set_json_response (self, call);
+      else
+        set_text_response (self, call);
     }
 }
 
@@ -116,23 +443,100 @@ on_send_clicked (GtkButton *btn,
   const gchar *function;
   guint selected_method;
   GListModel *model;
+  AuthMode auth_mode;
+  RestProxy *proxy;
 
   g_return_if_fail (DEMO_IS_REST_PAGE (self));
 
   selected_method = gtk_drop_down_get_selected (GTK_DROP_DOWN (self->httpmethod));
   model = gtk_drop_down_get_model (GTK_DROP_DOWN (self->httpmethod));
   http = gtk_string_list_get_string (GTK_STRING_LIST (model), selected_method);
+  auth_mode = get_current_auth_mode (self);
 
   url = gtk_editable_get_text (GTK_EDITABLE (self->host));
   function = gtk_editable_get_text (GTK_EDITABLE (self->function));
-  RestProxy *proxy = rest_proxy_new (url, FALSE);
+  switch (auth_mode)
+    {
+    case AUTHMODE_NO:
+      proxy = rest_proxy_new (url, FALSE);
+      break;
+    // TODO: provide a direct auth possibility in librest
+    case AUTHMODE_BASIC:
+      {
+        const gchar *username, *password;
+
+        username = gtk_editable_get_text (GTK_EDITABLE (self->basic_username));
+        password = gtk_editable_get_text (GTK_EDITABLE (self->basic_password));
+
+        proxy = rest_proxy_new_with_authentication (url, FALSE, username, password);
+        break;
+      }
+    // thanks to libsoup there is no difference as the server defines the used authentication mechanism
+    case AUTHMODE_DIGEST:
+      {
+        const gchar *username, *password;
+
+        username = gtk_editable_get_text (GTK_EDITABLE (self->basic_username));
+        password = gtk_editable_get_text (GTK_EDITABLE (self->basic_password));
+
+        proxy = rest_proxy_new_with_authentication (url, FALSE, username, password);
+        break;
+      }
+    case AUTHMODE_OAUTH1:
+      {
+        g_object_set (self->oauth1_proxy, "url-format", url, NULL);
+        proxy = self->oauth1_proxy;
+
+        break;
+      }
+    case AUTHMODE_OAUTH2:
+      {
+        proxy = rest_proxy_new (url, FALSE);
+        break;
+      }
+    }
+  SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_BODY);
+  rest_proxy_add_soup_feature (proxy, SOUP_SESSION_FEATURE (logger));
 
   RestProxyCall *call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_method (call, http);
   rest_proxy_call_set_function (call, function);
+
+  /* get params */
+  model = demo_table_get_model (DEMO_TABLE (self->paramslb));
+  for (guint i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      DemoTableRow *h = (DemoTableRow *)g_list_model_get_item (model, i);
+      rest_proxy_call_add_param (call, demo_table_row_get_key (h), demo_table_row_get_value (h));
+    }
+
+  /* get headers */
+  model = demo_table_get_model (DEMO_TABLE (self->headerslb));
+  for (guint i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      DemoTableRow *h = (DemoTableRow *)g_list_model_get_item (model, i);
+      rest_proxy_call_add_header (call, demo_table_row_get_key (h), demo_table_row_get_value (h));
+    }
+
   rest_proxy_call_invoke_async (call, NULL, call_returned, self);
 }
 
+static void
+on_auth_method_activated (GtkDropDown *dropdown,
+                          GParamSpec *pspec,
+                          gpointer     user_data)
+{
+  DemoRestPage *self = (DemoRestPage *)user_data;
+  g_autofree gchar *page_name = NULL;
+  GtkStringObject *obj = NULL;
+
+  g_return_if_fail (DEMO_IS_REST_PAGE (self));
+
+  obj = GTK_STRING_OBJECT (gtk_drop_down_get_selected_item (dropdown));
+  page_name = g_utf8_strdown (gtk_string_object_get_string (obj), -1);
+  gtk_stack_set_visible_child_name (GTK_STACK (self->authentication_stack), g_strdelimit (page_name, " ", 
'_'));
+}
+
 static void
 demo_rest_page_class_init (DemoRestPageClass *klass)
 {
@@ -147,7 +551,35 @@ demo_rest_page_class_init (DemoRestPageClass *klass)
   gtk_widget_class_bind_template_child (widget_class, DemoRestPage, sourceview);
   gtk_widget_class_bind_template_child (widget_class, DemoRestPage, body);
   gtk_widget_class_bind_template_child (widget_class, DemoRestPage, notebook);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, authentication);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, authmethods);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, authentication_stack);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, headers);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, params);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, paramslb);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, headerslb);
+
+  /* basic auth */
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, basic_username);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, basic_password);
+  /* digest auth */
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, digest_username);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, digest_password);
+  /* oauth 1 auth */
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth1_client_identifier);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth1_client_secret);
+  /* oauth 2 auth */
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth2_client_identifier);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth2_client_secret);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth2_auth_url);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth2_token_url);
+  gtk_widget_class_bind_template_child (widget_class, DemoRestPage, oauth2_redirect_url);
+
+  /* callbacks */
   gtk_widget_class_bind_template_callback (widget_class, on_send_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_auth_method_activated);
+  gtk_widget_class_bind_template_callback (widget_class, on_oauth1_get_access_token_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, on_oauth2_get_access_token_clicked);
 }
 
 static void
@@ -162,6 +594,9 @@ demo_rest_page_init (DemoRestPage *self)
   gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (buffer), style);
 
   gtk_notebook_set_tab_label_text (GTK_NOTEBOOK (self->notebook), self->body, "Body");
+  gtk_notebook_set_tab_label_text (GTK_NOTEBOOK (self->notebook), self->params, "Params");
+  gtk_notebook_set_tab_label_text (GTK_NOTEBOOK (self->notebook), self->authentication, "Authentication");
+  gtk_notebook_set_tab_label_text (GTK_NOTEBOOK (self->notebook), self->headers, "Headers");
 }
 
 GtkWidget *
diff --git a/examples/demo/demo-rest-page.ui b/examples/demo/demo-rest-page.ui
index 417a96c..09d7c06 100644
--- a/examples/demo/demo-rest-page.ui
+++ b/examples/demo/demo-rest-page.ui
@@ -53,6 +53,339 @@
             </child>
           </object>
         </child>
+        <child>
+          <object class="GtkScrolledWindow" id="params">
+            <child>
+              <object class="DemoTable" id="paramslb">
+                <property name="title">New Param...</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="authentication">
+            <property name="orientation">horizontal</property>
+            <property name="margin-start">6</property>
+            <property name="margin-end">6</property>
+            <property name="margin-top">6</property>
+            <property name="margin-bottom">6</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkDropDown" id="authmethods">
+                <property name="valign">start</property>
+                <property name="width-request">100</property>
+                <signal name="notify::selected-item" handler="on_auth_method_activated" swapped="no" 
object="DemoRestPage"/>
+                <property name="model">
+                  <object class="GtkStringList" id="auth_method_options">
+                    <items>
+                      <item>No Auth</item>
+                      <item>Basic</item>
+                      <item>Digest</item>
+                      <item>OAuth1</item>
+                      <item>OAuth2</item>
+                    </items>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStack" id="authentication_stack">
+                <property name="hexpand">true</property>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">no_auth</property>
+                    <property name="child">
+                      <object class="GtkBox"/>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">basic</property>
+                    <property name="child">
+                      <object class="GtkGrid">
+                        <property name="column-spacing">8</property>
+                        <property name="column-homogeneous">true</property>
+                        <property name="row-spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Username:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="basic_username">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Password:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="basic_password">
+                            <property name="visibility">false</property>
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">digest</property>
+                    <property name="child">
+                      <object class="GtkGrid">
+                        <property name="column-spacing">8</property>
+                        <property name="column-homogeneous">true</property>
+                        <property name="row-spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Username:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="digest_username">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Password:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="digest_password">
+                            <property name="visibility">false</property>
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">oauth1</property>
+                    <property name="child">
+                      <object class="GtkGrid">
+                        <property name="column-spacing">8</property>
+                        <property name="column-homogeneous">true</property>
+                        <property name="row-spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Client Identifier:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth1_client_identifier">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Client Secret:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth1_client_secret">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="oauth1_get_access_token">
+                            <property name="label">Get access token...</property>
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">2</property>
+                            </layout>
+                            <signal name="clicked" handler="on_oauth1_get_access_token_clicked" swapped="no" 
object="DemoRestPage"/>
+                            <style>
+                              <class name="suggested-action"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">oauth2</property>
+                    <property name="child">
+                      <object class="GtkGrid">
+                        <property name="column-spacing">8</property>
+                        <property name="column-homogeneous">true</property>
+                        <property name="row-spacing">6</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Client Identifier:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth2_client_identifier">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Client Secret:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth2_client_secret">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Authorization URL:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">2</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth2_auth_url">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">2</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Token URL:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">3</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth2_token_url">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">3</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="label">Redirect URL:</property>
+                            <property name="xalign">1.0</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">4</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="oauth2_redirect_url">
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">4</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="oauth2_get_access_token">
+                            <property name="label">Get access token...</property>
+                            <layout>
+                              <property name="column">1</property>
+                              <property name="row">5</property>
+                            </layout>
+                            <signal name="clicked" handler="on_oauth2_get_access_token_clicked" swapped="no" 
object="DemoRestPage"/>
+                            <style>
+                              <class name="suggested-action"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow" id="headers">
+            <child>
+              <object class="DemoTable" id="headerslb">
+                <property name="title">New Header...</property>
+              </object>
+            </child>
+          </object>
+        </child>
       </object>
     </child>
   </template>
diff --git a/examples/demo/demo-table-row.c b/examples/demo/demo-table-row.c
new file mode 100644
index 0000000..1070a2a
--- /dev/null
+++ b/examples/demo/demo-table-row.c
@@ -0,0 +1,149 @@
+/* demo-table-row.c
+ *
+ * Copyright 2022 Günther Wagner <info gunibert de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "demo-table-row.h"
+
+struct _DemoTableRow
+{
+  GObject parent_instance;
+
+  gchar *key;
+  gchar *value;
+};
+
+G_DEFINE_FINAL_TYPE (DemoTableRow, demo_table_row, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_KEY,
+  PROP_VALUE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+DemoTableRow *
+demo_table_row_new (void)
+{
+  return g_object_new (DEMO_TYPE_TABLE_ROW, NULL);
+}
+
+static void
+demo_table_row_finalize (GObject *object)
+{
+  DemoTableRow *self = (DemoTableRow *)object;
+
+  g_clear_pointer (&self->key, g_free);
+  g_clear_pointer (&self->value, g_free);
+
+  G_OBJECT_CLASS (demo_table_row_parent_class)->finalize (object);
+}
+
+static void
+demo_table_row_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  DemoTableRow *self = DEMO_TABLE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+    case PROP_VALUE:
+      g_value_set_string (value, self->value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+demo_table_row_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  DemoTableRow *self = DEMO_TABLE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+    case PROP_VALUE:
+      self->value = g_value_dup_string (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+
+static void
+demo_table_row_class_init (DemoTableRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = demo_table_row_finalize;
+  object_class->get_property = demo_table_row_get_property;
+  object_class->set_property = demo_table_row_set_property;
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "Key",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VALUE] =
+    g_param_spec_string ("value",
+                         "Value",
+                         "Value",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+demo_table_row_init (DemoTableRow *self)
+{
+}
+
+gchar *
+demo_table_row_get_key (DemoTableRow *self)
+{
+  g_return_val_if_fail (DEMO_IS_TABLE_ROW (self), NULL);
+
+  return self->key;
+}
+
+gchar *
+demo_table_row_get_value (DemoTableRow *self)
+{
+  g_return_val_if_fail (DEMO_IS_TABLE_ROW (self), NULL);
+
+  return self->value;
+}
diff --git a/examples/demo/demo-table-row.h b/examples/demo/demo-table-row.h
new file mode 100644
index 0000000..ba93656
--- /dev/null
+++ b/examples/demo/demo-table-row.h
@@ -0,0 +1,35 @@
+/* demo-table-row.h
+ *
+ * Copyright 2022 Günther Wagner <info gunibert de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define DEMO_TYPE_TABLE_ROW (demo_table_row_get_type())
+
+G_DECLARE_FINAL_TYPE (DemoTableRow, demo_table_row, DEMO, TABLE_ROW, GObject)
+
+DemoTableRow *demo_table_row_new       (void);
+gchar        *demo_table_row_get_key   (DemoTableRow *self);
+gchar        *demo_table_row_get_value (DemoTableRow *self);
+
+G_END_DECLS
diff --git a/examples/demo/demo-table-row.ui b/examples/demo/demo-table-row.ui
new file mode 100644
index 0000000..7f71888
--- /dev/null
+++ b/examples/demo/demo-table-row.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <object class="GtkListBoxRow" id="row">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="valign">start</property>
+        <child>
+          <object class="GtkEntry" id="key">
+            <property name="hexpand">true</property>
+            <style>
+              <class name="flat"/>
+              <class name="frame"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="value">
+            <property name="hexpand">true</property>
+            <style>
+              <class name="flat"/>
+              <class name="frame"/>
+            </style>
+          </object>
+        </child>
+        <style>
+          <class name="linked"/>
+        </style>
+      </object>
+    </child>
+    <style>
+      <class name="headerrow"/>
+    </style>
+  </object>
+</interface>
diff --git a/examples/demo/demo-table.c b/examples/demo/demo-table.c
new file mode 100644
index 0000000..3591c5c
--- /dev/null
+++ b/examples/demo/demo-table.c
@@ -0,0 +1,210 @@
+/* demo-table.c
+ *
+ * Copyright 2022 Günther Wagner <info gunibert de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "demo-table.h"
+#include "demo-table-row.h"
+
+struct _DemoTable
+{
+  GtkWidget parent_instance;
+  char *title;
+
+  GtkListBox *lb;
+  GListModel *model;
+
+  GtkWidget *dummy_row;
+};
+
+G_DEFINE_FINAL_TYPE (DemoTable, demo_table, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+GtkWidget *
+demo_table_new (char *title)
+{
+  return g_object_new (DEMO_TYPE_TABLE, "title", title, NULL);
+}
+
+static void
+demo_table_finalize (GObject *object)
+{
+  DemoTable *self = (DemoTable *)object;
+
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (demo_table_parent_class)->finalize (object);
+}
+
+static void
+demo_table_dispose (GObject *object)
+{
+  DemoTable *self = (DemoTable *)object;
+
+  gtk_widget_unparent (GTK_WIDGET (self->lb));
+
+  G_OBJECT_CLASS (demo_table_parent_class)->dispose (object);
+}
+
+static void
+demo_table_activated (GtkListBox    *box,
+                      GtkListBoxRow *row,
+                      gpointer       user_data)
+{
+  DemoTable *self = (DemoTable *)user_data;
+
+  g_return_if_fail (DEMO_IS_TABLE (self));
+
+  if (GTK_WIDGET (row) == self->dummy_row)
+    {
+      GListStore *s = G_LIST_STORE (self->model);
+      g_list_store_append (s, demo_table_row_new ());
+    }
+}
+
+static GtkWidget *
+create_row (gpointer item,
+            gpointer user_data)
+{
+  GtkBuilder *builder;
+
+  builder = gtk_builder_new_from_resource ("/org/gnome/RestDemo/demo-table-row.ui");
+
+  GObject *key_entry = gtk_builder_get_object (builder, "key");
+  g_object_bind_property (key_entry, "text", item, "key", G_BINDING_DEFAULT);
+
+  GObject *value_entry = gtk_builder_get_object (builder, "value");
+  g_object_bind_property (value_entry, "text", item, "value", G_BINDING_DEFAULT);
+  return GTK_WIDGET (gtk_builder_get_object (builder, "row"));
+}
+
+static GtkWidget *
+create_dummy_row (DemoTable *self)
+{
+  GtkWidget *row;
+  GtkWidget *label;
+
+  g_return_val_if_fail (DEMO_IS_TABLE (self), NULL);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", self->title,
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  g_object_bind_property (self, "title", label, "label", G_BINDING_DEFAULT);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "child", label,
+                      "visible", TRUE,
+                      NULL);
+
+  return row;
+}
+
+static void
+demo_table_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  DemoTable *self = DEMO_TABLE (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+demo_table_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  DemoTable *self = DEMO_TABLE (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      g_clear_pointer (&self->title, g_free);
+      self->title = g_value_dup_string (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+demo_table_class_init (DemoTableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = demo_table_finalize;
+  object_class->dispose = demo_table_dispose;
+  object_class->set_property = demo_table_set_property;
+  object_class->get_property = demo_table_get_property;
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_TITLE,
+                                   properties [PROP_TITLE]);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+}
+
+static void
+demo_table_init (DemoTable *self)
+{
+  self->lb = GTK_LIST_BOX (gtk_list_box_new ());
+  gtk_widget_set_hexpand (GTK_WIDGET (self->lb), TRUE);
+  gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->lb), GTK_SELECTION_NONE);
+  gtk_widget_set_parent (GTK_WIDGET (self->lb), GTK_WIDGET (self));
+  g_signal_connect (self->lb, "row-activated", G_CALLBACK (demo_table_activated), self);
+
+  self->model = G_LIST_MODEL (g_list_store_new (DEMO_TYPE_TABLE_ROW));
+  gtk_list_box_bind_model (self->lb, self->model, create_row, self, NULL);
+
+  self->dummy_row = create_dummy_row (self);
+  gtk_list_box_append (self->lb, self->dummy_row);
+}
+
+GListModel *
+demo_table_get_model (DemoTable *self)
+{
+  g_return_val_if_fail (DEMO_IS_TABLE (self), NULL);
+
+  return self->model;
+}
diff --git a/examples/demo/demo-table.h b/examples/demo/demo-table.h
new file mode 100644
index 0000000..0b570db
--- /dev/null
+++ b/examples/demo/demo-table.h
@@ -0,0 +1,34 @@
+/* demo-table.h
+ *
+ * Copyright 2022 Günther Wagner <info gunibert de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DEMO_TYPE_TABLE (demo_table_get_type())
+
+G_DECLARE_FINAL_TYPE (DemoTable, demo_table, DEMO, TABLE, GtkWidget)
+
+GtkWidget  *demo_table_new       (char *title);
+GListModel *demo_table_get_model (DemoTable *self);
+
+G_END_DECLS
diff --git a/examples/demo/demo-window.c b/examples/demo/demo-window.c
index 32681d5..63c48d8 100644
--- a/examples/demo/demo-window.c
+++ b/examples/demo/demo-window.c
@@ -37,14 +37,6 @@ demo_window_new (GtkApplication *app)
                        NULL);
 }
 
-static void
-demo_window_finalize (GObject *object)
-{
-  DemoWindow *self = (DemoWindow *)object;
-
-  G_OBJECT_CLASS (demo_window_parent_class)->finalize (object);
-}
-
 static void
 on_add_tab_clicked (GtkButton *btn,
                     gpointer   user_data)
@@ -66,10 +58,8 @@ on_add_tab_clicked (GtkButton *btn,
 static void
 demo_window_class_init (DemoWindowClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->finalize = demo_window_finalize;
   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/RestDemo/demo-window.ui");
   gtk_widget_class_bind_template_child (widget_class, DemoWindow, tabview);
   gtk_widget_class_bind_template_callback (widget_class, on_add_tab_clicked);
diff --git a/examples/demo/meson.build b/examples/demo/meson.build
index dae36b3..43d77e9 100644
--- a/examples/demo/meson.build
+++ b/examples/demo/meson.build
@@ -2,6 +2,8 @@ demo_sources = [
   'demo-main.c',
   'demo-window.c',
   'demo-rest-page.c',
+  'demo-table.c',
+  'demo-table-row.c',
 ]
 
 demo_deps = [
diff --git a/examples/demo/org.gnome.RestDemo.gresource.xml b/examples/demo/org.gnome.RestDemo.gresource.xml
index ecde4f3..e58e441 100644
--- a/examples/demo/org.gnome.RestDemo.gresource.xml
+++ b/examples/demo/org.gnome.RestDemo.gresource.xml
@@ -3,5 +3,7 @@
   <gresource prefix="/org/gnome/RestDemo">
     <file>demo-window.ui</file>
     <file>demo-rest-page.ui</file>
+    <file>demo-table-row.ui</file>
+    <file>style.css</file>
   </gresource>
 </gresources>
diff --git a/examples/demo/org.gnome.RestDemo.json b/examples/demo/org.gnome.RestDemo.json
index 1ce49cf..bd0fde4 100644
--- a/examples/demo/org.gnome.RestDemo.json
+++ b/examples/demo/org.gnome.RestDemo.json
@@ -23,6 +23,17 @@
         "*.a"
     ],
     "modules" : [
+        {
+            "name" : "libsoup",
+            "builddir" : true,
+            "buildsystem" : "meson",
+            "sources" : [
+                {
+                    "type" : "git",
+                    "url" : "https://gitlab.gnome.org/GNOME/libsoup.git";
+                }
+            ]
+        },
         {
             "name" : "librest-demo",
             "builddir" : true,
@@ -34,7 +45,8 @@
                 }
             ],
             "config-opts" : [
-                "-Dgtk_doc=false"
+                "-Dgtk_doc=false",
+                "-Dsoup2=false"
             ]
         }
     ],
diff --git a/examples/demo/style.css b/examples/demo/style.css
new file mode 100644
index 0000000..266daf3
--- /dev/null
+++ b/examples/demo/style.css
@@ -0,0 +1,3 @@
+.headerrow {
+  padding: 0px;
+}


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