[libadwaita/wip/exalm/julian-demo] Add another example




commit 6c20cbe060d46e9a19b2464fb9e584b68f3ef0a9
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Mon Dec 27 18:18:42 2021 +0500

    Add another example

 examples/meson.build                         |   1 +
 examples/tasks/main.c                        |  27 +++
 examples/tasks/meson.build                   |  33 +++
 examples/tasks/org.example.Tasks.gschema.xml |  15 ++
 examples/tasks/tasks-list.c                  | 190 ++++++++++++++++
 examples/tasks/tasks-list.h                  |  24 ++
 examples/tasks/tasks-manager.c               | 219 +++++++++++++++++++
 examples/tasks/tasks-manager.h               |  25 +++
 examples/tasks/tasks-preferences-window.c    |  39 ++++
 examples/tasks/tasks-preferences-window.h    |  13 ++
 examples/tasks/tasks-preferences-window.ui   |  29 +++
 examples/tasks/tasks-task.c                  | 160 ++++++++++++++
 examples/tasks/tasks-task.h                  |  21 ++
 examples/tasks/tasks-utils.c                 |  88 ++++++++
 examples/tasks/tasks-utils.h                 |  18 ++
 examples/tasks/tasks-view.c                  | 311 ++++++++++++++++++++++++++
 examples/tasks/tasks-view.h                  |  13 ++
 examples/tasks/tasks-view.ui                 |  59 +++++
 examples/tasks/tasks-window.c                | 316 +++++++++++++++++++++++++++
 examples/tasks/tasks-window.h                |  13 ++
 examples/tasks/tasks-window.ui               | 212 ++++++++++++++++++
 examples/tasks/tasks.gresource.xml           |   8 +
 22 files changed, 1834 insertions(+)
---
diff --git a/examples/meson.build b/examples/meson.build
index 0cd0f1d9..b7ff3bab 100644
--- a/examples/meson.build
+++ b/examples/meson.build
@@ -1 +1,2 @@
 subdir('hello-world')
