/* gtd-task-row.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 "GtdTaskRow" #include "gtd-debug.h" #include "gtd-edit-pane.h" #include "gtd-manager.h" #include "gtd-markdown-renderer.h" #include "gtd-provider.h" #include "gtd-star-widget.h" #include "gtd-task-row.h" #include "gtd-task.h" #include "gtd-task-list.h" #include "gtd-task-list-view.h" #include "gtd-utils-private.h" #include "gtd-widget.h" #include #include #include struct _GtdTaskRow { GtkListBoxRow parent; /**/ GtkWidget *content_box; GtkWidget *main_box; GtkWidget *done_check; GtkWidget *edit_panel_revealer; GtkWidget *header_event_box; GtdStarWidget *star_widget; GtkWidget *title_entry; /* task widgets */ GtkLabel *task_date_label; GtkLabel *task_list_label; /* dnd widgets */ GtkWidget *dnd_box; GtkWidget *dnd_icon; gint clicked_x; gint clicked_y; /* data */ GtdTask *task; GtdEditPane *edit_pane; GtdMarkdownRenderer *renderer; GPtrArray *bindings; gboolean active; gboolean changed; }; #define PRIORITY_ICON_SIZE 8 static void on_star_widget_activated_cb (GtdStarWidget *star_widget, GParamSpec *pspec, GtdTaskRow *self); G_DEFINE_TYPE (GtdTaskRow, gtd_task_row, GTK_TYPE_LIST_BOX_ROW) enum { ENTER, EXIT, REMOVE_TASK, NUM_SIGNALS }; enum { PROP_0, PROP_RENDERER, PROP_TASK, LAST_PROP }; static guint signals[NUM_SIGNALS] = { 0, }; /* * Auxiliary methods */ static gboolean date_to_label_binding_cb (GBinding *binding, const GValue *from_value, GValue *to_value, gpointer user_data) { g_autofree gchar *new_label = NULL; GDateTime *dt; g_return_val_if_fail (GTD_IS_TASK_ROW (user_data), FALSE); dt = g_value_get_boxed (from_value); if (dt) { g_autoptr (GDateTime) today = g_date_time_new_now_local (); if (g_date_time_get_year (dt) == g_date_time_get_year (today) && g_date_time_get_month (dt) == g_date_time_get_month (today)) { if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today)) { new_label = g_strdup (_("Today")); } else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) + 1) { new_label = g_strdup (_("Tomorrow")); } else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) - 1) { new_label = g_strdup (_("Yesterday")); } else if (g_date_time_get_day_of_year (dt) > g_date_time_get_day_of_month (today) && g_date_time_get_day_of_year (dt) < g_date_time_get_day_of_month (today) + 7) { new_label = g_date_time_format (dt, "%A"); } else { new_label = g_date_time_format (dt, "%x"); } } else { new_label = g_date_time_format (dt, "%x"); } } else { new_label = g_strdup (""); } g_value_set_string (to_value, new_label); return TRUE; } static GtkWidget* create_transient_row (GtdTaskRow *self) { GtdTaskRow *new_row; new_row = GTD_TASK_ROW (gtd_task_row_new (self->task, self->renderer)); gtk_widget_set_size_request (GTK_WIDGET (new_row), gtk_widget_get_allocated_width (GTK_WIDGET (self)), -1); gtk_revealer_set_reveal_child (GTK_REVEALER (new_row->edit_panel_revealer), self->active); gtk_widget_add_css_class (GTK_WIDGET (new_row), "background"); gtk_widget_set_opacity (GTK_WIDGET (new_row), 0.66); return GTK_WIDGET (new_row); } /* * Callbacks */ static void on_task_updated_cb (GObject *object, GAsyncResult *result, gpointer user_data) { g_autoptr (GError) error = NULL; gtd_provider_update_task_finish (GTD_PROVIDER (object), result, &error); if (error) { g_warning ("Error updating task: %s", error->message); return; } } static void on_remove_task_cb (GtdEditPane *edit_panel, GtdTask *task, GtdTaskRow *self) { g_signal_emit (self, signals[REMOVE_TASK], 0); } static void on_button_press_event_cb (GtkGestureClick *gesture, gint n_press, gdouble x, gdouble y, GtdTaskRow *self) { GtkWidget *widget; gdouble real_x; gdouble real_y; widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); gtk_widget_translate_coordinates (widget, GTK_WIDGET (self), x, y, &real_x, &real_y); self->clicked_x = real_x; self->clicked_y = real_y; GTD_TRACE_MSG ("GtkGestureClick:pressed received from a %s at %.1lf,%.1lf (%.1lf,%.1lf)", G_OBJECT_TYPE_NAME (widget), x, y, real_x, real_y); } static GdkContentProvider* on_drag_prepare_cb (GtkDragSource *source, gdouble x, gdouble y, GtdTaskRow *self) { GTD_ENTRY; GTD_RETURN (gdk_content_provider_new_typed (GTD_TYPE_TASK, self->task)); } static void on_drag_begin_cb (GtkDragSource *source, GdkDrag *drag, GtdTaskRow *self) { GtkWidget *drag_icon; GtkWidget *new_row; GtkWidget *widget; GTD_ENTRY; widget = GTK_WIDGET (self); gtk_widget_set_cursor_from_name (widget, "grabbing"); new_row = create_transient_row (self); drag_icon = gtk_drag_icon_get_for_drag (drag); gtk_drag_icon_set_child (GTK_DRAG_ICON (drag_icon), new_row); gdk_drag_set_hotspot (drag, self->clicked_x, self->clicked_y); gtk_widget_hide (widget); GTD_EXIT; } static gboolean on_drag_cancelled_cb (GtkDragSource *source, GdkDrag *drag, GdkDragCancelReason result, GtdTaskRow *self) { GTD_ENTRY; gtk_widget_set_cursor_from_name (GTK_WIDGET (self), NULL); gtk_widget_show (GTK_WIDGET (self)); GTD_RETURN (FALSE); } static void on_complete_check_toggled_cb (GtkCheckButton *button, GtdTaskRow *self) { GTD_ENTRY; g_assert (GTD_IS_TASK (self->task)); gtd_task_row_set_active (self, FALSE); gtd_task_set_complete (self->task, gtk_check_button_get_active (button)); gtd_provider_update_task (gtd_task_get_provider (self->task), self->task, NULL, on_task_updated_cb, self); GTD_EXIT; } static void on_complete_changed_cb (GtdTaskRow *self, GParamSpec *pspec, GtdTask *task) { gboolean complete; complete = gtd_task_get_complete (task); if (complete) gtk_widget_add_css_class (GTK_WIDGET (self), "complete"); else gtk_widget_remove_css_class (GTK_WIDGET (self), "complete"); /* Update the toggle button as well */ g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self); gtk_check_button_set_active (GTK_CHECK_BUTTON (self->done_check), complete); g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self); } static void on_task_important_changed_cb (GtdTask *task, GParamSpec *pspec, GtdTaskRow *self) { g_signal_handlers_block_by_func (self->star_widget, on_star_widget_activated_cb, self); gtd_star_widget_set_active (self->star_widget, gtd_task_get_important (task)); g_signal_handlers_unblock_by_func (self->star_widget, on_star_widget_activated_cb, self); } static void on_star_widget_activated_cb (GtdStarWidget *star_widget, GParamSpec *pspec, GtdTaskRow *self) { g_signal_handlers_block_by_func (self->task, on_task_important_changed_cb, self); gtd_task_set_important (self->task, gtd_star_widget_get_active (star_widget)); gtd_provider_update_task (gtd_task_get_provider (self->task), self->task, NULL, on_task_updated_cb, self); g_signal_handlers_unblock_by_func (self->task, on_task_important_changed_cb, self); } static gboolean on_key_pressed_cb (GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType modifiers, GtdTaskRow *self) { GTD_ENTRY; /* Exit when pressing Esc without modifiers */ if (keyval == GDK_KEY_Escape && !(modifiers & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) { GTD_TRACE_MSG ("Escape pressed, closing task…"); gtd_task_row_set_active (self, FALSE); } GTD_RETURN (GDK_EVENT_PROPAGATE); } static void on_task_changed_cb (GtdTaskRow *self) { g_debug ("Task changed"); self->changed = TRUE; } /* * GObject overrides */ static void gtd_task_row_finalize (GObject *object) { GtdTaskRow *self = GTD_TASK_ROW (object); if (self->changed) { if (self->task) { gtd_provider_update_task (gtd_task_get_provider (self->task), self->task, NULL, on_task_updated_cb, self); } self->changed = FALSE; } g_clear_object (&self->task); G_OBJECT_CLASS (gtd_task_row_parent_class)->finalize (object); } static void gtd_task_row_dispose (GObject *object) { GtdTaskRow *self; GtdTask *task; self = GTD_TASK_ROW (object); task = self->task; g_clear_pointer (&self->bindings, g_ptr_array_unref); if (task) g_signal_handlers_disconnect_by_func (task, on_complete_changed_cb, self); G_OBJECT_CLASS (gtd_task_row_parent_class)->dispose (object); } static void gtd_task_row_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtdTaskRow *self = GTD_TASK_ROW (object); switch (prop_id) { case PROP_RENDERER: g_value_set_object (value, self->renderer); break; case PROP_TASK: g_value_set_object (value, self->task); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtd_task_row_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtdTaskRow *self = GTD_TASK_ROW (object); switch (prop_id) { case PROP_RENDERER: self->renderer = g_value_get_object (value); break; case PROP_TASK: gtd_task_row_set_task (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtd_task_row_class_init (GtdTaskRowClass *klass) { GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = gtd_task_row_dispose; object_class->finalize = gtd_task_row_finalize; object_class->get_property = gtd_task_row_get_property; object_class->set_property = gtd_task_row_set_property; /** * GtdTaskRow::renderer: * * The internal markdown renderer. */ g_object_class_install_property ( object_class, PROP_RENDERER, g_param_spec_object ("renderer", "Renderer", "Renderer", GTD_TYPE_MARKDOWN_RENDERER, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * GtdTaskRow::task: * * The task that this row represents, or %NULL. */ g_object_class_install_property ( object_class, PROP_TASK, g_param_spec_object ("task", "Task of the row", "The task that this row represents", GTD_TYPE_TASK, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * GtdTaskRow::enter: * * Emitted when the row is focused and in the editing state. */ signals[ENTER] = g_signal_new ("enter", GTD_TYPE_TASK_ROW, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtdTaskRow::exit: * * Emitted when the row is unfocused and leaves the editing state. */ signals[EXIT] = g_signal_new ("exit", GTD_TYPE_TASK_ROW, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtdTaskRow::remove-task: * * Emitted when the user wants to delete the task represented by this row. */ signals[REMOVE_TASK] = g_signal_new ("remove-task", GTD_TYPE_TASK_ROW, G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-row.ui"); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, content_box); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_box); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_icon); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, done_check); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, edit_panel_revealer); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, header_event_box); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, main_box); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, star_widget); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_date_label); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_list_label); gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, title_entry); gtk_widget_class_bind_template_callback (widget_class, on_button_press_event_cb); gtk_widget_class_bind_template_callback (widget_class, on_complete_check_toggled_cb); gtk_widget_class_bind_template_callback (widget_class, on_key_pressed_cb); gtk_widget_class_bind_template_callback (widget_class, on_remove_task_cb); gtk_widget_class_bind_template_callback (widget_class, on_task_changed_cb); gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); gtk_widget_class_set_css_name (widget_class, "taskrow"); } static void gtd_task_row_init (GtdTaskRow *self) { GtkDragSource *drag_source; self->bindings = g_ptr_array_new_with_free_func ((GDestroyNotify) g_binding_unbind); self->active = FALSE; gtk_widget_init_template (GTK_WIDGET (self)); drag_source = gtk_drag_source_new (); gtk_drag_source_set_actions (drag_source, GDK_ACTION_MOVE); g_signal_connect (drag_source, "prepare", G_CALLBACK (on_drag_prepare_cb), self); g_signal_connect (drag_source, "drag-begin", G_CALLBACK (on_drag_begin_cb), self); g_signal_connect (drag_source, "drag-cancel", G_CALLBACK (on_drag_cancelled_cb), self); gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_source)); gtk_widget_set_cursor_from_name (self->dnd_icon, "grab"); gtk_widget_set_cursor_from_name (self->header_event_box, "pointer"); } GtkWidget* gtd_task_row_new (GtdTask *task, GtdMarkdownRenderer *renderer) { return g_object_new (GTD_TYPE_TASK_ROW, "task", task, "renderer", renderer, NULL); } void gtd_task_row_set_task (GtdTaskRow *self, GtdTask *task) { GBinding *binding; GtdTask *old_task; g_return_if_fail (GTD_IS_TASK_ROW (self)); old_task = self->task; if (old_task) { g_signal_handlers_disconnect_by_func (old_task, on_complete_changed_cb, self); g_signal_handlers_disconnect_by_func (old_task, on_task_important_changed_cb, self); g_signal_handlers_disconnect_by_func (old_task, on_star_widget_activated_cb, self); g_ptr_array_set_size (self->bindings, 0); } g_set_object (&self->task, task); if (task) { gtk_label_set_label (self->task_list_label, gtd_task_list_get_name (gtd_task_get_list (task))); g_signal_handlers_block_by_func (self->title_entry, on_task_changed_cb, self); g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self); binding = g_object_bind_property (task, "loading", self, "sensitive", G_BINDING_DEFAULT | G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE); g_ptr_array_add (self->bindings, binding); binding = g_object_bind_property (task, "title", self->title_entry, "text", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); g_ptr_array_add (self->bindings, binding); binding = g_object_bind_property_full (task, "due-date", self->task_date_label, "label", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, date_to_label_binding_cb, NULL, self, NULL); g_ptr_array_add (self->bindings, binding); on_complete_changed_cb (self, NULL, task); g_signal_connect_object (task, "notify::complete", G_CALLBACK (on_complete_changed_cb), self, G_CONNECT_SWAPPED); on_task_important_changed_cb (task, NULL, self); g_signal_connect_object (task, "notify::important", G_CALLBACK (on_task_important_changed_cb), self, 0); g_signal_connect_object (self->star_widget, "notify::active", G_CALLBACK (on_star_widget_activated_cb), self, 0); g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self); g_signal_handlers_unblock_by_func (self->title_entry, on_task_changed_cb, self); } } /** * gtd_task_row_get_task: * @row: a #GtdTaskRow * * Retrieves the #GtdTask that @row manages, or %NULL if none * is set. * * Returns: (transfer none): the internal task of @row */ GtdTask* gtd_task_row_get_task (GtdTaskRow *row) { g_return_val_if_fail (GTD_IS_TASK_ROW (row), NULL); return row->task; } /** * gtd_task_row_set_list_name_visible: * @row: a #GtdTaskRow * @show_list_name: %TRUE to show the list name, %FALSE to hide it * * Sets @row's list name label visibility to @show_list_name. */ void gtd_task_row_set_list_name_visible (GtdTaskRow *row, gboolean show_list_name) { g_return_if_fail (GTD_IS_TASK_ROW (row)); gtk_widget_set_visible (GTK_WIDGET (row->task_list_label), show_list_name); } /** * gtd_task_row_set_due_date_visible: * @row: a #GtdTaskRow * @show_due_date: %TRUE to show the due, %FALSE to hide it * * Sets @row's due date label visibility to @show_due_date. */ void gtd_task_row_set_due_date_visible (GtdTaskRow *row, gboolean show_due_date) { g_return_if_fail (GTD_IS_TASK_ROW (row)); gtk_widget_set_visible (GTK_WIDGET (row->task_date_label), show_due_date); } gboolean gtd_task_row_get_active (GtdTaskRow *self) { g_return_val_if_fail (GTD_IS_TASK_ROW (self), FALSE); return self->active; } void gtd_task_row_set_active (GtdTaskRow *self, gboolean active) { GDateTime *dt; g_return_if_fail (GTD_IS_TASK_ROW (self)); if (self->active == active) return; self->active = active; /* Create or destroy the edit panel */ if (active && !self->edit_pane) { GTD_TRACE_MSG ("Creating edit pane"); self->edit_pane = GTD_EDIT_PANE (gtd_edit_pane_new ()); gtd_edit_pane_set_markdown_renderer (self->edit_pane, self->renderer); gtd_edit_pane_set_task (self->edit_pane, self->task); gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), GTK_WIDGET (self->edit_pane)); gtk_widget_show (GTK_WIDGET (self->edit_pane)); g_signal_connect_swapped (self->edit_pane, "changed", G_CALLBACK (on_task_changed_cb), self); g_signal_connect (self->edit_pane, "remove-task", G_CALLBACK (on_remove_task_cb), self); } else if (!active && self->edit_pane) { /* For some reason, setting the due date before closing the Edit Row has * the potential to cause the Task List to update, causing the active task row * to become corrupted, and crash Endeavour. My guess is this is an upstream * behaviour, so work around it by setting the task due date during close */ dt = gtd_edit_pane_get_new_date_time (self->edit_pane); if (gtd_task_get_due_date (self->task) != dt) { gtd_task_set_due_date (self->task, gtd_edit_pane_get_new_date_time (self->edit_pane)); } GTD_TRACE_MSG ("Destroying edit pane"); gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), NULL); self->edit_pane = NULL; } /* And reveal or hide it */ gtk_revealer_set_reveal_child (GTK_REVEALER (self->edit_panel_revealer), active); /* Toggle title editability */ gtk_editable_set_editable (GTK_EDITABLE (self->title_entry), active); gtk_widget_set_sensitive (GTK_WIDGET (self->title_entry), active); /* Save the task if it is not being loaded */ if (!active && !gtd_object_get_loading (GTD_OBJECT (self->task)) && self->changed) { g_debug ("Saving task…"); gtd_provider_update_task (gtd_task_get_provider (self->task), self->task, NULL, on_task_updated_cb, self); self->changed = FALSE; } if (active) { gtk_widget_add_css_class (GTK_WIDGET (self), "card"); } else { gtk_widget_remove_css_class (GTK_WIDGET (self), "card"); } g_signal_emit (self, active ? signals[ENTER] : signals[EXIT], 0); } void gtd_task_row_set_sizegroups (GtdTaskRow *self, GtkSizeGroup *name_group, GtkSizeGroup *date_group) { gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_list_label)); gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_date_label)); }