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, ¤t_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