+subdir('tasks')
diff --git a/examples/tasks/main.c b/examples/tasks/main.c
new file mode 100644
index 00000000..e37cf0bf
--- /dev/null
+++ b/examples/tasks/main.c
@@ -0,0 +1,27 @@
+#include "tasks-window.h"
+
+#include <adwaita.h>
+
+static void
+activate_cb (GtkApplication *app)
+{
+  GtkWindow *window;
+
+  window = gtk_application_get_active_window (app);
+  if (window == NULL)
+    window = tasks_window_new (app);
+
+  gtk_window_present (window);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_autoptr(AdwApplication) app =
+    adw_application_new ("org.example.Tasks", G_APPLICATION_NON_UNIQUE); // FIXME _NONE
+
+  g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL);
+
+  return g_application_run (G_APPLICATION (app), argc, argv);
+}
diff --git a/examples/tasks/meson.build b/examples/tasks/meson.build
new file mode 100644
index 00000000..3290900e
--- /dev/null
+++ b/examples/tasks/meson.build
@@ -0,0 +1,33 @@
+# Uncomment everything to build separately
+# project('tasks', 'c')
+
+gnome = import('gnome')
+
+executable('tasks',
+  'main.c',
+
+  'tasks-list.c',
+  'tasks-manager.c',
+  'tasks-task.c',
+
+  'tasks-preferences-window.c',
+  'tasks-utils.c',
+  'tasks-view.c',
+  'tasks-window.c',
+  gnome.compile_resources('tasks-resources',
+    'tasks.gresource.xml',
+    c_name: 'tasks'
+  ),
+  gnome.compile_schemas(),
+  dependencies: [
+    dependency('gtk4'),
+    dependency('libadwaita-1'),
+  ],
+#  install: true,
+)
+
+#install_data('org.example.Tasks.gschema.xml',
+#  install_dir: join_paths(get_option('datadir'), 'glib-2.0' / 'schemas')
+#)
+
+#gnome.post_install(glib_compile_schemas: true)
diff --git a/examples/tasks/org.example.Tasks.gschema.xml b/examples/tasks/org.example.Tasks.gschema.xml
new file mode 100644
index 00000000..6c3b7d0f
--- /dev/null
+++ b/examples/tasks/org.example.Tasks.gschema.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist gettext-domain="example2">
+  <schema id="org.example.Tasks" path="/org/example/Tasks/">
+    <key name="tasks" type="a(sa(sb))">
+      <default>[]</default>
+      <summary>Tasks</summary>
+      <description>Saved tasks.</description>
+    </key>
+    <key name="show-completed" type="b">
+      <default>true</default>
+      <summary>Show Completed</summary>
+      <description>Whether to show completed tasks.</description>
+    </key>
+  </schema>
+</schemalist>
diff --git a/examples/tasks/tasks-list.c b/examples/tasks/tasks-list.c
new file mode 100644
index 00000000..e1760b75
--- /dev/null
+++ b/examples/tasks/tasks-list.c
@@ -0,0 +1,190 @@
+#include "tasks-list.h"
+
+#include <gio/gio.h>
+
+struct _TasksList
+{
+  GObject parent_instance;
+
+  char *title;
+  GListStore *tasks;
+};
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void tasks_list_list_model_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (TasksList, tasks_list, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, tasks_list_list_model_init))
+
+static void
+tasks_list_finalize (GObject *object)
+{
+  TasksList *self = TASKS_LIST (object);
+
+  g_clear_object (&self->tasks);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (tasks_list_parent_class)->finalize (object);
+}
+
+static void
+tasks_list_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  TasksList *self = TASKS_LIST (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    g_value_set_string (value, tasks_list_get_title (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_list_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  TasksList *self = TASKS_LIST (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    tasks_list_set_title (self, g_value_get_string (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_list_class_init (TasksListClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tasks_list_finalize;
+  object_class->get_property = tasks_list_get_property;
+  object_class->set_property = tasks_list_set_property;
+
+  props[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         G_PARAM_CONSTRUCT |
+                         G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS |
+                         G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+tasks_list_init (TasksList *self)
+{
+  self->tasks = g_list_store_new (TASKS_TYPE_TASK);
+
+  g_signal_connect_swapped (self->tasks, "items-changed",
+                            G_CALLBACK (g_list_model_items_changed), self);
+}
+
+static gpointer
+tasks_list_get_item (GListModel *model,
+                     guint       position)
+{
+  TasksList *self = TASKS_LIST (model);
+
+  return g_list_model_get_item (G_LIST_MODEL (self->tasks), position);
+}
+
+static guint
+tasks_list_get_n_items (GListModel *model)
+{
+  TasksList *self = TASKS_LIST (model);
+
+  return g_list_model_get_n_items (G_LIST_MODEL (self->tasks));
+}
+
+static GType
+tasks_list_get_item_type (GListModel *model)
+{
+  return TASKS_TYPE_TASK;
+}
+
+static void
+tasks_list_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item = tasks_list_get_item;
+  iface->get_n_items = tasks_list_get_n_items;
+  iface->get_item_type = tasks_list_get_item_type;
+}
+
+TasksList *
+tasks_list_new (const char *title)
+{
+  g_return_val_if_fail (title != NULL, NULL);
+
+  return g_object_new (TASKS_TYPE_LIST, "title", title, NULL);
+}
+
+const char *
+tasks_list_get_title (TasksList *self)
+{
+  g_return_val_if_fail (TASKS_IS_LIST (self), NULL);
+
+  return self->title;
+}
+
+void
+tasks_list_set_title (TasksList  *self,
+                      const char *title)
+{
+  g_return_if_fail (TASKS_IS_LIST (self));
+  g_return_if_fail (title != NULL);
+
+  if (!g_strcmp0 (title, self->title))
+    return;
+
+  g_clear_pointer (&self->title, g_free);
+  self->title = g_strdup (title);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+void
+tasks_list_add (TasksList *self,
+                TasksTask *task)
+{
+  g_return_if_fail (TASKS_IS_LIST (self));
+  g_return_if_fail (TASKS_IS_TASK (task));
+
+  g_list_store_append (self->tasks, task);
+}
+
+guint
+tasks_list_remove (TasksList *self,
+                   TasksTask *task)
+{
+  guint position;
+
+  g_return_val_if_fail (TASKS_IS_LIST (self), 0);
+  g_return_val_if_fail (TASKS_IS_TASK (task), 0);
+
+  if (!g_list_store_find (self->tasks, task, &position))
+    return G_MAXUINT;
+
+  g_list_store_remove (self->tasks, position);
+
+  return position;
+}
diff --git a/examples/tasks/tasks-list.h b/examples/tasks/tasks-list.h
new file mode 100644
index 00000000..97a2621f
--- /dev/null
+++ b/examples/tasks/tasks-list.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <glib-object.h>
+
+#include "tasks-task.h"
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_LIST (tasks_list_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksList, tasks_list, TASKS, LIST, GObject)
+
+TasksList  *tasks_list_new       (const char *title);
+
+const char *tasks_list_get_title (TasksList  *self);
+void        tasks_list_set_title (TasksList  *self,
+                                  const char *title);
+
+void        tasks_list_add       (TasksList  *self,
+                                  TasksTask  *task);
+guint       tasks_list_remove    (TasksList  *self,
+                                  TasksTask  *task);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-manager.c b/examples/tasks/tasks-manager.c
new file mode 100644
index 00000000..849d5fb6
--- /dev/null
+++ b/examples/tasks/tasks-manager.c
@@ -0,0 +1,219 @@
+#include "tasks-manager.h"
+
+#include <gio/gio.h>
+
+struct _TasksManager
+{
+  GObject parent_instance;
+
+  GListStore *lists;
+};
+
+static void tasks_manager_list_model_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (TasksManager, tasks_manager, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, tasks_manager_list_model_init))
+
+static TasksManager *default_instance = NULL;
+
+static GVariant *
+serialize_lists (TasksManager *self)
+{
+  GVariantBuilder builder;
+  guint i, n_lists;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sa(sb))"));
+
+  n_lists = g_list_model_get_n_items (G_LIST_MODEL (self->lists));
+  for (i = 0; i < n_lists; i++) {
+    g_autoptr (TasksList) list =
+      g_list_model_get_item (G_LIST_MODEL (self->lists), i);
+    int j, n_tasks;
+
+    g_variant_builder_open (&builder, G_VARIANT_TYPE ("(sa(sb))"));
+    g_variant_builder_add (&builder, "s", tasks_list_get_title (list));
+
+    g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(sb)"));
+
+    n_tasks = g_list_model_get_n_items (G_LIST_MODEL (list));
+    for (j = 0; j < n_tasks; j++) {
+      g_autoptr (TasksTask) task =
+        g_list_model_get_item (G_LIST_MODEL (list), j);
+
+      g_variant_builder_open (&builder, G_VARIANT_TYPE ("(sb)"));
+      g_variant_builder_add (&builder, "s", tasks_task_get_title (task));
+      g_variant_builder_add (&builder, "b", tasks_task_get_done (task));
+      g_variant_builder_close (&builder);
+    }
+
+    g_variant_builder_close (&builder);
+    g_variant_builder_close (&builder);
+  }
+
+  return g_variant_ref (g_variant_builder_end (&builder));
+}
+
+static TasksList *
+parse_list (GVariant *variant)
+{
+  TasksList *list = NULL;
+  g_autoptr (GVariantIter) tasks_iter = NULL;
+  g_autofree char *title = NULL;
+  const char *task_title;
+  gboolean done;
+
+  g_variant_get (variant, ("(sa(sb))"), &title, &tasks_iter);
+
+  list = tasks_list_new (title);
+
+  while (g_variant_iter_loop (tasks_iter, "(sb)", &task_title, &done)) {
+    g_autoptr (TasksTask) task = NULL;
+
+    task = tasks_task_new (task_title);
+    tasks_task_set_done (task, done);
+    tasks_list_add (list, task);
+  }
+
+  return list;
+}
+
+static void
+load (TasksManager *self)
+{
+  g_autoptr (GSettings) settings = g_settings_new ("org.example.Tasks");
+  GVariant *variant = g_settings_get_value (settings, "tasks");
+  GVariantIter iter;
+  GVariant *child;
+
+  g_variant_iter_init (&iter, variant);
+  while ((child = g_variant_iter_next_value (&iter))) {
+    g_autoptr (TasksList) list = parse_list (child);
+
+    tasks_manager_add_list (self, list);
+
+    g_variant_unref (child);
+  }
+}
+
+static void
+tasks_manager_finalize (GObject *object)
+{
+  TasksManager *self = TASKS_MANAGER (object);
+
+  g_clear_object (&self->lists);
+
+  G_OBJECT_CLASS (tasks_manager_parent_class)->finalize (object);
+}
+
+static void
+tasks_manager_class_init (TasksManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tasks_manager_finalize;
+}
+
+static void
+tasks_manager_init (TasksManager *self)
+{
+  self->lists = g_list_store_new (TASKS_TYPE_LIST);
+
+  g_signal_connect_swapped (self->lists, "items-changed",
+                            G_CALLBACK (g_list_model_items_changed), self);
+
+  load (self);
+}
+
+static gpointer
+tasks_manager_get_item (GListModel *model,
+                        guint       position)
+{
+  TasksManager *self = TASKS_MANAGER (model);
+
+  return g_list_model_get_item (G_LIST_MODEL (self->lists), position);
+}
+
+static guint
+tasks_manager_get_n_items (GListModel *model)
+{
+  TasksManager *self = TASKS_MANAGER (model);
+
+  return g_list_model_get_n_items (G_LIST_MODEL (self->lists));
+}
+
+static GType
+tasks_manager_get_item_type (GListModel *model)
+{
+  return TASKS_TYPE_LIST;
+}
+
+static void
+tasks_manager_list_model_init (GListModelInterface *iface)
+{
+  iface->get_item = tasks_manager_get_item;
+  iface->get_n_items = tasks_manager_get_n_items;
+  iface->get_item_type = tasks_manager_get_item_type;
+}
+
+TasksManager *
+tasks_manager_get_default (void)
+{
+  if (!default_instance)
+    default_instance = g_object_new (TASKS_TYPE_MANAGER, NULL);
+
+  return default_instance;
+}
+
+void
+tasks_manager_add_list (TasksManager *self,
+                        TasksList    *list)
+{
+  g_return_if_fail (TASKS_IS_MANAGER (self));
+  g_return_if_fail (TASKS_IS_LIST (list));
+
+  g_list_store_append (self->lists, list);
+}
+
+guint
+tasks_manager_remove_list (TasksManager *self,
+                           TasksList    *list)
+{
+  guint position;
+
+  g_return_val_if_fail (TASKS_IS_MANAGER (self), 0);
+  g_return_val_if_fail (TASKS_IS_LIST (list), 0);
+
+  position = tasks_manager_get_position (self, list);
+  g_list_store_remove (self->lists, position);
+
+  return position;
+}
+
+guint
+tasks_manager_get_position (TasksManager *self,
+                            TasksList    *list)
+{
+  guint position;
+
+  g_return_val_if_fail (TASKS_IS_MANAGER (self), 0);
+  g_return_val_if_fail (TASKS_IS_LIST (list), 0);
+
+  if (g_list_store_find (self->lists, list, &position))
+    return position;
+
+  return G_MAXUINT;
+}
+
+void
+tasks_manager_save (TasksManager *self)
+{
+  g_autoptr (GVariant) variant = NULL;
+  g_autoptr (GSettings) settings = NULL;
+
+  g_return_if_fail (TASKS_IS_MANAGER (self));
+
+  variant = serialize_lists (self);
+  settings = g_settings_new ("org.example.Tasks");
+
+  g_settings_set_value (settings, "tasks", variant);
+}
diff --git a/examples/tasks/tasks-manager.h b/examples/tasks/tasks-manager.h
new file mode 100644
index 00000000..4c2efe01
--- /dev/null
+++ b/examples/tasks/tasks-manager.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <glib-object.h>
+
+#include "tasks-list.h"
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_MANAGER (tasks_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksManager, tasks_manager, TASKS, MANAGER, GObject)
+
+TasksManager *tasks_manager_get_default (void);
+
+void          tasks_manager_add_list     (TasksManager *self,
+                                          TasksList    *list);
+guint         tasks_manager_remove_list  (TasksManager *self,
+                                          TasksList    *list);
+
+guint         tasks_manager_get_position (TasksManager *self,
+                                          TasksList    *list);
+
+void          tasks_manager_save         (TasksManager *self);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-preferences-window.c b/examples/tasks/tasks-preferences-window.c
new file mode 100644
index 00000000..a4248b09
--- /dev/null
+++ b/examples/tasks/tasks-preferences-window.c
@@ -0,0 +1,39 @@
+#include "tasks-preferences-window.h"
+
+struct _TasksPreferencesWindow
+{
+  AdwPreferencesWindow parent_instance;
+
+  GtkSwitch *show_completed_switch;
+};
+
+G_DEFINE_TYPE (TasksPreferencesWindow, tasks_preferences_window, ADW_TYPE_PREFERENCES_WINDOW)
+
+static void
+tasks_preferences_window_class_init (TasksPreferencesWindowClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/example/Tasks/tasks-preferences-window.ui");
+  gtk_widget_class_bind_template_child (widget_class, TasksPreferencesWindow, show_completed_switch);
+}
+
+static void
+tasks_preferences_window_init (TasksPreferencesWindow *self)
+{
+  GSettings *settings;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  settings = g_settings_new ("org.example.Tasks");
+
+  g_settings_bind (settings, "show-completed",
+                   self->show_completed_switch, "active",
+                   G_SETTINGS_BIND_DEFAULT);
+}
+
+GtkWindow *
+tasks_preferences_window_new (void)
+{
+  return g_object_new (TASKS_TYPE_PREFERENCES_WINDOW, NULL);
+}
diff --git a/examples/tasks/tasks-preferences-window.h b/examples/tasks/tasks-preferences-window.h
new file mode 100644
index 00000000..355c97ee
--- /dev/null
+++ b/examples/tasks/tasks-preferences-window.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_PREFERENCES_WINDOW (tasks_preferences_window_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksPreferencesWindow, tasks_preferences_window, TASKS, PREFERENCES_WINDOW, 
AdwPreferencesWindow)
+
+GtkWindow *tasks_preferences_window_new (void);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-preferences-window.ui b/examples/tasks/tasks-preferences-window.ui
new file mode 100644
index 00000000..c726984d
--- /dev/null
+++ b/examples/tasks/tasks-preferences-window.ui
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="TasksPreferencesWindow" parent="AdwPreferencesWindow">
+    <property name="default-height">200</property>
+    <property name="modal">True</property>
+    <property name="search-enabled">False</property>
+    <child>
+      <object class="AdwPreferencesPage">
+        <property name="title" translatable="yes">General</property>
+        <child>
+          <object class="AdwPreferencesGroup">
+            <child>
+              <object class="AdwActionRow">
+                <property name="title" translatable="yes">Show Completed</property>
+                <property name="activatable-widget">show_completed_switch</property>
+                <child type="suffix">
+                  <object class="GtkSwitch" id="show_completed_switch">
+                    <property name="valign">center</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/examples/tasks/tasks-task.c b/examples/tasks/tasks-task.c
new file mode 100644
index 00000000..ded3beba
--- /dev/null
+++ b/examples/tasks/tasks-task.c
@@ -0,0 +1,160 @@
+#include "tasks-task.h"
+
+struct _TasksTask
+{
+  GObject parent_instance;
+
+  char *title;
+  gboolean done;
+};
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_DONE,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (TasksTask, tasks_task, G_TYPE_OBJECT)
+
+static void
+tasks_task_finalize (GObject *object)
+{
+  TasksTask *self = TASKS_TASK (object);
+
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (tasks_task_parent_class)->finalize (object);
+}
+
+static void
+tasks_task_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  TasksTask *self = TASKS_TASK (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    g_value_set_string (value, tasks_task_get_title (self));
+    break;
+  case PROP_DONE:
+    g_value_set_boolean (value, tasks_task_get_done (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_task_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  TasksTask *self = TASKS_TASK (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    tasks_task_set_title (self, g_value_get_string (value));
+    break;
+  case PROP_DONE:
+    tasks_task_set_done (self, g_value_get_boolean (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_task_class_init (TasksTaskClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tasks_task_finalize;
+  object_class->get_property = tasks_task_get_property;
+  object_class->set_property = tasks_task_set_property;
+
+  props[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         G_PARAM_CONSTRUCT |
+                         G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS |
+                         G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_DONE] =
+    g_param_spec_boolean ("done",
+                          "Done",
+                          "Done",
+                          FALSE,
+                          G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS |
+                          G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+tasks_task_init (TasksTask *self)
+{
+}
+
+TasksTask *
+tasks_task_new (const char *title)
+{
+  g_return_val_if_fail (title != NULL, NULL);
+
+  return g_object_new (TASKS_TYPE_TASK, "title", title, NULL);
+}
+
+const char *
+tasks_task_get_title (TasksTask *self)
+{
+  g_return_val_if_fail (TASKS_IS_TASK (self), NULL);
+
+  return self->title;
+}
+
+void
+tasks_task_set_title (TasksTask  *self,
+                      const char *title)
+{
+  g_return_if_fail (TASKS_IS_TASK (self));
+  g_return_if_fail (title != NULL);
+
+  if (!g_strcmp0 (title, self->title))
+    return;
+
+  g_clear_pointer (&self->title, g_free);
+  self->title = g_strdup (title);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+gboolean
+tasks_task_get_done (TasksTask *self)
+{
+  g_return_val_if_fail (TASKS_IS_TASK (self), FALSE);
+
+  return self->done;
+}
+
+void
+tasks_task_set_done (TasksTask *self,
+                     gboolean   done)
+{
+  g_return_if_fail (TASKS_IS_TASK (self));
+
+  if (done == self->done)
+    return;
+
+  self->done = done;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DONE]);
+}
diff --git a/examples/tasks/tasks-task.h b/examples/tasks/tasks-task.h
new file mode 100644
index 00000000..72be25e6
--- /dev/null
+++ b/examples/tasks/tasks-task.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_TASK (tasks_task_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksTask, tasks_task, TASKS, TASK, GObject)
+
+TasksTask  *tasks_task_new       (const char *title);
+
+const char *tasks_task_get_title (TasksTask  *self);
+void        tasks_task_set_title (TasksTask  *self,
+                                  const char *title);
+
+gboolean    tasks_task_get_done  (TasksTask  *self);
+void        tasks_task_set_done  (TasksTask  *self,
+                                  gboolean    done);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-utils.c b/examples/tasks/tasks-utils.c
new file mode 100644
index 00000000..287da90b
--- /dev/null
+++ b/examples/tasks/tasks-utils.c
@@ -0,0 +1,88 @@
+#include "tasks-utils.h"
+
+#include <glib/gi18n.h>
+
+static void
+dialog_response_cb (GtkDialog       *dialog,
+                    GtkResponseType  response,
+                    gpointer         user_data)
+{
+  TasksDialogFunc callback = g_object_get_data (G_OBJECT (dialog), "callback");
+  GtkEditable *entry = g_object_get_data (G_OBJECT (dialog), "entry");
+
+  gtk_window_destroy (GTK_WINDOW (dialog));
+
+  if (response != GTK_RESPONSE_ACCEPT)
+    return;
+
+  callback (gtk_editable_get_text (entry), user_data);
+}
+
+static void
+entry_changed_cb (GtkDialog *dialog,
+                  GtkWidget *entry)
+{
+  GtkWidget *button;
+  const char *text;
+  gboolean empty;
+
+  text = gtk_editable_get_text (GTK_EDITABLE (entry));
+  button = gtk_dialog_get_widget_for_response (dialog, GTK_RESPONSE_ACCEPT);
+  empty = !(text && text[0]);
+
+  gtk_widget_set_sensitive (button, !empty);
+
+  if (empty)
+    gtk_widget_add_css_class (entry, "error");
+  else
+    gtk_widget_remove_css_class (entry, "error");
+}
+
+void
+tasks_show_dialog (GtkWindow       *parent,
+                   const char      *title,
+                   const char      *accept_label,
+                   const char      *placeholder,
+                   const char      *value,
+                   TasksDialogFunc  callback,
+                   gpointer         user_data)
+{
+  GtkWidget *dialog, *content_area, *entry;
+
+  dialog = gtk_dialog_new_with_buttons (title,
+                                        parent,
+                                        GTK_DIALOG_MODAL |
+                                        GTK_DIALOG_DESTROY_WITH_PARENT |
+                                        GTK_DIALOG_USE_HEADER_BAR,
+                                        _("Cancel"),
+                                        GTK_RESPONSE_CANCEL,
+                                        accept_label,
+                                        GTK_RESPONSE_ACCEPT,
+                                        NULL);
+  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
+
+  content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+  entry = gtk_entry_new ();
+  gtk_widget_set_margin_top (entry, 12);
+  gtk_widget_set_margin_bottom (entry, 12);
+  gtk_widget_set_margin_start (entry, 12);
+  gtk_widget_set_margin_end (entry, 12);
+  gtk_editable_set_text (GTK_EDITABLE (entry), value);
+  gtk_entry_set_placeholder_text (GTK_ENTRY (entry), placeholder);
+  gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
+
+  g_signal_connect_swapped (entry, "changed",
+                            G_CALLBACK (entry_changed_cb), dialog);
+
+  entry_changed_cb (GTK_DIALOG (dialog), entry);
+  gtk_widget_remove_css_class (entry, "error");
+
+  gtk_box_append (GTK_BOX (content_area), entry);
+  g_object_set_data (G_OBJECT (dialog), "entry", entry);
+  g_object_set_data (G_OBJECT (dialog), "callback", callback);
+
+  g_signal_connect (dialog, "response",
+                    G_CALLBACK (dialog_response_cb), user_data);
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
diff --git a/examples/tasks/tasks-utils.h b/examples/tasks/tasks-utils.h
new file mode 100644
index 00000000..c3b97a56
--- /dev/null
+++ b/examples/tasks/tasks-utils.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef void (* TasksDialogFunc) (const char *value,
+                                  gpointer    user_data);
+
+void tasks_show_dialog (GtkWindow       *parent,
+                        const char      *title,
+                        const char      *accept_label,
+                        const char      *placeholder,
+                        const char      *value,
+                        TasksDialogFunc  callback,
+                        gpointer         user_data);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-view.c b/examples/tasks/tasks-view.c
new file mode 100644
index 00000000..34c0d2c2
--- /dev/null
+++ b/examples/tasks/tasks-view.c
@@ -0,0 +1,311 @@
+#include "tasks-view.h"
+
+#include "tasks-list.h"
+#include "tasks-task.h"
+#include "tasks-utils.h"
+
+#include <glib/gi18n.h>
+
+struct _TasksView
+{
+  AdwBin parent_instance;
+
+  GtkListBox *tasks_list;
+  GtkEditable *new_task_entry;
+  GMenu *task_menu;
+
+  TasksList *list;
+  TasksTask *current_task;
+  GtkFilterListModel *filter_model;
+  GSettings *settings;
+};
+
+enum {
+  PROP_0,
+  PROP_LIST,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (TasksView, tasks_view, ADW_TYPE_BIN)
+
+static void
+update_filter (TasksView *self)
+{
+  gboolean show_completed;
+
+  if (!self->filter_model)
+    return;
+
+  show_completed = g_settings_get_boolean (self->settings, "show-completed");
+
+  if (show_completed) {
+    gtk_filter_list_model_set_filter (self->filter_model, NULL);
+  } else {
+    g_autoptr (GtkBoolFilter) filter = NULL;
+    GtkExpression *expr = gtk_property_expression_new (TASKS_TYPE_TASK, NULL, "done");
+
+    filter = gtk_bool_filter_new (expr);
+    gtk_bool_filter_set_invert (filter, TRUE);
+
+    gtk_filter_list_model_set_filter (self->filter_model, GTK_FILTER (filter));
+  }
+}
+
+static void
+notify_task_menu_visible_cb (TasksView  *self,
+                             GParamSpec *pspec,
+                             GtkWidget  *popover)
+{
+  GtkWidget *row = gtk_widget_get_ancestor (popover, ADW_TYPE_ACTION_ROW);
+
+  if (gtk_widget_get_visible (popover)) {
+
+    self->current_task = g_object_get_data (G_OBJECT (row), "task");
+
+    gtk_widget_add_css_class (row, "has-open-popup");
+  } else {
+    gtk_widget_remove_css_class (row, "has-open-popup");
+  }
+}
+
+static GtkWidget *
+create_task_row (TasksTask *task,
+                 TasksView *self)
+{
+  GtkWidget *row;
+  GtkWidget *check;
+  GtkWidget *menu_button;
+  GtkPopover *popover;
+
+  check = gtk_check_button_new ();
+  gtk_widget_set_valign (check, GTK_ALIGN_CENTER);
+  gtk_widget_set_can_focus (check, FALSE);
+
+  menu_button = gtk_menu_button_new ();
+  gtk_widget_set_valign (menu_button, GTK_ALIGN_CENTER);
+  gtk_widget_add_css_class (menu_button, "flat");
+  gtk_menu_button_set_icon_name (GTK_MENU_BUTTON (menu_button),
+                                 "view-more-symbolic");
+  gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (menu_button),
+                                  G_MENU_MODEL (self->task_menu));
+
+  popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (menu_button));
+  g_signal_connect_swapped (popover, "notify::visible",
+                            G_CALLBACK (notify_task_menu_visible_cb), self);
+
+  row = adw_action_row_new ();
+  adw_action_row_add_prefix (ADW_ACTION_ROW (row), check);
+  adw_action_row_add_suffix (ADW_ACTION_ROW (row), menu_button);
+  adw_action_row_set_activatable_widget (ADW_ACTION_ROW (row), check);
+
+  g_object_bind_property (task, "title", row, "title",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (task, "done", check, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+
+  g_object_set_data (G_OBJECT (row), "task", task);
+
+  return row;
+}
+
+static void
+tasks_changed_cb (TasksView *self)
+{
+  guint n_tasks = 0;
+
+  if (self->list)
+    n_tasks = g_list_model_get_n_items (G_LIST_MODEL (self->filter_model));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->tasks_list), n_tasks > 0);
+
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "task.rename", n_tasks > 0);
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "task.delete", n_tasks > 0);
+}
+
+static void
+set_list (TasksView *self,
+          TasksList *list)
+{
+  if (self->list == list)
+    return;
+
+  if (self->list) {
+    g_signal_handlers_disconnect_by_func (self->list,
+                                          G_CALLBACK (tasks_changed_cb),
+                                          self);
+
+    gtk_list_box_bind_model (self->tasks_list, NULL, NULL, NULL, NULL);
+
+    g_clear_object (&self->filter_model);
+  }
+
+  g_set_object (&self->list, list);
+
+  if (self->list) {
+    self->filter_model = gtk_filter_list_model_new (NULL, NULL);
+
+    gtk_filter_list_model_set_model (self->filter_model, G_LIST_MODEL (list));
+
+    update_filter (self);
+
+    g_signal_connect_swapped (self->filter_model,
+                              "items-changed",
+                              G_CALLBACK (tasks_changed_cb),
+                              self);
+
+    gtk_list_box_bind_model (self->tasks_list,
+                             G_LIST_MODEL (self->filter_model),
+                             (GtkListBoxCreateWidgetFunc) create_task_row,
+                             self,
+                             NULL);
+  }
+
+  tasks_changed_cb (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIST]);
+}
+
+
+static void
+task_rename_dialog_cb (const char *value,
+                       TasksView  *self)
+{
+  tasks_task_set_title (self->current_task, value);
+}
+
+static void
+task_rename_cb (TasksView *self)
+{
+  GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+
+  tasks_show_dialog (GTK_WINDOW (root),
+                     _("Rename Task"),
+                     _("Rename"),
+                     _("Name"),
+                     tasks_task_get_title (self->current_task),
+                     (TasksDialogFunc) task_rename_dialog_cb,
+                     self);
+}
+
+static void
+task_delete_cb (TasksView *self)
+{
+  g_assert (self->current_task);
+
+  tasks_list_remove (self->list, self->current_task);
+
+  self->current_task = NULL;
+}
+
+static void
+new_task_activate_cb (TasksView *self)
+{
+  const char *title = gtk_editable_get_text (self->new_task_entry);
+  g_autoptr (TasksTask) task = NULL;
+
+  if (!title[0])
+    return;
+
+  task = tasks_task_new (title);
+
+  tasks_list_add (self->list, task);
+
+  gtk_editable_set_text (self->new_task_entry, "");
+}
+
+static void
+tasks_view_dispose (GObject *object)
+{
+  TasksView *self = TASKS_VIEW (object);
+
+  set_list (self, NULL);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (tasks_view_parent_class)->dispose (object);
+}
+
+static void
+tasks_view_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  TasksView *self = TASKS_VIEW (object);
+
+  switch (prop_id) {
+  case PROP_LIST:
+    g_value_set_object (value, self->list);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_view_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  TasksView *self = TASKS_VIEW (object);
+
+  switch (prop_id) {
+  case PROP_LIST:
+    set_list (self, g_value_get_object (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+tasks_view_class_init (TasksViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = tasks_view_dispose;
+  object_class->get_property = tasks_view_get_property;
+  object_class->set_property = tasks_view_set_property;
+
+  props[PROP_LIST] =
+    g_param_spec_object ("list",
+                         "List",
+                         "List",
+                         TASKS_TYPE_LIST,
+                         G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS |
+                         G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/example/Tasks/tasks-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, TasksView, tasks_list);
+  gtk_widget_class_bind_template_child (widget_class, TasksView, new_task_entry);
+  gtk_widget_class_bind_template_child (widget_class, TasksView, task_menu);
+  gtk_widget_class_bind_template_callback (widget_class, new_task_activate_cb);
+
+  gtk_widget_class_install_action (widget_class, "task.rename", NULL,
+                                   (GtkWidgetActionActivateFunc) task_rename_cb);
+  gtk_widget_class_install_action (widget_class, "task.delete", NULL,
+                                   (GtkWidgetActionActivateFunc) task_delete_cb);
+}
+
+static void
+tasks_view_init (TasksView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->settings = g_settings_new ("org.example.Tasks");
+
+  g_signal_connect_swapped (self->settings, "changed::show-completed",
+                            G_CALLBACK (update_filter), self);
+}
+
+GtkWidget *
+tasks_view_new (void)
+{
+  return g_object_new (TASKS_TYPE_VIEW, NULL);
+}
diff --git a/examples/tasks/tasks-view.h b/examples/tasks/tasks-view.h
new file mode 100644
index 00000000..294b60cd
--- /dev/null
+++ b/examples/tasks/tasks-view.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_VIEW (tasks_view_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksView, tasks_view, TASKS, VIEW, AdwBin)
+
+GtkWidget *tasks_view_new (void);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-view.ui b/examples/tasks/tasks-view.ui
new file mode 100644
index 00000000..4f8dc905
--- /dev/null
+++ b/examples/tasks/tasks-view.ui
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <menu id="task_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Rename</attribute>
+        <attribute name="action">task.rename</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Delete</attribute>
+        <attribute name="action">task.delete</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="TasksView" parent="AdwBin">
+    <property name="child">
+      <object class="GtkScrolledWindow">
+        <property name="vexpand">True</property>
+        <property name="child">
+          <object class="GtkViewport">
+            <property name="scroll-to-focus">True</property>
+            <property name="child">
+              <object class="AdwClamp">
+                <property name="child">
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">18</property>
+                    <property name="margin-top">24</property>
+                    <property name="margin-bottom">24</property>
+                    <property name="margin-start">12</property>
+                    <property name="margin-end">12</property>
+                    <child>
+                      <object class="GtkListBox" id="tasks_list">
+                        <property name="visible">False</property>
+                        <property name="selection-mode">none</property>
+                        <style>
+                          <class name="boxed-list"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="new_task_entry">
+                        <property name="placeholder-text" translatable="yes">Enter a Taskā€¦</property>
+                        <property name="secondary-icon-name">list-add-symbolic</property>
+                        <signal name="activate" handler="new_task_activate_cb" swapped="yes"/>
+                        <signal name="icon-release" handler="new_task_activate_cb" swapped="yes"/>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </property>
+          </object>
+        </property>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/examples/tasks/tasks-window.c b/examples/tasks/tasks-window.c
new file mode 100644
index 00000000..497e4f1b
--- /dev/null
+++ b/examples/tasks/tasks-window.c
@@ -0,0 +1,316 @@
+#include "tasks-window.h"
+
+#include "tasks-list.h"
+#include "tasks-manager.h"
+#include "tasks-preferences-window.h"
+#include "tasks-utils.h"
+#include "tasks-view.h"
+
+#include <glib/gi18n.h>
+
+struct _TasksWindow
+{
+  AdwApplicationWindow parent_instance;
+
+  GtkStack *empty_stack;
+  AdwLeaflet *leaflet;
+  GtkListBox *list;
+  TasksView *view;
+
+  TasksList *current_list;
+};
+
+enum {
+  PROP_0,
+  PROP_CURRENT_LIST,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (TasksWindow, tasks_window, ADW_TYPE_APPLICATION_WINDOW)
+
+static GtkWidget *
+create_list_row (TasksList   *list,
+                 TasksWindow *self)
+{
+  GtkWidget *row, *label;
+
+  label = gtk_label_new (NULL);
+  gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+  gtk_label_set_xalign (GTK_LABEL (label), 0);
+
+  g_object_bind_property (list, "title", label, "label",
+                          G_BINDING_SYNC_CREATE);
+
+  row = gtk_list_box_row_new ();
+  gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), label);
+
+  g_object_set_data (G_OBJECT (row), "list", list);
+
+  return row;
+}
+
+static void
+select_current_row (TasksWindow *self)
+{
+  guint position;
+  GtkListBoxRow *row;
+
+  if (!self->current_list)
+    return;
+
+  position = tasks_manager_get_position (tasks_manager_get_default (),
+                                         self->current_list);
+  row = gtk_list_box_get_row_at_index (self->list, position);
+
+  gtk_list_box_select_row (self->list, row);
+}
+
+static void
+view_back_cb (TasksWindow *self)
+{
+  adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_BACK);
+}
+
+static void
+set_current_list (TasksWindow *self,
+                  TasksList   *list)
+{
+  self->current_list = list;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURRENT_LIST]);
+
+  select_current_row (self);
+}
+
+static void
+list_new_dialog_cb (const char  *value,
+                    TasksWindow *self)
+{
+  g_autoptr (TasksList) list = tasks_list_new (value);
+
+  tasks_manager_add_list (tasks_manager_get_default (), list);
+
+  set_current_list (self, list);
+
+  adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_FORWARD);
+}
+
+static void
+list_new_cb (TasksWindow *self)
+{
+  tasks_show_dialog (GTK_WINDOW (self),
+                     _("New List"),
+                     _("Create"),
+                     _("Name"),
+                     "",
+                     (TasksDialogFunc) list_new_dialog_cb,
+                     self);
+}
+
+static void
+list_rename_dialog_cb (const char  *value,
+                       TasksWindow *self)
+{
+  tasks_list_set_title (self->current_list, value);
+}
+
+static void
+list_rename_cb (TasksWindow *self)
+{
+  tasks_show_dialog (GTK_WINDOW (self),
+                     _("Rename List"),
+                     _("Rename"),
+                     _("Name"),
+                     tasks_list_get_title (self->current_list),
+                     (TasksDialogFunc) list_rename_dialog_cb,
+                     self);
+}
+
+static void
+list_delete_cb (TasksWindow *self)
+{
+  TasksManager *manager = tasks_manager_get_default ();
+  guint position;
+
+  position = tasks_manager_remove_list (manager, self->current_list);
+
+  if (position > 0) {
+    g_autoptr (TasksList) list = NULL;
+
+    list = g_list_model_get_item (G_LIST_MODEL (manager), position - 1);
+
+    set_current_list (self, list);
+  } else {
+    set_current_list (self, NULL);
+  }
+
+  adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_BACK);
+}
+
+static void
+win_preferences_cb (TasksWindow *self)
+{
+  GtkWindow *window = tasks_preferences_window_new ();
+
+  gtk_window_set_transient_for (window, GTK_WINDOW (self));
+  gtk_window_present (window);
+}
+
+static void
+win_about_cb (TasksWindow *self)
+{
+  gtk_show_about_dialog (GTK_WINDOW (self),
+                         "title", _("About Tasks"),
+                         "program-name", _("Tasks"),
+                         "logo-icon-name", "application-x-executable",
+                         "version", "1.2.3",
+                         NULL);
+}
+
+static void
+notify_leaflet_folded_cb (TasksWindow *self)
+{
+  if (adw_leaflet_get_folded (self->leaflet)) {
+    gtk_list_box_set_selection_mode (self->list, GTK_SELECTION_NONE);
+  } else {
+    gtk_list_box_set_selection_mode (self->list, GTK_SELECTION_SINGLE);
+    select_current_row (self);
+  }
+}
+
+static void
+row_activated_cb (TasksWindow   *self,
+                  GtkListBoxRow *row)
+{
+  set_current_list (self, g_object_get_data (G_OBJECT (row), "list"));
+
+  adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_FORWARD);
+}
+
+static void
+lists_changed_cb (TasksWindow *self)
+{
+  TasksManager *manager = tasks_manager_get_default ();
+  guint n_lists = g_list_model_get_n_items (G_LIST_MODEL (manager));
+
+  if (n_lists > 0)
+    gtk_stack_set_visible_child_name (self->empty_stack, "main");
+  else
+    gtk_stack_set_visible_child_name (self->empty_stack, "empty");
+
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "list.rename", n_lists > 0);
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "list.delete", n_lists > 0);
+}
+
+static void
+tasks_window_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  TasksWindow *self = TASKS_WINDOW (object);
+
+  switch (prop_id) {
+  case PROP_CURRENT_LIST:
+    g_value_set_object (value, self->current_list);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static gboolean
+tasks_window_close_request (GtkWindow *window)
+{
+  tasks_manager_save (tasks_manager_get_default ());
+
+  return FALSE;
+}
+
+static void
+tasks_window_class_init (TasksWindowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass);
+
+  object_class->get_property = tasks_window_get_property;
+  window_class->close_request = tasks_window_close_request;
+
+  props[PROP_CURRENT_LIST] =
+    g_param_spec_object ("current-list",
+                         "Current List",
+                         "Current List",
+                         TASKS_TYPE_LIST,
+                         G_PARAM_READABLE |
+                         G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/example/Tasks/tasks-window.ui");
+  gtk_widget_class_bind_template_child (widget_class, TasksWindow, empty_stack);
+  gtk_widget_class_bind_template_child (widget_class, TasksWindow, leaflet);
+  gtk_widget_class_bind_template_child (widget_class, TasksWindow, list);
+  gtk_widget_class_bind_template_child (widget_class, TasksWindow, view);
+  gtk_widget_class_bind_template_callback (widget_class, notify_leaflet_folded_cb);
+  gtk_widget_class_bind_template_callback (widget_class, row_activated_cb);
+
+  gtk_widget_class_install_action (widget_class, "view.back", NULL,
+                                   (GtkWidgetActionActivateFunc) view_back_cb);
+
+  gtk_widget_class_install_action (widget_class, "list.new", NULL,
+                                   (GtkWidgetActionActivateFunc) list_new_cb);
+  gtk_widget_class_install_action (widget_class, "list.rename", NULL,
+                                   (GtkWidgetActionActivateFunc) list_rename_cb);
+  gtk_widget_class_install_action (widget_class, "list.delete", NULL,
+                                   (GtkWidgetActionActivateFunc) list_delete_cb);
+
+  gtk_widget_class_install_action (widget_class, "win.preferences", NULL,
+                                   (GtkWidgetActionActivateFunc) win_preferences_cb);
+  gtk_widget_class_install_action (widget_class, "win.about", NULL,
+                                   (GtkWidgetActionActivateFunc) win_about_cb);
+
+  g_type_ensure (TASKS_TYPE_LIST);
+  g_type_ensure (TASKS_TYPE_VIEW);
+}
+
+static void
+tasks_window_init (TasksWindow *self)
+{
+  TasksManager *manager = tasks_manager_get_default ();
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_list_box_bind_model (self->list,
+                           G_LIST_MODEL (manager),
+                           (GtkListBoxCreateWidgetFunc) create_list_row,
+                           self,
+                           NULL);
+
+  g_signal_connect_swapped (manager,
+                            "items-changed",
+                            G_CALLBACK (lists_changed_cb),
+                            self);
+
+  if (g_list_model_get_n_items (G_LIST_MODEL (manager)) > 0) {
+    g_autoptr (TasksList) list = NULL;
+
+    list = g_list_model_get_item (G_LIST_MODEL (manager), 0);
+
+    set_current_list (self, list);
+
+    lists_changed_cb (self);
+  }
+}
+
+GtkWindow *
+tasks_window_new (GtkApplication *app)
+{
+  g_return_val_if_fail (GTK_IS_APPLICATION (app), NULL);
+
+  return g_object_new (TASKS_TYPE_WINDOW,
+                       "application", app,
+                       NULL);
+}
diff --git a/examples/tasks/tasks-window.h b/examples/tasks/tasks-window.h
new file mode 100644
index 00000000..294bf32d
--- /dev/null
+++ b/examples/tasks/tasks-window.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define TASKS_TYPE_WINDOW (tasks_window_get_type())
+
+G_DECLARE_FINAL_TYPE (TasksWindow, tasks_window, TASKS, WINDOW, AdwApplicationWindow)
+
+GtkWindow *tasks_window_new (GtkApplication *app);
+
+G_END_DECLS
diff --git a/examples/tasks/tasks-window.ui b/examples/tasks/tasks-window.ui
new file mode 100644
index 00000000..bc467a68
--- /dev/null
+++ b/examples/tasks/tasks-window.ui
@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <menu id="primary_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Preferences</attribute>
+        <attribute name="action">win.preferences</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_About Tasks</attribute>
+        <attribute name="action">win.about</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="list_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Rename</attribute>
+        <attribute name="action">list.rename</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Delete</attribute>
+        <attribute name="action">list.delete</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="TasksWindow" parent="AdwApplicationWindow">
+    <property name="default-width">800</property>
+    <property name="default-height">600</property>
+    <binding name="title">
+      <lookup name="title">
+        <lookup name="current-list">TasksWindow</lookup>
+      </lookup>
+    </binding>
+    <property name="content">
+      <object class="GtkStack" id="empty_stack">
+        <property name="transition-type">crossfade</property>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">empty</property>
+            <property name="child">
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkHeaderBar">
+                    <property name="title-widget">
+                      <object class="AdwWindowTitle"/>
+                    </property>
+                    <style>
+                      <class name="flat"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkWindowHandle">
+                    <property name="vexpand">True</property>
+                    <property name="child">
+                      <object class="AdwStatusPage">
+                        <property name="icon-name">checkbox-checked-symbolic</property>
+                        <property name="title" translatable="yes">No Tasks</property>
+                        <property name="description" translatable="yes">Create some tasks to start using the 
app.</property>
+                        <property name="child">
+                          <object class="GtkButton">
+                            <property name="label" translatable="yes">_New List</property>
+                            <property name="use-underline">True</property>
+                            <property name="halign">center</property>
+                            <property name="action-name">list.new</property>
+                            <style>
+                              <class name="pill"/>
+                              <class name="suggested-action"/>
+                            </style>
+                          </object>
+                        </property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">main</property>
+            <property name="child">
+              <object class="AdwLeaflet" id="leaflet">
+                <property name="can-navigate-back">True</property>
+                <property name="fold-threshold-policy">natural</property>
+                <signal name="notify::folded" handler="notify_leaflet_folded_cb" swapped="yes"/>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="width-request">200</property>
+                    <child>
+                      <object class="AdwHeaderBar">
+                        <binding name="show-end-title-buttons">
+                          <lookup name="folded">leaflet</lookup>
+                        </binding>
+                        <property name="title-widget">
+                          <object class="AdwWindowTitle"/>
+                        </property>
+                        <child type="start">
+                          <object class="GtkToggleButton">
+                            <property name="icon-name">list-add-symbolic</property>
+                            <property name="tooltip-text" translatable="yes">New List</property>
+                            <property name="action-name">list.new</property>
+                          </object>
+                        </child>
+                        <child type="end">
+                          <object class="GtkMenuButton">
+                            <property name="direction">none</property>
+                            <property name="menu-model">primary_menu</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkScrolledWindow">
+                        <property name="vexpand">True</property>
+                        <property name="child">
+                          <object class="GtkViewport">
+                            <property name="scroll-to-focus">True</property>
+                            <property name="child">
+                              <object class="GtkListBox" id="list">
+                                <style>
+                                  <class name="navigation-sidebar"/>
+                                </style>
+                                <signal name="row-activated" handler="row_activated_cb" swapped="true"/>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="label">List 1</property>
+                                    <property name="xalign">0</property>
+                                  </object>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="label">List 2</property>
+                                    <property name="xalign">0</property>
+                                  </object>
+                                </child>
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="label">List 3</property>
+                                    <property name="xalign">0</property>
+                                  </object>
+                                </child>
+                              </object>
+                            </property>
+                          </object>
+                        </property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="AdwLeafletPage">
+                    <property name="navigatable">False</property>
+                    <property name="child">
+                      <object class="GtkSeparator"/>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="hexpand">True</property>
+                    <child>
+                      <object class="AdwHeaderBar">
+                        <binding name="show-start-title-buttons">
+                          <lookup name="folded">leaflet</lookup>
+                        </binding>
+                        <property name="title-widget">
+                          <object class="AdwWindowTitle">
+                            <property name="title" bind-source="TasksWindow" bind-property="title" 
bind-flags="sync-create"/>
+                          </object>
+                        </property>
+                        <child type="start">
+                          <object class="GtkButton">
+                            <binding name="visible">
+                              <lookup name="folded">leaflet</lookup>
+                            </binding>
+                            <property name="icon-name">go-previous-symbolic</property>
+                            <property name="tooltip-text" translatable="yes">Back</property>
+                            <property name="action-name">view.back</property>
+                          </object>
+                        </child>
+                        <child type="end">
+                          <object class="GtkMenuButton">
+                            <property name="icon-name">view-more-symbolic</property>
+                            <property name="menu-model">list_menu</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="TasksView" id="view">
+                        <binding name="list">
+                          <lookup name="current-list">TasksWindow</lookup>
+                        </binding>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
diff --git a/examples/tasks/tasks.gresource.xml b/examples/tasks/tasks.gresource.xml
new file mode 100644
index 00000000..cbb30bd7
--- /dev/null
+++ b/examples/tasks/tasks.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/example/Tasks">
+    <file>tasks-preferences-window.ui</file>
+    <file>tasks-view.ui</file>
+    <file>tasks-window.ui</file>
+  </gresource>
+</gresources>


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