1 /* gtd-plugin-background.c
2 *
3 * Copyright (C) 2017-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 "GtdPluginBackground"
20
21 #include "config.h"
22
23 #include "gtd-debug.h"
24 #include "gtd-plugin-background.h"
25
26 #include <glib/gi18n.h>
27 #include <gdk/gdk.h>
28 #include <gtk/gtk.h>
29
30 #include <libportal/portal.h>
31 #include <libportal/portal-gtk4.h>
32
33 #define AUTOSTART_NOTIFICATION_ID "Gtd::BackgroundPlugin::autostart_notification"
34 #define AUTOSTART_NOTIFICATION_TIMEOUT 3 /* seconds */
35 #define MAX_BODY_LENGTH 50 /* chars */
36
37 #define DESKTOP_PORTAL_BUS_NAME "org.freedesktop.portal.Desktop"
38 #define DESKTOP_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
39 #define DESKTOP_PORTAL_BACKGROUND_INTERFACE "org.freedesktop.portal.Background"
40
41 struct _GtdPluginBackground
42 {
43 PeasExtensionBase parent;
44
45 GtkWidget *preferences_panel;
46
47 GDBusProxy *desktop_portal;
48
49 GSettings *settings;
50 gboolean startup_notification;
51 gboolean show_notifications;
52
53 gboolean enabled;
54 gboolean autostart;
55
56 XdpPortal *portal;
57
58 guint startup_notification_timeout_id;
59 };
60
61 static void on_tasklist_notified (GtdPluginBackground *self);
62
63 static void gtd_activatable_iface_init (GtdActivatableInterface *iface);
64
65 G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginBackground, gtd_plugin_background, PEAS_TYPE_EXTENSION_BASE,
66 0,
67 G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE,
68 gtd_activatable_iface_init))
69
70 enum {
71 PROP_0,
72 PROP_PREFERENCES_PANEL,
73 N_PROPS
74 };
75
76 /*
77 * Auxiliary methods
78 */
79
80 static inline GtkWindow*
get_window(void)81 get_window (void)
82 {
83 GtkApplication *app = GTK_APPLICATION (g_application_get_default ());
84
85 return GTK_WINDOW (gtk_application_get_active_window (app));
86 }
87
88 static gboolean
is_gnome_todo_active(void)89 is_gnome_todo_active (void)
90 {
91 GListModel *toplevels;
92 guint i;
93
94 toplevels = gtk_window_get_toplevels ();
95 for (i = 0; i < g_list_model_get_n_items (toplevels); i++)
96 {
97 g_autoptr (GtkWidget) toplevel = g_list_model_get_item (toplevels, i);
98
99 if (GTK_IS_WINDOW (toplevel) && gtk_window_is_active (GTK_WINDOW (toplevel)))
100 return TRUE;
101 }
102
103 return FALSE;
104 }
105
106 static void
on_request_background_called_cb(GObject * object,GAsyncResult * result,gpointer user_data)107 on_request_background_called_cb (GObject *object,
108 GAsyncResult *result,
109 gpointer user_data)
110 {
111 g_autoptr (GError) error = NULL;
112
113 GTD_ENTRY;
114
115 xdp_portal_request_background_finish (XDP_PORTAL (object), result, &error);
116
117 if (error)
118 g_warning ("Error requesting background: %s", error->message);
119
120 GTD_EXIT;
121 }
122
123 static void
update_background_portal(GtdPluginBackground * self)124 update_background_portal (GtdPluginBackground *self)
125 {
126 g_autoptr (GPtrArray) commandline = NULL;
127 XdpBackgroundFlags background_flags;
128 XdpParent *parent = NULL;
129 GtkWindow *window;
130
131 GTD_ENTRY;
132
133 window = get_window ();
134 parent = xdp_parent_new_gtk (window);
135 background_flags = XDP_BACKGROUND_FLAG_ACTIVATABLE;
136
137 if (self->autostart)
138 {
139 commandline = g_ptr_array_new_with_free_func (g_free);
140 g_ptr_array_add (commandline, g_strdup ("gnome-todo"));
141 g_ptr_array_add (commandline, g_strdup ("--gapplication-service"));
142
143 background_flags |= XDP_BACKGROUND_FLAG_AUTOSTART;
144 }
145
146 xdp_portal_request_background (self->portal,
147 parent,
148 NULL,
149 commandline,
150 background_flags,
151 NULL,
152 on_request_background_called_cb,
153 self);
154
155 xdp_parent_free (parent);
156
157 GTD_EXIT;
158 }
159
160 static void
on_window_active_changed_cb(GtkWindow * window,GParamSpec * pspec,GtdPluginBackground * self)161 on_window_active_changed_cb (GtkWindow *window,
162 GParamSpec *pspec,
163 GtdPluginBackground *self)
164 {
165 GTD_ENTRY;
166
167 g_signal_handlers_disconnect_by_func (window, on_window_active_changed_cb, self);
168 update_background_portal (self);
169
170 GTD_EXIT;
171 }
172
173 static void
start_update(GtdPluginBackground * self)174 start_update (GtdPluginBackground *self)
175 {
176 GtkWindow *window;
177
178 GTD_ENTRY;
179
180 window = get_window ();
181 if (gtk_widget_get_visible (GTK_WIDGET (window)) && is_gnome_todo_active ())
182 {
183 update_background_portal (self);
184 }
185 else
186 {
187 g_signal_connect_object (window,
188 "notify::is-active",
189 G_CALLBACK (on_window_active_changed_cb),
190 self,
191 G_CONNECT_AFTER);
192 }
193
194 GTD_EXIT;
195 }
196
197 static gchar*
format_notification_body(GList * tasks,guint n_tasks)198 format_notification_body (GList *tasks,
199 guint n_tasks)
200 {
201 GString *string, *aux;
202 GList *l;
203 guint current_task, length;
204
205 aux = g_string_new ("");
206 string = g_string_new ("");
207 current_task = length = 0;
208
209 for (l = tasks; l != NULL; l = l->next)
210 {
211 GtdTask *task = l->data;
212
213 length += g_utf8_strlen (gtd_task_get_title (task), -1);
214
215 if (length > MAX_BODY_LENGTH)
216 break;
217
218 /* Prepend comma */
219 if (current_task > 0)
220 g_string_append (aux, ", ");
221
222 g_string_append (aux, gtd_task_get_title (task));
223 current_task++;
224 }
225
226 if (current_task == 0)
227 {
228 /* The first task has a huge title. Let's ellipsize it */
229 g_string_append (string, gtd_task_get_title (tasks->data));
230 g_string_truncate (string, MAX_BODY_LENGTH - 1);
231 g_string_append (string, "…");
232 }
233 else if (current_task < n_tasks)
234 {
235 /* Some tasks fit, but we need to append a text explaining there are more */
236 g_string_append_printf (string,
237 g_dngettext (GETTEXT_PACKAGE,
238 "%1$s and one more task",
239 "%1$s and %2$d other tasks",
240 n_tasks - current_task),
241 aux->str,
242 n_tasks - current_task);
243 }
244 else
245 {
246 /* We were able to print all task titles */
247 g_string_append (string, aux->str);
248 }
249
250 g_string_free (aux, TRUE);
251
252 return g_string_free (string, FALSE);
253 }
254
255 static inline gboolean
is_today(GDateTime * now,GDateTime * dt)256 is_today (GDateTime *now,
257 GDateTime *dt)
258 {
259 return g_date_time_get_year (dt) == g_date_time_get_year (now) &&
260 g_date_time_get_month (dt) == g_date_time_get_month (now) &&
261 g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (now);
262 }
263
264 static GList*
get_tasks_for_today(guint * n_events)265 get_tasks_for_today (guint *n_events)
266 {
267 g_autoptr (GDateTime) now;
268 GListModel *lists;
269 GtdManager *manager;
270 GList *result;
271 guint n_tasks;
272 guint i;
273
274 now = g_date_time_new_now_local ();
275 result = NULL;
276 n_tasks = 0;
277 manager = gtd_manager_get_default ();
278 lists = gtd_manager_get_task_lists_model (manager);
279
280 for (i = 0; i < g_list_model_get_n_items (lists); i++)
281 {
282 g_autoptr (GListModel) list = NULL;
283 guint j;
284
285 list = g_list_model_get_item (lists, i);
286
287 for (j = 0; j < g_list_model_get_n_items (list); j++)
288 {
289 GDateTime *due_date;
290 GtdTask *task;
291
292 task = g_list_model_get_item (list, j);
293
294 due_date = gtd_task_get_due_date (task);
295
296 if (!due_date || !is_today (now, due_date) || gtd_task_get_complete (task))
297 continue;
298
299 n_tasks += 1;
300 result = g_list_prepend (result, task);
301 }
302 }
303
304 if (n_events)
305 *n_events = n_tasks;
306
307 return result;
308 }
309
310 static void
send_notification(GtdPluginBackground * self)311 send_notification (GtdPluginBackground *self)
312 {
313 GNotification *notification;
314 GApplication *app;
315 GtdWindow *window;
316 guint n_tasks;
317 GList *tasks;
318 gchar *title;
319 gchar *body;
320
321 window = GTD_WINDOW (get_window ());
322
323 /*
324 * If the user already focused To Do's window, we don't have to
325 * notify about the number of tasks.
326 */
327 if (gtk_window_is_active (GTK_WINDOW (window)))
328 return;
329
330 /* The user don't want to be bothered with notifications */
331 if (!g_settings_get_boolean (self->settings, "show-notifications"))
332 return;
333
334 app = g_application_get_default ();
335 tasks = get_tasks_for_today (&n_tasks);
336
337 /* If n_tasks == 0, tasks == NULL, thus we don't need to free it */
338 if (n_tasks == 0)
339 return;
340
341 title = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
342 "You have %d task for today",
343 "You have %d tasks for today",
344 n_tasks),
345 n_tasks);
346
347 body = format_notification_body (tasks, n_tasks);
348
349 /* Build up the notification */
350 notification = g_notification_new (title);
351 g_notification_set_body (notification, body);
352 g_notification_set_default_action (notification, "app.activate");
353
354 g_application_send_notification (app, AUTOSTART_NOTIFICATION_ID, notification);
355
356 g_clear_pointer (&tasks, g_list_free);
357 g_clear_object (¬ification);
358 }
359
360 /*
361 * Callbacks
362 */
363 static void
on_startup_changed(GSettings * settings,const gchar * key,GtdPluginBackground * self)364 on_startup_changed (GSettings *settings,
365 const gchar *key,
366 GtdPluginBackground *self)
367 {
368 self->autostart = g_settings_get_boolean (settings, key);
369
370 start_update (self);
371 }
372
373 static gboolean
on_startup_timeout_cb(GtdPluginBackground * self)374 on_startup_timeout_cb (GtdPluginBackground *self)
375 {
376 send_notification (self);
377
378 self->startup_notification_timeout_id = 0;
379
380 g_signal_handlers_disconnect_by_func (gtd_manager_get_default (),
381 on_tasklist_notified,
382 self);
383
384 return G_SOURCE_REMOVE;
385 }
386
387 static void
on_tasklist_notified(GtdPluginBackground * self)388 on_tasklist_notified (GtdPluginBackground *self)
389 {
390 /* Remove previously set timeout */
391 if (self->startup_notification_timeout_id > 0)
392 {
393 g_source_remove (self->startup_notification_timeout_id);
394 self->startup_notification_timeout_id = 0;
395 }
396
397 self->startup_notification_timeout_id = g_timeout_add_seconds (AUTOSTART_NOTIFICATION_TIMEOUT,
398 (GSourceFunc) on_startup_timeout_cb,
399 self);
400 }
401
402 static void
watch_manager_for_new_lists(GtdPluginBackground * self)403 watch_manager_for_new_lists (GtdPluginBackground *self)
404 {
405 GtdManager *manager = gtd_manager_get_default ();
406
407 g_signal_connect_swapped (manager,
408 "list-added",
409 G_CALLBACK (on_tasklist_notified),
410 self);
411
412 g_signal_connect_swapped (manager,
413 "list-changed",
414 G_CALLBACK (on_tasklist_notified),
415 self);
416
417 g_signal_connect_swapped (manager,
418 "list-removed",
419 G_CALLBACK (on_tasklist_notified),
420 self);
421
422 g_signal_connect_swapped (gtd_manager_get_clock (manager),
423 "day-changed",
424 G_CALLBACK (send_notification),
425 self);
426 }
427
428 /*
429 * GtdActivatable interface implementation
430 */
431 static void
gtd_plugin_background_activate(GtdActivatable * activatable)432 gtd_plugin_background_activate (GtdActivatable *activatable)
433 {
434 GtdPluginBackground *self;
435 GtkWindow *window;
436
437 self = GTD_PLUGIN_BACKGROUND (activatable);
438 window = get_window ();
439
440 self->enabled = TRUE;
441
442 gtk_window_set_hide_on_close (window, TRUE);
443
444 on_startup_changed (self->settings, "run-on-startup", self);
445 g_signal_connect (self->settings,
446 "changed::run-on-startup",
447 G_CALLBACK (on_startup_changed),
448 self);
449
450 /* Start watching the manager to notify the user about today's tasks */
451 watch_manager_for_new_lists (self);
452 }
453
454 static void
gtd_plugin_background_deactivate(GtdActivatable * activatable)455 gtd_plugin_background_deactivate (GtdActivatable *activatable)
456 {
457 GtdPluginBackground *self;
458 GtdManager *manager;
459 GtkWindow *window;
460
461 self = GTD_PLUGIN_BACKGROUND (activatable);
462 manager = gtd_manager_get_default ();
463 window = get_window ();
464
465 self->enabled = FALSE;
466 self->autostart = FALSE;
467
468 gtk_window_set_hide_on_close (window, FALSE);
469
470 g_signal_handlers_disconnect_by_func (self->settings,
471 on_startup_changed,
472 self);
473
474 g_signal_handlers_disconnect_by_func (manager,
475 on_tasklist_notified,
476 self);
477
478 g_signal_handlers_disconnect_by_func (gtd_manager_get_clock (manager),
479 send_notification,
480 self);
481
482 /* Deactivate the timeout */
483 if (self->startup_notification_timeout_id > 0)
484 {
485 g_source_remove (self->startup_notification_timeout_id);
486 self->startup_notification_timeout_id = 0;
487 }
488
489 /* Deactivate auto startup */
490 start_update (self);
491 }
492
493 static GtkWidget*
gtd_plugin_background_get_preferences_panel(GtdActivatable * activatable)494 gtd_plugin_background_get_preferences_panel (GtdActivatable *activatable)
495 {
496 GtdPluginBackground *self = GTD_PLUGIN_BACKGROUND (activatable);
497
498 return self->preferences_panel;
499 }
500
501 static void
gtd_activatable_iface_init(GtdActivatableInterface * iface)502 gtd_activatable_iface_init (GtdActivatableInterface *iface)
503 {
504 iface->activate = gtd_plugin_background_activate;
505 iface->deactivate = gtd_plugin_background_deactivate;
506 iface->get_preferences_panel = gtd_plugin_background_get_preferences_panel;
507 }
508
509 static void
gtd_plugin_background_finalize(GObject * object)510 gtd_plugin_background_finalize (GObject *object)
511 {
512 GtdPluginBackground *self = (GtdPluginBackground *)object;
513
514 g_clear_object (&self->settings);
515 g_clear_object (&self->portal);
516
517 G_OBJECT_CLASS (gtd_plugin_background_parent_class)->finalize (object);
518 }
519
520 static void
gtd_plugin_background_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)521 gtd_plugin_background_get_property (GObject *object,
522 guint prop_id,
523 GValue *value,
524 GParamSpec *pspec)
525 {
526 GtdPluginBackground *self = GTD_PLUGIN_BACKGROUND (object);
527
528 switch (prop_id)
529 {
530 case PROP_PREFERENCES_PANEL:
531 g_value_set_object (value, self->preferences_panel);
532 break;
533
534 default:
535 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
536 }
537 }
538
539 static void
gtd_plugin_background_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)540 gtd_plugin_background_set_property (GObject *object,
541 guint prop_id,
542 const GValue *value,
543 GParamSpec *pspec)
544 {
545 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
546 }
547
548 static void
gtd_plugin_background_class_init(GtdPluginBackgroundClass * klass)549 gtd_plugin_background_class_init (GtdPluginBackgroundClass *klass)
550 {
551 GObjectClass *object_class = G_OBJECT_CLASS (klass);
552
553 object_class->finalize = gtd_plugin_background_finalize;
554 object_class->get_property = gtd_plugin_background_get_property;
555 object_class->set_property = gtd_plugin_background_set_property;
556
557 g_object_class_override_property (object_class,
558 PROP_PREFERENCES_PANEL,
559 "preferences-panel");
560 }
561
562 static void
gtd_plugin_background_init(GtdPluginBackground * self)563 gtd_plugin_background_init (GtdPluginBackground *self)
564 {
565 GtkBuilder *builder;
566
567 /* Load the settings */
568 self->settings = g_settings_new ("org.gnome.todo.plugins.background");
569
570 /* And the preferences panel */
571 builder = gtk_builder_new_from_resource ("/org/gnome/todo/plugins/background/ui/preferences.ui");
572
573 self->preferences_panel = GTK_WIDGET (gtk_builder_get_object (builder, "main_box"));
574 self->portal = xdp_portal_new ();
575
576 g_settings_bind (self->settings,
577 "run-on-startup",
578 gtk_builder_get_object (builder, "startup_switch"),
579 "active",
580 G_SETTINGS_BIND_DEFAULT);
581
582 g_settings_bind (self->settings,
583 "show-notifications",
584 gtk_builder_get_object (builder, "notifications_switch"),
585 "active",
586 G_SETTINGS_BIND_DEFAULT);
587 }
588
589 static void
gtd_plugin_background_class_finalize(GtdPluginBackgroundClass * klass)590 gtd_plugin_background_class_finalize (GtdPluginBackgroundClass *klass)
591 {
592 }
593
594 G_MODULE_EXPORT void
gtd_plugin_background_register_types(PeasObjectModule * module)595 gtd_plugin_background_register_types (PeasObjectModule *module)
596 {
597 gtd_plugin_background_register_type (G_TYPE_MODULE (module));
598
599 peas_object_module_register_extension_type (module,
600 GTD_TYPE_ACTIVATABLE,
601 GTD_TYPE_PLUGIN_BACKGROUND);
602 }
603