1 /* gtd-task-list-view.c
2  *
3  * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define G_LOG_DOMAIN "GtdTaskListView"
20 
21 #include "gtd-debug.h"
22 #include "gtd-edit-pane.h"
23 #include "gtd-empty-list-widget.h"
24 #include "gtd-task-list-view.h"
25 #include "gtd-task-list-view-model.h"
26 #include "gtd-manager.h"
27 #include "gtd-markdown-renderer.h"
28 #include "gtd-new-task-row.h"
29 #include "gtd-notification.h"
30 #include "gtd-provider.h"
31 #include "gtd-task.h"
32 #include "gtd-task-list.h"
33 #include "gtd-task-row.h"
34 #include "gtd-utils-private.h"
35 #include "gtd-widget.h"
36 #include "gtd-window.h"
37 
38 #include <glib.h>
39 #include <glib/gi18n.h>
40 #include <gtk/gtk.h>
41 
42 /**
43  * SECTION:gtd-task-list-view
44  * @Short_description: A widget to display tasklists
45  * @Title:GtdTaskListView
46  *
47  * The #GtdTaskListView widget shows the tasks of a #GtdTaskList with
48  * various options to fine-tune the appearance. Alternatively, one can
49  * pass a #GList of #GtdTask objects.
50  *
51  * It supports custom sorting and header functions, so the tasks can be
52  * sorted in various ways. See the "Today" and "Scheduled" panels for reference
53  * implementations.
54  *
55  * Example:
56  * |[
57  * GtdTaskListView *view = gtd_task_list_view_new ();
58  *
59  * gtd_task_list_view_set_model (view, model);
60  *
61  * // Date which tasks will be automatically assigned
62  * gtd_task_list_view_set_default_date (view, now);
63  * ]|
64  *
65  */
66 
67 struct _GtdTaskListView
68 {
69   GtkBox                  parent;
70 
71   GtdEmptyListWidget    *empty_list_widget;
72   GtkListBox            *listbox;
73   GtkStack              *main_stack;
74   GtkWidget             *scrolled_window;
75 
76   /* internal */
77   gboolean               can_toggle;
78   gboolean               show_due_date;
79   gboolean               show_list_name;
80   GtdTaskListViewModel  *view_model;
81   GListModel            *model;
82   GDateTime             *default_date;
83 
84   GListModel            *incomplete_tasks_model;
85   guint                  n_incomplete_tasks;
86 
87   guint                  scroll_to_bottom_handler_id;
88 
89   GHashTable            *task_to_row;
90 
91   /* Markup renderer*/
92   GtdMarkdownRenderer   *renderer;
93 
94   /* DnD */
95   guint                  scroll_timeout_id;
96   gboolean               scroll_up;
97 
98   /* action */
99   GActionGroup          *action_group;
100 
101   struct {
102     GtkWidget *top;
103     GtkWidget *bottom;
104   } drag_highlight;
105 
106   /* Custom header function data */
107   GtdTaskListViewHeaderFunc header_func;
108   gpointer                  header_user_data;
109 
110   GtdTaskRow             *active_row;
111   GtkSizeGroup           *due_date_sizegroup;
112   GtkSizeGroup           *tasklist_name_sizegroup;
113 };
114 
115 #define DND_SCROLL_OFFSET            24 //px
116 #define TASK_REMOVED_NOTIFICATION_ID "task-removed-id"
117 
118 
119 static gboolean      filter_complete_func                        (gpointer            item,
120                                                                   gpointer            user_data);
121 
122 static void          on_clear_completed_tasks_activated_cb       (GSimpleAction      *simple,
123                                                                   GVariant           *parameter,
124                                                                   gpointer            user_data);
125 
126 static void          on_incomplete_tasks_items_changed_cb        (GListModel         *model,
127                                                                   guint               position,
128                                                                   guint               n_removed,
129                                                                   guint               n_added,
130                                                                   GtdTaskListView    *self);
131 
132 static void          on_remove_task_row_cb                       (GtdTaskRow         *row,
133                                                                   GtdTaskListView    *self);
134 
135 static void          on_task_row_entered_cb                      (GtdTaskListView    *self,
136                                                                   GtdTaskRow         *row);
137 
138 static void          on_task_row_exited_cb                       (GtdTaskListView    *self,
139                                                                   GtdTaskRow         *row);
140 
141 static gboolean      scroll_to_bottom_cb                         (gpointer            data);
142 
143 
144 G_DEFINE_TYPE (GtdTaskListView, gtd_task_list_view, GTK_TYPE_BOX)
145 
146 static const GActionEntry gtd_task_list_view_entries[] = {
147   { "clear-completed-tasks", on_clear_completed_tasks_activated_cb },
148 };
149 
150 typedef struct
151 {
152   GtdTaskListView *view;
153   GtdTask         *task;
154 } RemoveTaskData;
155 
156 enum {
157   PROP_0,
158   PROP_SHOW_LIST_NAME,
159   PROP_SHOW_DUE_DATE,
160   LAST_PROP
161 };
162 
163 
164 /*
165  * Auxiliary methods
166  */
167 
168 static inline GtdTaskRow*
task_row_from_row(GtkListBoxRow * row)169 task_row_from_row (GtkListBoxRow *row)
170 {
171   GtkWidget *child = gtk_list_box_row_get_child (row);
172 
173   if (!GTD_IS_TASK_ROW (child))
174     return NULL;
175 
176   return GTD_TASK_ROW (child);
177 }
178 
179 static void
set_active_row(GtdTaskListView * self,GtdTaskRow * row)180 set_active_row (GtdTaskListView *self,
181                 GtdTaskRow      *row)
182 {
183   if (self->active_row == row)
184     return;
185 
186   if (self->active_row)
187     gtd_task_row_set_active (self->active_row, FALSE);
188 
189   self->active_row = row;
190 
191   if (row)
192     {
193       gtd_task_row_set_active (row, TRUE);
194       gtk_widget_grab_focus (GTK_WIDGET (row));
195     }
196 }
197 
198 static void
schedule_scroll_to_bottom(GtdTaskListView * self)199 schedule_scroll_to_bottom (GtdTaskListView *self)
200 {
201   if (self->scroll_to_bottom_handler_id > 0)
202     return;
203 
204   self->scroll_to_bottom_handler_id = g_timeout_add (250, scroll_to_bottom_cb, self);
205 }
206 
207 static void
update_empty_state(GtdTaskListView * self)208 update_empty_state (GtdTaskListView *self)
209 {
210   gboolean show_empty_list_widget;
211   gboolean is_empty;
212 
213   g_assert (GTD_IS_TASK_LIST_VIEW (self));
214 
215   if (!self->model)
216     return;
217 
218   is_empty = g_list_model_get_n_items (self->model) == 0;
219   gtd_empty_list_widget_set_is_empty (self->empty_list_widget, is_empty);
220 
221   show_empty_list_widget = !GTD_IS_TASK_LIST (self->model) &&
222                            (is_empty || self->n_incomplete_tasks == 0);
223   gtk_stack_set_visible_child_name (self->main_stack,
224                                     show_empty_list_widget ? "empty-list" : "task-list");
225 }
226 
227 static void
update_incomplete_tasks_model(GtdTaskListView * self)228 update_incomplete_tasks_model (GtdTaskListView *self)
229 {
230   if (!self->incomplete_tasks_model)
231     {
232       g_autoptr (GtkFilterListModel) filter_model = NULL;
233       GtkCustomFilter *filter;
234 
235       filter = gtk_custom_filter_new (filter_complete_func, self, NULL);
236       filter_model = gtk_filter_list_model_new (NULL, GTK_FILTER (filter));
237       gtk_filter_list_model_set_incremental (filter_model, TRUE);
238 
239       self->incomplete_tasks_model = G_LIST_MODEL (g_steal_pointer (&filter_model));
240     }
241 
242   gtk_filter_list_model_set_model (GTK_FILTER_LIST_MODEL (self->incomplete_tasks_model),
243                                    self->model);
244   self->n_incomplete_tasks = g_list_model_get_n_items (self->incomplete_tasks_model);
245 
246   g_signal_connect (self->incomplete_tasks_model,
247                     "items-changed",
248                     G_CALLBACK (on_incomplete_tasks_items_changed_cb),
249                     self);
250 }
251 
252 
253 /*
254  * Callbacks
255  */
256 
257 static gboolean
filter_complete_func(gpointer item,gpointer user_data)258 filter_complete_func (gpointer item,
259                       gpointer user_data)
260 {
261   GtdTask *task = (GtdTask*) item;
262   return !gtd_task_get_complete (task);
263 }
264 
265 static void
on_incomplete_tasks_items_changed_cb(GListModel * model,guint position,guint n_removed,guint n_added,GtdTaskListView * self)266 on_incomplete_tasks_items_changed_cb (GListModel      *model,
267                                       guint            position,
268                                       guint            n_removed,
269                                       guint            n_added,
270                                       GtdTaskListView *self)
271 {
272   self->n_incomplete_tasks -= n_removed;
273   self->n_incomplete_tasks += n_added;
274 
275   update_empty_state (self);
276 }
277 
278 static void
on_empty_list_widget_add_tasks_cb(GtdEmptyListWidget * empty_list_widget,GtdTaskListView * self)279 on_empty_list_widget_add_tasks_cb (GtdEmptyListWidget *empty_list_widget,
280                                    GtdTaskListView    *self)
281 {
282   gtk_stack_set_visible_child_name (self->main_stack, "task-list");
283 }
284 
285 static void
on_new_task_row_entered_cb(GtdTaskListView * self,GtdNewTaskRow * row)286 on_new_task_row_entered_cb (GtdTaskListView *self,
287                             GtdNewTaskRow   *row)
288 {
289   set_active_row (self, NULL);
290 }
291 
292 static void
on_new_task_row_exited_cb(GtdTaskListView * self,GtdNewTaskRow * row)293 on_new_task_row_exited_cb (GtdTaskListView *self,
294                            GtdNewTaskRow   *row)
295 {
296 }
297 
298 static GtkWidget*
create_row_for_task_cb(gpointer item,gpointer user_data)299 create_row_for_task_cb (gpointer item,
300                         gpointer user_data)
301 {
302   GtdTaskListView *self;
303   GtkWidget *listbox_row;
304   GtkWidget *row;
305 
306   self = GTD_TASK_LIST_VIEW (user_data);
307 
308   listbox_row = gtk_list_box_row_new ();
309 
310   if (GTD_IS_TASK (item))
311     {
312       row = gtd_task_row_new (item, self->renderer);
313 
314       gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row), self->show_list_name);
315       gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row), self->show_due_date);
316 
317       g_signal_connect_swapped (row, "enter", G_CALLBACK (on_task_row_entered_cb), self);
318       g_signal_connect_swapped (row, "exit", G_CALLBACK (on_task_row_exited_cb), self);
319 
320       g_signal_connect (row, "remove-task", G_CALLBACK (on_remove_task_row_cb), self);
321     }
322   else
323     {
324       row = gtd_new_task_row_new ();
325 
326       g_signal_connect_swapped (row, "enter", G_CALLBACK (on_new_task_row_entered_cb), self);
327       g_signal_connect_swapped (row, "exit", G_CALLBACK (on_new_task_row_exited_cb), self);
328 
329       gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (listbox_row), FALSE);
330     }
331 
332   gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (listbox_row), row);
333 
334   g_object_bind_property (row, "visible", listbox_row, "visible", G_BINDING_BIDIRECTIONAL);
335 
336   g_hash_table_insert (self->task_to_row, item, row);
337 
338   return listbox_row;
339 }
340 
341 static gboolean
scroll_to_bottom_cb(gpointer data)342 scroll_to_bottom_cb (gpointer data)
343 {
344   GtdTaskListView *self = GTD_TASK_LIST_VIEW (data);
345   GtkWidget *widget;
346   GtkRoot *root;
347 
348   widget = GTK_WIDGET (self);
349   root = gtk_widget_get_root (widget);
350 
351   if (!root)
352     return G_SOURCE_CONTINUE;
353 
354   self->scroll_to_bottom_handler_id = 0;
355 
356   /*
357    * Only focus the new task row if the current list is visible,
358    * and the focused widget isn't inside this list view.
359    */
360   if (gtk_widget_get_visible (widget) &&
361       gtk_widget_get_child_visible (widget) &&
362       gtk_widget_get_mapped (widget) &&
363       !gtk_widget_is_ancestor (gtk_window_get_focus (GTK_WINDOW (root)), widget))
364     {
365       GtkWidget *new_task_row;
366       gboolean ignored;
367 
368       new_task_row = gtk_widget_get_last_child (GTK_WIDGET (self->listbox));
369       gtk_widget_grab_focus (new_task_row);
370       g_signal_emit_by_name (self->scrolled_window, "scroll-child", GTK_SCROLL_END, FALSE, &ignored);
371     }
372 
373   return G_SOURCE_REMOVE;
374 }
375 
376 static void
on_task_removed_cb(GObject * source,GAsyncResult * result,gpointer user_data)377 on_task_removed_cb (GObject      *source,
378                     GAsyncResult *result,
379                     gpointer      user_data)
380 {
381   g_autoptr (GError) error = NULL;
382 
383   gtd_provider_remove_task_finish (GTD_PROVIDER (source), result, &error);
384 
385   if (error)
386     g_warning ("Error removing task list: %s", error->message);
387 }
388 
389 static void
on_clear_completed_tasks_activated_cb(GSimpleAction * simple,GVariant * parameter,gpointer user_data)390 on_clear_completed_tasks_activated_cb (GSimpleAction *simple,
391                                        GVariant      *parameter,
392                                        gpointer       user_data)
393 {
394   GtdTaskListView *self;
395   GListModel *model;
396   guint i;
397 
398   self = GTD_TASK_LIST_VIEW (user_data);
399   model = self->model;
400 
401   for (i = 0; i < g_list_model_get_n_items (model); i++)
402     {
403       g_autoptr (GtdTask) task = g_list_model_get_item (model, i);
404 
405       if (!gtd_task_get_complete (task))
406         continue;
407 
408       gtd_provider_remove_task (gtd_task_get_provider (task),
409                                 task,
410                                 NULL,
411                                 on_task_removed_cb,
412                                 self);
413     }
414 }
415 
416 static void
on_remove_task_action_cb(GtdNotification * notification,gpointer user_data)417 on_remove_task_action_cb (GtdNotification *notification,
418                           gpointer         user_data)
419 {
420   RemoveTaskData *data = user_data;
421 
422   gtd_provider_remove_task (gtd_task_get_provider (data->task),
423                             data->task,
424                             NULL,
425                             on_task_removed_cb,
426                             data->view);
427 
428   g_clear_pointer (&data, g_free);
429 }
430 
431 static void
on_undo_remove_task_action_cb(GtdNotification * notification,gpointer user_data)432 on_undo_remove_task_action_cb (GtdNotification *notification,
433                                gpointer         user_data)
434 {
435   RemoveTaskData *data;
436   GtdTaskList *list;
437 
438   data = user_data;
439 
440   /*
441    * Readd task to the list. This will emit GListModel:items-changed (since
442    * GtdTaskList implements GListModel) and the row will be added back.
443    */
444   list = gtd_task_get_list (data->task);
445   gtd_task_list_add_task (list, data->task);
446 
447   g_free (data);
448 }
449 
450 static void
on_remove_task_row_cb(GtdTaskRow * row,GtdTaskListView * self)451 on_remove_task_row_cb (GtdTaskRow      *row,
452                        GtdTaskListView *self)
453 {
454   g_autofree gchar *text = NULL;
455   GtdNotification *notification;
456   RemoveTaskData *data;
457   GtdTaskList *list;
458   GtdWindow *window;
459   GtdTask *task;
460 
461   task = gtd_task_row_get_task (row);
462 
463   text = g_strdup_printf (_("Task <b>%s</b> removed"), gtd_task_get_title (task));
464   window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (self)));
465 
466   data = g_new0 (RemoveTaskData, 1);
467   data->view = self;
468   data->task = task;
469 
470   /* Remove task from the list */
471   list = gtd_task_get_list (task);
472   gtd_task_list_remove_task (list, task);
473 
474   /* Notify about the removal */
475   notification = gtd_notification_new (text, 5000.0);
476 
477   gtd_notification_set_primary_action (notification,
478                                        (GtdNotificationActionFunc) on_remove_task_action_cb,
479                                        data);
480 
481   gtd_notification_set_secondary_action (notification,
482                                          _("Undo"),
483                                          (GtdNotificationActionFunc) on_undo_remove_task_action_cb,
484                                          data);
485 
486   gtd_window_notify (window, notification);
487 
488 
489   /* Clear the active row */
490   set_active_row (self, NULL);
491 }
492 
493 static void
on_task_row_entered_cb(GtdTaskListView * self,GtdTaskRow * row)494 on_task_row_entered_cb (GtdTaskListView *self,
495                         GtdTaskRow      *row)
496 {
497   set_active_row (self, row);
498 }
499 
500 static void
on_task_row_exited_cb(GtdTaskListView * self,GtdTaskRow * row)501 on_task_row_exited_cb (GtdTaskListView *self,
502                        GtdTaskRow      *row)
503 {
504   if (row == self->active_row)
505     set_active_row (self, NULL);
506 }
507 
508 static void
on_listbox_row_activated_cb(GtkListBox * listbox,GtkListBoxRow * row,GtdTaskListView * self)509 on_listbox_row_activated_cb (GtkListBox      *listbox,
510                              GtkListBoxRow   *row,
511                              GtdTaskListView *self)
512 {
513   GtdTaskRow *task_row;
514 
515   GTD_ENTRY;
516 
517   task_row = task_row_from_row (row);
518 
519   if (!task_row)
520     GTD_RETURN ();
521 
522   /* Toggle the row */
523   if (gtd_task_row_get_active (task_row))
524     set_active_row (self, NULL);
525   else
526     set_active_row (self, task_row);
527 
528   GTD_EXIT;
529 }
530 
531 
532 /*
533  * Custom sorting functions
534  */
535 
536 static void
internal_header_func(GtkListBoxRow * row,GtkListBoxRow * before,GtdTaskListView * self)537 internal_header_func (GtkListBoxRow   *row,
538                       GtkListBoxRow   *before,
539                       GtdTaskListView *self)
540 {
541   GtkWidget *header;
542   GtdTask *row_task;
543   GtdTask *before_task;
544 
545   if (!self->header_func)
546     return;
547 
548   row_task = before_task = NULL;
549 
550   if (!task_row_from_row (row))
551     return;
552 
553   if (row)
554     row_task = gtd_task_row_get_task (task_row_from_row (row));
555 
556   if (before)
557       before_task = gtd_task_row_get_task (task_row_from_row (before));
558 
559   header = self->header_func (row_task, before_task, self->header_user_data);
560 
561   if (header)
562     {
563       GtkWidget *real_header = gtd_widget_new ();
564       gtk_widget_insert_before (header, real_header, NULL);
565 
566       header = real_header;
567     }
568 
569   gtk_list_box_row_set_header (row, header);
570 }
571 
572 
573 /*
574  * Drag n' Drop functions
575  */
576 
577 static void
clear_drag_highlights(GtdTaskListView * self)578 clear_drag_highlights (GtdTaskListView *self)
579 {
580   if (self->drag_highlight.top)
581     {
582       gtk_widget_remove_css_class (self->drag_highlight.top, "top-highlight");
583       self->drag_highlight.top = NULL;
584     }
585 
586   if (self->drag_highlight.bottom)
587     {
588       gtk_widget_remove_css_class (self->drag_highlight.bottom, "bottom-highlight");
589       self->drag_highlight.bottom = NULL;
590     }
591 }
592 
593 static void
update_row_drag_highlight(GtdTaskListView * self,gdouble y)594 update_row_drag_highlight (GtdTaskListView *self,
595                            gdouble          y)
596 {
597   GtkAllocation row_allocation;
598   GtkListBoxRow *hovered_row;
599   GtkListBoxRow *bottom_highlight;
600   GtkListBoxRow *top_highlight;
601 
602   hovered_row = gtk_list_box_get_row_at_y (self->listbox, y);
603   bottom_highlight = NULL;
604   top_highlight = NULL;
605 
606   gtk_widget_get_allocation (GTK_WIDGET (hovered_row), &row_allocation);
607 
608   /*
609    * If the pointer if in the top part of the row, move the DnD row to
610    * the previous row.
611    */
612   if (y < row_allocation.y + row_allocation.height / 2)
613     {
614       GtkWidget *aux;
615 
616       top_highlight = hovered_row;
617 
618       /* Search for a valid task row */
619       for (aux = gtk_widget_get_prev_sibling (GTK_WIDGET (hovered_row));
620            aux;
621            aux = gtk_widget_get_prev_sibling (aux))
622         {
623           /* Skip DnD, New task and hidden rows */
624           if (!gtk_widget_get_visible (aux))
625             continue;
626 
627           bottom_highlight = GTK_LIST_BOX_ROW (aux);
628           break;
629         }
630     }
631   else
632     {
633       GtkWidget *aux;
634 
635       bottom_highlight = hovered_row;
636 
637       /* Search for a valid task row */
638       for (aux = gtk_widget_get_next_sibling (GTK_WIDGET (hovered_row));
639            aux;
640            aux = gtk_widget_get_next_sibling (aux))
641         {
642           /* Skip DnD, New task and hidden rows */
643           if (!gtk_widget_get_visible (aux))
644             continue;
645 
646           top_highlight = GTK_LIST_BOX_ROW (aux);
647           break;
648         }
649     }
650 
651   /* Don't add drag highlights to the new task row */
652   if (top_highlight && !GTD_IS_TASK_ROW (task_row_from_row (top_highlight)))
653     top_highlight = NULL;
654   if (bottom_highlight && !GTD_IS_TASK_ROW (task_row_from_row (bottom_highlight)))
655     bottom_highlight = NULL;
656 
657   /* Unhighlight previously highlighted rows */
658   clear_drag_highlights (self);
659 
660   self->drag_highlight.top = GTK_WIDGET (top_highlight);
661   self->drag_highlight.bottom = GTK_WIDGET (bottom_highlight);
662 
663   /* Highlight new rows */
664   if (self->drag_highlight.top)
665     gtk_widget_add_css_class (self->drag_highlight.top, "top-highlight");
666   if (self->drag_highlight.bottom)
667     gtk_widget_add_css_class (self->drag_highlight.bottom, "bottom-highlight");
668 }
669 
670 static GtkListBoxRow*
get_drop_row_at_y(GtdTaskListView * self,gdouble y)671 get_drop_row_at_y (GtdTaskListView *self,
672                    gdouble          y)
673 {
674   GtkAllocation row_allocation;
675   GtkListBoxRow *hovered_row;
676   GtkListBoxRow *task_row;
677   GtkListBoxRow *drop_row;
678 
679   hovered_row = gtk_list_box_get_row_at_y (self->listbox, y);
680 
681   /* Small optimization when hovering the first row */
682   if (gtk_list_box_row_get_index (hovered_row) == 0)
683     return hovered_row;
684 
685   drop_row = NULL;
686   task_row = hovered_row;
687 
688   gtk_widget_get_allocation (GTK_WIDGET (hovered_row), &row_allocation);
689 
690   /*
691    * If the pointer if in the top part of the row, move the DnD row to
692    * the previous row.
693    */
694   if (y < row_allocation.y + row_allocation.height / 2)
695     {
696       GtkWidget *aux;
697 
698       /* Search for a valid task row */
699       for (aux = gtk_widget_get_prev_sibling (GTK_WIDGET (hovered_row));
700            aux;
701            aux = gtk_widget_get_prev_sibling (aux))
702         {
703           /* Skip DnD, New task and hidden rows */
704           if (!gtk_widget_get_visible (aux))
705             continue;
706 
707           drop_row = GTK_LIST_BOX_ROW (aux);
708           break;
709         }
710     }
711   else
712     {
713       drop_row = task_row;
714     }
715 
716   return task_row_from_row (drop_row) ? drop_row : NULL;
717 }
718 
719 static inline gboolean
scroll_to_dnd(gpointer user_data)720 scroll_to_dnd (gpointer user_data)
721 {
722   GtdTaskListView *self = GTD_TASK_LIST_VIEW (user_data);
723   GtkAdjustment *vadjustment;
724   gint value;
725 
726   vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window));
727   value = gtk_adjustment_get_value (vadjustment) + (self->scroll_up ? -6 : 6);
728 
729   gtk_adjustment_set_value (vadjustment,
730                             CLAMP (value, 0, gtk_adjustment_get_upper (vadjustment)));
731 
732   return G_SOURCE_CONTINUE;
733 }
734 
735 static void
check_dnd_scroll(GtdTaskListView * self,gboolean should_cancel,gdouble y)736 check_dnd_scroll (GtdTaskListView *self,
737                   gboolean         should_cancel,
738                   gdouble          y)
739 {
740   gdouble current_y, height;
741 
742   if (should_cancel)
743     {
744       if (self->scroll_timeout_id > 0)
745         {
746           g_source_remove (self->scroll_timeout_id);
747           self->scroll_timeout_id = 0;
748         }
749 
750       return;
751     }
752 
753   height = gtk_widget_get_allocated_height (self->scrolled_window);
754   gtk_widget_translate_coordinates (GTK_WIDGET (self->listbox),
755                                     self->scrolled_window,
756                                     0, y,
757                                     NULL, &current_y);
758 
759   if (current_y < DND_SCROLL_OFFSET || current_y > height - DND_SCROLL_OFFSET)
760     {
761       if (self->scroll_timeout_id > 0)
762         return;
763 
764       /* Start the autoscroll */
765       self->scroll_up = current_y < DND_SCROLL_OFFSET;
766       self->scroll_timeout_id = g_timeout_add (25,
767                                                scroll_to_dnd,
768                                                self);
769     }
770   else
771     {
772       if (self->scroll_timeout_id == 0)
773         return;
774 
775       /* Cancel the autoscroll */
776       g_source_remove (self->scroll_timeout_id);
777       self->scroll_timeout_id = 0;
778     }
779 }
780 
781 static GdkDragAction
on_drop_target_drag_enter_cb(GtkDropTarget * drop_target,gdouble x,gdouble y,GtdTaskListView * self)782 on_drop_target_drag_enter_cb (GtkDropTarget   *drop_target,
783                               gdouble          x,
784                               gdouble          y,
785                               GtdTaskListView *self)
786 {
787   GTD_ENTRY;
788 
789   update_row_drag_highlight (self, y);
790 
791   GTD_RETURN (GDK_ACTION_MOVE);
792 }
793 
794 static void
on_drop_target_drag_leave_cb(GtkDropTarget * drop_target,GtdTaskListView * self)795 on_drop_target_drag_leave_cb (GtkDropTarget   *drop_target,
796                               GtdTaskListView *self)
797 {
798   GTD_ENTRY;
799 
800   clear_drag_highlights (self);
801   check_dnd_scroll (self, TRUE, -1);
802 
803   GTD_EXIT;
804 }
805 
806 static GdkDragAction
on_drop_target_drag_motion_cb(GtkDropTarget * drop_target,gdouble x,gdouble y,GtdTaskListView * self)807 on_drop_target_drag_motion_cb (GtkDropTarget   *drop_target,
808                                gdouble          x,
809                                gdouble          y,
810                                GtdTaskListView *self)
811 {
812   GdkDrop *drop;
813   GdkDrag *drag;
814 
815   GTD_ENTRY;
816 
817   drop = gtk_drop_target_get_current_drop (drop_target);
818   drag = gdk_drop_get_drag (drop);
819 
820   if (!drag)
821     {
822       g_info ("Only dragging task rows is supported");
823       GTD_GOTO (fail);
824     }
825 
826   update_row_drag_highlight (self, y);
827   check_dnd_scroll (self, FALSE, y);
828   GTD_RETURN (GDK_ACTION_MOVE);
829 
830 fail:
831   GTD_RETURN (0);
832 }
833 
834 static gboolean
on_drop_target_drag_drop_cb(GtkDropTarget * drop_target,const GValue * value,gdouble x,gdouble y,GtdTaskListView * self)835 on_drop_target_drag_drop_cb (GtkDropTarget   *drop_target,
836                              const GValue    *value,
837                              gdouble          x,
838                              gdouble          y,
839                              GtdTaskListView *self)
840 {
841   GtkListBoxRow *drop_row;
842   GtdTaskRow *hovered_row;
843   GtkWidget *row;
844   GtdTask *hovered_task;
845   GtdTask *source_task;
846   GdkDrop *drop;
847   GdkDrag *drag;
848   gint64 current_position;
849   gint64 new_position;
850 
851   GTD_ENTRY;
852 
853   drop = gtk_drop_target_get_current_drop (drop_target);
854   drag = gdk_drop_get_drag (drop);
855 
856   if (!drag)
857     {
858       g_info ("Only dragging task rows is supported");
859       GTD_RETURN (FALSE);
860     }
861 
862   clear_drag_highlights (self);
863 
864   source_task = g_value_get_object (value);
865   g_assert (source_task != NULL);
866 
867   /*
868    * When the drag operation began, the source row was hidden. Now is the time
869    * to show it again.
870    */
871   row = g_hash_table_lookup (self->task_to_row, source_task);
872   gtk_widget_show (row);
873 
874   drop_row = get_drop_row_at_y (self, y);
875   if (!drop_row)
876     {
877       check_dnd_scroll (self, TRUE, -1);
878       GTD_RETURN (FALSE);
879     }
880 
881   hovered_row = task_row_from_row (drop_row);
882   hovered_task = gtd_task_row_get_task (hovered_row);
883   new_position = gtd_task_get_position (hovered_task);
884   current_position = gtd_task_get_position (source_task);
885 
886   GTD_TRACE_MSG ("Dropping task %p at %ld", source_task, new_position);
887 
888   if (new_position != current_position)
889     {
890       gtd_task_list_move_task_to_position (GTD_TASK_LIST (self->model),
891                                            source_task,
892                                            new_position);
893     }
894 
895   check_dnd_scroll (self, TRUE, -1);
896 
897   GTD_RETURN (TRUE);
898 }
899 
900 
901 /*
902  * GObject overrides
903  */
904 
905 static void
gtd_task_list_view_finalize(GObject * object)906 gtd_task_list_view_finalize (GObject *object)
907 {
908   GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
909 
910   g_clear_handle_id (&self->scroll_to_bottom_handler_id, g_source_remove);
911   g_clear_pointer (&self->task_to_row, g_hash_table_destroy);
912   g_clear_pointer (&self->default_date, g_date_time_unref);
913   g_clear_object (&self->incomplete_tasks_model);
914   g_clear_object (&self->renderer);
915   g_clear_object (&self->model);
916 
917   G_OBJECT_CLASS (gtd_task_list_view_parent_class)->finalize (object);
918 }
919 
920 static void
gtd_task_list_view_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)921 gtd_task_list_view_get_property (GObject    *object,
922                                  guint       prop_id,
923                                  GValue     *value,
924                                  GParamSpec *pspec)
925 {
926   GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
927 
928   switch (prop_id)
929     {
930     case PROP_SHOW_DUE_DATE:
931       g_value_set_boolean (value, self->show_due_date);
932       break;
933 
934     case PROP_SHOW_LIST_NAME:
935       g_value_set_boolean (value, self->show_list_name);
936       break;
937 
938     default:
939       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
940     }
941 }
942 
943 static void
gtd_task_list_view_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)944 gtd_task_list_view_set_property (GObject      *object,
945                                  guint         prop_id,
946                                  const GValue *value,
947                                  GParamSpec   *pspec)
948 {
949   GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
950 
951   switch (prop_id)
952     {
953     case PROP_SHOW_DUE_DATE:
954       gtd_task_list_view_set_show_due_date (self, g_value_get_boolean (value));
955       break;
956 
957     case PROP_SHOW_LIST_NAME:
958       gtd_task_list_view_set_show_list_name (self, g_value_get_boolean (value));
959       break;
960 
961     default:
962       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
963     }
964 }
965 
966 static void
gtd_task_list_view_constructed(GObject * object)967 gtd_task_list_view_constructed (GObject *object)
968 {
969   GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
970   G_OBJECT_CLASS (gtd_task_list_view_parent_class)->constructed (object);
971 
972   /* action_group */
973   self->action_group = G_ACTION_GROUP (g_simple_action_group_new ());
974 
975   g_action_map_add_action_entries (G_ACTION_MAP (self->action_group),
976                                    gtd_task_list_view_entries,
977                                    G_N_ELEMENTS (gtd_task_list_view_entries),
978                                    object);
979 }
980 
981 
982 /*
983  * GtkWidget overrides
984  */
985 
986 static void
gtd_task_list_view_map(GtkWidget * widget)987 gtd_task_list_view_map (GtkWidget *widget)
988 {
989   GtdTaskListView *self = GTD_TASK_LIST_VIEW (widget);
990   GtkRoot *root;
991 
992 
993   update_empty_state (self);
994 
995   GTK_WIDGET_CLASS (gtd_task_list_view_parent_class)->map (widget);
996 
997   root = gtk_widget_get_root (widget);
998 
999   /* Clear previously added "list" actions */
1000   gtk_widget_insert_action_group (GTK_WIDGET (root), "list", NULL);
1001 
1002   /* Add this instance's action group */
1003   gtk_widget_insert_action_group (GTK_WIDGET (root), "list", self->action_group);
1004 }
1005 
1006 static void
gtd_task_list_view_class_init(GtdTaskListViewClass * klass)1007 gtd_task_list_view_class_init (GtdTaskListViewClass *klass)
1008 {
1009   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1010   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1011 
1012   object_class->finalize = gtd_task_list_view_finalize;
1013   object_class->constructed = gtd_task_list_view_constructed;
1014   object_class->get_property = gtd_task_list_view_get_property;
1015   object_class->set_property = gtd_task_list_view_set_property;
1016 
1017   widget_class->map = gtd_task_list_view_map;
1018 
1019   g_type_ensure (GTD_TYPE_EDIT_PANE);
1020   g_type_ensure (GTD_TYPE_NEW_TASK_ROW);
1021   g_type_ensure (GTD_TYPE_TASK_ROW);
1022   g_type_ensure (GTD_TYPE_EMPTY_LIST_WIDGET);
1023 
1024   /**
1025    * GtdTaskListView::show-list-name:
1026    *
1027    * Whether the task rows should show the list name.
1028    */
1029   g_object_class_install_property (
1030         object_class,
1031         PROP_SHOW_LIST_NAME,
1032         g_param_spec_boolean ("show-list-name",
1033                               "Whether task rows show the list name",
1034                               "Whether task rows show the list name at the end of the row",
1035                               FALSE,
1036                               G_PARAM_READWRITE));
1037 
1038   /**
1039    * GtdTaskListView::show-due-date:
1040    *
1041    * Whether due dates of the tasks are shown.
1042    */
1043   g_object_class_install_property (
1044         object_class,
1045         PROP_SHOW_DUE_DATE,
1046         g_param_spec_boolean ("show-due-date",
1047                               "Whether due dates are shown",
1048                               "Whether due dates of the tasks are visible or not",
1049                               TRUE,
1050                               G_PARAM_READWRITE));
1051 
1052   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-view.ui");
1053 
1054   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, due_date_sizegroup);
1055   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, empty_list_widget);
1056   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, listbox);
1057   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, main_stack);
1058   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, tasklist_name_sizegroup);
1059   gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, scrolled_window);
1060 
1061   gtk_widget_class_bind_template_callback (widget_class, on_empty_list_widget_add_tasks_cb);
1062   gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb);
1063   gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_entered_cb);
1064   gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_exited_cb);
1065   gtk_widget_class_bind_template_callback (widget_class, on_task_row_entered_cb);
1066   gtk_widget_class_bind_template_callback (widget_class, on_task_row_exited_cb);
1067 
1068   gtk_widget_class_set_css_name (widget_class, "tasklistview");
1069 }
1070 
1071 static void
gtd_task_list_view_init(GtdTaskListView * self)1072 gtd_task_list_view_init (GtdTaskListView *self)
1073 {
1074   GtkDropTarget *target;
1075 
1076   self->task_to_row = g_hash_table_new (NULL, NULL);
1077 
1078   self->can_toggle = TRUE;
1079   self->show_due_date = TRUE;
1080   self->show_due_date = TRUE;
1081 
1082   gtk_widget_init_template (GTK_WIDGET (self));
1083 
1084   target = gtk_drop_target_new (GTD_TYPE_TASK, GDK_ACTION_MOVE);
1085   gtk_drop_target_set_preload (target, TRUE);
1086   g_signal_connect (target, "drop", G_CALLBACK (on_drop_target_drag_drop_cb), self);
1087   g_signal_connect (target, "enter", G_CALLBACK (on_drop_target_drag_enter_cb), self);
1088   g_signal_connect (target, "leave", G_CALLBACK (on_drop_target_drag_leave_cb), self);
1089   g_signal_connect (target, "motion", G_CALLBACK (on_drop_target_drag_motion_cb), self);
1090 
1091   gtk_widget_add_controller (GTK_WIDGET (self->listbox), GTK_EVENT_CONTROLLER (target));
1092 
1093   self->renderer = gtd_markdown_renderer_new ();
1094 
1095   self->view_model = gtd_task_list_view_model_new ();
1096   gtk_list_box_bind_model (self->listbox,
1097                            G_LIST_MODEL (self->view_model),
1098                            create_row_for_task_cb,
1099                            self,
1100                            NULL);
1101 }
1102 
1103 /**
1104  * gtd_task_list_view_new:
1105  *
1106  * Creates a new #GtdTaskListView
1107  *
1108  * Returns: (transfer full): a newly allocated #GtdTaskListView
1109  */
1110 GtkWidget*
gtd_task_list_view_new(void)1111 gtd_task_list_view_new (void)
1112 {
1113   return g_object_new (GTD_TYPE_TASK_LIST_VIEW, NULL);
1114 }
1115 
1116 /**
1117  * gtd_task_list_view_get_model:
1118  * @view: a #GtdTaskListView
1119  *
1120  * Retrieves the #GtdTaskList from @view, or %NULL if none was set.
1121  *
1122  * Returns: (transfer none): the #GListModel of @view, or %NULL is
1123  * none was set.
1124  */
1125 GListModel*
gtd_task_list_view_get_model(GtdTaskListView * view)1126 gtd_task_list_view_get_model (GtdTaskListView *view)
1127 {
1128   g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), NULL);
1129 
1130   return view->model;
1131 }
1132 
1133 /**
1134  * gtd_task_list_view_set_model:
1135  * @view: a #GtdTaskListView
1136  * @model: a #GListModel
1137  *
1138  * Sets the internal #GListModel of @view. The model must have
1139  * its element GType as @GtdTask.
1140  */
1141 void
gtd_task_list_view_set_model(GtdTaskListView * view,GListModel * model)1142 gtd_task_list_view_set_model (GtdTaskListView *view,
1143                               GListModel      *model)
1144 {
1145   g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
1146   g_return_if_fail (G_IS_LIST_MODEL (model));
1147 
1148   if (view->model == model)
1149     return;
1150 
1151   view->model = model;
1152 
1153   gtd_task_list_view_model_set_model (view->view_model, model);
1154   schedule_scroll_to_bottom (view);
1155   update_incomplete_tasks_model (view);
1156   update_empty_state (view);
1157 }
1158 
1159 /**
1160  * gtd_task_list_view_get_show_list_name:
1161  * @view: a #GtdTaskListView
1162  *
1163  * Whether @view shows the tasks' list names.
1164  *
1165  * Returns: %TRUE if @view show the tasks' list names, %FALSE otherwise
1166  */
1167 gboolean
gtd_task_list_view_get_show_list_name(GtdTaskListView * view)1168 gtd_task_list_view_get_show_list_name (GtdTaskListView *view)
1169 {
1170   g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), FALSE);
1171 
1172   return view->show_list_name;
1173 }
1174 
1175 /**
1176  * gtd_task_list_view_set_show_list_name:
1177  * @view: a #GtdTaskListView
1178  * @show_list_name: %TRUE to show list names, %FALSE to hide it
1179  *
1180  * Whether @view should should it's tasks' list name.
1181  */
1182 void
gtd_task_list_view_set_show_list_name(GtdTaskListView * view,gboolean show_list_name)1183 gtd_task_list_view_set_show_list_name (GtdTaskListView *view,
1184                                        gboolean         show_list_name)
1185 {
1186   g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
1187 
1188   if (view->show_list_name != show_list_name)
1189     {
1190       GtkWidget *child;
1191 
1192       view->show_list_name = show_list_name;
1193 
1194       for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox));
1195            child;
1196            child = gtk_widget_get_next_sibling (child))
1197         {
1198           GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child));
1199 
1200           if (!GTD_IS_TASK_ROW (row_child))
1201             continue;
1202 
1203           gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row_child), show_list_name);
1204         }
1205 
1206       g_object_notify (G_OBJECT (view), "show-list-name");
1207     }
1208 }
1209 
1210 /**
1211  * gtd_task_list_view_get_show_due_date:
1212  * @self: a #GtdTaskListView
1213  *
1214  * Retrieves whether the @self is showing the due dates of the tasks
1215  * or not.
1216  *
1217  * Returns: %TRUE if due dates are visible, %FALSE otherwise.
1218  */
1219 gboolean
gtd_task_list_view_get_show_due_date(GtdTaskListView * self)1220 gtd_task_list_view_get_show_due_date (GtdTaskListView *self)
1221 {
1222   g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), FALSE);
1223 
1224   return self->show_due_date;
1225 }
1226 
1227 /**
1228  * gtd_task_list_view_set_show_due_date:
1229  * @self: a #GtdTaskListView
1230  * @show_due_date: %TRUE to show due dates, %FALSE otherwise
1231  *
1232  * Sets whether @self shows the due dates of the tasks or not.
1233  */
1234 void
gtd_task_list_view_set_show_due_date(GtdTaskListView * self,gboolean show_due_date)1235 gtd_task_list_view_set_show_due_date (GtdTaskListView *self,
1236                                       gboolean         show_due_date)
1237 {
1238   GtkWidget *child;
1239 
1240   g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self));
1241 
1242   if (self->show_due_date == show_due_date)
1243     return;
1244 
1245   self->show_due_date = show_due_date;
1246 
1247   for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
1248        child;
1249        child = gtk_widget_get_next_sibling (child))
1250     {
1251       GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child));
1252 
1253       if (!GTD_IS_TASK_ROW (row_child))
1254         continue;
1255 
1256       gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row_child), show_due_date);
1257     }
1258 
1259   g_object_notify (G_OBJECT (self), "show-due-date");
1260 }
1261 
1262 /**
1263  * gtd_task_list_view_set_header_func:
1264  * @view: a #GtdTaskListView
1265  * @func: (closure user_data) (scope call) (nullable): the header function
1266  * @user_data: data passed to @func
1267  *
1268  * Sets @func as the header function of @view. You can safely call
1269  * %gtk_list_box_row_set_header from within @func.
1270  *
1271  * Do not unref nor free any of the passed data.
1272  */
1273 void
gtd_task_list_view_set_header_func(GtdTaskListView * view,GtdTaskListViewHeaderFunc func,gpointer user_data)1274 gtd_task_list_view_set_header_func (GtdTaskListView           *view,
1275                                     GtdTaskListViewHeaderFunc  func,
1276                                     gpointer                   user_data)
1277 {
1278   g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
1279 
1280   if (func)
1281     {
1282       view->header_func = func;
1283       view->header_user_data = user_data;
1284 
1285       gtk_list_box_set_header_func (view->listbox,
1286                                     (GtkListBoxUpdateHeaderFunc) internal_header_func,
1287                                     view,
1288                                     NULL);
1289     }
1290   else
1291     {
1292       view->header_func = NULL;
1293       view->header_user_data = NULL;
1294 
1295       gtk_list_box_set_header_func (view->listbox,
1296                                     NULL,
1297                                     NULL,
1298                                     NULL);
1299     }
1300 }
1301 
1302 /**
1303  * gtd_task_list_view_get_default_date:
1304  * @self: a #GtdTaskListView
1305  *
1306  * Retrieves the current default date which new tasks are set to.
1307  *
1308  * Returns: (nullable): a #GDateTime, or %NULL
1309  */
1310 GDateTime*
gtd_task_list_view_get_default_date(GtdTaskListView * self)1311 gtd_task_list_view_get_default_date (GtdTaskListView *self)
1312 {
1313   g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), NULL);
1314 
1315   return self->default_date;
1316 }
1317 
1318 /**
1319  * gtd_task_list_view_set_default_date:
1320  * @self: a #GtdTaskListView
1321  * @default_date: (nullable): the default_date, or %NULL
1322  *
1323  * Sets the current default date.
1324  */
1325 void
gtd_task_list_view_set_default_date(GtdTaskListView * self,GDateTime * default_date)1326 gtd_task_list_view_set_default_date   (GtdTaskListView *self,
1327                                        GDateTime       *default_date)
1328 {
1329   g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self));
1330 
1331   if (self->default_date == default_date)
1332     return;
1333 
1334   g_clear_pointer (&self->default_date, g_date_time_unref);
1335   self->default_date = default_date ? g_date_time_ref (default_date) : NULL;
1336 }
1337