diff options
| author | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
|---|---|---|
| committer | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
| commit | 5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch) | |
| tree | ed28aefed8add0da1c55c08fdf80b23c4346e0dc /src/gui/gtd-task-list-view.c | |
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src/gui/gtd-task-list-view.c')
| -rw-r--r-- | src/gui/gtd-task-list-view.c | 1220 |
1 files changed, 1220 insertions, 0 deletions
diff --git a/src/gui/gtd-task-list-view.c b/src/gui/gtd-task-list-view.c new file mode 100644 index 0000000..6810c17 --- /dev/null +++ b/src/gui/gtd-task-list-view.c @@ -0,0 +1,1220 @@ +/* gtd-task-list-view.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdTaskListView" + +#include "gtd-debug.h" +#include "gtd-edit-pane.h" +#include "gtd-task-list-view.h" +#include "gtd-task-list-view-model.h" +#include "gtd-manager.h" +#include "gtd-markdown-renderer.h" +#include "gtd-new-task-row.h" +#include "gtd-notification.h" +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-row.h" +#include "gtd-utils-private.h" +#include "gtd-widget.h" +#include "gtd-window.h" + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +/** + * SECTION:gtd-task-list-view + * @Short_description: A widget to display tasklists + * @Title:GtdTaskListView + * + * The #GtdTaskListView widget shows the tasks of a #GtdTaskList with + * various options to fine-tune the appearance. Alternatively, one can + * pass a #GList of #GtdTask objects. + * + * It supports custom sorting and header functions, so the tasks can be + * sorted in various ways. See the "Today" and "Scheduled" panels for reference + * implementations. + * + * Example: + * |[ + * GtdTaskListView *view = gtd_task_list_view_new (); + * + * gtd_task_list_view_set_model (view, model); + * + * // Date which tasks will be automatically assigned + * gtd_task_list_view_set_default_date (view, now); + * ]| + * + */ + +struct _GtdTaskListView +{ + GtkBox parent; + + AdwStatusPage *empty_list_widget; + GtkListBox *listbox; + GtkStack *main_stack; + GtkWidget *scrolled_window; + + /* internal */ + gboolean can_toggle; + gboolean show_due_date; + gboolean show_list_name; + GtdTaskListViewModel *view_model; + GListModel *model; + GDateTime *default_date; + + GListModel *incomplete_tasks_model; + guint n_incomplete_tasks; + + guint scroll_to_bottom_handler_id; + + GHashTable *task_to_row; + + /* Markup renderer*/ + GtdMarkdownRenderer *renderer; + + /* DnD */ + guint scroll_timeout_id; + gboolean scroll_up; + + /* action */ + GActionGroup *action_group; + + /* Custom header function data */ + GtdTaskListViewHeaderFunc header_func; + gpointer header_user_data; + + GtdTaskRow *active_row; + GtkSizeGroup *due_date_sizegroup; + GtkSizeGroup *tasklist_name_sizegroup; +}; + +#define DND_SCROLL_OFFSET 24 //px +#define TASK_REMOVED_NOTIFICATION_ID "task-removed-id" + + +static gboolean filter_complete_func (gpointer item, + gpointer user_data); + +static void on_clear_completed_tasks_activated_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +static void on_incomplete_tasks_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTaskListView *self); + +static void on_remove_task_row_cb (GtdTaskRow *row, + GtdTaskListView *self); + +static void on_task_row_entered_cb (GtdTaskListView *self, + GtdTaskRow *row); + +static void on_task_row_exited_cb (GtdTaskListView *self, + GtdTaskRow *row); + +static gboolean scroll_to_bottom_cb (gpointer data); + + +G_DEFINE_TYPE (GtdTaskListView, gtd_task_list_view, GTK_TYPE_BOX) + +static const GActionEntry gtd_task_list_view_entries[] = { + { "clear-completed-tasks", on_clear_completed_tasks_activated_cb }, +}; + +typedef struct +{ + GtdTaskListView *view; + GtdTask *task; +} RemoveTaskData; + +enum { + PROP_0, + PROP_SHOW_LIST_NAME, + PROP_SHOW_DUE_DATE, + LAST_PROP +}; + + +/* + * Auxiliary methods + */ + +static void +set_active_row (GtdTaskListView *self, + GtdTaskRow *row) +{ + if (self->active_row == row) + return; + + if (self->active_row) + gtd_task_row_set_active (self->active_row, FALSE); + + self->active_row = row; + + if (row) + { + gtd_task_row_set_active (row, TRUE); + gtk_widget_grab_focus (GTK_WIDGET (row)); + } +} + +static void +schedule_scroll_to_bottom (GtdTaskListView *self) +{ + if (self->scroll_to_bottom_handler_id > 0) + return; + + self->scroll_to_bottom_handler_id = g_timeout_add (250, scroll_to_bottom_cb, self); +} + +static void +update_empty_state (GtdTaskListView *self) +{ + gboolean show_empty_list_widget; + gboolean is_empty; + + g_assert (GTD_IS_TASK_LIST_VIEW (self)); + + if (!self->model) + return; + + is_empty = g_list_model_get_n_items (self->model) == 0; + + show_empty_list_widget = !GTD_IS_TASK_LIST (self->model) && + (is_empty || self->n_incomplete_tasks == 0); + gtk_stack_set_visible_child_name (self->main_stack, + show_empty_list_widget ? "empty-list" : "task-list"); +} + +static void +update_incomplete_tasks_model (GtdTaskListView *self) +{ + if (!self->incomplete_tasks_model) + { + g_autoptr (GtkFilterListModel) filter_model = NULL; + GtkCustomFilter *filter; + + filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + filter_model = gtk_filter_list_model_new (NULL, GTK_FILTER (filter)); + gtk_filter_list_model_set_incremental (filter_model, TRUE); + + self->incomplete_tasks_model = G_LIST_MODEL (g_steal_pointer (&filter_model)); + } + + gtk_filter_list_model_set_model (GTK_FILTER_LIST_MODEL (self->incomplete_tasks_model), + self->model); + self->n_incomplete_tasks = g_list_model_get_n_items (self->incomplete_tasks_model); + + g_signal_connect (self->incomplete_tasks_model, + "items-changed", + G_CALLBACK (on_incomplete_tasks_items_changed_cb), + self); +} + + +/* + * Callbacks + */ + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + return !gtd_task_get_complete (task); +} + +static void +on_incomplete_tasks_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTaskListView *self) +{ + self->n_incomplete_tasks -= n_removed; + self->n_incomplete_tasks += n_added; + + update_empty_state (self); +} + +static void +on_empty_list_widget_add_tasks_cb (AdwStatusPage *empty_list_widget, + GtdTaskListView *self) +{ + gtk_stack_set_visible_child_name (self->main_stack, "task-list"); +} + +static void +on_new_task_row_entered_cb (GtdTaskListView *self, + GtdNewTaskRow *row) +{ + set_active_row (self, NULL); +} + +static void +on_new_task_row_exited_cb (GtdTaskListView *self, + GtdNewTaskRow *row) +{ +} + +static GtkWidget* +create_row_for_task_cb (gpointer item, + gpointer user_data) +{ + GtdTaskListView *self; + GtkWidget *row; + + self = GTD_TASK_LIST_VIEW (user_data); + + set_active_row (self, NULL); + + if (GTD_IS_TASK (item)) + { + row = gtd_task_row_new (item, self->renderer); + + gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row), self->show_list_name); + gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row), self->show_due_date); + + g_signal_connect_swapped (row, "enter", G_CALLBACK (on_task_row_entered_cb), self); + g_signal_connect_swapped (row, "exit", G_CALLBACK (on_task_row_exited_cb), self); + + g_signal_connect (row, "remove-task", G_CALLBACK (on_remove_task_row_cb), self); + } + else + { + row = gtd_new_task_row_new (); + + g_signal_connect_swapped (row, "enter", G_CALLBACK (on_new_task_row_entered_cb), self); + g_signal_connect_swapped (row, "exit", G_CALLBACK (on_new_task_row_exited_cb), self); + } + + g_hash_table_insert (self->task_to_row, item, row); + + return row; +} + +static gboolean +scroll_to_bottom_cb (gpointer data) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (data); + GtkWidget *widget; + GtkRoot *root; + + widget = GTK_WIDGET (self); + root = gtk_widget_get_root (widget); + + if (!root) + return G_SOURCE_CONTINUE; + + self->scroll_to_bottom_handler_id = 0; + + /* + * Only focus the new task row if the current list is visible, + * and the focused widget isn't inside this list view. + */ + if (gtk_widget_get_visible (widget) && + gtk_widget_get_child_visible (widget) && + gtk_widget_get_mapped (widget) && + !gtk_widget_is_ancestor (gtk_window_get_focus (GTK_WINDOW (root)), widget)) + { + GtkWidget *new_task_row; + gboolean ignored; + + new_task_row = gtk_widget_get_last_child (GTK_WIDGET (self->listbox)); + gtk_widget_grab_focus (new_task_row); + g_signal_emit_by_name (self->scrolled_window, "scroll-child", GTK_SCROLL_END, FALSE, &ignored); + } + + return G_SOURCE_REMOVE; +} + +static void +on_task_removed_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_remove_task_finish (GTD_PROVIDER (source), result, &error); + + if (error) + g_warning ("Error removing task list: %s", error->message); +} + +static void +on_clear_completed_tasks_activated_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdTaskListView *self; + GListModel *model; + guint i; + + self = GTD_TASK_LIST_VIEW (user_data); + model = self->model; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + g_autoptr (GtdTask) task = g_list_model_get_item (model, i); + + if (!gtd_task_get_complete (task)) + continue; + + gtd_provider_remove_task (gtd_task_get_provider (task), + task, + NULL, + on_task_removed_cb, + self); + } +} + +static void +on_remove_task_action_cb (GtdNotification *notification, + gpointer user_data) +{ + RemoveTaskData *data = user_data; + + gtd_provider_remove_task (gtd_task_get_provider (data->task), + data->task, + NULL, + on_task_removed_cb, + data->view); + + g_clear_pointer (&data, g_free); +} + +static void +on_undo_remove_task_action_cb (GtdNotification *notification, + gpointer user_data) +{ + RemoveTaskData *data; + GtdTaskList *list; + + data = user_data; + + /* + * Re-add task to the list. This will emit GListModel:items-changed (since + * GtdTaskList implements GListModel) and the row will be added back. + */ + list = gtd_task_get_list (data->task); + gtd_task_list_add_task (list, data->task); + + g_free (data); +} + +static void +on_remove_task_row_cb (GtdTaskRow *row, + GtdTaskListView *self) +{ + GtdManager *manager = gtd_manager_get_default (); + g_autofree gchar *text = NULL; + GtdNotification *notification; + RemoveTaskData *data; + GtdTaskList *list; + GtdTask *task; + + task = gtd_task_row_get_task (row); + + text = g_strdup_printf (_("Task <b>%s</b> removed"), gtd_task_get_title (task)); + + data = g_new0 (RemoveTaskData, 1); + data->view = self; + data->task = task; + + /* Remove task from the list */ + list = gtd_task_get_list (task); + gtd_task_list_remove_task (list, task); + + /* Notify about the removal */ + notification = gtd_notification_new (text); + + gtd_notification_set_dismissal_action (notification, + (GtdNotificationActionFunc) on_remove_task_action_cb, + data); + + gtd_notification_set_secondary_action (notification, + _("Undo"), + (GtdNotificationActionFunc) on_undo_remove_task_action_cb, + data); + + gtd_manager_send_notification (manager, notification); + + /* Clear the active row */ + set_active_row (self, NULL); +} + +static void +on_task_row_entered_cb (GtdTaskListView *self, + GtdTaskRow *row) +{ + set_active_row (self, row); +} + +static void +on_task_row_exited_cb (GtdTaskListView *self, + GtdTaskRow *row) +{ + if (row == self->active_row) + set_active_row (self, NULL); +} + +static void +on_listbox_row_activated_cb (GtkListBox *listbox, + GtkListBoxRow *row, + GtdTaskListView *self) +{ + GtdTaskRow *task_row; + + GTD_ENTRY; + + if (!GTD_IS_TASK_ROW (row)) + GTD_RETURN (); + + task_row = GTD_TASK_ROW (row); + + /* Toggle the row */ + if (gtd_task_row_get_active (task_row)) + set_active_row (self, NULL); + else + set_active_row (self, task_row); + + GTD_EXIT; +} + + +/* + * Custom sorting functions + */ + +static void +internal_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + GtdTaskListView *self) +{ + GtkWidget *header; + GtdTask *row_task; + GtdTask *before_task; + + if (!self->header_func) + return; + + row_task = before_task = NULL; + + if (!GTD_IS_TASK_ROW (row)) + return; + + if (row) + row_task = gtd_task_row_get_task (GTD_TASK_ROW (row)); + + if (before) + before_task = gtd_task_row_get_task (GTD_TASK_ROW (before)); + + header = self->header_func (row_task, before_task, self->header_user_data); + + if (header) + { + GtkWidget *real_header = gtd_widget_new (); + gtk_widget_insert_before (header, real_header, NULL); + + header = real_header; + } + + gtk_list_box_row_set_header (row, header); +} + + +/* + * Drag n' Drop functions + */ + +static GtkListBoxRow* +get_drop_row_at_y (GtdTaskListView *self, + gdouble y) +{ + GtkAllocation row_allocation; + GtkListBoxRow *hovered_row; + GtkListBoxRow *task_row; + GtkListBoxRow *drop_row; + + hovered_row = gtk_list_box_get_row_at_y (self->listbox, y); + + /* Small optimization when hovering the first row */ + if (gtk_list_box_row_get_index (hovered_row) == 0) + return GTD_IS_TASK_ROW (hovered_row) ? hovered_row : NULL; + + drop_row = NULL; + task_row = hovered_row; + + gtk_widget_get_allocation (GTK_WIDGET (hovered_row), &row_allocation); + + /* + * If the pointer if in the top part of the row, move the DnD row to + * the previous row. + */ + if (y < row_allocation.y + row_allocation.height / 2) + { + GtkWidget *aux; + + /* Search for a valid task row */ + for (aux = gtk_widget_get_prev_sibling (GTK_WIDGET (hovered_row)); + aux; + aux = gtk_widget_get_prev_sibling (aux)) + { + /* Skip DnD, New task and hidden rows */ + if (!gtk_widget_get_visible (aux)) + continue; + + drop_row = GTK_LIST_BOX_ROW (aux); + break; + } + } + else + { + drop_row = task_row; + } + + return GTD_IS_TASK_ROW (drop_row) ? drop_row : NULL; +} + +static inline gboolean +scroll_to_dnd (gpointer user_data) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (user_data); + GtkAdjustment *vadjustment; + gint value; + + vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window)); + value = gtk_adjustment_get_value (vadjustment) + (self->scroll_up ? -6 : 6); + + gtk_adjustment_set_value (vadjustment, + CLAMP (value, 0, gtk_adjustment_get_upper (vadjustment))); + + return G_SOURCE_CONTINUE; +} + +static void +check_dnd_scroll (GtdTaskListView *self, + gboolean should_cancel, + gdouble y) +{ + gdouble current_y, height; + + if (should_cancel) + { + if (self->scroll_timeout_id > 0) + { + g_source_remove (self->scroll_timeout_id); + self->scroll_timeout_id = 0; + } + + return; + } + + height = gtk_widget_get_allocated_height (self->scrolled_window); + gtk_widget_translate_coordinates (GTK_WIDGET (self->listbox), + self->scrolled_window, + 0, y, + NULL, ¤t_y); + + if (current_y < DND_SCROLL_OFFSET || current_y > height - DND_SCROLL_OFFSET) + { + if (self->scroll_timeout_id > 0) + return; + + /* Start the autoscroll */ + self->scroll_up = current_y < DND_SCROLL_OFFSET; + self->scroll_timeout_id = g_timeout_add (25, + scroll_to_dnd, + self); + } + else + { + if (self->scroll_timeout_id == 0) + return; + + /* Cancel the autoscroll */ + g_source_remove (self->scroll_timeout_id); + self->scroll_timeout_id = 0; + } +} + +static GdkDragAction +on_drop_target_drag_enter_cb (GtkDropTarget *drop_target, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GTD_ENTRY; + + gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y)); + + GTD_RETURN (GDK_ACTION_MOVE); +} + +static void +on_drop_target_drag_leave_cb (GtkDropTarget *drop_target, + GtdTaskListView *self) +{ + GTD_ENTRY; + + gtk_list_box_drag_unhighlight_row (self->listbox); + check_dnd_scroll (self, TRUE, -1); + + GTD_EXIT; +} + +static GdkDragAction +on_drop_target_drag_motion_cb (GtkDropTarget *drop_target, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GdkDrop *drop; + GdkDrag *drag; + + GTD_ENTRY; + + /* Clear the currently active row */ + set_active_row (self, NULL); + + drop = gtk_drop_target_get_current_drop (drop_target); + drag = gdk_drop_get_drag (drop); + + if (!drag) + { + g_info ("Only dragging task rows is supported"); + GTD_GOTO (fail); + } + + gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y)); + check_dnd_scroll (self, FALSE, y); + GTD_RETURN (GDK_ACTION_MOVE); + +fail: + GTD_RETURN (0); +} + +static gboolean +on_drop_target_drag_drop_cb (GtkDropTarget *drop_target, + const GValue *value, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GtkListBoxRow *drop_row; + GtkWidget *row; + GtdTask *hovered_task; + GtdTask *source_task; + GdkDrop *drop; + GdkDrag *drag; + gint64 current_position; + gint64 new_position; + + GTD_ENTRY; + + drop = gtk_drop_target_get_current_drop (drop_target); + drag = gdk_drop_get_drag (drop); + + if (!drag) + { + g_info ("Only dragging task rows is supported"); + GTD_RETURN (FALSE); + } + + gtk_list_box_drag_unhighlight_row (self->listbox); + + source_task = g_value_get_object (value); + g_assert (source_task != NULL); + + /* + * When the drag operation began, the source row was hidden. Now is the time + * to show it again. + */ + row = g_hash_table_lookup (self->task_to_row, source_task); + gtk_widget_show (row); + + drop_row = get_drop_row_at_y (self, y); + if (!drop_row) + { + check_dnd_scroll (self, TRUE, -1); + GTD_RETURN (FALSE); + } + + hovered_task = gtd_task_row_get_task (GTD_TASK_ROW (drop_row)); + new_position = gtd_task_get_position (hovered_task); + current_position = gtd_task_get_position (source_task); + + GTD_TRACE_MSG ("Dropping task %p at %ld", source_task, new_position); + + if (new_position != current_position) + { + gtd_task_list_move_task_to_position (GTD_TASK_LIST (self->model), + source_task, + new_position); + } + + check_dnd_scroll (self, TRUE, -1); + + GTD_RETURN (TRUE); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_view_finalize (GObject *object) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + g_clear_handle_id (&self->scroll_to_bottom_handler_id, g_source_remove); + g_clear_pointer (&self->task_to_row, g_hash_table_destroy); + g_clear_pointer (&self->default_date, g_date_time_unref); + g_clear_object (&self->incomplete_tasks_model); + g_clear_object (&self->renderer); + g_clear_object (&self->model); + + G_OBJECT_CLASS (gtd_task_list_view_parent_class)->finalize (object); +} + +static void +gtd_task_list_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + switch (prop_id) + { + case PROP_SHOW_DUE_DATE: + g_value_set_boolean (value, self->show_due_date); + break; + + case PROP_SHOW_LIST_NAME: + g_value_set_boolean (value, self->show_list_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + switch (prop_id) + { + case PROP_SHOW_DUE_DATE: + gtd_task_list_view_set_show_due_date (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_LIST_NAME: + gtd_task_list_view_set_show_list_name (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_view_constructed (GObject *object) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + GdkTexture *texture = gdk_texture_new_from_resource ("/org/gnome/todo/ui/assets/all-done.svg"); + G_OBJECT_CLASS (gtd_task_list_view_parent_class)->constructed (object); + + /* action_group */ + self->action_group = G_ACTION_GROUP (g_simple_action_group_new ()); + + adw_status_page_set_paintable (self->empty_list_widget, GDK_PAINTABLE (texture)); + + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + gtd_task_list_view_entries, + G_N_ELEMENTS (gtd_task_list_view_entries), + object); +} + + +/* + * GtkWidget overrides + */ + +static void +gtd_task_list_view_map (GtkWidget *widget) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (widget); + GtkRoot *root; + + + update_empty_state (self); + + GTK_WIDGET_CLASS (gtd_task_list_view_parent_class)->map (widget); + + root = gtk_widget_get_root (widget); + + /* Clear previously added "list" actions */ + gtk_widget_insert_action_group (GTK_WIDGET (root), "list", NULL); + + /* Add this instance's action group */ + gtk_widget_insert_action_group (GTK_WIDGET (root), "list", self->action_group); +} + +static void +gtd_task_list_view_class_init (GtdTaskListViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_task_list_view_finalize; + object_class->constructed = gtd_task_list_view_constructed; + object_class->get_property = gtd_task_list_view_get_property; + object_class->set_property = gtd_task_list_view_set_property; + + widget_class->map = gtd_task_list_view_map; + + g_type_ensure (GTD_TYPE_EDIT_PANE); + g_type_ensure (GTD_TYPE_NEW_TASK_ROW); + g_type_ensure (GTD_TYPE_TASK_ROW); + + /** + * GtdTaskListView::show-list-name: + * + * Whether the task rows should show the list name. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_LIST_NAME, + g_param_spec_boolean ("show-list-name", + "Whether task rows show the list name", + "Whether task rows show the list name at the end of the row", + FALSE, + G_PARAM_READWRITE)); + + /** + * GtdTaskListView::show-due-date: + * + * Whether due dates of the tasks are shown. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_DUE_DATE, + g_param_spec_boolean ("show-due-date", + "Whether due dates are shown", + "Whether due dates of the tasks are visible or not", + TRUE, + G_PARAM_READWRITE)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, due_date_sizegroup); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, empty_list_widget); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, main_stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, tasklist_name_sizegroup); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, scrolled_window); + + gtk_widget_class_bind_template_callback (widget_class, on_empty_list_widget_add_tasks_cb); + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_entered_cb); + gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_exited_cb); + gtk_widget_class_bind_template_callback (widget_class, on_task_row_entered_cb); + gtk_widget_class_bind_template_callback (widget_class, on_task_row_exited_cb); + + gtk_widget_class_set_css_name (widget_class, "tasklistview"); +} + +static void +gtd_task_list_view_init (GtdTaskListView *self) +{ + GtkDropTarget *target; + + self->task_to_row = g_hash_table_new (NULL, NULL); + + self->can_toggle = TRUE; + self->show_due_date = TRUE; + self->show_due_date = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + target = gtk_drop_target_new (GTD_TYPE_TASK, GDK_ACTION_MOVE); + gtk_drop_target_set_preload (target, TRUE); + g_signal_connect (target, "drop", G_CALLBACK (on_drop_target_drag_drop_cb), self); + g_signal_connect (target, "enter", G_CALLBACK (on_drop_target_drag_enter_cb), self); + g_signal_connect (target, "leave", G_CALLBACK (on_drop_target_drag_leave_cb), self); + g_signal_connect (target, "motion", G_CALLBACK (on_drop_target_drag_motion_cb), self); + + gtk_widget_add_controller (GTK_WIDGET (self->listbox), GTK_EVENT_CONTROLLER (target)); + + self->renderer = gtd_markdown_renderer_new (); + + self->view_model = gtd_task_list_view_model_new (); + gtk_list_box_bind_model (self->listbox, + G_LIST_MODEL (self->view_model), + create_row_for_task_cb, + self, + NULL); +} + +/** + * gtd_task_list_view_new: + * + * Creates a new #GtdTaskListView + * + * Returns: (transfer full): a newly allocated #GtdTaskListView + */ +GtkWidget* +gtd_task_list_view_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LIST_VIEW, NULL); +} + +/** + * gtd_task_list_view_get_model: + * @view: a #GtdTaskListView + * + * Retrieves the #GtdTaskList from @view, or %NULL if none was set. + * + * Returns: (transfer none): the #GListModel of @view, or %NULL is + * none was set. + */ +GListModel* +gtd_task_list_view_get_model (GtdTaskListView *view) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), NULL); + + return view->model; +} + +/** + * gtd_task_list_view_set_model: + * @view: a #GtdTaskListView + * @model: a #GListModel + * + * Sets the internal #GListModel of @view. The model must have + * its element GType as @GtdTask. + */ +void +gtd_task_list_view_set_model (GtdTaskListView *view, + GListModel *model) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + g_return_if_fail (G_IS_LIST_MODEL (model)); + + if (view->model == model) + return; + + view->model = model; + + gtd_task_list_view_model_set_model (view->view_model, model); + schedule_scroll_to_bottom (view); + update_incomplete_tasks_model (view); + update_empty_state (view); +} + +/** + * gtd_task_list_view_get_show_list_name: + * @view: a #GtdTaskListView + * + * Whether @view shows the tasks' list names. + * + * Returns: %TRUE if @view show the tasks' list names, %FALSE otherwise + */ +gboolean +gtd_task_list_view_get_show_list_name (GtdTaskListView *view) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), FALSE); + + return view->show_list_name; +} + +/** + * gtd_task_list_view_set_show_list_name: + * @view: a #GtdTaskListView + * @show_list_name: %TRUE to show list names, %FALSE to hide it + * + * Whether @view should should it's tasks' list name. + */ +void +gtd_task_list_view_set_show_list_name (GtdTaskListView *view, + gboolean show_list_name) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + + if (view->show_list_name != show_list_name) + { + GtkWidget *child; + + view->show_list_name = show_list_name; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child)); + + if (!GTD_IS_TASK_ROW (row_child)) + continue; + + gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row_child), show_list_name); + } + + g_object_notify (G_OBJECT (view), "show-list-name"); + } +} + +/** + * gtd_task_list_view_get_show_due_date: + * @self: a #GtdTaskListView + * + * Retrieves whether the @self is showing the due dates of the tasks + * or not. + * + * Returns: %TRUE if due dates are visible, %FALSE otherwise. + */ +gboolean +gtd_task_list_view_get_show_due_date (GtdTaskListView *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), FALSE); + + return self->show_due_date; +} + +/** + * gtd_task_list_view_set_show_due_date: + * @self: a #GtdTaskListView + * @show_due_date: %TRUE to show due dates, %FALSE otherwise + * + * Sets whether @self shows the due dates of the tasks or not. + */ +void +gtd_task_list_view_set_show_due_date (GtdTaskListView *self, + gboolean show_due_date) +{ + GtkWidget *child; + + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self)); + + if (self->show_due_date == show_due_date) + return; + + self->show_due_date = show_due_date; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child)); + + if (!GTD_IS_TASK_ROW (row_child)) + continue; + + gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row_child), show_due_date); + } + + g_object_notify (G_OBJECT (self), "show-due-date"); +} + +/** + * gtd_task_list_view_set_header_func: + * @view: a #GtdTaskListView + * @func: (closure user_data) (scope call) (nullable): the header function + * @user_data: data passed to @func + * + * Sets @func as the header function of @view. You can safely call + * %gtk_list_box_row_set_header from within @func. + * + * Do not unref nor free any of the passed data. + */ +void +gtd_task_list_view_set_header_func (GtdTaskListView *view, + GtdTaskListViewHeaderFunc func, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + + if (func) + { + view->header_func = func; + view->header_user_data = user_data; + + gtk_list_box_set_header_func (view->listbox, + (GtkListBoxUpdateHeaderFunc) internal_header_func, + view, + NULL); + } + else + { + view->header_func = NULL; + view->header_user_data = NULL; + + gtk_list_box_set_header_func (view->listbox, + NULL, + NULL, + NULL); + } +} + +/** + * gtd_task_list_view_get_default_date: + * @self: a #GtdTaskListView + * + * Retrieves the current default date which new tasks are set to. + * + * Returns: (nullable): a #GDateTime, or %NULL + */ +GDateTime* +gtd_task_list_view_get_default_date (GtdTaskListView *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), NULL); + + return self->default_date; +} + +/** + * gtd_task_list_view_set_default_date: + * @self: a #GtdTaskListView + * @default_date: (nullable): the default_date, or %NULL + * + * Sets the current default date. + */ +void +gtd_task_list_view_set_default_date (GtdTaskListView *self, + GDateTime *default_date) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self)); + + if (self->default_date == default_date) + return; + + g_clear_pointer (&self->default_date, g_date_time_unref); + self->default_date = default_date ? g_date_time_ref (default_date) : NULL; +} + |
