/* gtd-task-list-view.c * * Copyright (C) 2015-2020 Georges Basile Stavracas Neto * * 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 . */ #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 #include #include /** * 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 %s 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; }