[libadwaita/wip/exalm/julian-demo] Add another example
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/julian-demo] Add another example
- Date: Mon, 27 Dec 2021 13:18:30 +0000 (UTC)
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]