From 5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Sat, 27 Dec 2025 12:40:20 +0000 Subject: Import Upstream version 43.0 --- src/animation/gtd-animatable.c | 204 +++ src/animation/gtd-animatable.h | 85 + src/animation/gtd-animation-enums.h | 173 ++ src/animation/gtd-animation-utils.c | 168 ++ src/animation/gtd-animation-utils.h | 65 + src/animation/gtd-easing.c | 474 ++++++ src/animation/gtd-easing.h | 141 ++ src/animation/gtd-interval.c | 1134 +++++++++++++ src/animation/gtd-interval.h | 116 ++ src/animation/gtd-keyframe-transition.c | 716 +++++++++ src/animation/gtd-keyframe-transition.h | 83 + src/animation/gtd-property-transition.c | 359 +++++ src/animation/gtd-property-transition.h | 55 + src/animation/gtd-timeline.c | 1547 ++++++++++++++++++ src/animation/gtd-timeline.h | 150 ++ src/animation/gtd-transition.c | 655 ++++++++ src/animation/gtd-transition.h | 85 + src/core/gtd-activatable.c | 129 ++ src/core/gtd-activatable.h | 52 + src/core/gtd-clock.c | 292 ++++ src/core/gtd-clock.h | 33 + src/core/gtd-log.c | 103 ++ src/core/gtd-log.h | 27 + src/core/gtd-manager-protected.h | 29 + src/core/gtd-manager.c | 933 +++++++++++ src/core/gtd-manager.h | 86 + src/core/gtd-notification.c | 441 ++++++ src/core/gtd-notification.h | 68 + src/core/gtd-object.c | 313 ++++ src/core/gtd-object.h | 54 + src/core/gtd-plugin-manager.c | 323 ++++ src/core/gtd-plugin-manager.h | 45 + src/core/gtd-provider.c | 681 ++++++++ src/core/gtd-provider.h | 208 +++ src/core/gtd-task-list.c | 1161 ++++++++++++++ src/core/gtd-task-list.h | 118 ++ src/core/gtd-task.c | 993 ++++++++++++ src/core/gtd-task.h | 122 ++ src/endeavour.h | 56 + src/gtd-debug.h.in | 229 +++ src/gtd-enum-types.c.template | 39 + src/gtd-enum-types.h.template | 24 + src/gtd-types.h | 57 + src/gtd-utils-private.h | 29 + src/gtd-utils.c | 158 ++ src/gtd-utils.h | 35 + src/gtd-vcs.h.in | 29 + src/gui/assets/all-done.svg | 1 + src/gui/gtd-application.c | 331 ++++ src/gui/gtd-application.h | 36 + src/gui/gtd-bin-layout.c | 112 ++ src/gui/gtd-bin-layout.h | 32 + src/gui/gtd-color-button.c | 273 ++++ src/gui/gtd-color-button.h | 38 + src/gui/gtd-edit-pane.c | 602 +++++++ src/gui/gtd-edit-pane.h | 47 + src/gui/gtd-edit-pane.ui | 201 +++ src/gui/gtd-initial-setup-window.c | 247 +++ src/gui/gtd-initial-setup-window.h | 37 + src/gui/gtd-initial-setup-window.ui | 82 + src/gui/gtd-markdown-renderer.c | 357 +++++ src/gui/gtd-markdown-renderer.h | 35 + src/gui/gtd-max-size-layout.c | 474 ++++++ src/gui/gtd-max-size-layout.h | 52 + src/gui/gtd-menu-button.c | 1056 +++++++++++++ src/gui/gtd-menu-button.h | 104 ++ src/gui/gtd-new-task-row.c | 374 +++++ src/gui/gtd-new-task-row.h | 43 + src/gui/gtd-new-task-row.ui | 31 + src/gui/gtd-omni-area-addin.c | 69 + src/gui/gtd-omni-area-addin.h | 49 + src/gui/gtd-omni-area.c | 256 +++ src/gui/gtd-omni-area.h | 41 + src/gui/gtd-omni-area.ui | 85 + src/gui/gtd-panel.c | 313 ++++ src/gui/gtd-panel.h | 77 + src/gui/gtd-provider-popover.c | 245 +++ src/gui/gtd-provider-popover.h | 34 + src/gui/gtd-provider-popover.ui | 164 ++ src/gui/gtd-provider-row.c | 240 +++ src/gui/gtd-provider-row.h | 44 + src/gui/gtd-provider-row.ui | 71 + src/gui/gtd-provider-selector.c | 695 ++++++++ src/gui/gtd-provider-selector.h | 50 + src/gui/gtd-provider-selector.ui | 109 ++ src/gui/gtd-star-widget.c | 193 +++ src/gui/gtd-star-widget.h | 37 + src/gui/gtd-task-list-popover.c | 269 ++++ src/gui/gtd-task-list-popover.h | 34 + src/gui/gtd-task-list-popover.ui | 39 + src/gui/gtd-task-list-view.c | 1220 ++++++++++++++ src/gui/gtd-task-list-view.h | 73 + src/gui/gtd-task-list-view.ui | 97 ++ src/gui/gtd-task-row.c | 839 ++++++++++ src/gui/gtd-task-row.h | 54 + src/gui/gtd-task-row.ui | 127 ++ src/gui/gtd-widget.c | 1668 ++++++++++++++++++++ src/gui/gtd-widget.h | 110 ++ src/gui/gtd-window.c | 413 +++++ src/gui/gtd-window.h | 43 + src/gui/gtd-window.ui | 33 + src/gui/gtd-workspace.c | 158 ++ src/gui/gtd-workspace.h | 61 + src/gui/gui.gresource.xml | 25 + src/gui/menus.ui | 21 + src/gui/meson.build | 6 + src/gui/shortcuts-dialog.ui | 72 + src/main.c | 44 + src/meson.build | 339 ++++ src/models/gtd-list-model-filter.c | 569 +++++++ src/models/gtd-list-model-filter.h | 44 + src/models/gtd-list-model-sort.c | 500 ++++++ src/models/gtd-list-model-sort.h | 46 + src/models/gtd-list-store.c | 567 +++++++ src/models/gtd-list-store.h | 66 + src/models/gtd-task-list-view-model.c | 216 +++ src/models/gtd-task-list-view-model.h | 43 + src/models/gtd-task-model-private.h | 29 + src/models/gtd-task-model.c | 216 +++ src/models/gtd-task-model.h | 33 + .../all-tasks-panel/all-tasks-panel-plugin.c | 34 + .../all-tasks-panel/all-tasks-panel.gresource.xml | 6 + src/plugins/all-tasks-panel/all-tasks-panel.plugin | 13 + src/plugins/all-tasks-panel/gtd-all-tasks-panel.c | 491 ++++++ src/plugins/all-tasks-panel/gtd-all-tasks-panel.h | 34 + src/plugins/all-tasks-panel/meson.build | 12 + src/plugins/eds/e-source-endeavour.c | 128 ++ src/plugins/eds/e-source-endeavour.h | 35 + src/plugins/eds/eds-plugin.c | 30 + src/plugins/eds/eds.gresource.xml | 6 + src/plugins/eds/eds.plugin | 14 + src/plugins/eds/gtd-eds-autoptr.h | 27 + src/plugins/eds/gtd-eds.h | 32 + src/plugins/eds/gtd-plugin-eds.c | 323 ++++ src/plugins/eds/gtd-plugin-eds.h | 28 + src/plugins/eds/gtd-provider-eds.c | 1157 ++++++++++++++ src/plugins/eds/gtd-provider-eds.h | 61 + src/plugins/eds/gtd-provider-goa.c | 262 +++ src/plugins/eds/gtd-provider-goa.h | 43 + src/plugins/eds/gtd-provider-local.c | 150 ++ src/plugins/eds/gtd-provider-local.h | 37 + src/plugins/eds/gtd-task-eds.c | 650 ++++++++ src/plugins/eds/gtd-task-eds.h | 46 + src/plugins/eds/gtd-task-list-eds.c | 875 ++++++++++ src/plugins/eds/gtd-task-list-eds.h | 53 + src/plugins/eds/meson.build | 27 + src/plugins/inbox-panel/gtd-inbox-panel.c | 275 ++++ src/plugins/inbox-panel/gtd-inbox-panel.h | 32 + src/plugins/inbox-panel/inbox-panel-plugin.c | 32 + src/plugins/inbox-panel/inbox-panel.gresource.xml | 6 + src/plugins/inbox-panel/inbox-panel.plugin | 14 + src/plugins/inbox-panel/meson.build | 12 + src/plugins/meson.build | 36 + src/plugins/next-week-panel/gtd-next-week-panel.c | 573 +++++++ src/plugins/next-week-panel/gtd-next-week-panel.h | 34 + src/plugins/next-week-panel/meson.build | 12 + .../next-week-panel/next-week-panel-plugin.c | 33 + .../next-week-panel/next-week-panel.gresource.xml | 7 + src/plugins/next-week-panel/next-week-panel.plugin | 13 + src/plugins/next-week-panel/theme/Adwaita.css | 11 + src/plugins/peace/gtd-peace-omni-area-addin.c | 209 +++ src/plugins/peace/gtd-peace-omni-area-addin.h | 30 + src/plugins/peace/meson.build | 12 + src/plugins/peace/peace-plugin.c | 31 + src/plugins/peace/peace.gresource.xml | 6 + src/plugins/peace/peace.plugin | 12 + src/plugins/scheduled-panel/gtd-panel-scheduled.c | 518 ++++++ src/plugins/scheduled-panel/gtd-panel-scheduled.h | 35 + .../scheduled-panel/gtd-plugin-scheduled-panel.c | 153 ++ .../scheduled-panel/gtd-plugin-scheduled-panel.h | 37 + src/plugins/scheduled-panel/meson.build | 6 + .../scheduled-panel/scheduled-panel.gresource.xml | 7 + src/plugins/scheduled-panel/scheduled-panel.plugin | 13 + src/plugins/scheduled-panel/theme/Adwaita.css | 11 + .../task-lists-workspace/gtd-sidebar-list-row.c | 333 ++++ .../task-lists-workspace/gtd-sidebar-list-row.h | 37 + .../task-lists-workspace/gtd-sidebar-list-row.ui | 41 + .../task-lists-workspace/gtd-sidebar-panel-row.c | 180 +++ .../task-lists-workspace/gtd-sidebar-panel-row.h | 37 + .../task-lists-workspace/gtd-sidebar-panel-row.ui | 34 + .../gtd-sidebar-provider-row.c | 300 ++++ .../gtd-sidebar-provider-row.h | 39 + .../gtd-sidebar-provider-row.ui | 74 + src/plugins/task-lists-workspace/gtd-sidebar.c | 929 +++++++++++ src/plugins/task-lists-workspace/gtd-sidebar.h | 44 + src/plugins/task-lists-workspace/gtd-sidebar.ui | 144 ++ .../task-lists-workspace/gtd-task-list-panel.c | 636 ++++++++ .../task-lists-workspace/gtd-task-list-panel.h | 40 + .../task-lists-workspace/gtd-task-list-panel.ui | 113 ++ .../gtd-task-lists-workspace.c | 711 +++++++++ .../gtd-task-lists-workspace.h | 35 + .../gtd-task-lists-workspace.ui | 164 ++ src/plugins/task-lists-workspace/meson.build | 20 + .../task-lists-workspace-plugin.c | 31 + .../task-lists-workspace.gresource.xml | 12 + .../task-lists-workspace.plugin | 14 + src/plugins/today-panel/gtd-panel-today.c | 480 ++++++ src/plugins/today-panel/gtd-panel-today.h | 35 + .../today-panel/gtd-today-omni-area-addin.c | 304 ++++ .../today-panel/gtd-today-omni-area-addin.h | 30 + src/plugins/today-panel/meson.build | 14 + src/plugins/today-panel/theme/Adwaita.css | 11 + src/plugins/today-panel/today-panel-plugin.c | 34 + src/plugins/today-panel/today-panel.gresource.xml | 7 + src/plugins/today-panel/today-panel.plugin | 14 + src/themes/_omniarea.css | 13 + src/themes/_taskrow.css | 38 + src/themes/_widgets.css | 27 + src/themes/style.css | 58 + src/todo.gresource.xml | 9 + 210 files changed, 40274 insertions(+) create mode 100644 src/animation/gtd-animatable.c create mode 100644 src/animation/gtd-animatable.h create mode 100644 src/animation/gtd-animation-enums.h create mode 100644 src/animation/gtd-animation-utils.c create mode 100644 src/animation/gtd-animation-utils.h create mode 100644 src/animation/gtd-easing.c create mode 100644 src/animation/gtd-easing.h create mode 100644 src/animation/gtd-interval.c create mode 100644 src/animation/gtd-interval.h create mode 100644 src/animation/gtd-keyframe-transition.c create mode 100644 src/animation/gtd-keyframe-transition.h create mode 100644 src/animation/gtd-property-transition.c create mode 100644 src/animation/gtd-property-transition.h create mode 100644 src/animation/gtd-timeline.c create mode 100644 src/animation/gtd-timeline.h create mode 100644 src/animation/gtd-transition.c create mode 100644 src/animation/gtd-transition.h create mode 100644 src/core/gtd-activatable.c create mode 100644 src/core/gtd-activatable.h create mode 100644 src/core/gtd-clock.c create mode 100644 src/core/gtd-clock.h create mode 100644 src/core/gtd-log.c create mode 100644 src/core/gtd-log.h create mode 100644 src/core/gtd-manager-protected.h create mode 100644 src/core/gtd-manager.c create mode 100644 src/core/gtd-manager.h create mode 100644 src/core/gtd-notification.c create mode 100644 src/core/gtd-notification.h create mode 100644 src/core/gtd-object.c create mode 100644 src/core/gtd-object.h create mode 100644 src/core/gtd-plugin-manager.c create mode 100644 src/core/gtd-plugin-manager.h create mode 100644 src/core/gtd-provider.c create mode 100644 src/core/gtd-provider.h create mode 100644 src/core/gtd-task-list.c create mode 100644 src/core/gtd-task-list.h create mode 100644 src/core/gtd-task.c create mode 100644 src/core/gtd-task.h create mode 100644 src/endeavour.h create mode 100644 src/gtd-debug.h.in create mode 100644 src/gtd-enum-types.c.template create mode 100644 src/gtd-enum-types.h.template create mode 100644 src/gtd-types.h create mode 100644 src/gtd-utils-private.h create mode 100644 src/gtd-utils.c create mode 100644 src/gtd-utils.h create mode 100644 src/gtd-vcs.h.in create mode 100644 src/gui/assets/all-done.svg create mode 100644 src/gui/gtd-application.c create mode 100644 src/gui/gtd-application.h create mode 100644 src/gui/gtd-bin-layout.c create mode 100644 src/gui/gtd-bin-layout.h create mode 100644 src/gui/gtd-color-button.c create mode 100644 src/gui/gtd-color-button.h create mode 100644 src/gui/gtd-edit-pane.c create mode 100644 src/gui/gtd-edit-pane.h create mode 100644 src/gui/gtd-edit-pane.ui create mode 100644 src/gui/gtd-initial-setup-window.c create mode 100644 src/gui/gtd-initial-setup-window.h create mode 100644 src/gui/gtd-initial-setup-window.ui create mode 100644 src/gui/gtd-markdown-renderer.c create mode 100644 src/gui/gtd-markdown-renderer.h create mode 100644 src/gui/gtd-max-size-layout.c create mode 100644 src/gui/gtd-max-size-layout.h create mode 100644 src/gui/gtd-menu-button.c create mode 100644 src/gui/gtd-menu-button.h create mode 100644 src/gui/gtd-new-task-row.c create mode 100644 src/gui/gtd-new-task-row.h create mode 100644 src/gui/gtd-new-task-row.ui create mode 100644 src/gui/gtd-omni-area-addin.c create mode 100644 src/gui/gtd-omni-area-addin.h create mode 100644 src/gui/gtd-omni-area.c create mode 100644 src/gui/gtd-omni-area.h create mode 100644 src/gui/gtd-omni-area.ui create mode 100644 src/gui/gtd-panel.c create mode 100644 src/gui/gtd-panel.h create mode 100644 src/gui/gtd-provider-popover.c create mode 100644 src/gui/gtd-provider-popover.h create mode 100644 src/gui/gtd-provider-popover.ui create mode 100644 src/gui/gtd-provider-row.c create mode 100644 src/gui/gtd-provider-row.h create mode 100644 src/gui/gtd-provider-row.ui create mode 100644 src/gui/gtd-provider-selector.c create mode 100644 src/gui/gtd-provider-selector.h create mode 100644 src/gui/gtd-provider-selector.ui create mode 100644 src/gui/gtd-star-widget.c create mode 100644 src/gui/gtd-star-widget.h create mode 100644 src/gui/gtd-task-list-popover.c create mode 100644 src/gui/gtd-task-list-popover.h create mode 100644 src/gui/gtd-task-list-popover.ui create mode 100644 src/gui/gtd-task-list-view.c create mode 100644 src/gui/gtd-task-list-view.h create mode 100644 src/gui/gtd-task-list-view.ui create mode 100644 src/gui/gtd-task-row.c create mode 100644 src/gui/gtd-task-row.h create mode 100644 src/gui/gtd-task-row.ui create mode 100644 src/gui/gtd-widget.c create mode 100644 src/gui/gtd-widget.h create mode 100644 src/gui/gtd-window.c create mode 100644 src/gui/gtd-window.h create mode 100644 src/gui/gtd-window.ui create mode 100644 src/gui/gtd-workspace.c create mode 100644 src/gui/gtd-workspace.h create mode 100644 src/gui/gui.gresource.xml create mode 100644 src/gui/menus.ui create mode 100644 src/gui/meson.build create mode 100644 src/gui/shortcuts-dialog.ui create mode 100644 src/main.c create mode 100644 src/meson.build create mode 100644 src/models/gtd-list-model-filter.c create mode 100644 src/models/gtd-list-model-filter.h create mode 100644 src/models/gtd-list-model-sort.c create mode 100644 src/models/gtd-list-model-sort.h create mode 100644 src/models/gtd-list-store.c create mode 100644 src/models/gtd-list-store.h create mode 100644 src/models/gtd-task-list-view-model.c create mode 100644 src/models/gtd-task-list-view-model.h create mode 100644 src/models/gtd-task-model-private.h create mode 100644 src/models/gtd-task-model.c create mode 100644 src/models/gtd-task-model.h create mode 100644 src/plugins/all-tasks-panel/all-tasks-panel-plugin.c create mode 100644 src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml create mode 100644 src/plugins/all-tasks-panel/all-tasks-panel.plugin create mode 100644 src/plugins/all-tasks-panel/gtd-all-tasks-panel.c create mode 100644 src/plugins/all-tasks-panel/gtd-all-tasks-panel.h create mode 100644 src/plugins/all-tasks-panel/meson.build create mode 100644 src/plugins/eds/e-source-endeavour.c create mode 100644 src/plugins/eds/e-source-endeavour.h create mode 100644 src/plugins/eds/eds-plugin.c create mode 100644 src/plugins/eds/eds.gresource.xml create mode 100644 src/plugins/eds/eds.plugin create mode 100644 src/plugins/eds/gtd-eds-autoptr.h create mode 100644 src/plugins/eds/gtd-eds.h create mode 100644 src/plugins/eds/gtd-plugin-eds.c create mode 100644 src/plugins/eds/gtd-plugin-eds.h create mode 100644 src/plugins/eds/gtd-provider-eds.c create mode 100644 src/plugins/eds/gtd-provider-eds.h create mode 100644 src/plugins/eds/gtd-provider-goa.c create mode 100644 src/plugins/eds/gtd-provider-goa.h create mode 100644 src/plugins/eds/gtd-provider-local.c create mode 100644 src/plugins/eds/gtd-provider-local.h create mode 100644 src/plugins/eds/gtd-task-eds.c create mode 100644 src/plugins/eds/gtd-task-eds.h create mode 100644 src/plugins/eds/gtd-task-list-eds.c create mode 100644 src/plugins/eds/gtd-task-list-eds.h create mode 100644 src/plugins/eds/meson.build create mode 100644 src/plugins/inbox-panel/gtd-inbox-panel.c create mode 100644 src/plugins/inbox-panel/gtd-inbox-panel.h create mode 100644 src/plugins/inbox-panel/inbox-panel-plugin.c create mode 100644 src/plugins/inbox-panel/inbox-panel.gresource.xml create mode 100644 src/plugins/inbox-panel/inbox-panel.plugin create mode 100644 src/plugins/inbox-panel/meson.build create mode 100644 src/plugins/meson.build create mode 100644 src/plugins/next-week-panel/gtd-next-week-panel.c create mode 100644 src/plugins/next-week-panel/gtd-next-week-panel.h create mode 100644 src/plugins/next-week-panel/meson.build create mode 100644 src/plugins/next-week-panel/next-week-panel-plugin.c create mode 100644 src/plugins/next-week-panel/next-week-panel.gresource.xml create mode 100644 src/plugins/next-week-panel/next-week-panel.plugin create mode 100644 src/plugins/next-week-panel/theme/Adwaita.css create mode 100644 src/plugins/peace/gtd-peace-omni-area-addin.c create mode 100644 src/plugins/peace/gtd-peace-omni-area-addin.h create mode 100644 src/plugins/peace/meson.build create mode 100644 src/plugins/peace/peace-plugin.c create mode 100644 src/plugins/peace/peace.gresource.xml create mode 100644 src/plugins/peace/peace.plugin create mode 100644 src/plugins/scheduled-panel/gtd-panel-scheduled.c create mode 100644 src/plugins/scheduled-panel/gtd-panel-scheduled.h create mode 100644 src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c create mode 100644 src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h create mode 100644 src/plugins/scheduled-panel/meson.build create mode 100644 src/plugins/scheduled-panel/scheduled-panel.gresource.xml create mode 100644 src/plugins/scheduled-panel/scheduled-panel.plugin create mode 100644 src/plugins/scheduled-panel/theme/Adwaita.css create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-list-row.c create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-list-row.h create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar.c create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar.h create mode 100644 src/plugins/task-lists-workspace/gtd-sidebar.ui create mode 100644 src/plugins/task-lists-workspace/gtd-task-list-panel.c create mode 100644 src/plugins/task-lists-workspace/gtd-task-list-panel.h create mode 100644 src/plugins/task-lists-workspace/gtd-task-list-panel.ui create mode 100644 src/plugins/task-lists-workspace/gtd-task-lists-workspace.c create mode 100644 src/plugins/task-lists-workspace/gtd-task-lists-workspace.h create mode 100644 src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui create mode 100644 src/plugins/task-lists-workspace/meson.build create mode 100644 src/plugins/task-lists-workspace/task-lists-workspace-plugin.c create mode 100644 src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml create mode 100644 src/plugins/task-lists-workspace/task-lists-workspace.plugin create mode 100644 src/plugins/today-panel/gtd-panel-today.c create mode 100644 src/plugins/today-panel/gtd-panel-today.h create mode 100644 src/plugins/today-panel/gtd-today-omni-area-addin.c create mode 100644 src/plugins/today-panel/gtd-today-omni-area-addin.h create mode 100644 src/plugins/today-panel/meson.build create mode 100644 src/plugins/today-panel/theme/Adwaita.css create mode 100644 src/plugins/today-panel/today-panel-plugin.c create mode 100644 src/plugins/today-panel/today-panel.gresource.xml create mode 100644 src/plugins/today-panel/today-panel.plugin create mode 100644 src/themes/_omniarea.css create mode 100644 src/themes/_taskrow.css create mode 100644 src/themes/_widgets.css create mode 100644 src/themes/style.css create mode 100644 src/todo.gresource.xml (limited to 'src') diff --git a/src/animation/gtd-animatable.c b/src/animation/gtd-animatable.c new file mode 100644 index 0000000..b068b31 --- /dev/null +++ b/src/animation/gtd-animatable.c @@ -0,0 +1,204 @@ +/* gtd-animatable.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +/** + * SECTION:gtd-animatable + * @short_description: Interface for animatable classes + * + * #GtdAnimatable is an interface that allows a #GObject class + * to control how an widget will animate a property. + * + * Each #GtdAnimatable should implement the + * #GtdAnimatableInterface.interpolate_property() virtual function of the + * interface to compute the animation state between two values of an interval + * depending on a progress fwidget, expressed as a floating point value. + */ + +#include "gtd-animatable.h" + +#include "gtd-debug.h" +#include "gtd-interval.h" + +G_DEFINE_INTERFACE (GtdAnimatable, gtd_animatable, G_TYPE_OBJECT); + +static void +gtd_animatable_default_init (GtdAnimatableInterface *iface) +{ +} + +/** + * gtd_animatable_find_property: + * @animatable: a #GtdAnimatable + * @property_name: the name of the animatable property to find + * + * Finds the #GParamSpec for @property_name + * + * Return value: (transfer none) (nullable): The #GParamSpec for the given property + */ +GParamSpec * +gtd_animatable_find_property (GtdAnimatable *animatable, + const gchar *property_name) +{ + GtdAnimatableInterface *iface; + + g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL); + g_return_val_if_fail (property_name != NULL, NULL); + + GTD_TRACE_MSG ("[animation] Looking for property '%s'", property_name); + + iface = GTD_ANIMATABLE_GET_IFACE (animatable); + if (iface->find_property != NULL) + return iface->find_property (animatable, property_name); + + return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable), + property_name); +} + +/** + * gtd_animatable_get_initial_state: + * @animatable: a #GtdAnimatable + * @property_name: the name of the animatable property to retrieve + * @value: a #GValue initialized to the type of the property to retrieve + * + * Retrieves the current state of @property_name and sets @value with it + */ +void +gtd_animatable_get_initial_state (GtdAnimatable *animatable, + const gchar *property_name, + GValue *value) +{ + GtdAnimatableInterface *iface; + + g_return_if_fail (GTD_IS_ANIMATABLE (animatable)); + g_return_if_fail (property_name != NULL); + + GTD_TRACE_MSG ("[animation] Getting initial state of '%s'", property_name); + + iface = GTD_ANIMATABLE_GET_IFACE (animatable); + if (iface->get_initial_state != NULL) + iface->get_initial_state (animatable, property_name, value); + else + g_object_get_property (G_OBJECT (animatable), property_name, value); +} + +/** + * gtd_animatable_set_final_state: + * @animatable: a #GtdAnimatable + * @property_name: the name of the animatable property to set + * @value: the value of the animatable property to set + * + * Sets the current state of @property_name to @value + */ +void +gtd_animatable_set_final_state (GtdAnimatable *animatable, + const gchar *property_name, + const GValue *value) +{ + GtdAnimatableInterface *iface; + + g_return_if_fail (GTD_IS_ANIMATABLE (animatable)); + g_return_if_fail (property_name != NULL); + + GTD_TRACE_MSG ("[animation] Setting state of property '%s'", property_name); + + iface = GTD_ANIMATABLE_GET_IFACE (animatable); + if (iface->set_final_state != NULL) + iface->set_final_state (animatable, property_name, value); + else + g_object_set_property (G_OBJECT (animatable), property_name, value); +} + +/** + * gtd_animatable_interpolate_value: + * @animatable: a #GtdAnimatable + * @property_name: the name of the property to interpolate + * @interval: a #GtdInterval with the animation range + * @progress: the progress to use to interpolate between the + * initial and final values of the @interval + * @value: (out): return location for an initialized #GValue + * using the same type of the @interval + * + * Asks a #GtdAnimatable implementation to interpolate a + * a named property between the initial and final values of + * a #GtdInterval, using @progress as the interpolation + * value, and store the result inside @value. + * + * This function should be used for every property animation + * involving #GtdAnimatables. + * + * This function replaces gtd_animatable_animate_property(). + * + * Return value: %TRUE if the interpolation was successful, + * and %FALSE otherwise + */ +gboolean +gtd_animatable_interpolate_value (GtdAnimatable *animatable, + const gchar *property_name, + GtdInterval *interval, + gdouble progress, + GValue *value) +{ + GtdAnimatableInterface *iface; + + g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + g_return_val_if_fail (GTD_IS_INTERVAL (interval), FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + GTD_TRACE_MSG ("[animation] Interpolating '%s' (progress: %.3f)", + property_name, + progress); + + iface = GTD_ANIMATABLE_GET_IFACE (animatable); + if (iface->interpolate_value != NULL) + { + return iface->interpolate_value (animatable, property_name, + interval, + progress, + value); + } + else + { + return gtd_interval_compute_value (interval, progress, value); + } +} + +/** + * gtd_animatable_get_widget: + * @animatable: a #GtdAnimatable + * + * Get animated widget. + * + * Return value: (transfer none): a #GtdWidget + */ +GtdWidget * +gtd_animatable_get_widget (GtdAnimatable *animatable) +{ + GtdAnimatableInterface *iface; + + g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL); + + iface = GTD_ANIMATABLE_GET_IFACE (animatable); + + g_return_val_if_fail (iface->get_widget, NULL); + + return iface->get_widget (animatable); +} diff --git a/src/animation/gtd-animatable.h b/src/animation/gtd-animatable.h new file mode 100644 index 0000000..3edb07d --- /dev/null +++ b/src/animation/gtd-animatable.h @@ -0,0 +1,85 @@ +/* gtd-animatable.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_ANIMATABLE (gtd_animatable_get_type ()) +G_DECLARE_INTERFACE (GtdAnimatable, gtd_animatable, GTD, ANIMATABLE, GObject) + +/** + * GtdAnimatableInterface: + * @find_property: virtual function for retrieving the #GParamSpec of + * an animatable property + * @get_initial_state: virtual function for retrieving the initial + * state of an animatable property + * @set_final_state: virtual function for setting the state of an + * animatable property + * @interpolate_value: virtual function for interpolating the progress + * of a property + * @get_widget: virtual function for getting associated #GtdWidget + * + * Since: 1.0 + */ +struct _GtdAnimatableInterface +{ + /*< private >*/ + GTypeInterface parent_iface; + + /*< public >*/ + GParamSpec *(* find_property) (GtdAnimatable *animatable, + const gchar *property_name); + void (* get_initial_state) (GtdAnimatable *animatable, + const gchar *property_name, + GValue *value); + void (* set_final_state) (GtdAnimatable *animatable, + const gchar *property_name, + const GValue *value); + gboolean (* interpolate_value) (GtdAnimatable *animatable, + const gchar *property_name, + GtdInterval *interval, + gdouble progress, + GValue *value); + GtdWidget * (* get_widget) (GtdAnimatable *animatable); +}; + +GParamSpec *gtd_animatable_find_property (GtdAnimatable *animatable, + const gchar *property_name); + +void gtd_animatable_get_initial_state (GtdAnimatable *animatable, + const gchar *property_name, + GValue *value); + +void gtd_animatable_set_final_state (GtdAnimatable *animatable, + const gchar *property_name, + const GValue *value); + +gboolean gtd_animatable_interpolate_value (GtdAnimatable *animatable, + const gchar *property_name, + GtdInterval *interval, + gdouble progress, + GValue *value); + +GtdWidget * gtd_animatable_get_widget (GtdAnimatable *animatable); + +G_END_DECLS diff --git a/src/animation/gtd-animation-enums.h b/src/animation/gtd-animation-enums.h new file mode 100644 index 0000000..ed26f8e --- /dev/null +++ b/src/animation/gtd-animation-enums.h @@ -0,0 +1,173 @@ +/* gtd-animation-enums.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + + +/** + * GtdEaseMode: + * @GTD_CUSTOM_MODE: custom progress function + * @GTD_EASE_LINEAR: linear tweening + * @GTD_EASE_IN_QUAD: quadratic tweening + * @GTD_EASE_OUT_QUAD: quadratic tweening, inverse of + * %GTD_EASE_IN_QUAD + * @GTD_EASE_IN_OUT_QUAD: quadratic tweening, combininig + * %GTD_EASE_IN_QUAD and %GTD_EASE_OUT_QUAD + * @GTD_EASE_IN_CUBIC: cubic tweening + * @GTD_EASE_OUT_CUBIC: cubic tweening, invers of + * %GTD_EASE_IN_CUBIC + * @GTD_EASE_IN_OUT_CUBIC: cubic tweening, combining + * %GTD_EASE_IN_CUBIC and %GTD_EASE_OUT_CUBIC + * @GTD_EASE_IN_QUART: quartic tweening + * @GTD_EASE_OUT_QUART: quartic tweening, inverse of + * %GTD_EASE_IN_QUART + * @GTD_EASE_IN_OUT_QUART: quartic tweening, combining + * %GTD_EASE_IN_QUART and %GTD_EASE_OUT_QUART + * @GTD_EASE_IN_QUINT: quintic tweening + * @GTD_EASE_OUT_QUINT: quintic tweening, inverse of + * %GTD_EASE_IN_QUINT + * @GTD_EASE_IN_OUT_QUINT: fifth power tweening, combining + * %GTD_EASE_IN_QUINT and %GTD_EASE_OUT_QUINT + * @GTD_EASE_IN_SINE: sinusoidal tweening + * @GTD_EASE_OUT_SINE: sinusoidal tweening, inverse of + * %GTD_EASE_IN_SINE + * @GTD_EASE_IN_OUT_SINE: sine wave tweening, combining + * %GTD_EASE_IN_SINE and %GTD_EASE_OUT_SINE + * @GTD_EASE_IN_EXPO: exponential tweening + * @GTD_EASE_OUT_EXPO: exponential tweening, inverse of + * %GTD_EASE_IN_EXPO + * @GTD_EASE_IN_OUT_EXPO: exponential tweening, combining + * %GTD_EASE_IN_EXPO and %GTD_EASE_OUT_EXPO + * @GTD_EASE_IN_CIRC: circular tweening + * @GTD_EASE_OUT_CIRC: circular tweening, inverse of + * %GTD_EASE_IN_CIRC + * @GTD_EASE_IN_OUT_CIRC: circular tweening, combining + * %GTD_EASE_IN_CIRC and %GTD_EASE_OUT_CIRC + * @GTD_EASE_IN_ELASTIC: elastic tweening, with offshoot on start + * @GTD_EASE_OUT_ELASTIC: elastic tweening, with offshoot on end + * @GTD_EASE_IN_OUT_ELASTIC: elastic tweening with offshoot on both ends + * @GTD_EASE_IN_BACK: overshooting cubic tweening, with + * backtracking on start + * @GTD_EASE_OUT_BACK: overshooting cubic tweening, with + * backtracking on end + * @GTD_EASE_IN_OUT_BACK: overshooting cubic tweening, with + * backtracking on both ends + * @GTD_EASE_IN_BOUNCE: exponentially decaying parabolic (bounce) + * tweening, with bounce on start + * @GTD_EASE_OUT_BOUNCE: exponentially decaying parabolic (bounce) + * tweening, with bounce on end + * @GTD_EASE_IN_OUT_BOUNCE: exponentially decaying parabolic (bounce) + * tweening, with bounce on both ends + * @GTD_ANIMATION_LAST: last animation mode, used as a guard for + * registered global alpha functions + * + * The animation modes used by #ClutterAnimatable. This + * enumeration can be expanded in later versions of Clutter. + * + *
+ * Easing modes provided by Clutter + * + *
+ * + * Every global alpha function registered using clutter_alpha_register_func() + * or clutter_alpha_register_closure() will have a logical id greater than + * %GTD_ANIMATION_LAST. + * + * Since: 1.0 + */ +typedef enum +{ + GTD_CUSTOM_MODE = 0, + + /* linear */ + GTD_EASE_LINEAR, + + /* quadratic */ + GTD_EASE_IN_QUAD, + GTD_EASE_OUT_QUAD, + GTD_EASE_IN_OUT_QUAD, + + /* cubic */ + GTD_EASE_IN_CUBIC, + GTD_EASE_OUT_CUBIC, + GTD_EASE_IN_OUT_CUBIC, + + /* quartic */ + GTD_EASE_IN_QUART, + GTD_EASE_OUT_QUART, + GTD_EASE_IN_OUT_QUART, + + /* quintic */ + GTD_EASE_IN_QUINT, + GTD_EASE_OUT_QUINT, + GTD_EASE_IN_OUT_QUINT, + + /* sinusoidal */ + GTD_EASE_IN_SINE, + GTD_EASE_OUT_SINE, + GTD_EASE_IN_OUT_SINE, + + /* exponential */ + GTD_EASE_IN_EXPO, + GTD_EASE_OUT_EXPO, + GTD_EASE_IN_OUT_EXPO, + + /* circular */ + GTD_EASE_IN_CIRC, + GTD_EASE_OUT_CIRC, + GTD_EASE_IN_OUT_CIRC, + + /* elastic */ + GTD_EASE_IN_ELASTIC, + GTD_EASE_OUT_ELASTIC, + GTD_EASE_IN_OUT_ELASTIC, + + /* overshooting cubic */ + GTD_EASE_IN_BACK, + GTD_EASE_OUT_BACK, + GTD_EASE_IN_OUT_BACK, + + /* exponentially decaying parabolic */ + GTD_EASE_IN_BOUNCE, + GTD_EASE_OUT_BOUNCE, + GTD_EASE_IN_OUT_BOUNCE, + + /* guard, before registered alpha functions */ + GTD_EASE_LAST +} GtdEaseMode; + +/** + * GtdTimelineDirection: + * @GTD_TIMELINE_FORWARD: forward direction for a timeline + * @GTD_TIMELINE_BACKWARD: backward direction for a timeline + * + * The direction of a #GtdTimeline + */ +typedef enum +{ + GTD_TIMELINE_FORWARD, + GTD_TIMELINE_BACKWARD +} GtdTimelineDirection; + +G_END_DECLS diff --git a/src/animation/gtd-animation-utils.c b/src/animation/gtd-animation-utils.c new file mode 100644 index 0000000..7ab2198 --- /dev/null +++ b/src/animation/gtd-animation-utils.c @@ -0,0 +1,168 @@ +/* gtd-animation-utils.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-animation-utils.h" + + +typedef struct +{ + GType value_type; + GtdProgressFunc func; +} ProgressData; + +G_LOCK_DEFINE_STATIC (progress_funcs); +static GHashTable *progress_funcs = NULL; + +gboolean +gtd_has_progress_function (GType gtype) +{ + const char *type_name = g_type_name (gtype); + + if (progress_funcs == NULL) + return FALSE; + + return g_hash_table_lookup (progress_funcs, type_name) != NULL; +} + +gboolean +gtd_run_progress_function (GType gtype, + const GValue *initial, + const GValue *final, + gdouble progress, + GValue *retval) +{ + ProgressData *pdata; + gboolean res; + + G_LOCK (progress_funcs); + + if (G_UNLIKELY (!progress_funcs)) + { + res = FALSE; + goto out; + } + + pdata = g_hash_table_lookup (progress_funcs, g_type_name (gtype)); + if (G_UNLIKELY (!pdata)) + { + res = FALSE; + goto out; + } + + res = pdata->func (initial, final, progress, retval); + +out: + G_UNLOCK (progress_funcs); + + return res; +} + +static void +progress_data_destroy (gpointer data) +{ + g_free (data); +} + +/** + * gtd_interval_register_progress_func: (skip) + * @value_type: a #GType + * @func: a #GtdProgressFunc, or %NULL to unset a previously + * set progress function + * + * Sets the progress function for a given @value_type, like: + * + * |[ + * gtd_interval_register_progress_func (MY_TYPE_FOO, + * my_foo_progress); + * ]| + * + * Whenever a #GtdInterval instance using the default + * #GtdInterval::compute_value implementation is set as an + * interval between two #GValue of type @value_type, it will call + * @func to establish the value depending on the given progress, + * for instance: + * + * |[ + * static gboolean + * my_int_progress (const GValue *a, + * const GValue *b, + * gdouble progress, + * GValue *retval) + * { + * gint ia = g_value_get_int (a); + * gint ib = g_value_get_int (b); + * gint res = factor * (ib - ia) + ia; + * + * g_value_set_int (retval, res); + * + * return TRUE; + * } + * + * gtd_interval_register_progress_func (G_TYPE_INT, my_int_progress); + * ]| + * + * To unset a previously set progress function of a #GType, pass %NULL + * for @func. + * + * Since: 1.0 + */ +void +gtd_interval_register_progress_func (GType value_type, + GtdProgressFunc func) +{ + ProgressData *progress_func; + const char *type_name; + + g_return_if_fail (value_type != G_TYPE_INVALID); + + type_name = g_type_name (value_type); + + G_LOCK (progress_funcs); + + if (G_UNLIKELY (!progress_funcs)) + progress_funcs = g_hash_table_new_full (NULL, NULL, NULL, progress_data_destroy); + + progress_func = g_hash_table_lookup (progress_funcs, type_name); + + if (G_UNLIKELY (progress_func)) + { + if (func == NULL) + { + g_hash_table_remove (progress_funcs, type_name); + g_free (progress_func); + } + else + { + progress_func->func = func; + } + } + else + { + progress_func = g_new0 (ProgressData, 1); + progress_func->value_type = value_type; + progress_func->func = func; + + g_hash_table_replace (progress_funcs, + (gpointer) type_name, + progress_func); + } + + G_UNLOCK (progress_funcs); +} diff --git a/src/animation/gtd-animation-utils.h b/src/animation/gtd-animation-utils.h new file mode 100644 index 0000000..b41bc12 --- /dev/null +++ b/src/animation/gtd-animation-utils.h @@ -0,0 +1,65 @@ +/* gtd-animation-utils.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + + +/** + * GtdProgressFunc: + * @a: the initial value of an interval + * @b: the final value of an interval + * @progress: the progress factor, between 0 and 1 + * @retval: the value used to store the progress + * + * Prototype of the progress function used to compute the value + * between the two ends @a and @b of an interval depending on + * the value of @progress. + * + * The #GValue in @retval is already initialized with the same + * type as @a and @b. + * + * This function will be called by #GtdInterval if the + * type of the values of the interval was registered using + * gtd_interval_register_progress_func(). + * + * Return value: %TRUE if the function successfully computed + * the value and stored it inside @retval + */ +typedef gboolean (* GtdProgressFunc) (const GValue *a, + const GValue *b, + gdouble progress, + GValue *retval); + +void gtd_interval_register_progress_func (GType value_type, + GtdProgressFunc func); + + +gboolean gtd_has_progress_function (GType gtype); +gboolean gtd_run_progress_function (GType gtype, + const GValue *initial, + const GValue *final, + gdouble progress, + GValue *retval); + +G_END_DECLS diff --git a/src/animation/gtd-easing.c b/src/animation/gtd-easing.c new file mode 100644 index 0000000..c674f91 --- /dev/null +++ b/src/animation/gtd-easing.c @@ -0,0 +1,474 @@ +/* gtd-easing.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-easing.h" + +#include + +gdouble +gtd_linear (gdouble t, + gdouble d) +{ + return t / d; +} + +gdouble +gtd_ease_in_quad (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return p * p; +} + +gdouble +gtd_ease_out_quad (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return -1.0 * p * (p - 2); +} + +gdouble +gtd_ease_in_out_quad (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + + if (p < 1) + return 0.5 * p * p; + + p -= 1; + + return -0.5 * (p * (p - 2) - 1); +} + +gdouble +gtd_ease_in_cubic (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return p * p * p; +} + +gdouble +gtd_ease_out_cubic (gdouble t, + gdouble d) +{ + gdouble p = t / d - 1; + + return p * p * p + 1; +} + +gdouble +gtd_ease_in_out_cubic (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + + if (p < 1) + return 0.5 * p * p * p; + + p -= 2; + + return 0.5 * (p * p * p + 2); +} + +gdouble +gtd_ease_in_quart (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return p * p * p * p; +} + +gdouble +gtd_ease_out_quart (gdouble t, + gdouble d) +{ + gdouble p = t / d - 1; + + return -1.0 * (p * p * p * p - 1); +} + +gdouble +gtd_ease_in_out_quart (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + + if (p < 1) + return 0.5 * p * p * p * p; + + p -= 2; + + return -0.5 * (p * p * p * p - 2); +} + +gdouble +gtd_ease_in_quint (gdouble t, + gdouble d) + { + gdouble p = t / d; + + return p * p * p * p * p; +} + +gdouble +gtd_ease_out_quint (gdouble t, + gdouble d) +{ + gdouble p = t / d - 1; + + return p * p * p * p * p + 1; +} + +gdouble +gtd_ease_in_out_quint (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + + if (p < 1) + return 0.5 * p * p * p * p * p; + + p -= 2; + + return 0.5 * (p * p * p * p * p + 2); +} + +gdouble +gtd_ease_in_sine (gdouble t, + gdouble d) +{ + return -1.0 * cos (t / d * G_PI_2) + 1.0; +} + +gdouble +gtd_ease_out_sine (gdouble t, + gdouble d) +{ + return sin (t / d * G_PI_2); +} + +gdouble +gtd_ease_in_out_sine (gdouble t, + gdouble d) +{ + return -0.5 * (cos (G_PI * t / d) - 1); +} + +gdouble +gtd_ease_in_expo (gdouble t, + gdouble d) +{ + return (t == 0) ? 0.0 : pow (2, 10 * (t / d - 1)); +} + +gdouble +gtd_ease_out_expo (gdouble t, + gdouble d) +{ + return (t == d) ? 1.0 : -pow (2, -10 * t / d) + 1; +} + +gdouble +gtd_ease_in_out_expo (gdouble t, + gdouble d) +{ + gdouble p; + + if (t == 0) + return 0.0; + + if (t == d) + return 1.0; + + p = t / (d / 2); + + if (p < 1) + return 0.5 * pow (2, 10 * (p - 1)); + + p -= 1; + + return 0.5 * (-pow (2, -10 * p) + 2); +} + +gdouble +gtd_ease_in_circ (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return -1.0 * (sqrt (1 - p * p) - 1); +} + +gdouble +gtd_ease_out_circ (gdouble t, + gdouble d) +{ + gdouble p = t / d - 1; + + return sqrt (1 - p * p); +} + +gdouble +gtd_ease_in_out_circ (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + + if (p < 1) + return -0.5 * (sqrt (1 - p * p) - 1); + + p -= 2; + + return 0.5 * (sqrt (1 - p * p) + 1); +} + +gdouble +gtd_ease_in_elastic (gdouble t, + gdouble d) +{ + gdouble p = d * .3; + gdouble s = p / 4; + gdouble q = t / d; + + if (q == 1) + return 1.0; + + q -= 1; + + return -(pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p)); +} + +gdouble +gtd_ease_out_elastic (gdouble t, + gdouble d) +{ + gdouble p = d * .3; + gdouble s = p / 4; + gdouble q = t / d; + + if (q == 1) + return 1.0; + + return pow (2, -10 * q) * sin ((q * d - s) * (2 * G_PI) / p) + 1.0; +} + +gdouble +gtd_ease_in_out_elastic (gdouble t, + gdouble d) +{ + gdouble p = d * (.3 * 1.5); + gdouble s = p / 4; + gdouble q = t / (d / 2); + + if (q == 2) + return 1.0; + + if (q < 1) + { + q -= 1; + + return -.5 * (pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p)); + } + else + { + q -= 1; + + return pow (2, -10 * q) + * sin ((q * d - s) * (2 * G_PI) / p) + * .5 + 1.0; + } +} + +gdouble +gtd_ease_in_back (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + return p * p * ((1.70158 + 1) * p - 1.70158); +} + +gdouble +gtd_ease_out_back (gdouble t, + gdouble d) +{ + gdouble p = t / d - 1; + + return p * p * ((1.70158 + 1) * p + 1.70158) + 1; +} + +gdouble +gtd_ease_in_out_back (gdouble t, + gdouble d) +{ + gdouble p = t / (d / 2); + gdouble s = 1.70158 * 1.525; + + if (p < 1) + return 0.5 * (p * p * ((s + 1) * p - s)); + + p -= 2; + + return 0.5 * (p * p * ((s + 1) * p + s) + 2); +} + +static inline gdouble +ease_out_bounce_internal (gdouble t, + gdouble d) +{ + gdouble p = t / d; + + if (p < (1 / 2.75)) + { + return 7.5625 * p * p; + } + else if (p < (2 / 2.75)) + { + p -= (1.5 / 2.75); + + return 7.5625 * p * p + .75; + } + else if (p < (2.5 / 2.75)) + { + p -= (2.25 / 2.75); + + return 7.5625 * p * p + .9375; + } + else + { + p -= (2.625 / 2.75); + + return 7.5625 * p * p + .984375; + } +} + +static inline gdouble +ease_in_bounce_internal (gdouble t, + gdouble d) +{ + return 1.0 - ease_out_bounce_internal (d - t, d); +} + +gdouble +gtd_ease_in_bounce (gdouble t, + gdouble d) +{ + return ease_in_bounce_internal (t, d); +} + +gdouble +gtd_ease_out_bounce (gdouble t, + gdouble d) +{ + return ease_out_bounce_internal (t, d); +} + +gdouble +gtd_ease_in_out_bounce (gdouble t, + gdouble d) +{ + if (t < d / 2) + return ease_in_bounce_internal (t * 2, d) * 0.5; + else + return ease_out_bounce_internal (t * 2 - d, d) * 0.5 + 1.0 * 0.5; +} + +/*< private > + * _gtd_animation_modes: + * + * A mapping of animation modes and easing functions. + */ +static const struct { + GtdEaseMode mode; + GtdEaseFunc func; + const char *name; +} _gtd_animation_modes[] = { + { GTD_CUSTOM_MODE, NULL, "custom" }, + + { GTD_EASE_LINEAR, gtd_linear, "linear" }, + { GTD_EASE_IN_QUAD, gtd_ease_in_quad, "easeInQuad" }, + { GTD_EASE_OUT_QUAD, gtd_ease_out_quad, "easeOutQuad" }, + { GTD_EASE_IN_OUT_QUAD, gtd_ease_in_out_quad, "easeInOutQuad" }, + { GTD_EASE_IN_CUBIC, gtd_ease_in_cubic, "easeInCubic" }, + { GTD_EASE_OUT_CUBIC, gtd_ease_out_cubic, "easeOutCubic" }, + { GTD_EASE_IN_OUT_CUBIC, gtd_ease_in_out_cubic, "easeInOutCubic" }, + { GTD_EASE_IN_QUART, gtd_ease_in_quart, "easeInQuart" }, + { GTD_EASE_OUT_QUART, gtd_ease_out_quart, "easeOutQuart" }, + { GTD_EASE_IN_OUT_QUART, gtd_ease_in_out_quart, "easeInOutQuart" }, + { GTD_EASE_IN_QUINT, gtd_ease_in_quint, "easeInQuint" }, + { GTD_EASE_OUT_QUINT, gtd_ease_out_quint, "easeOutQuint" }, + { GTD_EASE_IN_OUT_QUINT, gtd_ease_in_out_quint, "easeInOutQuint" }, + { GTD_EASE_IN_SINE, gtd_ease_in_sine, "easeInSine" }, + { GTD_EASE_OUT_SINE, gtd_ease_out_sine, "easeOutSine" }, + { GTD_EASE_IN_OUT_SINE, gtd_ease_in_out_sine, "easeInOutSine" }, + { GTD_EASE_IN_EXPO, gtd_ease_in_expo, "easeInExpo" }, + { GTD_EASE_OUT_EXPO, gtd_ease_out_expo, "easeOutExpo" }, + { GTD_EASE_IN_OUT_EXPO, gtd_ease_in_out_expo, "easeInOutExpo" }, + { GTD_EASE_IN_CIRC, gtd_ease_in_circ, "easeInCirc" }, + { GTD_EASE_OUT_CIRC, gtd_ease_out_circ, "easeOutCirc" }, + { GTD_EASE_IN_OUT_CIRC, gtd_ease_in_out_circ, "easeInOutCirc" }, + { GTD_EASE_IN_ELASTIC, gtd_ease_in_elastic, "easeInElastic" }, + { GTD_EASE_OUT_ELASTIC, gtd_ease_out_elastic, "easeOutElastic" }, + { GTD_EASE_IN_OUT_ELASTIC, gtd_ease_in_out_elastic, "easeInOutElastic" }, + { GTD_EASE_IN_BACK, gtd_ease_in_back, "easeInBack" }, + { GTD_EASE_OUT_BACK, gtd_ease_out_back, "easeOutBack" }, + { GTD_EASE_IN_OUT_BACK, gtd_ease_in_out_back, "easeInOutBack" }, + { GTD_EASE_IN_BOUNCE, gtd_ease_in_bounce, "easeInBounce" }, + { GTD_EASE_OUT_BOUNCE, gtd_ease_out_bounce, "easeOutBounce" }, + { GTD_EASE_IN_OUT_BOUNCE, gtd_ease_in_out_bounce, "easeInOutBounce" }, + + { GTD_EASE_LAST, NULL, "sentinel" }, +}; + +GtdEaseFunc +gtd_get_easing_func_for_mode (GtdEaseMode mode) +{ + g_assert (_gtd_animation_modes[mode].mode == mode); + g_assert (_gtd_animation_modes[mode].func != NULL); + + return _gtd_animation_modes[mode].func; +} + +const char * +gtd_get_easing_name_for_mode (GtdEaseMode mode) +{ + g_assert (_gtd_animation_modes[mode].mode == mode); + g_assert (_gtd_animation_modes[mode].func != NULL); + + return _gtd_animation_modes[mode].name; +} + +gdouble +gtd_easing_for_mode (GtdEaseMode mode, + gdouble t, + gdouble d) +{ + g_assert (_gtd_animation_modes[mode].mode == mode); + g_assert (_gtd_animation_modes[mode].func != NULL); + + return _gtd_animation_modes[mode].func (t, d); +} diff --git a/src/animation/gtd-easing.h b/src/animation/gtd-easing.h new file mode 100644 index 0000000..b83bb7f --- /dev/null +++ b/src/animation/gtd-easing.h @@ -0,0 +1,141 @@ +/* gtd-easing.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gtd-animation-enums.h" + +G_BEGIN_DECLS + +/** + * GtdEaseFunc: + * @t: elapsed time + * @d: total duration + * + * Internal type for the easing functions used by Gtd. + * + * Return value: the interpolated value, between -1.0 and 2.0 + */ +typedef gdouble (* GtdEaseFunc) (gdouble t, gdouble d); + +GtdEaseFunc gtd_get_easing_func_for_mode (GtdEaseMode mode); + +const gchar* gtd_get_easing_name_for_mode (GtdEaseMode mode); + +gdouble gtd_easing_for_mode (GtdEaseMode mode, + gdouble t, + gdouble d); + +gdouble gtd_ease_linear (gdouble t, + gdouble d); + +gdouble gtd_ease_in_quad (gdouble t, + gdouble d); + +gdouble gtd_ease_out_quad (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_quad (gdouble t, + gdouble d); + +gdouble gtd_ease_in_cubic (gdouble t, + gdouble d); + +gdouble gtd_ease_out_cubic (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_cubic (gdouble t, + gdouble d); + +gdouble gtd_ease_in_quart (gdouble t, + gdouble d); + +gdouble gtd_ease_out_quart (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_quart (gdouble t, + gdouble d); + +gdouble gtd_ease_in_quint (gdouble t, + gdouble d); + +gdouble gtd_ease_out_quint (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_quint (gdouble t, + gdouble d); + +gdouble gtd_ease_in_sine (gdouble t, + gdouble d); + +gdouble gtd_ease_out_sine (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_sine (gdouble t, + gdouble d); + +gdouble gtd_ease_in_expo (gdouble t, + gdouble d); + +gdouble gtd_ease_out_expo (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_expo (gdouble t, + gdouble d); + +gdouble gtd_ease_in_circ (gdouble t, + gdouble d); + +gdouble gtd_ease_out_circ (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_circ (gdouble t, + gdouble d); + +gdouble gtd_ease_in_elastic (gdouble t, + gdouble d); + +gdouble gtd_ease_out_elastic (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_elastic (gdouble t, + gdouble d); + +gdouble gtd_ease_in_back (gdouble t, + gdouble d); + +gdouble gtd_ease_out_back (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_back (gdouble t, + gdouble d); + +gdouble gtd_ease_in_bounce (gdouble t, + gdouble d); + +gdouble gtd_ease_out_bounce (gdouble t, + gdouble d); + +gdouble gtd_ease_in_out_bounce (gdouble t, + gdouble d); + +G_END_DECLS diff --git a/src/animation/gtd-interval.c b/src/animation/gtd-interval.c new file mode 100644 index 0000000..20efd82 --- /dev/null +++ b/src/animation/gtd-interval.c @@ -0,0 +1,1134 @@ +/* gtd-interval.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +/** + * SECTION:clutter-interval + * @short_description: An object holding an interval of two values + * + * #GtdInterval is a simple object that can hold two values + * defining an interval. #GtdInterval can hold any value that + * can be enclosed inside a #GValue. + * + * Once a #GtdInterval for a specific #GType has been instantiated + * the #GtdInterval:value-type property cannot be changed anymore. + * + * #GtdInterval starts with a floating reference; this means that + * any object taking a reference on a #GtdInterval instance should + * also take ownership of the interval by using g_object_ref_sink(). + * + * #GtdInterval can be subclassed to override the validation + * and value computation. + * + * #GtdInterval is available since Gtd 1.0 + */ + +#include "gtd-interval.h" + +#include "gtd-animation-utils.h" +#include "gtd-easing.h" + +#include +#include + +#include +#include +#include + +enum +{ + PROP_0, + PROP_VALUE_TYPE, + PROP_INITIAL, + PROP_FINAL, + PROP_LAST, +}; + +static GParamSpec *obj_props[PROP_LAST]; + +enum +{ + INITIAL, + FINAL, + RESULT, + N_VALUES, +}; + +typedef struct +{ + GType value_type; + + GValue *values; +} GtdIntervalPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdInterval, gtd_interval, G_TYPE_INITIALLY_UNOWNED); + + +static gboolean +gtd_interval_real_validate (GtdInterval *self, + GParamSpec *pspec) +{ + GType pspec_gtype = G_PARAM_SPEC_VALUE_TYPE (pspec); + + /* then check the fundamental types */ + switch (G_TYPE_FUNDAMENTAL (pspec_gtype)) + { + case G_TYPE_INT: + { + GParamSpecInt *pspec_int = G_PARAM_SPEC_INT (pspec); + gint a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_int->minimum && a <= pspec_int->maximum) && + (b >= pspec_int->minimum && b <= pspec_int->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_INT64: + { + GParamSpecInt64 *pspec_int = G_PARAM_SPEC_INT64 (pspec); + gint64 a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_int->minimum && a <= pspec_int->maximum) && + (b >= pspec_int->minimum && b <= pspec_int->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_UINT: + { + GParamSpecUInt *pspec_uint = G_PARAM_SPEC_UINT (pspec); + guint a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_uint->minimum && a <= pspec_uint->maximum) && + (b >= pspec_uint->minimum && b <= pspec_uint->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_UINT64: + { + GParamSpecUInt64 *pspec_int = G_PARAM_SPEC_UINT64 (pspec); + guint64 a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_int->minimum && a <= pspec_int->maximum) && + (b >= pspec_int->minimum && b <= pspec_int->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_CHAR: + { + GParamSpecChar *pspec_char = G_PARAM_SPEC_CHAR (pspec); + guchar a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_char->minimum && a <= pspec_char->maximum) && + (b >= pspec_char->minimum && b <= pspec_char->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_UCHAR: + { + GParamSpecUChar *pspec_uchar = G_PARAM_SPEC_UCHAR (pspec); + guchar a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_uchar->minimum && a <= pspec_uchar->maximum) && + (b >= pspec_uchar->minimum && b <= pspec_uchar->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_FLOAT: + { + GParamSpecFloat *pspec_flt = G_PARAM_SPEC_FLOAT (pspec); + float a, b; + + a = b = 0.f; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) && + (b >= pspec_flt->minimum && b <= pspec_flt->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_DOUBLE: + { + GParamSpecDouble *pspec_flt = G_PARAM_SPEC_DOUBLE (pspec); + double a, b; + + a = b = 0; + gtd_interval_get_interval (self, &a, &b); + if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) && + (b >= pspec_flt->minimum && b <= pspec_flt->maximum)) + return TRUE; + else + return FALSE; + } + break; + + case G_TYPE_BOOLEAN: + return TRUE; + + default: + break; + } + + return TRUE; +} + +static gboolean +gtd_interval_real_compute_value (GtdInterval *self, + gdouble factor, + GValue *value) +{ + GValue *initial, *final; + GType value_type; + gboolean retval = FALSE; + + initial = gtd_interval_peek_initial_value (self); + final = gtd_interval_peek_final_value (self); + + value_type = gtd_interval_get_value_type (self); + + if (gtd_has_progress_function (value_type)) + { + retval = gtd_run_progress_function (value_type, initial, final, factor, value); + if (retval) + return TRUE; + } + + switch (G_TYPE_FUNDAMENTAL (value_type)) + { + case G_TYPE_INT: + { + gint ia, ib, res; + + ia = g_value_get_int (initial); + ib = g_value_get_int (final); + + res = (factor * (ib - ia)) + ia; + + g_value_set_int (value, res); + + retval = TRUE; + } + break; + + case G_TYPE_CHAR: + { + gchar ia, ib, res; + + ia = g_value_get_schar (initial); + ib = g_value_get_schar (final); + + res = (factor * (ib - (gdouble) ia)) + ia; + + g_value_set_schar (value, res); + + retval = TRUE; + } + break; + + case G_TYPE_UINT: + { + guint ia, ib, res; + + ia = g_value_get_uint (initial); + ib = g_value_get_uint (final); + + res = (factor * (ib - (gdouble) ia)) + ia; + + g_value_set_uint (value, res); + + retval = TRUE; + } + break; + + case G_TYPE_UCHAR: + { + guchar ia, ib, res; + + ia = g_value_get_uchar (initial); + ib = g_value_get_uchar (final); + + res = (factor * (ib - (gdouble) ia)) + ia; + + g_value_set_uchar (value, res); + + retval = TRUE; + } + break; + + case G_TYPE_FLOAT: + case G_TYPE_DOUBLE: + { + gdouble ia, ib, res; + + if (value_type == G_TYPE_DOUBLE) + { + ia = g_value_get_double (initial); + ib = g_value_get_double (final); + } + else + { + ia = g_value_get_float (initial); + ib = g_value_get_float (final); + } + + res = (factor * (ib - ia)) + ia; + + if (value_type == G_TYPE_DOUBLE) + g_value_set_double (value, res); + else + g_value_set_float (value, res); + + retval = TRUE; + } + break; + + case G_TYPE_BOOLEAN: + if (factor > 0.5) + g_value_set_boolean (value, TRUE); + else + g_value_set_boolean (value, FALSE); + + retval = TRUE; + break; + + case G_TYPE_BOXED: + break; + + default: + break; + } + + /* We're trying to animate a property without knowing how to do that. Issue + * a warning with a hint to what could be done to fix that */ + if (G_UNLIKELY (retval == FALSE)) + { + g_warning ("%s: Could not compute progress between two %s. You can " + "register a progress function to instruct GtdInterval " + "how to deal with this GType", + G_STRLOC, + g_type_name (value_type)); + } + + return retval; +} + +static void +gtd_interval_finalize (GObject *object) +{ + GtdInterval *self = GTD_INTERVAL (object); + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + + if (G_IS_VALUE (&priv->values[INITIAL])) + g_value_unset (&priv->values[INITIAL]); + + if (G_IS_VALUE (&priv->values[FINAL])) + g_value_unset (&priv->values[FINAL]); + + if (G_IS_VALUE (&priv->values[RESULT])) + g_value_unset (&priv->values[RESULT]); + + g_free (priv->values); + + G_OBJECT_CLASS (gtd_interval_parent_class)->finalize (object); +} + +static void +gtd_interval_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdInterval *self = GTD_INTERVAL (gobject); + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + + switch (prop_id) + { + case PROP_VALUE_TYPE: + priv->value_type = g_value_get_gtype (value); + break; + + case PROP_INITIAL: + if (g_value_get_boxed (value) != NULL) + gtd_interval_set_initial_value (self, g_value_get_boxed (value)); + else if (G_IS_VALUE (&priv->values[INITIAL])) + g_value_unset (&priv->values[INITIAL]); + break; + + case PROP_FINAL: + if (g_value_get_boxed (value) != NULL) + gtd_interval_set_final_value (self, g_value_get_boxed (value)); + else if (G_IS_VALUE (&priv->values[FINAL])) + g_value_unset (&priv->values[FINAL]); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtd_interval_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdIntervalPrivate *priv; + + priv = gtd_interval_get_instance_private (GTD_INTERVAL (gobject)); + + switch (prop_id) + { + case PROP_VALUE_TYPE: + g_value_set_gtype (value, priv->value_type); + break; + + case PROP_INITIAL: + if (G_IS_VALUE (&priv->values[INITIAL])) + g_value_set_boxed (value, &priv->values[INITIAL]); + break; + + case PROP_FINAL: + if (G_IS_VALUE (&priv->values[FINAL])) + g_value_set_boxed (value, &priv->values[FINAL]); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtd_interval_class_init (GtdIntervalClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + klass->validate = gtd_interval_real_validate; + klass->compute_value = gtd_interval_real_compute_value; + + gobject_class->set_property = gtd_interval_set_property, + gobject_class->get_property = gtd_interval_get_property; + gobject_class->finalize = gtd_interval_finalize; + + /** + * GtdInterval:value-type: + * + * The type of the values in the interval. + */ + obj_props[PROP_VALUE_TYPE] = + g_param_spec_gtype ("value-type", + "Value Type", + "The type of the values in the interval", + G_TYPE_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + /** + * GtdInterval:initial: + * + * The initial value of the interval. + */ + obj_props[PROP_INITIAL] = + g_param_spec_boxed ("initial", + "Initial Value", + "Initial value of the interval", + G_TYPE_VALUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdInterval:final: + * + * The final value of the interval. + */ + obj_props[PROP_FINAL] = + g_param_spec_boxed ("final", + "Final Value", + "Final value of the interval", + G_TYPE_VALUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} + +static void +gtd_interval_init (GtdInterval *self) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + + priv->value_type = G_TYPE_INVALID; + priv->values = g_malloc0 (sizeof (GValue) * N_VALUES); +} + +static inline void +gtd_interval_set_value_internal (GtdInterval *self, + gint index_, + const GValue *value) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GType value_type; + + g_assert (index_ >= INITIAL && index_ <= RESULT); + + if (G_IS_VALUE (&priv->values[index_])) + g_value_unset (&priv->values[index_]); + + g_value_init (&priv->values[index_], priv->value_type); + + value_type = G_VALUE_TYPE (value); + if (value_type != priv->value_type || + !g_type_is_a (value_type, priv->value_type)) + { + if (g_value_type_compatible (value_type, priv->value_type)) + { + g_value_copy (value, &priv->values[index_]); + return; + } + + if (g_value_type_transformable (value_type, priv->value_type)) + { + GValue transform = G_VALUE_INIT; + + g_value_init (&transform, priv->value_type); + + if (g_value_transform (value, &transform)) + g_value_copy (&transform, &priv->values[index_]); + else + { + g_warning ("%s: Unable to convert a value of type '%s' into " + "the value type '%s' of the interval.", + G_STRLOC, + g_type_name (value_type), + g_type_name (priv->value_type)); + } + + g_value_unset (&transform); + } + } + else + g_value_copy (value, &priv->values[index_]); +} + +static inline void +gtd_interval_get_value_internal (GtdInterval *self, + gint index_, + GValue *value) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + + g_assert (index_ >= INITIAL && index_ <= RESULT); + + g_value_copy (&priv->values[index_], value); +} + +static gboolean +gtd_interval_set_initial_internal (GtdInterval *self, + va_list *args) +{; + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GType gtype = priv->value_type; + GValue value = G_VALUE_INIT; + gchar *error; + + /* initial value */ + G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error); + + if (error) + { + g_warning ("%s: %s", G_STRLOC, error); + + /* we leak the value here as it might not be in a valid state + * given the error and calling g_value_unset() might lead to + * undefined behaviour + */ + g_free (error); + return FALSE; + } + + gtd_interval_set_value_internal (self, INITIAL, &value); + g_value_unset (&value); + + return TRUE; +} + +static gboolean +gtd_interval_set_final_internal (GtdInterval *self, + va_list *args) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GType gtype = priv->value_type; + GValue value = G_VALUE_INIT; + gchar *error; + + /* initial value */ + G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error); + + if (error) + { + g_warning ("%s: %s", G_STRLOC, error); + + /* we leak the value here as it might not be in a valid state + * given the error and calling g_value_unset() might lead to + * undefined behaviour + */ + g_free (error); + return FALSE; + } + + gtd_interval_set_value_internal (self, FINAL, &value); + g_value_unset (&value); + + return TRUE; +} + +static void +gtd_interval_get_interval_valist (GtdInterval *self, + va_list var_args) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GType gtype = priv->value_type; + GValue value = G_VALUE_INIT; + gchar *error; + + /* initial value */ + g_value_init (&value, gtype); + gtd_interval_get_initial_value (self, &value); + G_VALUE_LCOPY (&value, var_args, 0, &error); + if (error) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + g_value_unset (&value); + return; + } + + g_value_unset (&value); + + /* final value */ + g_value_init (&value, gtype); + gtd_interval_get_final_value (self, &value); + G_VALUE_LCOPY (&value, var_args, 0, &error); + if (error) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + g_value_unset (&value); + return; + } + + g_value_unset (&value); +} + +/** + * gtd_interval_new: + * @gtype: the type of the values in the interval + * @...: the initial value and the final value of the interval + * + * Creates a new #GtdInterval holding values of type @gtype. + * + * This function avoids using a #GValue for the initial and final values + * of the interval: + * + * |[ + * interval = gtd_interval_new (G_TYPE_FLOAT, 0.0, 1.0); + * interval = gtd_interval_new (G_TYPE_BOOLEAN, FALSE, TRUE); + * interval = gtd_interval_new (G_TYPE_INT, 0, 360); + * ]| + * + * Return value: the newly created #GtdInterval + */ +GtdInterval * +gtd_interval_new (GType gtype, + ...) +{ + GtdInterval *retval; + va_list args; + + g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL); + + retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL); + + va_start (args, gtype); + + if (!gtd_interval_set_initial_internal (retval, &args)) + goto out; + + gtd_interval_set_final_internal (retval, &args); + +out: + va_end (args); + + return retval; +} + +/** + * gtd_interval_new_with_values: + * @gtype: the type of the values in the interval + * @initial: (allow-none): a #GValue holding the initial value of the interval + * @final: (allow-none): a #GValue holding the final value of the interval + * + * Creates a new #GtdInterval of type @gtype, between @initial + * and @final. + * + * This function is useful for language bindings. + * + * Return value: the newly created #GtdInterval + */ +GtdInterval * +gtd_interval_new_with_values (GType gtype, + const GValue *initial, + const GValue *final) +{ + g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL); + g_return_val_if_fail (initial == NULL || G_VALUE_TYPE (initial) == gtype, NULL); + g_return_val_if_fail (final == NULL || G_VALUE_TYPE (final) == gtype, NULL); + + return g_object_new (GTD_TYPE_INTERVAL, + "value-type", gtype, + "initial", initial, + "final", final, + NULL); +} + +/** + * gtd_interval_clone: + * @interval: a #GtdInterval + * + * Creates a copy of @interval. + * + * Return value: (transfer full): the newly created #GtdInterval + */ +GtdInterval * +gtd_interval_clone (GtdInterval *self) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GtdInterval *retval; + GType gtype; + GValue *tmp; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL); + g_return_val_if_fail (priv->value_type != G_TYPE_INVALID, NULL); + + gtype = priv->value_type; + retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL); + + tmp = gtd_interval_peek_initial_value (self); + gtd_interval_set_initial_value (retval, tmp); + + tmp = gtd_interval_peek_final_value (self); + gtd_interval_set_final_value (retval, tmp); + + return retval; +} + +/** + * gtd_interval_get_value_type: + * @interval: a #GtdInterval + * + * Retrieves the #GType of the values inside @interval. + * + * Return value: the type of the value, or G_TYPE_INVALID + */ +GType +gtd_interval_get_value_type (GtdInterval *self) +{ + GtdIntervalPrivate *priv; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), G_TYPE_INVALID); + + priv = gtd_interval_get_instance_private (self); + return priv->value_type; +} + +/** + * gtd_interval_set_initial_value: (rename-to gtd_interval_set_initial) + * @interval: a #GtdInterval + * @value: a #GValue + * + * Sets the initial value of @interval to @value. The value is copied + * inside the #GtdInterval. + */ +void +gtd_interval_set_initial_value (GtdInterval *self, + const GValue *value) +{ + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (value != NULL); + + gtd_interval_set_value_internal (self, INITIAL, value); +} + +/** + * gtd_interval_set_initial: (skip) + * @interval: a #GtdInterval + * @...: the initial value of the interval. + * + * Variadic arguments version of gtd_interval_set_initial_value(). + * + * This function is meant as a convenience for the C API. + * + * Language bindings should use gtd_interval_set_initial_value() + * instead. + */ +void +gtd_interval_set_initial (GtdInterval *self, + ...) +{ + va_list args; + + g_return_if_fail (GTD_IS_INTERVAL (self)); + + va_start (args, self); + gtd_interval_set_initial_internal (self, &args); + va_end (args); +} + +/** + * gtd_interval_get_initial_value: + * @interval: a #GtdInterval + * @value: (out caller-allocates): a #GValue + * + * Retrieves the initial value of @interval and copies + * it into @value. + * + * The passed #GValue must be initialized to the value held by + * the #GtdInterval. + */ +void +gtd_interval_get_initial_value (GtdInterval *self, + GValue *value) +{ + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (value != NULL); + + gtd_interval_get_value_internal (self, INITIAL, value); +} + +/** + * gtd_interval_peek_initial_value: + * @interval: a #GtdInterval + * + * Gets the pointer to the initial value of @interval + * + * Return value: (transfer none): the initial value of the interval. + * The value is owned by the #GtdInterval and it should not be + * modified or freed + */ +GValue * +gtd_interval_peek_initial_value (GtdInterval *self) +{ + GtdIntervalPrivate *priv; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL); + + priv = gtd_interval_get_instance_private (self); + return priv->values + INITIAL; +} + +/** + * gtd_interval_set_final_value: (rename-to gtd_interval_set_final) + * @interval: a #GtdInterval + * @value: a #GValue + * + * Sets the final value of @interval to @value. The value is + * copied inside the #GtdInterval. + */ +void +gtd_interval_set_final_value (GtdInterval *self, + const GValue *value) +{ + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (value != NULL); + + gtd_interval_set_value_internal (self, FINAL, value); +} + +/** + * gtd_interval_get_final_value: + * @interval: a #GtdInterval + * @value: (out caller-allocates): a #GValue + * + * Retrieves the final value of @interval and copies + * it into @value. + * + * The passed #GValue must be initialized to the value held by + * the #GtdInterval. + */ +void +gtd_interval_get_final_value (GtdInterval *self, + GValue *value) +{ + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (value != NULL); + + gtd_interval_get_value_internal (self, FINAL, value); +} + +/** + * gtd_interval_set_final: (skip) + * @interval: a #GtdInterval + * @...: the final value of the interval + * + * Variadic arguments version of gtd_interval_set_final_value(). + * + * This function is meant as a convenience for the C API. + * + * Language bindings should use gtd_interval_set_final_value() instead. + */ +void +gtd_interval_set_final (GtdInterval *self, + ...) +{ + va_list args; + + g_return_if_fail (GTD_IS_INTERVAL (self)); + + va_start (args, self); + gtd_interval_set_final_internal (self, &args); + va_end (args); +} + +/** + * gtd_interval_peek_final_value: + * @interval: a #GtdInterval + * + * Gets the pointer to the final value of @interval + * + * Return value: (transfer none): the final value of the interval. + * The value is owned by the #GtdInterval and it should not be + * modified or freed + */ +GValue * +gtd_interval_peek_final_value (GtdInterval *self) +{ + GtdIntervalPrivate *priv; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL); + + priv = gtd_interval_get_instance_private (self); + return priv->values + FINAL; +} + +/** + * gtd_interval_set_interval: + * @interval: a #GtdInterval + * @...: the initial and final values of the interval + * + * Variable arguments wrapper for gtd_interval_set_initial_value() + * and gtd_interval_set_final_value() that avoids using the + * #GValue arguments: + * + * |[ + * gtd_interval_set_interval (self, 0, 50); + * gtd_interval_set_interval (self, 1.0, 0.0); + * gtd_interval_set_interval (self, FALSE, TRUE); + * ]| + * + * This function is meant for the convenience of the C API; bindings + * should reimplement this function using the #GValue-based API. + */ +void +gtd_interval_set_interval (GtdInterval *self, + ...) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + va_list args; + + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (priv->value_type != G_TYPE_INVALID); + + va_start (args, self); + + if (!gtd_interval_set_initial_internal (self, &args)) + goto out; + + gtd_interval_set_final_internal (self, &args); + +out: + va_end (args); +} + +/** + * gtd_interval_get_interval: + * @interval: a #GtdInterval + * @...: return locations for the initial and final values of + * the interval + * + * Variable arguments wrapper for gtd_interval_get_initial_value() + * and gtd_interval_get_final_value() that avoids using the + * #GValue arguments: + * + * |[ + * gint a = 0, b = 0; + * gtd_interval_get_interval (self, &a, &b); + * ]| + * + * This function is meant for the convenience of the C API; bindings + * should reimplement this function using the #GValue-based API. + */ +void +gtd_interval_get_interval (GtdInterval *self, + ...) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + va_list args; + + g_return_if_fail (GTD_IS_INTERVAL (self)); + g_return_if_fail (priv->value_type != G_TYPE_INVALID); + + va_start (args, self); + gtd_interval_get_interval_valist (self, args); + va_end (args); +} + +/** + * gtd_interval_validate: + * @interval: a #GtdInterval + * @pspec: a #GParamSpec + * + * Validates the initial and final values of @interval against + * a #GParamSpec. + * + * Return value: %TRUE if the #GtdInterval is valid, %FALSE otherwise + */ +gboolean +gtd_interval_validate (GtdInterval *self, + GParamSpec *pspec) +{ + g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE); + g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), FALSE); + + return GTD_INTERVAL_GET_CLASS (self)->validate (self, pspec); +} + +/** + * gtd_interval_compute_value: + * @interval: a #GtdInterval + * @factor: the progress factor, between 0 and 1 + * @value: (out caller-allocates): return location for an initialized #GValue + * + * Computes the value between the @interval boundaries given the + * progress @factor and copies it into @value. + * + * Return value: %TRUE if the operation was successful + */ +gboolean +gtd_interval_compute_value (GtdInterval *self, + gdouble factor, + GValue *value) +{ + g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + return GTD_INTERVAL_GET_CLASS (self)->compute_value (self, factor, value); +} + +/** + * gtd_interval_compute: + * @interval: a #GtdInterval + * @factor: the progress factor, between 0 and 1 + * + * Computes the value between the @interval boundaries given the + * progress @factor + * + * Unlike gtd_interval_compute_value(), this function will + * return a const pointer to the computed value + * + * You should use this function if you immediately pass the computed + * value to another function that makes a copy of it, like + * g_object_set_property() + * + * Return value: (transfer none): a pointer to the computed value, + * or %NULL if the computation was not successfull + */ +const GValue * +gtd_interval_compute (GtdInterval *self, + gdouble factor) +{ + GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self); + GValue *value; + gboolean res; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL); + + value = &(priv->values[RESULT]); + + if (G_VALUE_TYPE (value) == G_TYPE_INVALID) + g_value_init (value, priv->value_type); + + res = GTD_INTERVAL_GET_CLASS (self)->compute_value (self, factor, value); + + if (res) + return priv->values + RESULT; + + return NULL; +} + +/** + * gtd_interval_is_valid: + * @interval: a #GtdInterval + * + * Checks if the @interval has a valid initial and final values. + * + * Return value: %TRUE if the #GtdInterval has an initial and + * final values, and %FALSE otherwise + */ +gboolean +gtd_interval_is_valid (GtdInterval *self) +{ + GtdIntervalPrivate *priv; + + g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE); + + priv = gtd_interval_get_instance_private (self); + + return G_IS_VALUE (&priv->values[INITIAL]) && + G_IS_VALUE (&priv->values[FINAL]); +} diff --git a/src/animation/gtd-interval.h b/src/animation/gtd-interval.h new file mode 100644 index 0000000..efcfb03 --- /dev/null +++ b/src/animation/gtd-interval.h @@ -0,0 +1,116 @@ +/* gtd-interval.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_INTERVAL (gtd_interval_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdInterval, gtd_interval, GTD, INTERVAL, GInitiallyUnowned) + +/** + * GtdIntervalClass: + * @validate: virtual function for validating an interval + * using a #GParamSpec + * @compute_value: virtual function for computing the value + * inside an interval using an adimensional factor between 0 and 1 + * + * The #GtdIntervalClass contains only private data. + * + * Since: 1.0 + */ +struct _GtdIntervalClass +{ + /*< private >*/ + GInitiallyUnownedClass parent_class; + + /*< public >*/ + gboolean (* validate) (GtdInterval *self, + GParamSpec *pspec); + gboolean (* compute_value) (GtdInterval *self, + gdouble factor, + GValue *value); + + /*< private >*/ + /* padding for future expansion */ + void (*_gtd_reserved1) (void); + void (*_gtd_reserved2) (void); + void (*_gtd_reserved3) (void); + void (*_gtd_reserved4) (void); + void (*_gtd_reserved5) (void); + void (*_gtd_reserved6) (void); +}; + +GtdInterval* gtd_interval_new (GType gtype, + ...); + +GtdInterval* gtd_interval_new_with_values (GType gtype, + const GValue *initial, + const GValue *final); + + +GtdInterval* gtd_interval_clone (GtdInterval *self); + +GType gtd_interval_get_value_type (GtdInterval *self); + +void gtd_interval_set_initial (GtdInterval *self, + ...); + +void gtd_interval_set_initial_value (GtdInterval *self, + const GValue *value); + +void gtd_interval_get_initial_value (GtdInterval *self, + GValue *value); + +GValue* gtd_interval_peek_initial_value (GtdInterval *self); + +void gtd_interval_set_final (GtdInterval *self, + ...); + +void gtd_interval_set_final_value (GtdInterval *self, + const GValue *value); + +void gtd_interval_get_final_value (GtdInterval *self, + GValue *value); + +GValue* gtd_interval_peek_final_value (GtdInterval *self); + +void gtd_interval_set_interval (GtdInterval *self, + ...); + +void gtd_interval_get_interval (GtdInterval *self, + ...); + +gboolean gtd_interval_validate (GtdInterval *self, + GParamSpec *pspec); + +gboolean gtd_interval_compute_value (GtdInterval *self, + gdouble factor, + GValue *value); + +const GValue* gtd_interval_compute (GtdInterval *self, + gdouble factor); + +gboolean gtd_interval_is_valid (GtdInterval *self); + + +G_END_DECLS diff --git a/src/animation/gtd-keyframe-transition.c b/src/animation/gtd-keyframe-transition.c new file mode 100644 index 0000000..a77fea2 --- /dev/null +++ b/src/animation/gtd-keyframe-transition.c @@ -0,0 +1,716 @@ +/* gtd-keyframe-transition.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * SECTION:gtd-keyframe-transition + * @Title: GtdKeyframeTransition + * @Short_Description: Keyframe property transition + * + * #GtdKeyframeTransition allows animating a property by defining + * "key frames": values at a normalized position on the transition + * duration. + * + * The #GtdKeyframeTransition interpolates the value of the property + * to which it's bound across these key values. + * + * Setting up a #GtdKeyframeTransition means providing the times, + * values, and easing modes between these key frames, for instance: + * + * |[ + * GtdTransition *keyframe; + * + * keyframe = gtd_keyframe_transition_new ("opacity"); + * gtd_transition_set_from (keyframe, G_TYPE_UINT, 255); + * gtd_transition_set_to (keyframe, G_TYPE_UINT, 0); + * gtd_keyframe_transition_set (GTD_KEYFRAME_TRANSITION (keyframe), + * G_TYPE_UINT, + * 1, /* number of key frames */ + * 0.5, 128, GTD_EASE_IN_OUT_CUBIC); + * ]| + * + * The example above sets up a keyframe transition for the #GtdActor:opacity + * property of a #GtdActor; the transition starts and sets the value of the + * property to fully transparent; between the start of the transition and its mid + * point, it will animate the property to half opacity, using an easy in/easy out + * progress. Once the transition reaches the mid point, it will linearly fade the + * actor out until it reaches the end of the transition. + * + * The #GtdKeyframeTransition will add an implicit key frame between the last + * and the 1.0 value, to interpolate to the final value of the transition's + * interval. + */ + +#include "gtd-keyframe-transition.h" + +#include "gtd-debug.h" +#include "gtd-easing.h" +#include "gtd-interval.h" +#include "gtd-timeline.h" + +#include +#include + +typedef struct _KeyFrame +{ + double key; + + double start; + double end; + + GtdEaseMode mode; + + GtdInterval *interval; +} KeyFrame; + +typedef struct +{ + GArray *frames; + + gint current_frame; +} GtdKeyframeTransitionPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdKeyframeTransition, gtd_keyframe_transition, GTD_TYPE_PROPERTY_TRANSITION) + +static void +key_frame_free (gpointer data) +{ + if (data != NULL) + { + KeyFrame *key = data; + + g_object_unref (key->interval); + } +} + +static int +sort_by_key (gconstpointer a, + gconstpointer b) +{ + const KeyFrame *k_a = a; + const KeyFrame *k_b = b; + + if (fabs (k_a->key - k_b->key) < 0.0001) + return 0; + + if (k_a->key > k_b->key) + return 1; + + return -1; +} + +static inline void +gtd_keyframe_transition_sort_frames (GtdKeyframeTransition *self) +{ + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames != NULL) + g_array_sort (priv->frames, sort_by_key); +} + +static inline void +gtd_keyframe_transition_init_frames (GtdKeyframeTransition *self, + gssize n_key_frames) +{ + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + guint i; + + priv->frames = g_array_sized_new (FALSE, FALSE, + sizeof (KeyFrame), + n_key_frames); + g_array_set_clear_func (priv->frames, key_frame_free); + + /* we add an implicit key frame that goes to 1.0, so that the + * user doesn't have to do that an can simply add key frames + * in between 0.0 and 1.0 + */ + for (i = 0; i < n_key_frames + 1; i++) + { + KeyFrame frame; + + if (i == n_key_frames) + frame.key = 1.0; + else + frame.key = 0.0; + + frame.mode = GTD_EASE_LINEAR; + frame.interval = NULL; + + g_array_insert_val (priv->frames, i, frame); + } +} + +static inline void +gtd_keyframe_transition_update_frames (GtdKeyframeTransition *self) +{ + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + guint i; + + if (priv->frames == NULL) + return; + + for (i = 0; i < priv->frames->len; i++) + { + KeyFrame *cur_frame = &g_array_index (priv->frames, KeyFrame, i); + KeyFrame *prev_frame; + + if (i > 0) + prev_frame = &g_array_index (priv->frames, KeyFrame, i - 1); + else + prev_frame = NULL; + + if (prev_frame != NULL) + { + cur_frame->start = prev_frame->key; + + if (prev_frame->interval != NULL) + { + const GValue *value; + + value = gtd_interval_peek_final_value (prev_frame->interval); + + if (cur_frame->interval != NULL) + gtd_interval_set_initial_value (cur_frame->interval, value); + else + { + cur_frame->interval = + gtd_interval_new_with_values (G_VALUE_TYPE (value), value, NULL); + } + } + } + else + cur_frame->start = 0.0; + + cur_frame->end = cur_frame->key; + } +} + +static void +gtd_keyframe_transition_compute_value (GtdTransition *transition, + GtdAnimatable *animatable, + GtdInterval *interval, + gdouble progress) +{ + GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (transition); + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + GtdTimeline *timeline = GTD_TIMELINE (self); + GtdTransitionClass *parent_class; + GtdTimelineDirection direction; + GtdInterval *real_interval; + gdouble real_progress; + double t, d, p; + KeyFrame *cur_frame = NULL; + + real_interval = interval; + real_progress = progress; + + /* if we don't have any keyframe, we behave like our parent class */ + if (priv->frames == NULL) + goto out; + + direction = gtd_timeline_get_direction (timeline); + + /* we need a normalized linear value */ + t = gtd_timeline_get_elapsed_time (timeline); + d = gtd_timeline_get_duration (timeline); + p = t / d; + + if (priv->current_frame < 0) + { + if (direction == GTD_TIMELINE_FORWARD) + priv->current_frame = 0; + else + priv->current_frame = priv->frames->len - 1; + } + + cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame); + + /* skip to the next key frame, depending on the direction of the timeline */ + if (direction == GTD_TIMELINE_FORWARD) + { + if (p > cur_frame->end) + { + priv->current_frame = MIN (priv->current_frame + 1, + (gint) priv->frames->len - 1); + cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame); + } + } + else + { + if (p < cur_frame->start) + { + priv->current_frame = MAX (priv->current_frame - 1, 0); + + cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame); + } + } + + /* if we are at the boundaries of the transition, use the from and to + * value from the transition + */ + if (priv->current_frame == 0) + { + const GValue *value; + + value = gtd_interval_peek_initial_value (interval); + gtd_interval_set_initial_value (cur_frame->interval, value); + } + else if (priv->current_frame == (gint) priv->frames->len - 1) + { + const GValue *value; + + cur_frame->mode = gtd_timeline_get_progress_mode (timeline); + + value = gtd_interval_peek_final_value (interval); + gtd_interval_set_final_value (cur_frame->interval, value); + } + + /* update the interval to be used to interpolate the property */ + real_interval = cur_frame->interval; + + /* normalize the progress and apply the easing mode */ + real_progress = gtd_easing_for_mode (cur_frame->mode, + (p - cur_frame->start), + (cur_frame->end - cur_frame->start)); + +#ifdef GTD_ENABLE_DEBUG + if (GTD_HAS_DEBUG (ANIMATION)) + { + char *from, *to; + const GValue *value; + + value = gtd_interval_peek_initial_value (cur_frame->interval); + from = g_strdup_value_contents (value); + + value = gtd_interval_peek_final_value (cur_frame->interval); + to = g_strdup_value_contents (value); + + GTD_TRACE_MSG ("[animation] cur_frame [%d] => { %g, %s, %s %s %s } - " + "progress: %g, sub-progress: %g\n", + priv->current_frame, + cur_frame->key, + gtd_get_easing_name_for_mode (cur_frame->mode), + from, + direction == GTD_TIMELINE_FORWARD ? "->" : "<-", + to, + p, + real_progress); + + g_free (from); + g_free (to); + } +#endif /* GTD_ENABLE_DEBUG */ + +out: + parent_class = + GTD_TRANSITION_CLASS (gtd_keyframe_transition_parent_class); + parent_class->compute_value (transition, animatable, real_interval, real_progress); +} + +static void +gtd_keyframe_transition_started (GtdTimeline *timeline) +{ + GtdKeyframeTransition *self; + GtdKeyframeTransitionPrivate *priv; + + self = GTD_KEYFRAME_TRANSITION (timeline); + priv = gtd_keyframe_transition_get_instance_private (self); + + priv->current_frame = -1; + + gtd_keyframe_transition_sort_frames (self); + gtd_keyframe_transition_update_frames (self); +} + +static void +gtd_keyframe_transition_completed (GtdTimeline *timeline) +{ + GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (timeline); + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + + priv->current_frame = -1; +} + +static void +gtd_keyframe_transition_finalize (GObject *gobject) +{ + GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (gobject); + GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames != NULL) + g_array_unref (priv->frames); + + G_OBJECT_CLASS (gtd_keyframe_transition_parent_class)->finalize (gobject); +} + +static void +gtd_keyframe_transition_class_init (GtdKeyframeTransitionClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass); + GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass); + + gobject_class->finalize = gtd_keyframe_transition_finalize; + + timeline_class->started = gtd_keyframe_transition_started; + timeline_class->completed = gtd_keyframe_transition_completed; + + transition_class->compute_value = gtd_keyframe_transition_compute_value; +} + +static void +gtd_keyframe_transition_init (GtdKeyframeTransition *self) +{ +} + +/** + * gtd_keyframe_transition_new: + * @property_name: the property to animate + * + * Creates a new #GtdKeyframeTransition for @property_name. + * + * Return value: (transfer full): the newly allocated + * #GtdKeyframeTransition instance. Use g_object_unref() when + * done to free its resources. + * + * Since: 1.12 + */ +GtdTransition * +gtd_keyframe_transition_new (const gchar *property_name) +{ + return g_object_new (GTD_TYPE_KEYFRAME_TRANSITION, + "property-name", property_name, + NULL); +} + +/** + * gtd_keyframe_transition_set_key_frames: + * @transition: a #GtdKeyframeTransition + * @n_key_frames: the number of values + * @key_frames: (array length=n_key_frames): an array of keys between 0.0 + * and 1.0, one for each key frame + * + * Sets the keys for each key frame inside @transition. + * + * If @transition does not hold any key frame, @n_key_frames key frames + * will be created; if @transition already has key frames, @key_frames must + * have at least as many elements as the number of key frames. + * + * Since: 1.12 + */ +void +gtd_keyframe_transition_set_key_frames (GtdKeyframeTransition *self, + guint n_key_frames, + const gdouble *key_frames) +{ + GtdKeyframeTransitionPrivate *priv; + guint i; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + g_return_if_fail (n_key_frames > 0); + g_return_if_fail (key_frames != NULL); + + priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames == NULL) + gtd_keyframe_transition_init_frames (self, n_key_frames); + else + g_return_if_fail (n_key_frames == priv->frames->len - 1); + + for (i = 0; i < n_key_frames; i++) + { + KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i); + + frame->key = key_frames[i]; + } +} + +/** + * gtd_keyframe_transition_set_values: + * @transition: a #GtdKeyframeTransition + * @n_values: the number of values + * @values: (array length=n_values): an array of values, one for each + * key frame + * + * Sets the values for each key frame inside @transition. + * + * If @transition does not hold any key frame, @n_values key frames will + * be created; if @transition already has key frames, @values must have + * at least as many elements as the number of key frames. + * + * Since: 1.12 + */ +void +gtd_keyframe_transition_set_values (GtdKeyframeTransition *self, + guint n_values, + const GValue *values) +{ + GtdKeyframeTransitionPrivate *priv; + guint i; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + g_return_if_fail (n_values > 0); + g_return_if_fail (values != NULL); + + priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames == NULL) + gtd_keyframe_transition_init_frames (self, n_values); + else + g_return_if_fail (n_values == priv->frames->len - 1); + + for (i = 0; i < n_values; i++) + { + KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i); + + if (frame->interval) + gtd_interval_set_final_value (frame->interval, &values[i]); + else + frame->interval = + gtd_interval_new_with_values (G_VALUE_TYPE (&values[i]), NULL, + &values[i]); + } +} + +/** + * gtd_keyframe_transition_set_modes: + * @transition: a #GtdKeyframeTransition + * @n_modes: the number of easing modes + * @modes: (array length=n_modes): an array of easing modes, one for + * each key frame + * + * Sets the easing modes for each key frame inside @transition. + * + * If @transition does not hold any key frame, @n_modes key frames will + * be created; if @transition already has key frames, @modes must have + * at least as many elements as the number of key frames. + */ +void +gtd_keyframe_transition_set_modes (GtdKeyframeTransition *self, + guint n_modes, + const GtdEaseMode *modes) +{ + GtdKeyframeTransitionPrivate *priv; + guint i; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + g_return_if_fail (n_modes > 0); + g_return_if_fail (modes != NULL); + + priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames == NULL) + gtd_keyframe_transition_init_frames (self, n_modes); + else + g_return_if_fail (n_modes == priv->frames->len - 1); + + for (i = 0; i < n_modes; i++) + { + KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i); + + frame->mode = modes[i]; + } +} + +/** + * gtd_keyframe_transition_set: (skip) + * @transition: a #GtdKeyframeTransition + * @gtype: the type of the values to use for the key frames + * @n_key_frames: the number of key frames between the initial + * and final values + * @...: a list of tuples, containing the key frame index, the value + * at the key frame, and the animation mode + * + * Sets the key frames of the @transition. + * + * This variadic arguments function is a convenience for C developers; + * language bindings should use gtd_keyframe_transition_set_key_frames(), + * gtd_keyframe_transition_set_modes(), and + * gtd_keyframe_transition_set_values() instead. + */ +void +gtd_keyframe_transition_set (GtdKeyframeTransition *self, + GType gtype, + guint n_key_frames, + ...) +{ + GtdKeyframeTransitionPrivate *priv; + va_list args; + guint i; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + g_return_if_fail (gtype != G_TYPE_INVALID); + g_return_if_fail (n_key_frames > 0); + + priv = gtd_keyframe_transition_get_instance_private (self); + + if (priv->frames == NULL) + gtd_keyframe_transition_init_frames (self, n_key_frames); + else + g_return_if_fail (n_key_frames == priv->frames->len - 1); + + va_start (args, n_key_frames); + + for (i = 0; i < n_key_frames; i++) + { + KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i); + GValue value = G_VALUE_INIT; + char *error = NULL; + + frame->key = va_arg (args, gdouble); + + G_VALUE_COLLECT_INIT (&value, gtype, args, 0, &error); + if (error != NULL) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + break; + } + + frame->mode = va_arg (args, GtdEaseMode); + + g_clear_object (&frame->interval); + frame->interval = gtd_interval_new_with_values (gtype, NULL, &value); + + g_value_unset (&value); + } + + va_end (args); +} + +/** + * gtd_keyframe_transition_clear: + * @transition: a #GtdKeyframeTransition + * + * Removes all key frames from @transition. + */ +void +gtd_keyframe_transition_clear (GtdKeyframeTransition *self) +{ + GtdKeyframeTransitionPrivate *priv; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + + priv = gtd_keyframe_transition_get_instance_private (self); + if (priv->frames != NULL) + { + g_array_unref (priv->frames); + priv->frames = NULL; + } +} + +/** + * gtd_keyframe_transition_get_n_key_frames: + * @transition: a #GtdKeyframeTransition + * + * Retrieves the number of key frames inside @transition. + * + * Return value: the number of key frames + */ +guint +gtd_keyframe_transition_get_n_key_frames (GtdKeyframeTransition *self) +{ + GtdKeyframeTransitionPrivate *priv; + + g_return_val_if_fail (GTD_IS_KEYFRAME_TRANSITION (self), 0); + + priv = gtd_keyframe_transition_get_instance_private (self); + if (priv->frames == NULL) + return 0; + + return priv->frames->len - 1; +} + +/** + * gtd_keyframe_transition_set_key_frame: + * @transition: a #GtdKeyframeTransition + * @index_: the index of the key frame + * @key: the key of the key frame + * @mode: the easing mode of the key frame + * @value: a #GValue containing the value of the key frame + * + * Sets the details of the key frame at @index_ inside @transition. + * + * The @transition must already have a key frame at @index_, and @index_ + * must be smaller than the number of key frames inside @transition. + */ +void +gtd_keyframe_transition_set_key_frame (GtdKeyframeTransition *self, + guint index_, + double key, + GtdEaseMode mode, + const GValue *value) +{ + GtdKeyframeTransitionPrivate *priv; + KeyFrame *frame; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + + priv = gtd_keyframe_transition_get_instance_private (self); + + g_return_if_fail (priv->frames != NULL); + g_return_if_fail (index_ < priv->frames->len - 1); + + frame = &g_array_index (priv->frames, KeyFrame, index_); + frame->key = key; + frame->mode = mode; + gtd_interval_set_final_value (frame->interval, value); +} + +/** + * gtd_keyframe_transition_get_key_frame: + * @transition: a #GtdKeyframeTransition + * @index_: the index of the key frame + * @key: (out) (allow-none): return location for the key, or %NULL + * @mode: (out) (allow-none): return location for the easing mode, or %NULL + * @value: (out caller-allocates): a #GValue initialized with the type of + * the values + * + * Retrieves the details of the key frame at @index_ inside @transition. + * + * The @transition must already have key frames set, and @index_ must be + * smaller than the number of key frames. + */ +void +gtd_keyframe_transition_get_key_frame (GtdKeyframeTransition *self, + guint index_, + double *key, + GtdEaseMode *mode, + GValue *value) +{ + GtdKeyframeTransitionPrivate *priv; + const KeyFrame *frame; + + g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self)); + + priv = gtd_keyframe_transition_get_instance_private (self); + g_return_if_fail (priv->frames != NULL); + g_return_if_fail (index_ < priv->frames->len - 1); + + frame = &g_array_index (priv->frames, KeyFrame, index_); + + if (key != NULL) + *key = frame->key; + + if (mode != NULL) + *mode = frame->mode; + + if (value != NULL) + gtd_interval_get_final_value (frame->interval, value); +} diff --git a/src/animation/gtd-keyframe-transition.h b/src/animation/gtd-keyframe-transition.h new file mode 100644 index 0000000..d3d5024 --- /dev/null +++ b/src/animation/gtd-keyframe-transition.h @@ -0,0 +1,83 @@ +/* gtd-keyframe-transition.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-property-transition.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_KEYFRAME_TRANSITION (gtd_keyframe_transition_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdKeyframeTransition, gtd_keyframe_transition, GTD, KEYFRAME_TRANSITION, GtdPropertyTransition) + +/** + * GtdKeyframeTransitionClass: + * + * The `GtdKeyframeTransitionClass` structure contains only + * private data. + * + * Since: 1.12 + */ +struct _GtdKeyframeTransitionClass +{ + /*< private >*/ + GtdPropertyTransitionClass parent_class; + + gpointer _padding[8]; +}; + + +GtdTransition* gtd_keyframe_transition_new (const gchar *property_name); + + +void gtd_keyframe_transition_set_key_frames (GtdKeyframeTransition *transition, + guint n_key_frames, + const gdouble *key_frames); + +void gtd_keyframe_transition_set_values (GtdKeyframeTransition *transition, + guint n_values, + const GValue *values); + +void gtd_keyframe_transition_set_modes (GtdKeyframeTransition *transition, + guint n_modes, + const GtdEaseMode *modes); + +void gtd_keyframe_transition_set (GtdKeyframeTransition *transition, + GType gtype, + guint n_key_frames, + ...); + +void gtd_keyframe_transition_set_key_frame (GtdKeyframeTransition *transition, + guint index_, + double key, + GtdEaseMode mode, + const GValue *value); + +void gtd_keyframe_transition_get_key_frame (GtdKeyframeTransition *transition, + guint index_, + double *key, + GtdEaseMode *mode, + GValue *value); + +guint gtd_keyframe_transition_get_n_key_frames (GtdKeyframeTransition *transition); + +void gtd_keyframe_transition_clear (GtdKeyframeTransition *transition); + +G_END_DECLS diff --git a/src/animation/gtd-property-transition.c b/src/animation/gtd-property-transition.c new file mode 100644 index 0000000..f0ceb88 --- /dev/null +++ b/src/animation/gtd-property-transition.c @@ -0,0 +1,359 @@ +/* gtd-property-transition.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +/** + * SECTION:gtd-property-transition + * @Title: GtdPropertyTransition + * @Short_Description: Property transitions + * + * #GtdPropertyTransition is a specialized #GtdTransition that + * can be used to tween a property of a #GtdAnimatable instance. + */ + +#include "gtd-property-transition.h" + +#include "gtd-animatable.h" +#include "gtd-debug.h" +#include "gtd-interval.h" +#include "gtd-transition.h" + +typedef struct +{ + gchar *property_name; + + GParamSpec *pspec; +} GtdPropertyTransitionPrivate; + +enum +{ + PROP_0, + PROP_PROPERTY_NAME, + PROP_LAST, +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdPropertyTransition, gtd_property_transition, GTD_TYPE_TRANSITION) + +static inline void +gtd_property_transition_ensure_interval (GtdPropertyTransition *self, + GtdAnimatable *animatable, + GtdInterval *interval) +{ + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + GValue *value_p; + + if (gtd_interval_is_valid (interval)) + return; + + /* if no initial value has been set, use the current value */ + value_p = gtd_interval_peek_initial_value (interval); + if (!G_IS_VALUE (value_p)) + { + g_value_init (value_p, gtd_interval_get_value_type (interval)); + gtd_animatable_get_initial_state (animatable, + priv->property_name, + value_p); + } + + /* if no final value has been set, use the current value */ + value_p = gtd_interval_peek_final_value (interval); + if (!G_IS_VALUE (value_p)) + { + g_value_init (value_p, gtd_interval_get_value_type (interval)); + gtd_animatable_get_initial_state (animatable, + priv->property_name, + value_p); + } +} + +static void +gtd_property_transition_attached (GtdTransition *transition, + GtdAnimatable *animatable) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition); + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + GtdInterval *interval; + + if (priv->property_name == NULL) + return; + + priv->pspec = + gtd_animatable_find_property (animatable, priv->property_name); + + if (priv->pspec == NULL) + return; + + interval = gtd_transition_get_interval (transition); + if (interval == NULL) + return; + + gtd_property_transition_ensure_interval (self, animatable, interval); +} + +static void +gtd_property_transition_detached (GtdTransition *transition, + GtdAnimatable *animatable) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition); + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + + priv->pspec = NULL; +} + +static void +gtd_property_transition_compute_value (GtdTransition *transition, + GtdAnimatable *animatable, + GtdInterval *interval, + gdouble progress) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition); + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + GValue value = G_VALUE_INIT; + GType p_type, i_type; + gboolean res; + + /* if we have a GParamSpec we also have an animatable instance */ + if (priv->pspec == NULL) + return; + + gtd_property_transition_ensure_interval (self, animatable, interval); + + p_type = G_PARAM_SPEC_VALUE_TYPE (priv->pspec); + i_type = gtd_interval_get_value_type (interval); + + g_value_init (&value, i_type); + + res = gtd_animatable_interpolate_value (animatable, + priv->property_name, + interval, + progress, + &value); + + if (res) + { + if (i_type != p_type || g_type_is_a (i_type, p_type)) + { + if (g_value_type_transformable (i_type, p_type)) + { + GValue transform = G_VALUE_INIT; + + g_value_init (&transform, p_type); + + if (g_value_transform (&value, &transform)) + { + gtd_animatable_set_final_state (animatable, + priv->property_name, + &transform); + } + else + g_warning ("%s: Unable to convert a value of type '%s' from " + "the value type '%s' of the interval.", + G_STRLOC, + g_type_name (p_type), + g_type_name (i_type)); + + g_value_unset (&transform); + } + } + else + gtd_animatable_set_final_state (animatable, + priv->property_name, + &value); + } + + g_value_unset (&value); +} + +static void +gtd_property_transition_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject); + + switch (prop_id) + { + case PROP_PROPERTY_NAME: + gtd_property_transition_set_property_name (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +gtd_property_transition_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject); + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + + switch (prop_id) + { + case PROP_PROPERTY_NAME: + g_value_set_string (value, priv->property_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +gtd_property_transition_finalize (GObject *gobject) +{ + GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject); + GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self); + + g_free (priv->property_name); + + G_OBJECT_CLASS (gtd_property_transition_parent_class)->finalize (gobject); +} + +static void +gtd_property_transition_class_init (GtdPropertyTransitionClass *klass) +{ + GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + transition_class->attached = gtd_property_transition_attached; + transition_class->detached = gtd_property_transition_detached; + transition_class->compute_value = gtd_property_transition_compute_value; + + gobject_class->set_property = gtd_property_transition_set_property; + gobject_class->get_property = gtd_property_transition_get_property; + gobject_class->finalize = gtd_property_transition_finalize; + + /** + * GtdPropertyTransition:property-name: + * + * The name of the property of a #GtdAnimatable to animate. + */ + obj_props[PROP_PROPERTY_NAME] = + g_param_spec_string ("property-name", + "Property Name", + "The name of the property to animate", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} + +static void +gtd_property_transition_init (GtdPropertyTransition *self) +{ +} + +/** + * gtd_property_transition_new_for_actor: + * @actor: a #GtdActor + * @property_name: (allow-none): a property of @animatable, or %NULL + * + * Creates a new #GtdPropertyTransition. + * + * Return value: (transfer full): the newly created #GtdPropertyTransition. + * Use g_object_unref() when done + */ +GtdTransition * +gtd_property_transition_new_for_actor (GtdWidget *widget, + const char *property_name) +{ + return g_object_new (GTD_TYPE_PROPERTY_TRANSITION, + "widget", widget, + "property-name", property_name, + NULL); +} + +/** + * gtd_property_transition_new: + * @property_name: (allow-none): a property of @animatable, or %NULL + * + * Creates a new #GtdPropertyTransition. + * + * Return value: (transfer full): the newly created #GtdPropertyTransition. + * Use g_object_unref() when done + */ +GtdTransition * +gtd_property_transition_new (const char *property_name) +{ + return g_object_new (GTD_TYPE_PROPERTY_TRANSITION, + "property-name", property_name, + NULL); +} + +/** + * gtd_property_transition_set_property_name: + * @transition: a #GtdPropertyTransition + * @property_name: (allow-none): a property name + * + * Sets the #GtdPropertyTransition:property-name property of @transition. + */ +void +gtd_property_transition_set_property_name (GtdPropertyTransition *self, + const gchar *property_name) +{ + GtdPropertyTransitionPrivate *priv; + GtdAnimatable *animatable; + + g_return_if_fail (GTD_IS_PROPERTY_TRANSITION (self)); + + priv = gtd_property_transition_get_instance_private (self); + + if (g_strcmp0 (priv->property_name, property_name) == 0) + return; + + g_free (priv->property_name); + priv->property_name = g_strdup (property_name); + priv->pspec = NULL; + + animatable = gtd_transition_get_animatable (GTD_TRANSITION (self)); + if (animatable) + priv->pspec = gtd_animatable_find_property (animatable, priv->property_name); + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROPERTY_NAME]); +} + +/** + * gtd_property_transition_get_property_name: + * @transition: a #GtdPropertyTransition + * + * Retrieves the value of the #GtdPropertyTransition:property-name + * property. + * + * Return value: the name of the property being animated, or %NULL if + * none is set. The returned string is owned by the @transition and + * it should not be freed. + */ +const char * +gtd_property_transition_get_property_name (GtdPropertyTransition *self) +{ + GtdPropertyTransitionPrivate *priv; + + g_return_val_if_fail (GTD_IS_PROPERTY_TRANSITION (self), NULL); + + priv = gtd_property_transition_get_instance_private (self); + return priv->property_name; +} diff --git a/src/animation/gtd-property-transition.h b/src/animation/gtd-property-transition.h new file mode 100644 index 0000000..3b38dda --- /dev/null +++ b/src/animation/gtd-property-transition.h @@ -0,0 +1,55 @@ +/* gtd-property-transition.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-transition.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PROPERTY_TRANSITION (gtd_property_transition_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdPropertyTransition, gtd_property_transition, GTD, PROPERTY_TRANSITION, GtdTransition) + +/** + * GtdPropertyTransitionClass: + * + * The #GtdPropertyTransitionClass structure + * contains private data. + */ +struct _GtdPropertyTransitionClass +{ + /*< private >*/ + GtdTransitionClass parent_class; + + gpointer _padding[8]; +}; + +GtdTransition * gtd_property_transition_new_for_widget (GtdWidget *widget, + const char *property_name); + +GtdTransition * gtd_property_transition_new (const char *property_name); + +void gtd_property_transition_set_property_name (GtdPropertyTransition *transition, + const char *property_name); + +const char * gtd_property_transition_get_property_name (GtdPropertyTransition *transition); + +G_END_DECLS diff --git a/src/animation/gtd-timeline.c b/src/animation/gtd-timeline.c new file mode 100644 index 0000000..da16fd9 --- /dev/null +++ b/src/animation/gtd-timeline.c @@ -0,0 +1,1547 @@ +/* + * Gtd. + * + * An OpenGL based 'interactive canvas' library. + * + * Authored By Matthew Allum + * + * Copyright (C) 2006 OpenedHand + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/** + * SECTION:gtd-timeline + * @short_description: A class for time-based events + * + * #GtdTimeline is a base class for managing time-based event that cause + * GTK to redraw, such as animations. + * + * Each #GtdTimeline instance has a duration: once a timeline has been + * started, using gtd_timeline_start(), it will emit a signal that can + * be used to update the state of the widgets. + * + * It is important to note that #GtdTimeline is not a generic API for + * calling closures after an interval; each Timeline is tied into the master + * clock used to drive the frame cycle. If you need to schedule a closure + * after an interval, see gtd_threads_add_timeout() instead. + * + * Users of #GtdTimeline should connect to the #GtdTimeline::new-frame + * signal, which is emitted each time a timeline is advanced during the maste + * clock iteration. The #GtdTimeline::new-frame signal provides the time + * elapsed since the beginning of the timeline, in milliseconds. A normalized + * progress value can be obtained by calling gtd_timeline_get_progress(). + * By using gtd_timeline_get_delta() it is possible to obtain the wallclock + * time elapsed since the last emission of the #GtdTimeline::new-frame + * signal. + * + * Initial state can be set up by using the #GtdTimeline::started signal, + * while final state can be set up by using the #GtdTimeline::stopped + * signal. The #GtdTimeline guarantees the emission of at least a single + * #GtdTimeline::new-frame signal, as well as the emission of the + * #GtdTimeline::completed signal every time the #GtdTimeline reaches + * its #GtdTimeline:duration. + * + * It is possible to connect to specific points in the timeline progress by + * adding markers using gtd_timeline_add_marker_at_time() and connecting + * to the #GtdTimeline::marker-reached signal. + * + * Timelines can be made to loop once they reach the end of their duration, by + * using gtd_timeline_set_repeat_count(); a looping timeline will still + * emit the #GtdTimeline::completed signal once it reaches the end of its + * duration at each repeat. If you want to be notified of the end of the last + * repeat, use the #GtdTimeline::stopped signal. + * + * Timelines have a #GtdTimeline:direction: the default direction is + * %GTD_TIMELINE_FORWARD, and goes from 0 to the duration; it is possible + * to change the direction to %GTD_TIMELINE_BACKWARD, and have the timeline + * go from the duration to 0. The direction can be automatically reversed + * when reaching completion by using the #GtdTimeline:auto-reverse property. + * + * Timelines are used in the Gtd animation framework by classes like + * #GtdTransition. + */ + +#define G_LOG_DOMAIN "GtdTimeline" + +#include "gtd-timeline.h" + +#include "gtd-debug.h" +#include "endeavour.h" + +typedef struct +{ + GtdTimelineDirection direction; + + GtdWidget *widget; + guint frame_tick_id; + + gint64 duration_us; + gint64 delay_us; + + /* The current amount of elapsed time */ + gint64 elapsed_time_us; + + /* The elapsed time since the last frame was fired */ + gint64 delta_us; + + /* Time we last advanced the elapsed time and showed a frame */ + gint64 last_frame_time_us; + + gint64 start_us; + + /* How many times the timeline should repeat */ + gint repeat_count; + + /* The number of times the timeline has repeated */ + gint current_repeat; + + GtdTimelineProgressFunc progress_func; + gpointer progress_data; + GDestroyNotify progress_notify; + GtdEaseMode progress_mode; + + guint is_playing : 1; + + /* If we've just started playing and haven't yet gotten + * a tick from the master clock + */ + guint auto_reverse : 1; +} GtdTimelinePrivate; + +enum +{ + PROP_0, + + PROP_AUTO_REVERSE, + PROP_DELAY, + PROP_DURATION, + PROP_DIRECTION, + PROP_REPEAT_COUNT, + PROP_PROGRESS_MODE, + PROP_WIDGET, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +enum +{ + NEW_FRAME, + STARTED, + PAUSED, + COMPLETED, + STOPPED, + + LAST_SIGNAL +}; + +static guint timeline_signals[LAST_SIGNAL] = { 0, }; + +static void set_is_playing (GtdTimeline *self, + gboolean is_playing); + +G_DEFINE_TYPE_WITH_PRIVATE (GtdTimeline, gtd_timeline, G_TYPE_OBJECT) + +static inline gint64 +us_to_ms (gint64 ms) +{ + return ms / 1000; +} + +static inline gint64 +ms_to_us (gint64 us) +{ + return us * 1000; +} + +static inline gboolean +is_waiting_for_delay (GtdTimeline *self, + gint64 frame_time_us) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + return priv->start_us + priv->delay_us > frame_time_us; +} + +static void +emit_frame_signal (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + gint64 elapsed_time_ms = us_to_ms (priv->elapsed_time_us); + + GTD_TRACE_MSG ("Emitting ::new-frame signal on timeline[%p]", self); + + g_signal_emit (self, timeline_signals[NEW_FRAME], 0, elapsed_time_ms); +} + +static gboolean +is_complete (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + return (priv->direction == GTD_TIMELINE_FORWARD + ? priv->elapsed_time_us >= priv->duration_us + : priv->elapsed_time_us <= 0); +} + +static gboolean +maybe_loop_timeline (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + GtdTimelineDirection saved_direction = priv->direction; + gint64 overflow_us = priv->elapsed_time_us; + gint64 end_us; + + /* Update the current elapsed time in case the signal handlers + * want to take a peek. If we clamp elapsed time, then we need + * to correpondingly reduce elapsed_time_delta to reflect the correct + * range of times */ + if (priv->direction == GTD_TIMELINE_FORWARD) + priv->elapsed_time_us = priv->duration_us; + else if (priv->direction == GTD_TIMELINE_BACKWARD) + priv->elapsed_time_us = 0; + + end_us = priv->elapsed_time_us; + + /* Emit the signal */ + emit_frame_signal (self); + + /* Did the signal handler modify the elapsed time? */ + if (priv->elapsed_time_us != end_us) + return TRUE; + + /* Note: If the new-frame signal handler paused the timeline + * on the last frame we will still go ahead and send the + * completed signal */ + GTD_TRACE_MSG ("Timeline [%p] completed (cur: %ldµs, tot: %ldµs)", + self, + priv->elapsed_time_us, + priv->delta_us); + + if (priv->is_playing && + (priv->repeat_count == 0 || + priv->repeat_count == priv->current_repeat)) + { + /* We stop the timeline now, so that the completed signal handler + * may choose to re-start the timeline + */ + set_is_playing (self, FALSE); + + g_signal_emit (self, timeline_signals[COMPLETED], 0); + g_signal_emit (self, timeline_signals[STOPPED], 0, TRUE); + } + else + { + g_signal_emit (self, timeline_signals[COMPLETED], 0); + } + + priv->current_repeat += 1; + + if (priv->auto_reverse) + { + /* :auto-reverse changes the direction of the timeline */ + if (priv->direction == GTD_TIMELINE_FORWARD) + priv->direction = GTD_TIMELINE_BACKWARD; + else + priv->direction = GTD_TIMELINE_FORWARD; + + g_object_notify_by_pspec (G_OBJECT (self), + obj_props[PROP_DIRECTION]); + } + + /* + * Again check to see if the user has manually played with + * the elapsed time, before we finally stop or loop the timeline, + * except changing time from 0 -> duration (or vice-versa) + * since these are considered equivalent + */ + if (priv->elapsed_time_us != end_us && + !((priv->elapsed_time_us == 0 && end_us == priv->duration_us) || + (priv->elapsed_time_us == priv->duration_us && end_us == 0))) + { + return TRUE; + } + + if (priv->repeat_count == 0) + { + gtd_timeline_rewind (self); + return FALSE; + } + + /* Try and interpolate smoothly around a loop */ + if (saved_direction == GTD_TIMELINE_FORWARD) + priv->elapsed_time_us = overflow_us - priv->duration_us; + else + priv->elapsed_time_us = priv->duration_us + overflow_us; + + /* Or if the direction changed, we try and bounce */ + if (priv->direction != saved_direction) + priv->elapsed_time_us = priv->duration_us - priv->elapsed_time_us; + + return TRUE; +} + +static gboolean +tick_timeline (GtdTimeline *self, + gint64 tick_time_us) +{ + GtdTimelinePrivate *priv; + gboolean should_continue; + gboolean complete; + gint64 elapsed_us; + + priv = gtd_timeline_get_instance_private (self); + + GTD_TRACE_MSG ("Timeline [%p] ticked (elapsed_time: %ldµs, delta_us: %ldµs, " + "last_frame_time: %ldµs, tick_time: %ldµs)", + self, + priv->elapsed_time_us, + priv->delta_us, + priv->last_frame_time_us, + tick_time_us); + + /* Check the is_playing variable before performing the timeline tick. + * This is necessary, as if a timeline is stopped in response to a + * frame clock generated signal of a different timeline, this code can + * still be reached. + */ + if (!priv->is_playing) + return FALSE; + + elapsed_us = tick_time_us - priv->last_frame_time_us; + priv->last_frame_time_us = tick_time_us; + + if (is_waiting_for_delay (self, tick_time_us)) + { + GTD_TRACE_MSG ("- waiting for delay"); + return G_SOURCE_CONTINUE; + } + + /* if the clock rolled back between ticks we need to + * account for it; the best course of action, since the + * clock roll back can happen by any arbitrary amount + * of milliseconds, is to drop a frame here + */ + if (elapsed_us <= 0) + return TRUE; + + priv->delta_us = elapsed_us; + + GTD_TRACE_MSG ("Timeline [%p] activated (elapsed time: %ldµs, " + "duration: %ldµs, delta_us: %ldµs)", + self, + priv->elapsed_time_us, + priv->duration_us, + priv->delta_us); + + g_object_ref (self); + + /* Advance time */ + if (priv->direction == GTD_TIMELINE_FORWARD) + priv->elapsed_time_us += priv->delta_us; + else + priv->elapsed_time_us -= priv->delta_us; + + complete = is_complete (self); + should_continue = !complete ? priv->is_playing : maybe_loop_timeline (self); + + /* If we have not reached the end of the timeline */ + if (!complete) + emit_frame_signal (self); + + g_object_unref (self); + + return should_continue; +} + +static gboolean +frame_tick_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + GtdTimeline *self = GTD_TIMELINE (user_data); + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + gint64 frame_time_us; + + frame_time_us = gdk_frame_clock_get_frame_time (frame_clock); + + if (tick_timeline (self, frame_time_us)) + return G_SOURCE_CONTINUE; + + priv->frame_tick_id = 0; + return G_SOURCE_REMOVE; +} + +static void +add_tick_callback (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + g_assert (!(priv->frame_tick_id > 0 && !priv->widget)); + + if (priv->frame_tick_id == 0) + { + priv->frame_tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (priv->widget), + frame_tick_cb, + self, + NULL); + } +} + +static void +remove_tick_callback (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + g_assert (!(priv->frame_tick_id > 0 && !priv->widget)); + + if (priv->frame_tick_id > 0) + { + gtk_widget_remove_tick_callback (GTK_WIDGET (priv->widget), priv->frame_tick_id); + priv->frame_tick_id = 0; + } +} + +static void +set_is_playing (GtdTimeline *self, + gboolean is_playing) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + is_playing = !!is_playing; + + if (is_playing == priv->is_playing) + return; + + priv->is_playing = is_playing; + + if (priv->is_playing) + { + priv->start_us = g_get_monotonic_time (); + priv->last_frame_time_us = priv->start_us; + priv->current_repeat = 0; + + add_tick_callback (self); + } + else + { + remove_tick_callback (self); + } +} + +static gdouble +timeline_progress_func (GtdTimeline *self, + gdouble elapsed, + gdouble duration, + gpointer user_data) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + return gtd_easing_for_mode (priv->progress_mode, elapsed, duration); +} + + +/* + * GObject overrides + */ + +static void +gtd_timeline_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTimeline *self = GTD_TIMELINE (object); + + switch (prop_id) + { + case PROP_DELAY: + gtd_timeline_set_delay (self, g_value_get_uint (value)); + break; + + case PROP_DURATION: + gtd_timeline_set_duration (self, g_value_get_uint (value)); + break; + + case PROP_DIRECTION: + gtd_timeline_set_direction (self, g_value_get_enum (value)); + break; + + case PROP_AUTO_REVERSE: + gtd_timeline_set_auto_reverse (self, g_value_get_boolean (value)); + break; + + case PROP_REPEAT_COUNT: + gtd_timeline_set_repeat_count (self, g_value_get_int (value)); + break; + + case PROP_PROGRESS_MODE: + gtd_timeline_set_progress_mode (self, g_value_get_enum (value)); + break; + + case PROP_WIDGET: + gtd_timeline_set_widget (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtd_timeline_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTimeline *self = GTD_TIMELINE (object); + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + switch (prop_id) + { + case PROP_DELAY: + g_value_set_uint (value, us_to_ms (priv->delay_us)); + break; + + case PROP_DURATION: + g_value_set_uint (value, gtd_timeline_get_duration (self)); + break; + + case PROP_DIRECTION: + g_value_set_enum (value, priv->direction); + break; + + case PROP_AUTO_REVERSE: + g_value_set_boolean (value, priv->auto_reverse); + break; + + case PROP_REPEAT_COUNT: + g_value_set_int (value, priv->repeat_count); + break; + + case PROP_PROGRESS_MODE: + g_value_set_enum (value, priv->progress_mode); + break; + + case PROP_WIDGET: + g_value_set_object (value, priv->widget); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtd_timeline_dispose (GObject *object) +{ + GtdTimeline *self = GTD_TIMELINE (object); + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + if (priv->progress_notify != NULL) + { + priv->progress_notify (priv->progress_data); + priv->progress_func = NULL; + priv->progress_data = NULL; + priv->progress_notify = NULL; + } + + G_OBJECT_CLASS (gtd_timeline_parent_class)->dispose (object); +} + +static void +gtd_timeline_class_init (GtdTimelineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + /** + * GtdTimeline::widget: + * + * The widget the timeline is associated with. This will determine what frame + * clock will drive it. + */ + obj_props[PROP_WIDGET] = + g_param_spec_object ("widget", + "Widget", + "Associated GtdWidget", + GTD_TYPE_WIDGET, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * GtdTimeline:delay: + * + * A delay, in milliseconds, that should be observed by the + * timeline before actually starting. + */ + obj_props[PROP_DELAY] = + g_param_spec_uint ("delay", + "Delay", + "Delay before start", + 0, G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTimeline:duration: + * + * Duration of the timeline in milliseconds. + */ + obj_props[PROP_DURATION] = + g_param_spec_uint ("duration", + "Duration", + "Duration of the timeline in milliseconds", + 0, G_MAXUINT, + 1000, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTimeline:direction:GIT + * + * The direction of the timeline, either %GTD_TIMELINE_FORWARD or + * %GTD_TIMELINE_BACKWARD. + */ + obj_props[PROP_DIRECTION] = + g_param_spec_enum ("direction", + "Direction", + "Direction of the timeline", + GTD_TYPE_TIMELINE_DIRECTION, + GTD_TIMELINE_FORWARD, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTimeline:auto-reverse: + * + * If the direction of the timeline should be automatically reversed + * when reaching the end. + */ + obj_props[PROP_AUTO_REVERSE] = + g_param_spec_boolean ("auto-reverse", + "Auto Reverse", + "Whether the direction should be reversed when reaching the end", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTimeline:repeat-count: + * + * Defines how many times the timeline should repeat. + * + * If the repeat count is 0, the timeline does not repeat. + * + * If the repeat count is set to -1, the timeline will repeat until it is + * stopped. + */ + obj_props[PROP_REPEAT_COUNT] = + g_param_spec_int ("repeat-count", + "Repeat Count", + "How many times the timeline should repeat", + -1, G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTimeline:progress-mode: + * + * Controls the way a #GtdTimeline computes the normalized progress. + */ + obj_props[PROP_PROGRESS_MODE] = + g_param_spec_enum ("progress-mode", + "Progress Mode", + "How the timeline should compute the progress", + GTD_TYPE_EASE_MODE, + GTD_EASE_LINEAR, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + object_class->dispose = gtd_timeline_dispose; + object_class->set_property = gtd_timeline_set_property; + object_class->get_property = gtd_timeline_get_property; + g_object_class_install_properties (object_class, PROP_LAST, obj_props); + + /** + * GtdTimeline::new-frame: + * @timeline: the timeline which received the signal + * @msecs: the elapsed time between 0 and duration + * + * The ::new-frame signal is emitted for each timeline running + * timeline before a new frame is drawn to give animations a chance + * to update the scene. + */ + timeline_signals[NEW_FRAME] = + g_signal_new ("new-frame", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTimelineClass, new_frame), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_INT); + /** + * GtdTimeline::completed: + * @timeline: the #GtdTimeline which received the signal + * + * The #GtdTimeline::completed signal is emitted when the timeline's + * elapsed time reaches the value of the #GtdTimeline:duration + * property. + * + * This signal will be emitted even if the #GtdTimeline is set to be + * repeating. + * + * If you want to get notification on whether the #GtdTimeline has + * been stopped or has finished its run, including its eventual repeats, + * you should use the #GtdTimeline::stopped signal instead. + */ + timeline_signals[COMPLETED] = + g_signal_new ("completed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTimelineClass, completed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * GtdTimeline::started: + * @timeline: the #GtdTimeline which received the signal + * + * The ::started signal is emitted when the timeline starts its run. + * This might be as soon as gtd_timeline_start() is invoked or + * after the delay set in the GtdTimeline:delay property has + * expired. + */ + timeline_signals[STARTED] = + g_signal_new ("started", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTimelineClass, started), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * GtdTimeline::paused: + * @timeline: the #GtdTimeline which received the signal + * + * The ::paused signal is emitted when gtd_timeline_pause() is invoked. + */ + timeline_signals[PAUSED] = + g_signal_new ("paused", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTimelineClass, paused), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * GtdTimeline::stopped: + * @timeline: the #GtdTimeline that emitted the signal + * @is_finished: %TRUE if the signal was emitted at the end of the + * timeline. + * + * The #GtdTimeline::stopped signal is emitted when the timeline + * has been stopped, either because gtd_timeline_stop() has been + * called, or because it has been exhausted. + * + * This is different from the #GtdTimeline::completed signal, + * which gets emitted after every repeat finishes. + * + * If the #GtdTimeline has is marked as infinitely repeating, + * this signal will never be emitted. + */ + timeline_signals[STOPPED] = + g_signal_new ("stopped", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTimelineClass, stopped), + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); +} + +static void +gtd_timeline_init (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + priv->progress_mode = GTD_EASE_LINEAR; + priv->progress_func = timeline_progress_func; +} + +/** + * gtd_timeline_new: + * @duration_ms: Duration of the timeline in milliseconds + * + * Creates a new #GtdTimeline with a duration of @duration_ms milli seconds. + * + * Return value: the newly created #GtdTimeline instance. Use + * g_object_unref() when done using it + */ +GtdTimeline * +gtd_timeline_new (guint duration_ms) +{ + return g_object_new (GTD_TYPE_TIMELINE, + "duration", duration_ms, + NULL); +} + +/** + * gtd_timeline_new_for_widget: + * @widget: The #GtdWidget the timeline is associated with + * @duration_ms: Duration of the timeline in milliseconds + * + * Creates a new #GtdTimeline with a duration of @duration milli seconds. + * + * Return value: the newly created #GtdTimeline instance. Use + * g_object_unref() when done using it + */ +GtdTimeline * +gtd_timeline_new_for_widget (GtdWidget *widget, + guint duration_ms) +{ + return g_object_new (GTD_TYPE_TIMELINE, + "duration", duration_ms, + "widget", widget, + NULL); +} + +/** + * gtd_timeline_set_widget: + * @timeline: a #GtdTimeline + * @widget: a #GtdWidget + * + * Set the widget the timeline is associated with. + */ +void +gtd_timeline_set_widget (GtdTimeline *self, + GtdWidget *widget) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + if (priv->widget) + { + remove_tick_callback (self); + priv->widget = NULL; + } + + priv->widget = widget; + + if (priv->is_playing) + add_tick_callback (self); +} + +/** + * gtd_timeline_start: + * @timeline: A #GtdTimeline + * + * Starts the #GtdTimeline playing. + **/ +void +gtd_timeline_start (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->is_playing) + return; + + if (priv->duration_us == 0) + return; + + priv->delta_us = 0; + set_is_playing (self, TRUE); + + g_signal_emit (self, timeline_signals[STARTED], 0); +} + +/** + * gtd_timeline_pause: + * @timeline: A #GtdTimeline + * + * Pauses the #GtdTimeline on current frame + **/ +void +gtd_timeline_pause (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + if (!priv->is_playing) + return; + + priv->delta_us = 0; + set_is_playing (self, FALSE); + + g_signal_emit (self, timeline_signals[PAUSED], 0); +} + +/** + * gtd_timeline_stop: + * @timeline: A #GtdTimeline + * + * Stops the #GtdTimeline and moves to frame 0 + **/ +void +gtd_timeline_stop (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + gboolean was_playing; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + /* we check the is_playing here because pause() will return immediately + * if the timeline wasn't playing, so we don't know if it was actually + * stopped, and yet we still don't want to emit a ::stopped signal if + * the timeline was not playing in the first place. + */ + was_playing = priv->is_playing; + + gtd_timeline_pause (self); + gtd_timeline_rewind (self); + + if (was_playing) + g_signal_emit (self, timeline_signals[STOPPED], 0, FALSE); +} + +/** + * gtd_timeline_rewind: + * @timeline: A #GtdTimeline + * + * Rewinds #GtdTimeline to the first frame if its direction is + * %GTD_TIMELINE_FORWARD and the last frame if it is + * %GTD_TIMELINE_BACKWARD. + */ +void +gtd_timeline_rewind (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->direction == GTD_TIMELINE_FORWARD) + gtd_timeline_advance (self, 0); + else if (priv->direction == GTD_TIMELINE_BACKWARD) + gtd_timeline_advance (self, us_to_ms (priv->duration_us)); +} + +/** + * gtd_timeline_skip: + * @timeline: A #GtdTimeline + * @msecs: Amount of time to skip + * + * Advance timeline by the requested time in milliseconds + */ +void +gtd_timeline_skip (GtdTimeline *self, + guint msecs) +{ + GtdTimelinePrivate *priv; + gint64 us; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + us = ms_to_us (msecs); + + if (priv->direction == GTD_TIMELINE_FORWARD) + { + priv->elapsed_time_us += us; + + if (priv->elapsed_time_us > priv->duration_us) + priv->elapsed_time_us = 1; + } + else if (priv->direction == GTD_TIMELINE_BACKWARD) + { + priv->elapsed_time_us -= us; + + if (priv->elapsed_time_us < 1) + priv->elapsed_time_us = priv->duration_us - 1; + } + + priv->delta_us = 0; +} + +/** + * gtd_timeline_advance: + * @timeline: A #GtdTimeline + * @msecs: Time to advance to + * + * Advance timeline to the requested point. The point is given as a + * time in milliseconds since the timeline started. + * + * The @timeline will not emit the #GtdTimeline::new-frame + * signal for the given time. The first ::new-frame signal after the call to + * gtd_timeline_advance() will be emit the skipped markers. + */ +void +gtd_timeline_advance (GtdTimeline *self, + guint msecs) +{ + GtdTimelinePrivate *priv; + gint64 us; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + us = ms_to_us (msecs); + + priv->elapsed_time_us = MIN (us, priv->duration_us); +} + +/** + * gtd_timeline_get_elapsed_time: + * @timeline: A #GtdTimeline + * + * Request the current time position of the timeline. + * + * Return value: current elapsed time in milliseconds. + */ +guint +gtd_timeline_get_elapsed_time (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + return us_to_ms (priv->elapsed_time_us); +} + +/** + * gtd_timeline_is_playing: + * @timeline: A #GtdTimeline + * + * Queries state of a #GtdTimeline. + * + * Return value: %TRUE if timeline is currently playing + */ +gboolean +gtd_timeline_is_playing (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE); + + priv = gtd_timeline_get_instance_private (self); + return priv->is_playing; +} + +/** + * gtd_timeline_get_delay: + * @timeline: a #GtdTimeline + * + * Retrieves the delay set using gtd_timeline_set_delay(). + * + * Return value: the delay in milliseconds. + */ +guint +gtd_timeline_get_delay (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + return us_to_ms (priv->delay_us); +} + +/** + * gtd_timeline_set_delay: + * @timeline: a #GtdTimeline + * @msecs: delay in milliseconds + * + * Sets the delay, in milliseconds, before @timeline should start. + */ +void +gtd_timeline_set_delay (GtdTimeline *self, + guint msecs) +{ + GtdTimelinePrivate *priv; + gint64 us; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + us = ms_to_us (msecs); + + if (priv->delay_us != us) + { + priv->delay_us = us; + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DELAY]); + } +} + +/** + * gtd_timeline_get_duration: + * @timeline: a #GtdTimeline + * + * Retrieves the duration of a #GtdTimeline in milliseconds. + * See gtd_timeline_set_duration(). + * + * Return value: the duration of the timeline, in milliseconds. + */ +guint +gtd_timeline_get_duration (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + + return us_to_ms (priv->duration_us); +} + +/** + * gtd_timeline_set_duration: + * @timeline: a #GtdTimeline + * @msecs: duration of the timeline in milliseconds + * + * Sets the duration of the timeline, in milliseconds. The speed + * of the timeline depends on the GtdTimeline:fps setting. + */ +void +gtd_timeline_set_duration (GtdTimeline *self, + guint msecs) +{ + GtdTimelinePrivate *priv; + gint64 us; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + g_return_if_fail (msecs > 0); + + priv = gtd_timeline_get_instance_private (self); + us = ms_to_us (msecs); + + if (priv->duration_us != us) + { + priv->duration_us = us; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DURATION]); + } +} + +/** + * gtd_timeline_get_progress: + * @timeline: a #GtdTimeline + * + * The position of the timeline in a normalized [-1, 2] interval. + * + * The return value of this function is determined by the progress + * mode set using gtd_timeline_set_progress_mode(), or by the + * progress function set using gtd_timeline_set_progress_func(). + * + * Return value: the normalized current position in the timeline. + */ +gdouble +gtd_timeline_get_progress (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0.0); + + priv = gtd_timeline_get_instance_private (self); + + return priv->progress_func (self, + (gdouble) priv->elapsed_time_us, + (gdouble) priv->duration_us, + priv->progress_data); +} + +/** + * gtd_timeline_get_direction: + * @timeline: a #GtdTimeline + * + * Retrieves the direction of the timeline set with + * gtd_timeline_set_direction(). + * + * Return value: the direction of the timeline + */ +GtdTimelineDirection +gtd_timeline_get_direction (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_TIMELINE_FORWARD); + + priv = gtd_timeline_get_instance_private (self); + return priv->direction; +} + +/** + * gtd_timeline_set_direction: + * @timeline: a #GtdTimeline + * @direction: the direction of the timeline + * + * Sets the direction of @timeline, either %GTD_TIMELINE_FORWARD or + * %GTD_TIMELINE_BACKWARD. + */ +void +gtd_timeline_set_direction (GtdTimeline *self, + GtdTimelineDirection direction) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->direction != direction) + { + priv->direction = direction; + + if (priv->elapsed_time_us == 0) + priv->elapsed_time_us = priv->duration_us; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DIRECTION]); + } +} + +/** + * gtd_timeline_get_delta: + * @timeline: a #GtdTimeline + * + * Retrieves the amount of time elapsed since the last + * GtdTimeline::new-frame signal. + * + * This function is only useful inside handlers for the ::new-frame + * signal, and its behaviour is undefined if the timeline is not + * playing. + * + * Return value: the amount of time in milliseconds elapsed since the + * last frame + */ +guint +gtd_timeline_get_delta (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + if (!gtd_timeline_is_playing (self)) + return 0; + + priv = gtd_timeline_get_instance_private (self); + return us_to_ms (priv->delta_us); +} + +/** + * gtd_timeline_set_auto_reverse: + * @timeline: a #GtdTimeline + * @reverse: %TRUE if the @timeline should reverse the direction + * + * Sets whether @timeline should reverse the direction after the + * emission of the #GtdTimeline::completed signal. + * + * Setting the #GtdTimeline:auto-reverse property to %TRUE is the + * equivalent of connecting a callback to the #GtdTimeline::completed + * signal and changing the direction of the timeline from that callback; + * for instance, this code: + * + * |[ + * static void + * reverse_timeline (GtdTimeline *self) + * { + * GtdTimelineDirection dir = gtd_timeline_get_direction (self); + * + * if (dir == GTD_TIMELINE_FORWARD) + * dir = GTD_TIMELINE_BACKWARD; + * else + * dir = GTD_TIMELINE_FORWARD; + * + * gtd_timeline_set_direction (self, dir); + * } + * ... + * timeline = gtd_timeline_new (1000); + * gtd_timeline_set_repeat_count (self, -1); + * g_signal_connect (self, "completed", + * G_CALLBACK (reverse_timeline), + * NULL); + * ]| + * + * can be effectively replaced by: + * + * |[ + * timeline = gtd_timeline_new (1000); + * gtd_timeline_set_repeat_count (self, -1); + * gtd_timeline_set_auto_reverse (self); + * ]| + */ +void +gtd_timeline_set_auto_reverse (GtdTimeline *self, + gboolean reverse) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + reverse = !!reverse; + + priv = gtd_timeline_get_instance_private (self); + + if (priv->auto_reverse != reverse) + { + priv->auto_reverse = reverse; + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_AUTO_REVERSE]); + } +} + +/** + * gtd_timeline_get_auto_reverse: + * @timeline: a #GtdTimeline + * + * Retrieves the value set by gtd_timeline_set_auto_reverse(). + * + * Return value: %TRUE if the timeline should automatically reverse, and + * %FALSE otherwise + */ +gboolean +gtd_timeline_get_auto_reverse (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE); + + priv = gtd_timeline_get_instance_private (self); + return priv->auto_reverse; +} + +/** + * gtd_timeline_set_repeat_count: + * @timeline: a #GtdTimeline + * @count: the number of times the timeline should repeat + * + * Sets the number of times the @timeline should repeat. + * + * If @count is 0, the timeline never repeats. + * + * If @count is -1, the timeline will always repeat until + * it's stopped. + */ +void +gtd_timeline_set_repeat_count (GtdTimeline *self, + gint count) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + g_return_if_fail (count >= -1); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->repeat_count != count) + { + priv->repeat_count = count; + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REPEAT_COUNT]); + } +} + +/** + * gtd_timeline_get_repeat_count: + * @timeline: a #GtdTimeline + * + * Retrieves the number set using gtd_timeline_set_repeat_count(). + * + * Return value: the number of repeats + */ +gint +gtd_timeline_get_repeat_count (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + return priv->repeat_count; +} + +/** + * gtd_timeline_set_progress_func: + * @timeline: a #GtdTimeline + * @func: (scope notified) (allow-none): a progress function, or %NULL + * @data: (closure): data to pass to @func + * @notify: a function to be called when the progress function is removed + * or the timeline is disposed + * + * Sets a custom progress function for @timeline. The progress function will + * be called by gtd_timeline_get_progress() and will be used to compute + * the progress value based on the elapsed time and the total duration of the + * timeline. + * + * If @func is not %NULL, the #GtdTimeline:progress-mode property will + * be set to %GTD_CUSTOM_MODE. + * + * If @func is %NULL, any previously set progress function will be unset, and + * the #GtdTimeline:progress-mode property will be set to %GTD_EASE_LINEAR. + */ +void +gtd_timeline_set_progress_func (GtdTimeline *self, + GtdTimelineProgressFunc func, + gpointer data, + GDestroyNotify notify) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->progress_notify != NULL) + priv->progress_notify (priv->progress_data); + + priv->progress_func = func; + priv->progress_data = data; + priv->progress_notify = notify; + + if (priv->progress_func != NULL) + priv->progress_mode = GTD_CUSTOM_MODE; + else + priv->progress_mode = GTD_EASE_LINEAR; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]); +} + +/** + * gtd_timeline_set_progress_mode: + * @timeline: a #GtdTimeline + * @mode: the progress mode, as a #GtdEaseMode + * + * Sets the progress function using a value from the #GtdEaseMode + * enumeration. The @mode cannot be %GTD_CUSTOM_MODE or bigger than + * %GTD_ANIMATION_LAST. + */ +void +gtd_timeline_set_progress_mode (GtdTimeline *self, + GtdEaseMode mode) +{ + GtdTimelinePrivate *priv; + + g_return_if_fail (GTD_IS_TIMELINE (self)); + g_return_if_fail (mode < GTD_EASE_LAST); + g_return_if_fail (mode != GTD_CUSTOM_MODE); + + priv = gtd_timeline_get_instance_private (self); + + if (priv->progress_mode == mode) + return; + + if (priv->progress_notify != NULL) + priv->progress_notify (priv->progress_data); + + priv->progress_mode = mode; + priv->progress_func = timeline_progress_func; + priv->progress_data = NULL; + priv->progress_notify = NULL; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]); +} + +/** + * gtd_timeline_get_progress_mode: + * @timeline: a #GtdTimeline + * + * Retrieves the progress mode set using gtd_timeline_set_progress_mode() + * or gtd_timeline_set_progress_func(). + * + * Return value: a #GtdEaseMode + */ +GtdEaseMode +gtd_timeline_get_progress_mode (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_EASE_LINEAR); + + priv = gtd_timeline_get_instance_private (self); + return priv->progress_mode; +} + +/** + * gtd_timeline_get_duration_hint: + * @timeline: a #GtdTimeline + * + * Retrieves the full duration of the @timeline, taking into account the + * current value of the #GtdTimeline:repeat-count property. + * + * If the #GtdTimeline:repeat-count property is set to -1, this function + * will return %G_MAXINT64. + * + * The returned value is to be considered a hint, and it's only valid + * as long as the @timeline hasn't been changed. + * + * Return value: the full duration of the #GtdTimeline + */ +gint64 +gtd_timeline_get_duration_hint (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + gint64 duration_ms; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + duration_ms = us_to_ms (priv->duration_us); + + if (priv->repeat_count == 0) + return duration_ms; + else if (priv->repeat_count < 0) + return G_MAXINT64; + else + return priv->repeat_count * duration_ms; +} + +/** + * gtd_timeline_get_current_repeat: + * @timeline: a #GtdTimeline + * + * Retrieves the current repeat for a timeline. + * + * Repeats start at 0. + * + * Return value: the current repeat + */ +gint +gtd_timeline_get_current_repeat (GtdTimeline *self) +{ + GtdTimelinePrivate *priv; + + g_return_val_if_fail (GTD_IS_TIMELINE (self), 0); + + priv = gtd_timeline_get_instance_private (self); + return priv->current_repeat; +} + +/** + * gtd_timeline_get_widget: + * @timeline: a #GtdTimeline + * + * Get the widget the timeline is associated with. + * + * Returns: (transfer none): the associated #GtdWidget + */ +GtdWidget * +gtd_timeline_get_widget (GtdTimeline *self) +{ + GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self); + + return priv->widget; +} diff --git a/src/animation/gtd-timeline.h b/src/animation/gtd-timeline.h new file mode 100644 index 0000000..a8d6023 --- /dev/null +++ b/src/animation/gtd-timeline.h @@ -0,0 +1,150 @@ +/* gtd-timeline.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * Heavily inspired by Clutter, authored By Matthew Allum + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +#include "gtd-easing.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TIMELINE (gtd_timeline_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdTimeline, gtd_timeline, GTD, TIMELINE, GObject) + +/** + * GtdTimelineProgressFunc: + * @timeline: a #GtdTimeline + * @elapsed: the elapsed time, in milliseconds + * @total: the total duration of the timeline, in milliseconds, + * @user_data: data passed to the function + * + * A function for defining a custom progress. + * + * Return value: the progress, as a floating point value between -1.0 and 2.0. + */ +typedef gdouble (* GtdTimelineProgressFunc) (GtdTimeline *timeline, + gdouble elapsed, + gdouble total, + gpointer user_data); + +/** + * GtdTimelineClass: + * @started: class handler for the #GtdTimeline::started signal + * @completed: class handler for the #GtdTimeline::completed signal + * @paused: class handler for the #GtdTimeline::paused signal + * @new_frame: class handler for the #GtdTimeline::new-frame signal + * @stopped: class handler for the #GtdTimeline::stopped signal + * + * The #GtdTimelineClass structure contains only private data + */ +struct _GtdTimelineClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + void (*started) (GtdTimeline *timeline); + void (*completed) (GtdTimeline *timeline); + void (*paused) (GtdTimeline *timeline); + + void (*new_frame) (GtdTimeline *timeline, + gint msecs); + + void (*stopped) (GtdTimeline *timeline, + gboolean is_finished); +}; + +GtdTimeline* gtd_timeline_new_for_widget (GtdWidget *widget, + guint duration_ms); + +GtdWidget* gtd_timeline_get_widget (GtdTimeline *timeline); + +void gtd_timeline_set_widget (GtdTimeline *timeline, + GtdWidget *widget); + +guint gtd_timeline_get_duration (GtdTimeline *timeline); + +void gtd_timeline_set_duration (GtdTimeline *timeline, + guint msecs); + +GtdTimelineDirection gtd_timeline_get_direction (GtdTimeline *timeline); + +void gtd_timeline_set_direction (GtdTimeline *timeline, + GtdTimelineDirection direction); + +void gtd_timeline_start (GtdTimeline *timeline); + +void gtd_timeline_pause (GtdTimeline *timeline); + +void gtd_timeline_stop (GtdTimeline *timeline); + +void gtd_timeline_set_auto_reverse (GtdTimeline *timeline, + gboolean reverse); + +gboolean gtd_timeline_get_auto_reverse (GtdTimeline *timeline); + +void gtd_timeline_set_repeat_count (GtdTimeline *timeline, + gint count); + +gint gtd_timeline_get_repeat_count (GtdTimeline *timeline); + +void gtd_timeline_rewind (GtdTimeline *timeline); + +void gtd_timeline_skip (GtdTimeline *timeline, + guint msecs); + +void gtd_timeline_advance (GtdTimeline *timeline, + guint msecs); + +guint gtd_timeline_get_elapsed_time (GtdTimeline *timeline); + +gdouble gtd_timeline_get_progress (GtdTimeline *timeline); + +gboolean gtd_timeline_is_playing (GtdTimeline *timeline); + +void gtd_timeline_set_delay (GtdTimeline *timeline, + guint msecs); + +guint gtd_timeline_get_delay (GtdTimeline *timeline); + +guint gtd_timeline_get_delta (GtdTimeline *timeline); + +void gtd_timeline_set_progress_func (GtdTimeline *timeline, + GtdTimelineProgressFunc func, + gpointer data, + GDestroyNotify notify); + +void gtd_timeline_set_progress_mode (GtdTimeline *timeline, + GtdEaseMode mode); + +GtdEaseMode gtd_timeline_get_progress_mode (GtdTimeline *timeline); + +gint64 gtd_timeline_get_duration_hint (GtdTimeline *timeline); +gint gtd_timeline_get_current_repeat (GtdTimeline *timeline); + +G_END_DECLS + + +G_END_DECLS diff --git a/src/animation/gtd-transition.c b/src/animation/gtd-transition.c new file mode 100644 index 0000000..0ca1d3f --- /dev/null +++ b/src/animation/gtd-transition.c @@ -0,0 +1,655 @@ +/* gtd-transition.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +/** + * SECTION:gtd-transition + * @Title: GtdTransition + * @Short_Description: Transition between two values + * + * #GtdTransition is an abstract subclass of #GtdTimeline that + * computes the interpolation between two values, stored by a #GtdInterval. + */ + +#include "gtd-transition.h" + +#include "gtd-animatable.h" +#include "gtd-debug.h" +#include "gtd-interval.h" +#include "gtd-timeline.h" + +#include + +typedef struct +{ + GtdInterval *interval; + GtdAnimatable *animatable; + + guint remove_on_complete : 1; +} GtdTransitionPrivate; + +enum +{ + PROP_0, + PROP_INTERVAL, + PROP_ANIMATABLE, + PROP_REMOVE_ON_COMPLETE, + PROP_LAST, +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +static GQuark quark_animatable_set = 0; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtdTransition, gtd_transition, GTD_TYPE_TIMELINE) + +static void +gtd_transition_attach (GtdTransition *self, + GtdAnimatable *animatable) +{ + GTD_TRANSITION_GET_CLASS (self)->attached (self, animatable); +} + +static void +gtd_transition_detach (GtdTransition *self, + GtdAnimatable *animatable) +{ + GTD_TRANSITION_GET_CLASS (self)->detached (self, animatable); +} + +static void +gtd_transition_real_compute_value (GtdTransition *self, + GtdAnimatable *animatable, + GtdInterval *interval, + gdouble progress) +{ +} + +static void +gtd_transition_real_attached (GtdTransition *self, + GtdAnimatable *animatable) +{ +} + +static void +gtd_transition_real_detached (GtdTransition *self, + GtdAnimatable *animatable) +{ +} + +static void +gtd_transition_new_frame (GtdTimeline *timeline, + gint elapsed) +{ + GtdTransition *self = GTD_TRANSITION (timeline); + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + gdouble progress; + + if (!priv->interval || !priv->animatable) + return; + + progress = gtd_timeline_get_progress (timeline); + + GTD_TRANSITION_GET_CLASS (timeline)->compute_value (self, + priv->animatable, + priv->interval, + progress); +} + +static void +gtd_transition_stopped (GtdTimeline *timeline, + gboolean is_finished) +{ + GtdTransition *self = GTD_TRANSITION (timeline); + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + + if (is_finished && + priv->animatable != NULL && + priv->remove_on_complete) + { + gtd_transition_detach (GTD_TRANSITION (timeline), + priv->animatable); + g_clear_object (&priv->animatable); + } +} + +static void +gtd_transition_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTransition *self = GTD_TRANSITION (gobject); + + switch (prop_id) + { + case PROP_INTERVAL: + gtd_transition_set_interval (self, g_value_get_object (value)); + break; + + case PROP_ANIMATABLE: + gtd_transition_set_animatable (self, g_value_get_object (value)); + break; + + case PROP_REMOVE_ON_COMPLETE: + gtd_transition_set_remove_on_complete (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtd_transition_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTransition *self = GTD_TRANSITION (gobject); + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + + switch (prop_id) + { + case PROP_INTERVAL: + g_value_set_object (value, priv->interval); + break; + + case PROP_ANIMATABLE: + g_value_set_object (value, priv->animatable); + break; + + case PROP_REMOVE_ON_COMPLETE: + g_value_set_boolean (value, priv->remove_on_complete); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtd_transition_dispose (GObject *gobject) +{ + GtdTransition *self = GTD_TRANSITION (gobject); + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + + if (priv->animatable != NULL) + gtd_transition_detach (GTD_TRANSITION (gobject), + priv->animatable); + + g_clear_object (&priv->interval); + g_clear_object (&priv->animatable); + + G_OBJECT_CLASS (gtd_transition_parent_class)->dispose (gobject); +} + +static void +gtd_transition_class_init (GtdTransitionClass *klass) +{ + GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + quark_animatable_set = + g_quark_from_static_string ("-gtd-transition-animatable-set"); + + klass->compute_value = gtd_transition_real_compute_value; + klass->attached = gtd_transition_real_attached; + klass->detached = gtd_transition_real_detached; + + timeline_class->new_frame = gtd_transition_new_frame; + timeline_class->stopped = gtd_transition_stopped; + + gobject_class->set_property = gtd_transition_set_property; + gobject_class->get_property = gtd_transition_get_property; + gobject_class->dispose = gtd_transition_dispose; + + /** + * GtdTransition:interval: + * + * The #GtdInterval used to describe the initial and final states + * of the transition. + */ + obj_props[PROP_INTERVAL] = + g_param_spec_object ("interval", + "Interval", + "The interval of values to transition", + GTD_TYPE_INTERVAL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTransition:animatable: + * + * The #GtdAnimatable instance currently being animated. + */ + obj_props[PROP_ANIMATABLE] = + g_param_spec_object ("animatable", + "Animatable", + "The animatable object", + GTD_TYPE_ANIMATABLE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdTransition:remove-on-complete: + * + * Whether the #GtdTransition should be automatically detached + * from the #GtdTransition:animatable instance whenever the + * #GtdTimeline::stopped signal is emitted. + * + * The #GtdTransition:remove-on-complete property takes into + * account the value of the #GtdTimeline:repeat-count property, + * and it only detaches the transition if the transition is not + * repeating. + */ + obj_props[PROP_REMOVE_ON_COMPLETE] = + g_param_spec_boolean ("remove-on-complete", + "Remove on Complete", + "Detach the transition when completed", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} + +static void +gtd_transition_init (GtdTransition *self) +{ +} + +/** + * gtd_transition_set_interval: + * @transition: a #GtdTransition + * @interval: (allow-none): a #GtdInterval, or %NULL + * + * Sets the #GtdTransition:interval property using @interval. + * + * The @transition will acquire a reference on the @interval, sinking + * the floating flag on it if necessary. + */ +void +gtd_transition_set_interval (GtdTransition *self, + GtdInterval *interval) +{ + GtdTransitionPrivate *priv; + + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (interval == NULL || GTD_IS_INTERVAL (interval)); + + priv = gtd_transition_get_instance_private (self); + + if (priv->interval == interval) + return; + + g_clear_object (&priv->interval); + + if (interval != NULL) + priv->interval = g_object_ref_sink (interval); + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_INTERVAL]); +} + +/** + * gtd_transition_get_interval: + * @transition: a #GtdTransition + * + * Retrieves the interval set using gtd_transition_set_interval() + * + * Return value: (transfer none): a #GtdInterval, or %NULL; the returned + * interval is owned by the #GtdTransition and it should not be freed + * directly + */ +GtdInterval * +gtd_transition_get_interval (GtdTransition *self) +{ + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL); + + priv = gtd_transition_get_instance_private (self); + return priv->interval; +} + +/** + * gtd_transition_set_animatable: + * @transition: a #GtdTransition + * @animatable: (allow-none): a #GtdAnimatable, or %NULL + * + * Sets the #GtdTransition:animatable property. + * + * The @transition will acquire a reference to the @animatable instance, + * and will call the #GtdTransitionClass.attached() virtual function. + * + * If an existing #GtdAnimatable is attached to @self, the + * reference will be released, and the #GtdTransitionClass.detached() + * virtual function will be called. + */ +void +gtd_transition_set_animatable (GtdTransition *self, + GtdAnimatable *animatable) +{ + GtdTransitionPrivate *priv; + GtdWidget *widget; + + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (animatable == NULL || GTD_IS_ANIMATABLE (animatable)); + + priv = gtd_transition_get_instance_private (self); + + if (priv->animatable == animatable) + return; + + if (priv->animatable != NULL) + gtd_transition_detach (self, priv->animatable); + + g_clear_object (&priv->animatable); + + if (animatable != NULL) + { + priv->animatable = g_object_ref (animatable); + gtd_transition_attach (self, priv->animatable); + } + + widget = gtd_animatable_get_widget (animatable); + gtd_timeline_set_widget (GTD_TIMELINE (self), widget); +} + +/** + * gtd_transition_get_animatable: + * @transition: a #GtdTransition + * + * Retrieves the #GtdAnimatable set using gtd_transition_set_animatable(). + * + * Return value: (transfer none): a #GtdAnimatable, or %NULL; the returned + * animatable is owned by the #GtdTransition, and it should not be freed + * directly. + */ +GtdAnimatable * +gtd_transition_get_animatable (GtdTransition *self) +{ + GtdTransitionPrivate *priv; + + g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL); + + priv = gtd_transition_get_instance_private (self); + return priv->animatable; +} + +/** + * gtd_transition_set_remove_on_complete: + * @transition: a #GtdTransition + * @remove_complete: whether to detach @transition when complete + * + * Sets whether @transition should be detached from the #GtdAnimatable + * set using gtd_transition_set_animatable() when the + * #GtdTimeline::completed signal is emitted. + */ +void +gtd_transition_set_remove_on_complete (GtdTransition *self, + gboolean remove_complete) +{ + GtdTransitionPrivate *priv; + + g_return_if_fail (GTD_IS_TRANSITION (self)); + + priv = gtd_transition_get_instance_private (self); + remove_complete = !!remove_complete; + + if (priv->remove_on_complete == remove_complete) + return; + + priv->remove_on_complete = remove_complete; + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REMOVE_ON_COMPLETE]); +} + +/** + * gtd_transition_get_remove_on_complete: + * @transition: a #GtdTransition + * + * Retrieves the value of the #GtdTransition:remove-on-complete property. + * + * Return value: %TRUE if the @transition should be detached when complete, + * and %FALSE otherwise + */ +gboolean +gtd_transition_get_remove_on_complete (GtdTransition *self) +{ + GtdTransitionPrivate *priv; + + g_return_val_if_fail (GTD_IS_TRANSITION (self), FALSE); + + priv = gtd_transition_get_instance_private (self); + return priv->remove_on_complete; +} + +typedef void (* IntervalSetFunc) (GtdInterval *interval, + const GValue *value); + +static inline void +gtd_transition_set_value (GtdTransition *self, + IntervalSetFunc interval_set_func, + const GValue *value) +{ + GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self); + GType interval_type; + + if (priv->interval == NULL) + { + priv->interval = gtd_interval_new_with_values (G_VALUE_TYPE (value), NULL, NULL); + g_object_ref_sink (priv->interval); + } + + interval_type = gtd_interval_get_value_type (priv->interval); + + if (!g_type_is_a (G_VALUE_TYPE (value), interval_type)) + { + if (g_value_type_compatible (G_VALUE_TYPE (value), interval_type)) + { + interval_set_func (priv->interval, value); + return; + } + + if (g_value_type_transformable (G_VALUE_TYPE (value), interval_type)) + { + GValue transform = G_VALUE_INIT; + + g_value_init (&transform, interval_type); + if (g_value_transform (value, &transform)) + interval_set_func (priv->interval, &transform); + else + { + g_warning ("%s: Unable to convert a value of type '%s' into " + "the value type '%s' of the interval used by the " + "transition.", + G_STRLOC, + g_type_name (G_VALUE_TYPE (value)), + g_type_name (interval_type)); + } + + g_value_unset (&transform); + } + } + else + { + interval_set_func (priv->interval, value); + } +} + +/** + * gtd_transition_set_from_value: (rename-to gtd_transition_set_from) + * @transition: a #GtdTransition + * @value: a #GValue with the initial value of the transition + * + * Sets the initial value of the transition. + * + * This is a convenience function that will either create the + * #GtdInterval used by @self, or will update it if + * the #GtdTransition:interval is already set. + * + * This function will copy the contents of @value, so it is + * safe to call g_value_unset() after it returns. + * + * If @transition already has a #GtdTransition:interval set, + * then @value must hold the same type, or a transformable type, + * as the interval's #GtdInterval:value-type property. + * + * This function is meant to be used by language bindings. + */ +void +gtd_transition_set_from_value (GtdTransition *self, + const GValue *value) +{ + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (G_IS_VALUE (value)); + + gtd_transition_set_value (self, gtd_interval_set_initial_value, value); +} + +/** + * gtd_transition_set_to_value: (rename-to gtd_transition_set_to) + * @transition: a #GtdTransition + * @value: a #GValue with the final value of the transition + * + * Sets the final value of the transition. + * + * This is a convenience function that will either create the + * #GtdInterval used by @self, or will update it if + * the #GtdTransition:interval is already set. + * + * This function will copy the contents of @value, so it is + * safe to call g_value_unset() after it returns. + * + * If @transition already has a #GtdTransition:interval set, + * then @value must hold the same type, or a transformable type, + * as the interval's #GtdInterval:value-type property. + * + * This function is meant to be used by language bindings. + */ +void +gtd_transition_set_to_value (GtdTransition *self, + const GValue *value) +{ + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (G_IS_VALUE (value)); + + gtd_transition_set_value (self, + gtd_interval_set_final_value, + value); +} + +/** + * gtd_transition_set_from: (skip) + * @transition: a #GtdTransition + * @value_type: the type of the value to set + * @...: the initial value + * + * Sets the initial value of the transition. + * + * This is a convenience function that will either create the + * #GtdInterval used by @self, or will update it if + * the #GtdTransition:interval is already set. + * + * If @transition already has a #GtdTransition:interval set, + * then @value must hold the same type, or a transformable type, + * as the interval's #GtdInterval:value-type property. + * + * This is a convenience function for the C API; language bindings + * should use gtd_transition_set_from_value() instead. + */ +void +gtd_transition_set_from (GtdTransition *self, + GType value_type, + ...) +{ + GValue value = G_VALUE_INIT; + gchar *error = NULL; + va_list args; + + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (value_type != G_TYPE_INVALID); + + va_start (args, value_type); + + G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error); + + va_end (args); + + if (error != NULL) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + return; + } + + gtd_transition_set_value (self, gtd_interval_set_initial_value, &value); + + g_value_unset (&value); +} + +/** + * gtd_transition_set_to: (skip) + * @transition: a #GtdTransition + * @value_type: the type of the value to set + * @...: the final value + * + * Sets the final value of the transition. + * + * This is a convenience function that will either create the + * #GtdInterval used by @self, or will update it if + * the #GtdTransition:interval is already set. + * + * If @transition already has a #GtdTransition:interval set, + * then @value must hold the same type, or a transformable type, + * as the interval's #GtdInterval:value-type property. + * + * This is a convenience function for the C API; language bindings + * should use gtd_transition_set_to_value() instead. + */ +void +gtd_transition_set_to (GtdTransition *self, + GType value_type, + ...) +{ + GValue value = G_VALUE_INIT; + gchar *error = NULL; + va_list args; + + g_return_if_fail (GTD_IS_TRANSITION (self)); + g_return_if_fail (value_type != G_TYPE_INVALID); + + va_start (args, value_type); + + G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error); + + va_end (args); + + if (error != NULL) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + return; + } + + gtd_transition_set_value (self, gtd_interval_set_final_value, &value); + + g_value_unset (&value); +} diff --git a/src/animation/gtd-transition.h b/src/animation/gtd-transition.h new file mode 100644 index 0000000..32e20fa --- /dev/null +++ b/src/animation/gtd-transition.h @@ -0,0 +1,85 @@ +/* gtd-transition.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-timeline.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TRANSITION (gtd_transition_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdTransition, gtd_transition, GTD, TRANSITION, GtdTimeline) + +/** + * GtdTransitionClass: + * @attached: virtual function; called when a transition is attached to + * a #GtdAnimatable instance + * @detached: virtual function; called when a transition is detached from + * a #GtdAnimatable instance + * @compute_value: virtual function; called each frame to compute and apply + * the interpolation of the interval + * + * The #GtdTransitionClass structure contains + * private data. + * + * Since: 1.10 + */ +struct _GtdTransitionClass +{ + /*< private >*/ + GtdTimelineClass parent_class; + + /*< public >*/ + void (* attached) (GtdTransition *transition, + GtdAnimatable *animatable); + void (* detached) (GtdTransition *transition, + GtdAnimatable *animatable); + + void (* compute_value) (GtdTransition *transition, + GtdAnimatable *animatable, + GtdInterval *interval, + gdouble progress); + + /*< private >*/ + gpointer _padding[8]; +}; + +void gtd_transition_set_interval (GtdTransition *transition, + GtdInterval *interval); +GtdInterval * gtd_transition_get_interval (GtdTransition *transition); +void gtd_transition_set_from_value (GtdTransition *transition, + const GValue *value); +void gtd_transition_set_to_value (GtdTransition *transition, + const GValue *value); +void gtd_transition_set_from (GtdTransition *transition, + GType value_type, + ...); +void gtd_transition_set_to (GtdTransition *transition, + GType value_type, + ...); + +void gtd_transition_set_animatable (GtdTransition *transition, + GtdAnimatable *animatable); +GtdAnimatable * gtd_transition_get_animatable (GtdTransition *transition); +void gtd_transition_set_remove_on_complete (GtdTransition *transition, + gboolean remove_complete); +gboolean gtd_transition_get_remove_on_complete (GtdTransition *transition); + +G_END_DECLS diff --git a/src/core/gtd-activatable.c b/src/core/gtd-activatable.c new file mode 100644 index 0000000..fcfd60e --- /dev/null +++ b/src/core/gtd-activatable.c @@ -0,0 +1,129 @@ +/* gtd-activatable.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdActivatable" + +#include "gtd-activatable.h" +#include "gtd-panel.h" +#include "gtd-provider.h" + +/** + * SECTION:gtd-activatable + * @short_description:entry point for plugins + * @title:GtdActivatable + * @stability:Unstable + * + * The #GtdActivatable interface is the interface plugins must + * implement in order to be seen by Endeavour. + * + * When plugins are loaded, the gtd_activatable_activate() vfunc + * is called. Use this vfunc to load anything that depends on + * Endeavour. + * + * When plugins are unloaded, the gtd_activatable_deactivate() vfunc + * if called. Ideally, the implementation should undo everything that + * was done on gtd_activatable_activate(). + * + * A plugin implementation may expose one or more #GtdProvider instances, + * which are the data sources of Endeavour. See the 'eds' plugin for + * a reference on how to expose one ('local') and multiple (Online Accounts) + * providers. + * + * Plugins may also expose one or more #GtdPanel implementations. + * + * Optionally, a plugin may expose a preferences panel. See gtd_activatable_get_preferences_panel(). + */ + +G_DEFINE_INTERFACE (GtdActivatable, gtd_activatable, G_TYPE_OBJECT) + +static void +gtd_activatable_default_init (GtdActivatableInterface *iface) +{ + /** + * GtdActivatable::preferences-panel: + * + * The preferences panel of the plugin, or %NULL. + */ + g_object_interface_install_property (iface, + g_param_spec_object ("preferences-panel", + "Preferences panel", + "The preferences panel of the plugins", + GTK_TYPE_WIDGET, + G_PARAM_READABLE)); +} + +/** + * gtd_activatable_activate: + * @activatable: a #GtdActivatable + * + * Activates the extension. This is the starting point where + * the implementation does everything it needs to do. Avoid + * doing it earlier than this call. + * + * This function is called after the extension is loaded and + * the signals are connected. If you want to do anything before + * that, the _init function should be used instead. + */ +void +gtd_activatable_activate (GtdActivatable *activatable) +{ + g_return_if_fail (GTD_IS_ACTIVATABLE (activatable)); + + if (GTD_ACTIVATABLE_GET_IFACE (activatable)->activate) + GTD_ACTIVATABLE_GET_IFACE (activatable)->activate (activatable); +} + +/** + * gtd_activatable_deactivate: + * @activatable: a #GtdActivatable + * + * Deactivates the extension. Here, the extension should remove + * all providers and panels it set. + * + * This function is called before the extension is removed. At + * this point, the plugin manager already removed all providers + * and widgets this extension exported. If you want to do anything + * after the extension is removed, use GObject::finalize instead. + */ +void +gtd_activatable_deactivate (GtdActivatable *activatable) +{ + g_return_if_fail (GTD_IS_ACTIVATABLE (activatable)); + + if (GTD_ACTIVATABLE_GET_IFACE (activatable)->deactivate) + GTD_ACTIVATABLE_GET_IFACE (activatable)->deactivate (activatable); +} + +/** + * gtd_activatable_get_preferences_panel: + * @activatable: a #GtdActivatable + * + * Retrieve the preferences panel of @activatable if any. + * + * Returns: (transfer none)(nullable): a #GtkWidget, or %NULL + */ +GtkWidget* +gtd_activatable_get_preferences_panel (GtdActivatable *activatable) +{ + g_return_val_if_fail (GTD_IS_ACTIVATABLE (activatable), NULL); + + if (GTD_ACTIVATABLE_GET_IFACE (activatable)->get_preferences_panel) + return GTD_ACTIVATABLE_GET_IFACE (activatable)->get_preferences_panel (activatable); + + return NULL; +} diff --git a/src/core/gtd-activatable.h b/src/core/gtd-activatable.h new file mode 100644 index 0000000..2e26375 --- /dev/null +++ b/src/core/gtd-activatable.h @@ -0,0 +1,52 @@ +/* gtd-activatable.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_ACTIVATABLE_H +#define GTD_ACTIVATABLE_H + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_ACTIVATABLE (gtd_activatable_get_type ()) + +G_DECLARE_INTERFACE (GtdActivatable, gtd_activatable, GTD, ACTIVATABLE, GObject) + +struct _GtdActivatableInterface +{ + GTypeInterface parent; + + void (*activate) (GtdActivatable *activatable); + + void (*deactivate) (GtdActivatable *activatable); + + GtkWidget* (*get_preferences_panel) (GtdActivatable *activatable); +}; + +void gtd_activatable_activate (GtdActivatable *activatable); + +void gtd_activatable_deactivate (GtdActivatable *activatable); + +GtkWidget* gtd_activatable_get_preferences_panel (GtdActivatable *activatable); + +G_END_DECLS + +#endif /* GTD_ACTIVATABLE_H */ diff --git a/src/core/gtd-clock.c b/src/core/gtd-clock.c new file mode 100644 index 0000000..68263c6 --- /dev/null +++ b/src/core/gtd-clock.c @@ -0,0 +1,292 @@ +/* gtd-clock.c + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdClock" + +#include "gtd-clock.h" +#include "gtd-debug.h" + +#include + +struct _GtdClock +{ + GtdObject parent; + + guint timeout_id; + + GDateTime *current; + + GDBusProxy *logind; + GCancellable *cancellable; +}; + +static gboolean timeout_cb (gpointer user_data); + +G_DEFINE_TYPE (GtdClock, gtd_clock, GTD_TYPE_OBJECT) + +enum +{ + DAY_CHANGED, + HOUR_CHANGED, + MINUTE_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = { 0, }; + +/* + * Auxiliary methods + */ + +static void +update_current_date (GtdClock *self) +{ + g_autoptr (GDateTime) now = NULL; + gboolean minute_changed; + gboolean hour_changed; + gboolean day_changed; + + GTD_ENTRY; + + now = g_date_time_new_now_local (); + + day_changed = g_date_time_get_year (now) != g_date_time_get_year (self->current) || + g_date_time_get_day_of_year (now) != g_date_time_get_day_of_year (self->current); + hour_changed = day_changed || g_date_time_get_hour (now) != g_date_time_get_hour (self->current); + minute_changed = hour_changed || g_date_time_get_minute (now) != g_date_time_get_minute (self->current); + + if (day_changed) + g_signal_emit (self, signals[DAY_CHANGED], 0); + + if (hour_changed) + g_signal_emit (self, signals[HOUR_CHANGED], 0); + + if (minute_changed) + g_signal_emit (self, signals[MINUTE_CHANGED], 0); + + GTD_TRACE_MSG ("Ticking clock"); + + g_clear_pointer (&self->current, g_date_time_unref); + self->current = g_date_time_ref (now); + + GTD_EXIT; +} + +static void +schedule_update (GtdClock *self) +{ + g_autoptr (GDateTime) now = NULL; + guint seconds_between; + + /* Remove the previous timeout if we came from resume */ + if (self->timeout_id > 0) + { + g_source_remove (self->timeout_id); + self->timeout_id = 0; + } + + now = g_date_time_new_now_local (); + + seconds_between = 60 - g_date_time_get_second (now); + + self->timeout_id = g_timeout_add_seconds (seconds_between, timeout_cb, self); +} + +/* + * Callbacks + */ + +static void +logind_signal_received_cb (GDBusProxy *logind, + const gchar *sender, + const gchar *signal, + GVariant *params, + GtdClock *self) +{ + GVariant *child; + gboolean resuming; + + if (!g_str_equal (signal, "PrepareForSleep")) + return; + + child = g_variant_get_child_value (params, 0); + resuming = !g_variant_get_boolean (child); + + /* Only emit :update when resuming */ + if (resuming) + { + /* Reschedule the daily timeout */ + update_current_date (self); + schedule_update (self); + } + + g_clear_pointer (&child, g_variant_unref); +} + +static void +login_proxy_acquired_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdClock *self; + + self = GTD_CLOCK (user_data); + + gtd_object_pop_loading (GTD_OBJECT (self)); + + self->logind = g_dbus_proxy_new_for_bus_finish (res, &error); + + if (error) + { + g_warning ("Error acquiring org.freedesktop.login1: %s", error->message); + return; + } + + g_signal_connect (self->logind, + "g-signal", + G_CALLBACK (logind_signal_received_cb), + self); +} + +static gboolean +timeout_cb (gpointer user_data) +{ + GtdClock *self = user_data; + + self->timeout_id = 0; + + update_current_date (self); + schedule_update (self); + + return G_SOURCE_REMOVE; +} + + +/* + * GObject overrides + */ +static void +gtd_clock_finalize (GObject *object) +{ + GtdClock *self = (GtdClock *)object; + + g_cancellable_cancel (self->cancellable); + + if (self->timeout_id > 0) + { + g_source_remove (self->timeout_id); + self->timeout_id = 0; + } + + g_clear_pointer (&self->current, g_date_time_unref); + + g_clear_object (&self->cancellable); + g_clear_object (&self->logind); + + G_OBJECT_CLASS (gtd_clock_parent_class)->finalize (object); +} + +static void +gtd_clock_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_clock_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_clock_class_init (GtdClockClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_clock_finalize; + object_class->get_property = gtd_clock_get_property; + object_class->set_property = gtd_clock_set_property; + + /** + * GtdClock:day-changed: + * + * Emited when the day changes. + */ + signals[DAY_CHANGED] = g_signal_new ("day-changed", + GTD_TYPE_CLOCK, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + /** + * GtdClock:hour-changed: + * + * Emited when the current hour changes. + */ + signals[HOUR_CHANGED] = g_signal_new ("hour-changed", + GTD_TYPE_CLOCK, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + /** + * GtdClock:minute-changed: + * + * Emited when the current minute changes. + */ + signals[MINUTE_CHANGED] = g_signal_new ("minute-changed", + GTD_TYPE_CLOCK, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +gtd_clock_init (GtdClock *self) +{ + gtd_object_push_loading (GTD_OBJECT (self)); + + self->current = g_date_time_new_now_local (); + self->cancellable = g_cancellable_new (); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + self->cancellable, + login_proxy_acquired_cb, + self); + + schedule_update (self); +} + +GtdClock* +gtd_clock_new (void) +{ + return g_object_new (GTD_TYPE_CLOCK, NULL); +} diff --git a/src/core/gtd-clock.h b/src/core/gtd-clock.h new file mode 100644 index 0000000..ab09b0b --- /dev/null +++ b/src/core/gtd-clock.h @@ -0,0 +1,33 @@ +/* gtd-clock.h + * + * Copyright (C) 2017 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "gtd-object.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_CLOCK (gtd_clock_get_type()) + +G_DECLARE_FINAL_TYPE (GtdClock, gtd_clock, GTD, CLOCK, GtdObject) + +GtdClock* gtd_clock_new (void); + +G_END_DECLS diff --git a/src/core/gtd-log.c b/src/core/gtd-log.c new file mode 100644 index 0000000..fc2645d --- /dev/null +++ b/src/core/gtd-log.c @@ -0,0 +1,103 @@ +/* gtd-log.c + * + * Copyright (C) 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gtd-debug.h" +#include "gtd-log.h" + +#include +#include + +G_LOCK_DEFINE_STATIC (channel_lock); + +GIOChannel *standard_channel = NULL; + +static const gchar* ignored_domains[] = +{ + "GdkPixbuf", + NULL +}; + +static const gchar * +log_level_str (GLogLevelFlags log_level) +{ + switch (((gulong)log_level & G_LOG_LEVEL_MASK)) + { + case G_LOG_LEVEL_ERROR: return " \033[1;31mERROR\033[0m"; + case G_LOG_LEVEL_CRITICAL: return "\033[1;35mCRITICAL\033[0m"; + case G_LOG_LEVEL_WARNING: return " \033[1;33mWARNING\033[0m"; + case G_LOG_LEVEL_MESSAGE: return " \033[1;34mMESSAGE\033[0m"; + case G_LOG_LEVEL_INFO: return " \033[1;32mINFO\033[0m"; + case G_LOG_LEVEL_DEBUG: return " \033[1;32mDEBUG\033[0m"; + case GTD_LOG_LEVEL_TRACE: return " \033[1;36mTRACE\033[0m"; + + default: + return " UNKNOWN"; + } +} + +static void +gtd_log_handler (const gchar *domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + g_autoptr (GDateTime) now = NULL; + g_autofree gchar *buffer = NULL; + g_autofree gchar *ftime = NULL; + const gchar *level; + gint microsecond; + + /* Skip ignored log domains */ + if (domain && g_strv_contains (ignored_domains, domain)) + return; + + level = log_level_str (log_level); + now = g_date_time_new_now_local (); + ftime = g_date_time_format (now, "%H:%M:%S"); + microsecond = g_date_time_get_microsecond (now); + buffer = g_strdup_printf ("%s.%4.4d %28s: %s: %s\n", + ftime, + microsecond, + domain, + level, + message); + + /* Safely write to the channel */ + G_LOCK (channel_lock); + + g_io_channel_write_chars (standard_channel, buffer, -1, NULL, NULL); + g_io_channel_flush (standard_channel, NULL); + + G_UNLOCK (channel_lock); +} + +void +gtd_log_init (void) +{ + static gsize initialized = FALSE; + + if (g_once_init_enter (&initialized)) + { + standard_channel = g_io_channel_unix_new (STDOUT_FILENO); + + g_log_set_default_handler (gtd_log_handler, NULL); + + g_once_init_leave (&initialized, TRUE); + } +} + diff --git a/src/core/gtd-log.h b/src/core/gtd-log.h new file mode 100644 index 0000000..0ad53a8 --- /dev/null +++ b/src/core/gtd-log.h @@ -0,0 +1,27 @@ +/* gtd-log.h + * + * Copyright (C) 2017 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +void gtd_log_init (void); + +G_END_DECLS diff --git a/src/core/gtd-manager-protected.h b/src/core/gtd-manager-protected.h new file mode 100644 index 0000000..142419f --- /dev/null +++ b/src/core/gtd-manager-protected.h @@ -0,0 +1,29 @@ +/* gtd-manager-protected.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "gtd-types.h" + +G_BEGIN_DECLS + +void gtd_manager_load_plugins (GtdManager *manager); + +GtdPluginManager* gtd_manager_get_plugin_manager (GtdManager *manager); + +G_END_DECLS diff --git a/src/core/gtd-manager.c b/src/core/gtd-manager.c new file mode 100644 index 0000000..b499b76 --- /dev/null +++ b/src/core/gtd-manager.c @@ -0,0 +1,933 @@ +/* gtd-manager.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdManager" + +#include "models/gtd-list-store.h" +#include "models/gtd-task-model-private.h" +#include "gtd-clock.h" +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-manager-protected.h" +#include "gtd-notification.h" +#include "gtd-panel.h" +#include "gtd-plugin-manager.h" +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-utils.h" +#include "gtd-workspace.h" + +#include + +/** + * SECTION:gtd-manager + * @short_description:bridge between plugins and Endeavour + * @title:GtdManager + * @stability:Unstable + * @see_also:#GtdNotification,#GtdActivatable + * + * The #GtdManager object is a singleton object that exposes all the data + * inside the plugin to Endeavour, and vice-versa. From here, plugins have + * access to all the tasklists, tasks and panels of the other plugins. + * + * Objects can use gtd_manager_emit_error_message() to send errors to + * Endeavour. This will create a #GtdNotification internally. + */ + +struct _GtdManager +{ + GtdObject parent; + + GSettings *settings; + GtdPluginManager *plugin_manager; + + GListModel *inbox_model; + GListModel *lists_model; + GListModel *providers_model; + GListModel *tasks_model; + GListModel *unarchived_tasks_model; + + GList *providers; + GtdProvider *default_provider; + GtdClock *clock; + + GCancellable *cancellable; +}; + +G_DEFINE_TYPE (GtdManager, gtd_manager, GTD_TYPE_OBJECT) + +/* Singleton instance */ +static GtdManager *gtd_manager_instance = NULL; + +enum +{ + LIST_ADDED, + LIST_CHANGED, + LIST_REMOVED, + SHOW_ERROR_MESSAGE, + SHOW_NOTIFICATION, + PROVIDER_ADDED, + PROVIDER_REMOVED, + NUM_SIGNALS +}; + +enum +{ + PROP_0, + PROP_DEFAULT_PROVIDER, + PROP_CLOCK, + PROP_PLUGIN_MANAGER, + LAST_PROP +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + + +/* + * Auxiliary methods + */ + +static void +check_provider_is_default (GtdManager *self, + GtdProvider *provider) +{ + g_autofree gchar *default_provider = NULL; + + default_provider = g_settings_get_string (self->settings, "default-provider"); + + if (g_strcmp0 (default_provider, gtd_provider_get_id (provider)) == 0) + gtd_manager_set_default_provider (self, provider); +} + + +/* + * Callbacks + */ + +static gboolean +filter_archived_lists_func (gpointer item, + gpointer user_data) +{ + GtdTaskList *list; + GtdTask *task; + + task = (GtdTask*) item; + list = gtd_task_get_list (task); + + return !gtd_task_list_get_archived (list); +} + +static gboolean +filter_inbox_cb (gpointer item, + gpointer user_data) +{ + GtdProvider *provider = gtd_task_list_get_provider (item); + + return gtd_provider_get_inbox (provider) == item; +} + +static gint +compare_lists_cb (GtdTaskList *list_a, + GtdTaskList *list_b, + gpointer user_data) +{ + gint result; + + /* First, compare by their providers */ + result = gtd_provider_compare (gtd_task_list_get_provider (list_a), gtd_task_list_get_provider (list_b)); + + if (result != 0) + return result; + + return gtd_collate_compare_strings (gtd_task_list_get_name (list_a), gtd_task_list_get_name (list_b)); +} + +static void +on_task_list_modified_cb (GtdTaskList *list, + GtdTask *task, + GtdManager *self) +{ + GTD_ENTRY; + g_signal_emit (self, signals[LIST_CHANGED], 0, list); + GTD_EXIT; +} + +static void +on_list_added_cb (GtdProvider *provider, + GtdTaskList *list, + GtdManager *self) +{ + GTD_ENTRY; + + gtd_list_store_insert_sorted (GTD_LIST_STORE (self->lists_model), + list, + (GCompareDataFunc) compare_lists_cb, + self); + + g_signal_connect (list, + "task-added", + G_CALLBACK (on_task_list_modified_cb), + self); + + g_signal_connect (list, + "task-updated", + G_CALLBACK (on_task_list_modified_cb), + self); + + g_signal_connect (list, + "task-removed", + G_CALLBACK (on_task_list_modified_cb), + self); + + g_signal_emit (self, signals[LIST_ADDED], 0, list); + + GTD_EXIT; +} + +static void +on_list_changed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdManager *self) +{ + GtkFilter *filter; + + GTD_ENTRY; + + gtd_list_store_sort (GTD_LIST_STORE (self->lists_model), + (GCompareDataFunc) compare_lists_cb, + self); + + filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->unarchived_tasks_model)); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); + + g_signal_emit (self, signals[LIST_CHANGED], 0, list); + + GTD_EXIT; +} + +static void +on_list_removed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdManager *self) +{ + GTD_ENTRY; + + if (!list) + GTD_RETURN (); + + gtd_list_store_remove (GTD_LIST_STORE (self->lists_model), list); + + g_signal_handlers_disconnect_by_func (list, + on_task_list_modified_cb, + self); + + g_signal_emit (self, signals[LIST_REMOVED], 0, list); + + GTD_EXIT; +} + + +/* + * GObject overrides + */ + +static void +gtd_manager_finalize (GObject *object) +{ + GtdManager *self = (GtdManager *)object; + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + g_clear_object (&self->plugin_manager); + g_clear_object (&self->settings); + g_clear_object (&self->clock); + g_clear_object (&self->unarchived_tasks_model); + g_clear_object (&self->lists_model); + g_clear_object (&self->inbox_model); + + G_OBJECT_CLASS (gtd_manager_parent_class)->finalize (object); +} + +static void +gtd_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdManager *self = (GtdManager *) object; + + switch (prop_id) + { + case PROP_DEFAULT_PROVIDER: + g_value_set_object (value, self->default_provider); + break; + + case PROP_CLOCK: + g_value_set_object (value, self->clock); + break; + + case PROP_PLUGIN_MANAGER: + g_value_set_object (value, self->plugin_manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdManager *self = (GtdManager *) object; + + switch (prop_id) + { + case PROP_DEFAULT_PROVIDER: + if (g_set_object (&self->default_provider, g_value_get_object (value))) + g_object_notify (object, "default-provider"); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_manager_class_init (GtdManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_manager_finalize; + object_class->get_property = gtd_manager_get_property; + object_class->set_property = gtd_manager_set_property; + + /** + * GtdManager::default-provider: + * + * The default provider. + */ + g_object_class_install_property ( + object_class, + PROP_DEFAULT_PROVIDER, + g_param_spec_object ("default-provider", + "The default provider of the application", + "The default provider of the application", + GTD_TYPE_PROVIDER, + G_PARAM_READWRITE)); + + /** + * GtdManager::clock: + * + * The underlying clock of Endeavour. + */ + g_object_class_install_property ( + object_class, + PROP_CLOCK, + g_param_spec_object ("clock", + "The clock", + "The clock of the application", + GTD_TYPE_CLOCK, + G_PARAM_READABLE)); + + /** + * GtdManager::plugin-manager: + * + * The plugin manager. + */ + g_object_class_install_property ( + object_class, + PROP_PLUGIN_MANAGER, + g_param_spec_object ("plugin-manager", + "The plugin manager", + "The plugin manager of the application", + GTD_TYPE_PLUGIN_MANAGER, + G_PARAM_READABLE)); + + /** + * GtdManager::list-added: + * @manager: a #GtdManager + * @list: a #GtdTaskList + * + * The ::list-added signal is emmited after a #GtdTaskList + * is connected. + */ + signals[LIST_ADDED] = g_signal_new ("list-added", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + /** + * GtdManager::list-changed: + * @manager: a #GtdManager + * @list: a #GtdTaskList + * + * The ::list-changed signal is emmited after a #GtdTaskList + * has any of it's properties changed. + */ + signals[LIST_CHANGED] = g_signal_new ("list-changed", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + /** + * GtdManager::list-removed: + * @manager: a #GtdManager + * @list: a #GtdTaskList + * + * The ::list-removed signal is emmited after a #GtdTaskList + * is disconnected. + */ + signals[LIST_REMOVED] = g_signal_new ("list-removed", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + /** + * GtdManager::show-error-message: + * @manager: a #GtdManager + * @primary_text: the primary message + * @secondary_text: the detailed explanation of the error or the text to the notification button. + * @action : optionally action of type GtdNotificationActionFunc ignored if it's null. + * @user_data : user data passed to the action. + * + * Notifies about errors, and sends the error message for widgets + * to display. + */ + signals[SHOW_ERROR_MESSAGE] = g_signal_new ("show-error-message", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 4, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_POINTER, + G_TYPE_POINTER); + + /** + * GtdManager::show-notification: + * @manager: a #GtdManager + * @notification: the #GtdNotification + * + * Sends a notification. + */ + signals[SHOW_NOTIFICATION] = g_signal_new ("show-notification", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_NOTIFICATION); + + /** + * GtdManager::provider-added: + * @manager: a #GtdManager + * @provider: a #GtdProvider + * + * The ::provider-added signal is emmited after a #GtdProvider + * is added. + */ + signals[PROVIDER_ADDED] = g_signal_new ("provider-added", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PROVIDER); + + /** + * GtdManager::provider-removed: + * @manager: a #GtdManager + * @provider: a #GtdProvider + * + * The ::provider-removed signal is emmited after a #GtdProvider + * is removed from the list. + */ + signals[PROVIDER_REMOVED] = g_signal_new ("provider-removed", + GTD_TYPE_MANAGER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PROVIDER); +} + + +static void +gtd_manager_init (GtdManager *self) +{ + GtkCustomFilter *archived_lists_filter; + GtkCustomFilter *inbox_filter; + + inbox_filter = gtk_custom_filter_new (filter_inbox_cb, self, NULL); + archived_lists_filter = gtk_custom_filter_new (filter_archived_lists_func, self, NULL); + + self->settings = g_settings_new ("org.gnome.todo"); + self->plugin_manager = gtd_plugin_manager_new (); + self->clock = gtd_clock_new (); + self->cancellable = g_cancellable_new (); + self->lists_model = (GListModel*) gtd_list_store_new (GTD_TYPE_TASK_LIST); + self->inbox_model = (GListModel*) gtk_filter_list_model_new (self->lists_model, + GTK_FILTER (inbox_filter)); + self->tasks_model = (GListModel*) _gtd_task_model_new (self); + self->unarchived_tasks_model = (GListModel*) gtk_filter_list_model_new (self->tasks_model, + GTK_FILTER (archived_lists_filter)); + self->providers_model = (GListModel*) gtd_list_store_new (GTD_TYPE_PROVIDER); +} + +/** + * gtd_manager_get_default: + * + * Retrieves the singleton #GtdManager instance. You should always + * use this function instead of @gtd_manager_new. + * + * Returns: (transfer none): the singleton #GtdManager instance. + */ +GtdManager* +gtd_manager_get_default (void) +{ + if (!gtd_manager_instance) + gtd_manager_instance = gtd_manager_new (); + + return gtd_manager_instance; +} + +GtdManager* +gtd_manager_new (void) +{ + return g_object_new (GTD_TYPE_MANAGER, NULL); +} + +/** + * gtd_manager_get_providers: + * @manager: a #GtdManager + * + * Retrieves the list of available #GtdProvider. + * + * Returns: (transfer container) (element-type Gtd.Provider): a newly allocated #GList of + * #GtdStorage. Free with @g_list_free after use. + */ +GList* +gtd_manager_get_providers (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return g_list_copy (self->providers); +} + +/** + * gtd_manager_add_provider: + * @self: a #GtdManager + * @provider: a #GtdProvider + * + * Adds @provider to the list of providers. + */ +void +gtd_manager_add_provider (GtdManager *self, + GtdProvider *provider) +{ + g_autoptr (GList) lists = NULL; + GList *l; + + g_return_if_fail (GTD_IS_MANAGER (self)); + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + GTD_ENTRY; + + self->providers = g_list_prepend (self->providers, provider); + gtd_list_store_append (GTD_LIST_STORE (self->providers_model), provider); + + /* Add lists */ + lists = gtd_provider_get_task_lists (provider); + + for (l = lists; l != NULL; l = l->next) + on_list_added_cb (provider, l->data, self); + + g_object_connect (provider, + "signal::list-added", G_CALLBACK (on_list_added_cb), self, + "signal::list-changed", G_CALLBACK (on_list_changed_cb), self, + "signal::list-removed", G_CALLBACK (on_list_removed_cb), self, + NULL); + + /* If we just added the default provider, update the property */ + check_provider_is_default (self, provider); + + g_signal_emit (self, signals[PROVIDER_ADDED], 0, provider); + + GTD_EXIT; +} + +/** + * gtd_manager_remove_provider: + * @self: a #GtdManager + * @provider: a #GtdProvider + * + * Removes @provider from the list of providers. + */ +void +gtd_manager_remove_provider (GtdManager *self, + GtdProvider *provider) +{ + g_autoptr (GList) lists = NULL; + GList *l; + + g_return_if_fail (GTD_IS_MANAGER (self)); + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + GTD_ENTRY; + + self->providers = g_list_remove (self->providers, provider); + gtd_list_store_remove (GTD_LIST_STORE (self->providers_model), provider); + + /* Remove lists */ + lists = gtd_provider_get_task_lists (provider); + + for (l = lists; l != NULL; l = l->next) + on_list_removed_cb (provider, l->data, self); + + g_signal_handlers_disconnect_by_func (provider, on_list_added_cb, self); + g_signal_handlers_disconnect_by_func (provider, on_list_changed_cb, self); + g_signal_handlers_disconnect_by_func (provider, on_list_removed_cb, self); + + g_signal_emit (self, signals[PROVIDER_REMOVED], 0, provider); + + GTD_EXIT; +} + + +/** + * gtd_manager_get_default_provider: + * @manager: a #GtdManager + * + * Retrieves the default provider location. Default is "local". + * + * Returns: (transfer none): the default provider. + */ +GtdProvider* +gtd_manager_get_default_provider (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->default_provider; +} + +/** + * gtd_manager_set_default_provider: + * @manager: a #GtdManager + * @provider: (nullable): the default provider. + * + * Sets the provider. + */ +void +gtd_manager_set_default_provider (GtdManager *self, + GtdProvider *provider) +{ + g_return_if_fail (GTD_IS_MANAGER (self)); + + if (!g_set_object (&self->default_provider, provider)) + return; + + g_settings_set_string (self->settings, + "default-provider", + provider ? gtd_provider_get_id (provider) : "local"); + + g_object_notify (G_OBJECT (self), "default-provider"); +} + +/** + * gtd_manager_get_inbox: + * @self: a #GtdManager + * + * Retrieves the local inbox. + * + * Returns: (transfer none)(nullable): a #GtdTaskList + */ +GtdTaskList* +gtd_manager_get_inbox (GtdManager *self) +{ + GList *l; + + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + for (l = self->providers; l; l = l->next) + { + if (g_str_equal (gtd_provider_get_id (l->data), "local")) + return gtd_provider_get_inbox (l->data); + } + + return NULL; +} + +/** + * gtd_manager_get_settings: + * @manager: a #GtdManager + * + * Retrieves the internal #GSettings from @manager. + * + * Returns: (transfer none): the internal #GSettings of @manager + */ +GSettings* +gtd_manager_get_settings (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->settings; +} + +/** + * gtd_manager_get_is_first_run: + * @manager: a #GtdManager + * + * Retrieves the 'first-run' setting. + * + * Returns: %TRUE if Endeavour was never run before, %FALSE otherwise. + */ +gboolean +gtd_manager_get_is_first_run (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), FALSE); + + return g_settings_get_boolean (self->settings, "first-run"); +} + +/** + * gtd_manager_set_is_first_run: + * @manager: a #GtdManager + * @is_first_run: %TRUE to make it first run, %FALSE otherwise. + * + * Sets the 'first-run' setting. + */ +void +gtd_manager_set_is_first_run (GtdManager *self, + gboolean is_first_run) +{ + g_return_if_fail (GTD_IS_MANAGER (self)); + + g_settings_set_boolean (self->settings, "first-run", is_first_run); +} + +/** + * gtd_manager_emit_error_message: + * @self: a #GtdManager + * @title: (nullable): the title of the error + * @description: (nullable): detailed description of the error + * @function: (scope call)(nullable): function to be called when the notification is dismissed + * @user_data: user data + * + * Reports an error. + */ +void +gtd_manager_emit_error_message (GtdManager *self, + const gchar *title, + const gchar *description, + GtdErrorActionFunc function, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_MANAGER (self)); + + g_signal_emit (self, + signals[SHOW_ERROR_MESSAGE], + 0, + title, + description, + function, + user_data); +} + +/** + * gtd_manager_send_notification: + * @self: a #GtdManager + * @notification: a #GtdNotification + * + * Sends a notification to the notification system. + */ +void +gtd_manager_send_notification (GtdManager *self, + GtdNotification *notification) +{ + g_return_if_fail (GTD_IS_MANAGER (self)); + + g_signal_emit (self, signals[SHOW_NOTIFICATION], 0, notification); +} + +/** + * gtd_manager_get_clock: + * @self: a #GtdManager + * + * Retrieves the #GtdClock from @self. You can use the + * clock to know when your code should be updated. + * + * Returns: (transfer none): a #GtdClock + */ +GtdClock* +gtd_manager_get_clock (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->clock; +} + +/** + * gtd_manager_get_task_lists_model: + * @self: a #GtdManager + * + * Retrieves the #GListModel containing #GtdTaskLists from + * @self. You can use the this model to bind to GtkListBox + * or other widgets. + * + * The model is sorted. + * + * Returns: (transfer none): a #GListModel + */ +GListModel* +gtd_manager_get_task_lists_model (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->lists_model; +} + + +/** + * gtd_manager_get_all_tasks_model: + * @self: a #GtdManager + * + * Retrieves the #GListModel containing #GtdTasks from + * @self. You can use the this model to bind to GtkListBox + * or other widgets. + * + * The model is sorted. + * + * Returns: (transfer none): a #GListModel + */ +GListModel* +gtd_manager_get_all_tasks_model (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->tasks_model; +} + +/** + * gtd_manager_get_tasks_model: + * @self: a #GtdManager + * + * Retrieves the #GListModel containing #GtdTasks from + * @self. You can use the this model to bind to GtkListBox + * or other widgets. + * + * The model returned by this function is filtered to only + * contain tasks from unarchived lists. If you need all tasks, + * see gtd_manager_get_all_tasks_model(). + * + * The model is sorted. + * + * Returns: (transfer none): a #GListModel + */ +GListModel* +gtd_manager_get_tasks_model (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->unarchived_tasks_model; +} + +/** + * gtd_manager_get_inbox_model: + * @self: a #GtdManager + * + * Retrieves the #GListModel containing #GtdTaskLists that are + * inbox. + * + * Returns: (transfer none): a #GListModel + */ +GListModel* +gtd_manager_get_inbox_model (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->inbox_model; +} + +/** + * gtd_manager_get_providers_model: + * @self: a #GtdManager + * + * Retrieves the #GListModel containing #GtdProviders. + * + * Returns: (transfer none): a #GListModel + */ +GListModel* +gtd_manager_get_providers_model (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->providers_model; +} + +void +gtd_manager_load_plugins (GtdManager *self) +{ + GTD_ENTRY; + + gtd_plugin_manager_load_plugins (self->plugin_manager); + + GTD_EXIT; +} + +GtdPluginManager* +gtd_manager_get_plugin_manager (GtdManager *self) +{ + g_return_val_if_fail (GTD_IS_MANAGER (self), NULL); + + return self->plugin_manager; +} diff --git a/src/core/gtd-manager.h b/src/core/gtd-manager.h new file mode 100644 index 0000000..68d661b --- /dev/null +++ b/src/core/gtd-manager.h @@ -0,0 +1,86 @@ +/* gtd-manager.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "gtd-object.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_MANAGER (gtd_manager_get_type()) + +G_DECLARE_FINAL_TYPE (GtdManager, gtd_manager, GTD, MANAGER, GtdObject) + +/** + * GtdErrorActionFunc: + */ +typedef void (*GtdErrorActionFunc) (GtdNotification *notification, + gpointer user_data); + + +GtdManager* gtd_manager_new (void); + +GtdManager* gtd_manager_get_default (void); + +GList* gtd_manager_get_providers (GtdManager *manager); + +void gtd_manager_add_provider (GtdManager *self, + GtdProvider *provider); + +void gtd_manager_remove_provider (GtdManager *self, + GtdProvider *provider); + +GtdProvider* gtd_manager_get_default_provider (GtdManager *manager); + +void gtd_manager_set_default_provider (GtdManager *manager, + GtdProvider *provider); + +GtdTaskList* gtd_manager_get_inbox (GtdManager *self); + +GSettings* gtd_manager_get_settings (GtdManager *manager); + +gboolean gtd_manager_get_is_first_run (GtdManager *manager); + +void gtd_manager_set_is_first_run (GtdManager *manager, + gboolean is_first_run); + +void gtd_manager_emit_error_message (GtdManager *self, + const gchar *title, + const gchar *description, + GtdErrorActionFunc function, + gpointer user_data); + +void gtd_manager_send_notification (GtdManager *self, + GtdNotification *notification); + +GtdClock* gtd_manager_get_clock (GtdManager *self); + +GListModel* gtd_manager_get_task_lists_model (GtdManager *self); + +GListModel* gtd_manager_get_all_tasks_model (GtdManager *self); + +GListModel* gtd_manager_get_tasks_model (GtdManager *self); + +GListModel* gtd_manager_get_inbox_model (GtdManager *self); + +GListModel* gtd_manager_get_providers_model (GtdManager *self); + +G_END_DECLS diff --git a/src/core/gtd-notification.c b/src/core/gtd-notification.c new file mode 100644 index 0000000..eee3e46 --- /dev/null +++ b/src/core/gtd-notification.c @@ -0,0 +1,441 @@ +/* gtd-notification.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * Copyright (C) 2022 Jamie Murphy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdNotification" + +#include "gtd-notification.h" +#include "gtd-object.h" + +#include + +/** + * SECTION: gtd-notification + * @short_description: An auxiliary class around #AdwToast + * @title: GtdNotification + * @stability: Stable + * + * The #GtdNotification represents a notification shown at the top of + * the window. It is an auxiliary class around #AdwToast, and is used only + * to store toast data and callbacks. + * + * A notification may have a dismissal action that is called when the toast + * is dismissed, whether by user interaction or a timeout + * + * Optionally, the notification may have a secondary action + * (see gtd_notification_set_secondary_action()), shown as a button. + * + * A user should not create a UI for a #GtdNotification and should instead + * pass it to a #GtdManager, via gtd_manager_send_notification() + * + * Example: + * |[ + * GtdNotification *notification; + * + * notification = gtd_notification_new ("Something happened!"); + * + * gtd_notification_set_dismissal_action (notification, + * called_when_notification_is_dismissed, + * self); + * + * gtd_notification_set_secondary_action (notification, + * "Details", + * show_details, + * self); + * [...] + * ]| + */ + +typedef struct +{ + gchar *text; + + GtdNotificationActionFunc dismissal_action; + gboolean has_dismissal_action; + gpointer dismissal_action_data; + + GtdNotificationActionFunc secondary_action; + gboolean has_secondary_action; + gpointer secondary_action_data; + gchar *secondary_action_name; +} GtdNotificationPrivate; + +struct _GtdNotification +{ + GtdObject parent; + + /*< private >*/ + GtdNotificationPrivate *priv; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdNotification, gtd_notification, GTD_TYPE_OBJECT) + +enum +{ + PROP_0, + PROP_HAS_DISMISSAL_ACTION, + PROP_HAS_SECONDARY_ACTION, + PROP_SECONDARY_ACTION_NAME, + PROP_TEXT, + LAST_PROP +}; + +enum +{ + EXECUTED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +static void +gtd_notification_finalize (GObject *object) +{ + GtdNotification *self = (GtdNotification *)object; + GtdNotificationPrivate *priv = gtd_notification_get_instance_private (self); + + g_clear_pointer (&priv->secondary_action_name, g_free); + g_clear_pointer (&priv->text, g_free); + + G_OBJECT_CLASS (gtd_notification_parent_class)->finalize (object); +} + +static void +gtd_notification_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdNotification *self = GTD_NOTIFICATION (object); + + switch (prop_id) + { + case PROP_HAS_DISMISSAL_ACTION: + g_value_set_boolean (value, self->priv->has_dismissal_action); + break; + + case PROP_HAS_SECONDARY_ACTION: + g_value_set_boolean (value, self->priv->has_secondary_action); + break; + + case PROP_SECONDARY_ACTION_NAME: + g_value_set_string (value, self->priv->secondary_action_name ? self->priv->secondary_action_name : ""); + break; + + case PROP_TEXT: + g_value_set_string (value, gtd_notification_get_text (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_notification_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdNotification *self = GTD_NOTIFICATION (object); + + switch (prop_id) + { + case PROP_SECONDARY_ACTION_NAME: + gtd_notification_set_secondary_action (self, + g_value_get_string (value), + self->priv->secondary_action, + self->priv->secondary_action_data); + break; + + case PROP_TEXT: + gtd_notification_set_text (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_notification_class_init (GtdNotificationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_notification_finalize; + object_class->get_property = gtd_notification_get_property; + object_class->set_property = gtd_notification_set_property; + + /** + * GtdNotification::has-dismissal-action: + * + * @TRUE if the notification has a dismissal action or @FALSE otherwise. + */ + g_object_class_install_property ( + object_class, + PROP_HAS_DISMISSAL_ACTION, + g_param_spec_boolean ("has-dismissal-action", + "Whether the notification has a dismissal action", + "Whether the notification has the dismissal action.", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * GtdNotification::has-secondary-action: + * + * @TRUE if the notification has a secondary action or @FALSE otherwise. The + * secondary action is triggered only by user explicit input. + */ + g_object_class_install_property ( + object_class, + PROP_HAS_SECONDARY_ACTION, + g_param_spec_boolean ("has-secondary-action", + "Whether the notification has a secondary action", + "Whether the notification has the secondary action, activated by the user", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * GtdNotification::secondary-action-name: + * + * The main text of the notification, usually a markuped text. + */ + g_object_class_install_property ( + object_class, + PROP_SECONDARY_ACTION_NAME, + g_param_spec_string ("secondary-action-name", + "Text of the secondary action button", + "The text of the secondary action button", + "", + G_PARAM_READWRITE)); + + /** + * GtdNotification::text: + * + * The main text of the notification, usually a markuped text. + */ + g_object_class_install_property ( + object_class, + PROP_TEXT, + g_param_spec_string ("text", + "Notification message", + "The main message of the notification", + "", + G_PARAM_READWRITE)); + + /** + * GtdNotification::executed: + * + * The ::executed signal is emitted after the primary or secondary + * #GtdNotification action is executed. + */ + signals[EXECUTED] = g_signal_new ("executed", + GTD_TYPE_NOTIFICATION, + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + +static void +gtd_notification_init (GtdNotification *self) +{ + self->priv = gtd_notification_get_instance_private (self); + self->priv->secondary_action_name = NULL; + self->priv->text = NULL; +} + +/** + * gtd_notification_new: + * @text: (nullable): text of the notification + * + * Creates a new notification with @text. + * + * Returns: (transfer full): a new #GtdNotification + */ +GtdNotification* +gtd_notification_new (const gchar *text) +{ + return g_object_new (GTD_TYPE_NOTIFICATION, + "text", text, + NULL); +} + +/** + * gtd_notification_set_dismissal_action: + * @notification: a #GtdNotification + * @func: (closure user_data) (scope call) (nullable): the dismissal action function + * @user_data: data passed to @func + * + * Sets the dismissal action of @notification + */ +void +gtd_notification_set_dismissal_action (GtdNotification *notification, + GtdNotificationActionFunc func, + gpointer user_data) +{ + GtdNotificationPrivate *priv; + gboolean has_action; + + g_return_if_fail (GTD_IS_NOTIFICATION (notification)); + + priv = notification->priv; + has_action = (func != NULL); + + if (has_action != priv->has_dismissal_action) + { + priv->has_dismissal_action = has_action; + + priv->dismissal_action = has_action ? func : NULL; + priv->dismissal_action_data = has_action ? user_data : NULL; + + g_object_notify (G_OBJECT (notification), "has-dismissal-action"); + } +} + +/** + * gtd_notification_set_secondary_action: + * @notification: a #GtdNotification + * @name: the name of the secondary action + * @func: (closure user_data) (scope call) (nullable): the secondary action function + * @user_data: data passed to @func + * + * Sets the secondary action of @notification, which is triggered + * only on user explicit input. + */ +void +gtd_notification_set_secondary_action (GtdNotification *notification, + const gchar *name, + GtdNotificationActionFunc func, + gpointer user_data) +{ + GtdNotificationPrivate *priv; + gboolean has_action; + + g_return_if_fail (GTD_IS_NOTIFICATION (notification)); + + priv = notification->priv; + has_action = (func != NULL); + + if (has_action != priv->has_secondary_action) + { + priv->has_secondary_action = has_action; + + priv->secondary_action = has_action ? func : NULL; + priv->secondary_action_data = has_action ? user_data : NULL; + + if (priv->secondary_action_name != name) + { + g_clear_pointer (&priv->secondary_action_name, g_free); + priv->secondary_action_name = g_strdup (name); + + g_object_notify (G_OBJECT (notification), "secondary-action-name"); + } + + g_object_notify (G_OBJECT (notification), "has-secondary-action"); + } +} + +/** + * gtd_notification_get_text: + * @notification: a #GtdNotification + * + * Gets the text of @notification. + * + * Returns: (transfer none): the text of @notification. + */ +const gchar* +gtd_notification_get_text (GtdNotification *notification) +{ + g_return_val_if_fail (GTD_IS_NOTIFICATION (notification), NULL); + + return notification->priv->text ? notification->priv->text : ""; +} + +/** + * gtd_notification_set_text: + * @notification: a #GtdNotification + * @text: the user-visible text of @notification + * + * Sets the text of @notification to @text. + */ +void +gtd_notification_set_text (GtdNotification *notification, + const gchar *text) +{ + GtdNotificationPrivate *priv; + + g_return_if_fail (GTD_IS_NOTIFICATION (notification)); + + priv = notification->priv; + + if (g_strcmp0 (priv->text, text) != 0) + { + g_clear_pointer (&priv->text, g_free); + priv->text = g_strdup (text); + + g_object_notify (G_OBJECT (notification), "text"); + } +} + +/** + * gtd_notification_execute_dismissal_action: + * @notification: a #GtdNotification + * + * Executes the dismissal action of @notification if any. + */ +void +gtd_notification_execute_dismissal_action (GtdNotification *notification) +{ + GtdNotificationPrivate *priv; + + g_return_if_fail (GTD_IS_NOTIFICATION (notification)); + + priv = notification->priv; + + if (priv->dismissal_action) + priv->dismissal_action (notification, priv->dismissal_action_data); + + g_signal_emit (notification, signals[EXECUTED], 0); +} + +/** + * gtd_notification_execute_secondary_action: + * @notification: a #GtdNotification + * + * Executes the secondary action of @notification if any. + */ +void +gtd_notification_execute_secondary_action (GtdNotification *notification) +{ + GtdNotificationPrivate *priv; + + g_return_if_fail (GTD_IS_NOTIFICATION (notification)); + + priv = notification->priv; + + if (priv->secondary_action) + { + priv->secondary_action (notification, priv->secondary_action_data); + + g_signal_emit (notification, signals[EXECUTED], 0); + } +} diff --git a/src/core/gtd-notification.h b/src/core/gtd-notification.h new file mode 100644 index 0000000..fd420ad --- /dev/null +++ b/src/core/gtd-notification.h @@ -0,0 +1,68 @@ +/* gtd-notification.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_NOTIFICATION_H +#define GTD_NOTIFICATION_H + +#include "gtd-object.h" +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_NOTIFICATION (gtd_notification_get_type()) + +G_DECLARE_FINAL_TYPE (GtdNotification, gtd_notification, GTD, NOTIFICATION, GtdObject) + +/** + * GtdNotificationActionFunc: + * @notification: the #GtdNotification running the function + * @user_data: (closure): user data + * + * Will be called when the dismissal or secondary action of @notification + * is executed. + */ +typedef void (*GtdNotificationActionFunc) (GtdNotification *notification, + gpointer user_data); + + +GtdNotification* gtd_notification_new (const gchar *text); + +void gtd_notification_execute_dismissal_action (GtdNotification *notification); + +void gtd_notification_execute_secondary_action (GtdNotification *notification); + +void gtd_notification_set_dismissal_action (GtdNotification *notification, + GtdNotificationActionFunc func, + gpointer user_data); + +void gtd_notification_set_secondary_action (GtdNotification *notification, + const gchar *name, + GtdNotificationActionFunc func, + gpointer user_data); + +const gchar* gtd_notification_get_text (GtdNotification *notification); + +void gtd_notification_set_text (GtdNotification *notification, + const gchar *text); + +G_END_DECLS + +#endif /* GTD_NOTIFICATION_H */ diff --git a/src/core/gtd-object.c b/src/core/gtd-object.c new file mode 100644 index 0000000..c1157fc --- /dev/null +++ b/src/core/gtd-object.c @@ -0,0 +1,313 @@ +/* gtd-object.c + * + * Copyright © 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdObject" + +#include "gtd-object.h" + +#include + +/** + * SECTION:gtd-object + * @Short_description: base class for loadable and uniquely identifiable objects + * @Title: GtdObject + * + * #GtdObject is the base class of many object in Endeavour, and it useful for + * when a given object is loadable and/or uniquely identifiable. Some examples of + * it are #GtdTask, #GtdTaskList and #GtdNotification. + * + */ + +typedef struct +{ + guint64 loading; + gchar *uid; +} GtdObjectPrivate; + + +G_DEFINE_TYPE_WITH_PRIVATE (GtdObject, gtd_object, G_TYPE_OBJECT) + + +enum +{ + PROP_0, + PROP_LOADING, + PROP_UID, + N_PROPS +}; + + +static GParamSpec *properties[N_PROPS] = { NULL, }; + + +static const gchar* +gtd_object_real_get_uid (GtdObject *object) +{ + GtdObjectPrivate *priv; + + g_return_val_if_fail (GTD_IS_OBJECT (object), NULL); + + priv = gtd_object_get_instance_private (object); + + return priv->uid; +} + +static void +gtd_object_real_set_uid (GtdObject *object, + const gchar *uid) +{ + GtdObjectPrivate *priv; + + g_assert (GTD_IS_OBJECT (object)); + + priv = gtd_object_get_instance_private (object); + + if (g_strcmp0 (priv->uid, uid) == 0) + return; + + g_clear_pointer (&priv->uid, g_free); + priv->uid = g_strdup (uid); + + g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_UID]); +} + + +/* + * GObject overrides + */ + +static void +gtd_object_finalize (GObject *object) +{ + GtdObject *self = GTD_OBJECT (object); + GtdObjectPrivate *priv = gtd_object_get_instance_private (self); + + g_clear_pointer (&priv->uid, g_free); + + G_OBJECT_CLASS (gtd_object_parent_class)->finalize (object); +} + +static void +gtd_object_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdObject *self = GTD_OBJECT (object); + GtdObjectPrivate *priv = gtd_object_get_instance_private (self); + + switch (prop_id) + { + case PROP_LOADING: + g_value_set_boolean (value, priv->loading > 0); + break; + + case PROP_UID: + g_value_set_string (value, GTD_OBJECT_GET_CLASS (self)->get_uid (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_object_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdObject *self = GTD_OBJECT (object); + + switch (prop_id) + { + case PROP_UID: + GTD_OBJECT_GET_CLASS (self)->set_uid (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_object_class_init (GtdObjectClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + klass->get_uid = gtd_object_real_get_uid; + klass->set_uid = gtd_object_real_set_uid; + + object_class->finalize = gtd_object_finalize; + object_class->get_property = gtd_object_get_property; + object_class->set_property = gtd_object_set_property; + + /** + * GtdObject::uid: + * + * The unique identified of the object, set by the backend. + */ + properties[PROP_UID] = g_param_spec_string ("uid", + "Unique identifier of the object", + "The unique identifier of the object, defined by the backend", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdObject::loading: + * + * Whether the object is loading or not. + */ + properties[PROP_LOADING] = g_param_spec_boolean ("loading", + "Loading state of the object", + "Whether the object is loading or not", + TRUE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_object_init (GtdObject *self) +{ +} + +/** + * gtd_object_new: + * @uid: unique identifier of the object + * + * Creates a new #GtdObject object. + * + * Returns: (transfer full): a new #GtdObject + */ +GtdObject* +gtd_object_new (const gchar *uid) +{ + return g_object_new (GTD_TYPE_OBJECT, "uid", uid, NULL); +} + +/** + * gtd_object_get_uid: + * @object: a #GtdObject + * + * Retrieves the internal unique identifier of @object. + * + * Returns: (transfer none): the unique identifier of @object. Do + * not free after usage. + */ +const gchar* +gtd_object_get_uid (GtdObject *object) +{ + GtdObjectClass *class; + + g_return_val_if_fail (GTD_IS_OBJECT (object), NULL); + + class = GTD_OBJECT_GET_CLASS (object); + + g_assert (class); + return class->get_uid (object); +} + +/** + * gtd_object_set_uid: + * @object: a #GtdObject + * @uid: the unique identifier of @object + * + * Sets the unique identifier of @object to @uid. Only + * a #GtdBackend should do it. + */ +void +gtd_object_set_uid (GtdObject *object, + const gchar *uid) +{ + GtdObjectClass *class; + + g_return_if_fail (GTD_IS_OBJECT (object)); + + class = GTD_OBJECT_GET_CLASS (object); + + g_assert (class); + class->set_uid (object, uid); +} + +/** + * gtd_object_get_loading: + * @object: a #GtdObject + * + * Whether @object is loading or not. + * + * Returns: %TRUE if @object is loading, %FALSE otherwise. + */ +gboolean +gtd_object_get_loading (GtdObject *object) +{ + GtdObjectPrivate *priv; + + g_return_val_if_fail (GTD_IS_OBJECT (object), FALSE); + + priv = gtd_object_get_instance_private (object); + + return priv->loading > 0; +} + +/** + * gtd_object_push_loading: + * @object: a #GtdObject + * + * Increases the loading counter of @object by one. The object is marked + * as loading while the loading counter is greater than zero. + */ +void +gtd_object_push_loading (GtdObject *object) +{ + GtdObjectPrivate *priv; + + g_return_if_fail (GTD_IS_OBJECT (object)); + + priv = gtd_object_get_instance_private (object); + + priv->loading++; + + if (priv->loading == 1) + g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_LOADING]); +} + +/** + * gtd_object_pop_loading: + * @object: a #GtdObject + * + * Decreases the loading counter of @object by one. The object is marked + * as loading while the loading counter is greater than zero. + * + * It is a programming error to pop more times then push the loading the + * counter. + */ +void +gtd_object_pop_loading (GtdObject *object) +{ + GtdObjectPrivate *priv; + + g_return_if_fail (GTD_IS_OBJECT (object)); + + priv = gtd_object_get_instance_private (object); + + priv->loading--; + + if (priv->loading == 0) + g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_LOADING]); +} diff --git a/src/core/gtd-object.h b/src/core/gtd-object.h new file mode 100644 index 0000000..c0033d3 --- /dev/null +++ b/src/core/gtd-object.h @@ -0,0 +1,54 @@ +/* gtd-object.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_OBJECT (gtd_object_get_type()) + +G_DECLARE_DERIVABLE_TYPE (GtdObject, gtd_object, GTD, OBJECT, GObject) + +struct _GtdObjectClass +{ + GObjectClass parent; + + /* public */ + const gchar* (* get_uid) (GtdObject *object); + void (* set_uid) (GtdObject *object, + const gchar *uid); +}; + +GtdObject* gtd_object_new (const gchar *uid); + +const gchar* gtd_object_get_uid (GtdObject *object); + +void gtd_object_set_uid (GtdObject *object, + const gchar *uid); + +gboolean gtd_object_get_loading (GtdObject *object); + +void gtd_object_push_loading (GtdObject *object); + +void gtd_object_pop_loading (GtdObject *object); + +G_END_DECLS diff --git a/src/core/gtd-plugin-manager.c b/src/core/gtd-plugin-manager.c new file mode 100644 index 0000000..e159916 --- /dev/null +++ b/src/core/gtd-plugin-manager.c @@ -0,0 +1,323 @@ +/* gtd-plugin-manager.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPluginManager" + +#include "gtd-activatable.h" +#include "gtd-manager.h" +#include "gtd-panel.h" +#include "gtd-provider.h" +#include "gtd-plugin-manager.h" + +#include + +struct _GtdPluginManager +{ + GtdObject parent; + + GHashTable *info_to_extension; +}; + +G_DEFINE_TYPE (GtdPluginManager, gtd_plugin_manager, GTD_TYPE_OBJECT) + +enum +{ + PLUGIN_LOADED, + PLUGIN_UNLOADED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +static const gchar * const default_plugins[] = { + "all-tasks-panel", + "eds", + "task-lists-workspace", + "today-panel", + "inbox-panel", + "next-week-panel", + "peace", +}; + +static gboolean +gtd_str_equal0 (gconstpointer a, + gconstpointer b) +{ + if (a == b) + return TRUE; + else if (!a || !b) + return FALSE; + else + return g_str_equal (a, b); +} + +static gchar** +get_loaded_extensions (const gchar **extensions) +{ + g_autoptr (GPtrArray) loaded_plugins = NULL; + gsize i; + + loaded_plugins = g_ptr_array_new (); + + for (i = 0; extensions && extensions[i]; i++) + g_ptr_array_add (loaded_plugins, g_strdup (extensions[i])); + + for (i = 0; i < G_N_ELEMENTS (default_plugins); i++) + { + if (g_ptr_array_find_with_equal_func (loaded_plugins, + default_plugins[i], + gtd_str_equal0, + NULL)) + { + continue; + } + + g_ptr_array_add (loaded_plugins, g_strdup (default_plugins[i])); + } + + g_ptr_array_add (loaded_plugins, NULL); + + return (gchar**) g_ptr_array_free (g_steal_pointer (&loaded_plugins), FALSE); +} + +static gboolean +from_gsetting_to_property_func (GValue *value, + GVariant *variant, + gpointer user_data) +{ + g_autofree const gchar **extensions = NULL; + g_autofree gchar **loaded_extensions = NULL; + + extensions = g_variant_get_strv (variant, NULL); + loaded_extensions = get_loaded_extensions (extensions); + + g_value_take_boxed (value, g_steal_pointer (&loaded_extensions)); + + return TRUE; +} + +static GVariant* +from_property_to_gsetting_func (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + g_autofree gchar **loaded_extensions = NULL; + const gchar **extensions = NULL; + + extensions = g_value_get_boxed (value); + loaded_extensions = get_loaded_extensions (extensions); + + return g_variant_new_strv ((const gchar * const *)loaded_extensions, -1); +} + +static void +on_plugin_unloaded_cb (PeasEngine *engine, + PeasPluginInfo *info, + GtdPluginManager *self) +{ + GtdActivatable *activatable; + + activatable = g_hash_table_lookup (self->info_to_extension, info); + + if (!activatable) + return; + + /* Deactivates the extension */ + gtd_activatable_deactivate (activatable); + + /* Emit the signal */ + g_signal_emit (self, signals[PLUGIN_UNLOADED], 0, info, activatable); + + g_hash_table_remove (self->info_to_extension, info); + + /* Destroy the extension */ + g_clear_object (&activatable); +} + +static void +on_plugin_loaded_cb (PeasEngine *engine, + PeasPluginInfo *info, + GtdPluginManager *self) +{ + if (peas_engine_provides_extension (engine, info, GTD_TYPE_ACTIVATABLE)) + { + GtdActivatable *activatable; + PeasExtension *extension; + + /* + * Actually create the plugin object, + * which should load all the providers. + */ + extension = peas_engine_create_extension (engine, + info, + GTD_TYPE_ACTIVATABLE, + NULL); + + /* All extensions shall be GtdActivatable impls */ + activatable = GTD_ACTIVATABLE (extension); + + g_hash_table_insert (self->info_to_extension, + info, + extension); + + /* Activate extension */ + gtd_activatable_activate (activatable); + + /* Emit the signal */ + g_signal_emit (self, signals[PLUGIN_LOADED], 0, info, extension); + } +} + +static void +setup_engine (GtdPluginManager *self) +{ + PeasEngine *engine; + gchar *plugin_dir; + + engine = peas_engine_get_default (); + + /* Enable Python3 plugins */ + peas_engine_enable_loader (engine, "python3"); + + /* Let Peas search for plugins in the specified directory */ + plugin_dir = g_build_filename (PACKAGE_LIB_DIR, + "plugins", + NULL); + + peas_engine_add_search_path (engine, + plugin_dir, + NULL); + + g_free (plugin_dir); + + /* User-installed plugins shall be detected too */ + plugin_dir = g_build_filename (g_get_home_dir (), + ".local", + "lib", + "endeavour", + "plugins", + NULL); + + peas_engine_add_search_path (engine, plugin_dir, NULL); + peas_engine_prepend_search_path (engine, + "resource:///org/gnome/todo/plugins", + "resource:///org/gnome/todo/plugins"); + + g_free (plugin_dir); + + /* Hear about loaded plugins */ + g_signal_connect_after (engine, "load-plugin", G_CALLBACK (on_plugin_loaded_cb), self); + g_signal_connect (engine, "unload-plugin",G_CALLBACK (on_plugin_unloaded_cb), self); +} + +static void +gtd_plugin_manager_finalize (GObject *object) +{ + GtdPluginManager *self = (GtdPluginManager *)object; + + g_clear_pointer (&self->info_to_extension, g_hash_table_destroy); + + G_OBJECT_CLASS (gtd_plugin_manager_parent_class)->finalize (object); +} + +static void +gtd_plugin_manager_class_init (GtdPluginManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_plugin_manager_finalize; + + signals[PLUGIN_LOADED] = g_signal_new ("plugin-loaded", + GTD_TYPE_PLUGIN_MANAGER, + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + PEAS_TYPE_PLUGIN_INFO, + GTD_TYPE_ACTIVATABLE); + + signals[PLUGIN_UNLOADED] = g_signal_new ("plugin-unloaded", + GTD_TYPE_PLUGIN_MANAGER, + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + PEAS_TYPE_PLUGIN_INFO, + GTD_TYPE_ACTIVATABLE); +} + +static void +gtd_plugin_manager_init (GtdPluginManager *self) +{ + self->info_to_extension = g_hash_table_new (g_direct_hash, g_direct_equal); + + gtd_object_push_loading (GTD_OBJECT (self)); + + setup_engine (self); + + gtd_object_pop_loading (GTD_OBJECT (self)); +} + +GtdPluginManager* +gtd_plugin_manager_new (void) +{ + return g_object_new (GTD_TYPE_PLUGIN_MANAGER, NULL); +} + +void +gtd_plugin_manager_load_plugins (GtdPluginManager *self) +{ + PeasEngine *engine; + GSettings *settings; + + engine = peas_engine_get_default (); + settings = gtd_manager_get_settings (gtd_manager_get_default ()); + + g_settings_bind_with_mapping (settings, + "active-extensions", + engine, + "loaded-plugins", + G_SETTINGS_BIND_DEFAULT, + from_gsetting_to_property_func, + from_property_to_gsetting_func, + self, + NULL); +} + +GtdActivatable* +gtd_plugin_manager_get_plugin (GtdPluginManager *self, + PeasPluginInfo *info) +{ + g_return_val_if_fail (GTD_IS_PLUGIN_MANAGER (self), NULL); + + return g_hash_table_lookup (self->info_to_extension, info); +} + +GList* +gtd_plugin_manager_get_loaded_plugins (GtdPluginManager *self) +{ + g_return_val_if_fail (GTD_IS_PLUGIN_MANAGER (self), NULL); + + return g_hash_table_get_values (self->info_to_extension); +} diff --git a/src/core/gtd-plugin-manager.h b/src/core/gtd-plugin-manager.h new file mode 100644 index 0000000..7bef2c6 --- /dev/null +++ b/src/core/gtd-plugin-manager.h @@ -0,0 +1,45 @@ +/* gtd-plugin-manager.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PLUGIN_MANAGER_H +#define GTD_PLUGIN_MANAGER_H + +#include +#include + +#include "gtd-object.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PLUGIN_MANAGER (gtd_plugin_manager_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPluginManager, gtd_plugin_manager, GTD, PLUGIN_MANAGER, GtdObject) + +GtdPluginManager* gtd_plugin_manager_new (void); + +void gtd_plugin_manager_load_plugins (GtdPluginManager *self); + +GtdActivatable* gtd_plugin_manager_get_plugin (GtdPluginManager *self, + PeasPluginInfo *info); + +GList* gtd_plugin_manager_get_loaded_plugins (GtdPluginManager *self); + +G_END_DECLS + +#endif /* GTD_PLUGIN_MANAGER_H */ diff --git a/src/core/gtd-provider.c b/src/core/gtd-provider.c new file mode 100644 index 0000000..1d69a80 --- /dev/null +++ b/src/core/gtd-provider.c @@ -0,0 +1,681 @@ +/* gtd-provider.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProvider" + +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-utils.h" + +/** + * SECTION:gtd-provider + * @short_description:data sources for Endeavour + * @title: GtdProvider + * @stability:Unstable + * + * The #GtdProvider is the interface that Endeavour uses to + * connect to data sources. It must provide ways to create, update + * and remove tasks and tasklists. + * + * A provider implementation must also expose which is the default + * tasklist among the tasklists it manages. + */ + +G_DEFINE_INTERFACE (GtdProvider, gtd_provider, GTD_TYPE_OBJECT) + +enum +{ + LIST_ADDED, + LIST_CHANGED, + LIST_REMOVED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + + +static void +gtd_provider_default_init (GtdProviderInterface *iface) +{ + /** + * GtdProvider::enabled: + * + * Whether the #GtdProvider is enabled. + */ + g_object_interface_install_property (iface, + g_param_spec_boolean ("enabled", + "Identifier of the provider", + "The identifier of the provider", + FALSE, + G_PARAM_READABLE)); + + /** + * GtdProvider::icon: + * + * The icon of the #GtdProvider, e.g. the account icon + * of a GNOME Online Accounts' account. + */ + g_object_interface_install_property (iface, + g_param_spec_object ("icon", + "Icon of the provider", + "The icon of the provider", + G_TYPE_ICON, + G_PARAM_READABLE)); + + /** + * GtdProvider::id: + * + * The unique identifier of the #GtdProvider. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("id", + "Identifier of the provider", + "The identifier of the provider", + NULL, + G_PARAM_READABLE)); + + /** + * GtdProvider::name: + * + * The user-visible name of the #GtdProvider. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("name", + "Name of the provider", + "The user-visible name of the provider", + NULL, + G_PARAM_READABLE)); + + /** + * GtdProvider::provider-type: + * + * The type of the #GtdProvider. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("provider-type", + "Type of the provider", + "The type of the provider", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdProvider::description: + * + * The description of the #GtdProvider, e.g. the account user + * of a GNOME Online Accounts' account. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("description", + "Description of the provider", + "The description of the provider", + NULL, + G_PARAM_READABLE)); + + /** + * GtdProvider::list-added: + * @provider: a #GtdProvider + * @list: a #GtdTaskList + * + * The ::list-added signal is emmited after a #GtdTaskList + * is connected. + */ + signals[LIST_ADDED] = g_signal_new ("list-added", + GTD_TYPE_PROVIDER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + /** + * GtdProvider::list-changed: + * @provider: a #GtdProvider + * @list: a #GtdTaskList + * + * The ::list-changed signal is emmited after a #GtdTaskList + * has any of it's properties changed. + */ + signals[LIST_CHANGED] = g_signal_new ("list-changed", + GTD_TYPE_PROVIDER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + /** + * GtdProvider::list-removed: + * @provider: a #GtdProvider + * @list: a #GtdTaskList + * + * The ::list-removed signal is emmited after a #GtdTaskList + * is disconnected. + */ + signals[LIST_REMOVED] = g_signal_new ("list-removed", + GTD_TYPE_PROVIDER, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); +} + +/** + * gtd_provider_get_id: + * @provider: a #GtdProvider + * + * Retrieves the identifier of @provider. + * + * Returns: (transfer none): the id of @provider + */ +const gchar* +gtd_provider_get_id (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_id, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_id (provider); +} + +/** + * gtd_provider_get_name: + * @provider: a #GtdProvider + * + * Retrieves the user-visible name of @provider. + * + * Returns: (transfer none): the name of @provider + */ +const gchar* +gtd_provider_get_name (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_name, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_name (provider); +} + +/** + * gtd_provider_get_provider_type: + * @provider: a #GtdProvider + * + * Retrieves the type of the @provider. This should return the + * same value, regardless of the account name. + * + * For example: "todoist", "todo-txt" or "google" + * + * Returns: (transfer none): the type of the @provider + */ +const gchar* +gtd_provider_get_provider_type (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_name, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_provider_type (provider); +} + +/** + * gtd_provider_get_description: + * @provider: a #GtdProvider + * + * Retrieves the description of @provider. + * + * Returns: (transfer none): the description of @provider + */ +const gchar* +gtd_provider_get_description (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_description, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_description (provider); +} + +/** + * gtd_provider_get_enabled: + * @provider: a #GtdProvider + * + * Retrieves whether @provider is enabled or not. A disabled + * provider cannot be selected to be default nor be selected + * to add tasks to it. + * + * Returns: %TRUE if provider is enabled, %FALSE otherwise. + */ +gboolean +gtd_provider_get_enabled (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_enabled, FALSE); + + return GTD_PROVIDER_GET_IFACE (provider)->get_enabled (provider); +} + +/** + * gtd_provider_refresh: + * @provider: a #GtdProvider + * + * Asks the provider to refresh. Online providers may want to + * synchronize tasks and tasklists, credentials, etc, when this + * is called. + * + * This is an optional feature. Providers that do not implement + * the "refresh" vfunc will be ignored. + */ +void +gtd_provider_refresh (GtdProvider *provider) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + if (GTD_PROVIDER_GET_IFACE (provider)->refresh) + GTD_PROVIDER_GET_IFACE (provider)->refresh (provider); +} + +/** + * gtd_provider_get_icon: + * @provider: a #GtdProvider + * + * The icon of @provider. + * + * Returns: (transfer none): a #GIcon + */ +GIcon* +gtd_provider_get_icon (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_icon, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_icon (provider); +} + +/** + * gtd_provider_create_task: + * @provider: a #GtdProvider + * @list: a #GtdTaskLast + * @title: The task title + * @due_date: (nullable): a #GDateTime + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Creates the given task in @provider. + */ +void +gtd_provider_create_task (GtdProvider *provider, + GtdTaskList *list, + const gchar *title, + GDateTime *due_date, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->create_task); + + GTD_PROVIDER_GET_IFACE (provider)->create_task (provider, + list, + title, + due_date, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_create_task_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes creating the task. + * + * Returns: (transfer none)(nullable): a #GtdTask + */ +GtdTask* +gtd_provider_create_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->create_task_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->create_task_finish (self, result, error); +} + +/** + * gtd_provider_update_task: + * @provider: a #GtdProvider + * @task: a #GtdTask + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Updates the given task in @provider. + */ +void +gtd_provider_update_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->update_task); + + GTD_PROVIDER_GET_IFACE (provider)->update_task (provider, + task, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_update_task_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes updating the task list. + * + * Returns: %TRUE if task list was successfully updated, %FALSE otherwise + */ +gboolean +gtd_provider_update_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->update_task_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->update_task_finish (self, result, error); +} + +/** + * gtd_provider_remove_task: + * @provider: a #GtdProvider + * @task: a #GtdTask + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Removes the given task from @provider. + */ +void +gtd_provider_remove_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->remove_task); + + GTD_PROVIDER_GET_IFACE (provider)->remove_task (provider, + task, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_remove_task_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes removing the task. + * + * Returns: %TRUE if task was successfully removed, %FALSE otherwise + */ +gboolean +gtd_provider_remove_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->remove_task_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->remove_task_finish (self, result, error); +} + +/** + * gtd_provider_create_task_list: + * @provider: a #GtdProvider + * @name: (nullable): the name of the new task list + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Creates the given list in @provider. + */ +void +gtd_provider_create_task_list (GtdProvider *provider, + const gchar *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->create_task_list); + + GTD_PROVIDER_GET_IFACE (provider)->create_task_list (provider, + name, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_create_task_list_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes creating the task list. The provider will emit the + * GtdProvider:list-added signal after creating the task list. + * + * Returns: %TRUE if task list was successfully created, %FALSE otherwise + */ +gboolean +gtd_provider_create_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->create_task_list_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->create_task_list_finish (self, result, error); +} + +/** + * gtd_provider_update_task_list: + * @provider: a #GtdProvider + * @list: a #GtdTaskList + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Updates the given list in @provider. + */ +void +gtd_provider_update_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->update_task_list); + + GTD_PROVIDER_GET_IFACE (provider)->update_task_list (provider, + list, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_update_task_list_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes updating the task list. The provider will emit the + * GtdProvider:list-updated signal after updating the task list. + * + * Returns: %TRUE if task list was successfully updated, %FALSE otherwise + */ +gboolean +gtd_provider_update_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->update_task_list_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->update_task_list_finish (self, result, error); +} + +/** + * gtd_provider_remove_task_list: + * @provider: a #GtdProvider + * @list: a #GtdTaskList + * @cancellable: (nullable): a #GCancellable + * @callback: (scope async): a callback + * @user_data: (closure): user data for @callback + * + * Removes the given list from @provider. + */ +void +gtd_provider_remove_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_PROVIDER (provider)); + g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->remove_task_list); + + GTD_PROVIDER_GET_IFACE (provider)->remove_task_list (provider, + list, + cancellable, + callback, + user_data); +} + +/** + * gtd_provider_remove_task_list_finish: + * @self: a #GtdProvider + * @result: a #GAsyncResult + * @error: (out)(nullable): return location for a #GError + * + * Finishes removing the task list. The provider will emit the + * GtdProvider:list-removed signal after removing the task list. + * + * Returns: %TRUE if task list was successfully removed, %FALSE otherwise + */ +gboolean +gtd_provider_remove_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE); + g_return_val_if_fail (!error || !*error, FALSE); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->remove_task_list_finish, FALSE); + + return GTD_PROVIDER_GET_IFACE (self)->remove_task_list_finish (self, result, error); +} + +/** + * gtd_provider_get_task_lists: + * @provider: a #GtdProvider + * + * Retrieves the tasklists that this provider contains. + * + * Returns: (transfer container) (element-type Gtd.TaskList): the list of tasks, or %NULL + */ +GList* +gtd_provider_get_task_lists (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_task_lists, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_task_lists (provider); +} + +/** + * gtd_provider_get_inbox: + * @provider: a #GtdProvider + * + * Retrieves the inbox of @provider. + * + * Returns: (transfer none)(nullable): a #GtdTaskList + */ +GtdTaskList* +gtd_provider_get_inbox (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL); + g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_inbox, NULL); + + return GTD_PROVIDER_GET_IFACE (provider)->get_inbox (provider); +} + +/** + * gtd_provider_compare: + * @a: a #GtdProvider + * @b: a #GtdProvider + * + * Compares @a and @b. The sorting criteria is internal and + * may change. + * + * Returns: -1 if @a comes before @b, 1 for the oposite, and + * 0 if they're equal + */ +gint +gtd_provider_compare (GtdProvider *a, + GtdProvider *b) +{ + gint result; + + g_return_val_if_fail (GTD_IS_PROVIDER (a), 0); + g_return_val_if_fail (GTD_IS_PROVIDER (b), 0); + + if (a == b) + return 0; + + result = gtd_collate_compare_strings (gtd_provider_get_name (a), gtd_provider_get_name (b)); + + if (result != 0) + return result; + + return gtd_collate_compare_strings (gtd_provider_get_description (a), gtd_provider_get_description (b)); +} diff --git a/src/core/gtd-provider.h b/src/core/gtd-provider.h new file mode 100644 index 0000000..0b8e097 --- /dev/null +++ b/src/core/gtd-provider.h @@ -0,0 +1,208 @@ +/* gtd-storage.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PROVIDER_H +#define GTD_PROVIDER_H + +#include "gtd-object.h" +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER (gtd_provider_get_type ()) + +G_DECLARE_INTERFACE (GtdProvider, gtd_provider, GTD, PROVIDER, GtdObject) + +struct _GtdProviderInterface +{ + GTypeInterface parent; + + /* Information */ + const gchar* (*get_id) (GtdProvider *provider); + + const gchar* (*get_name) (GtdProvider *provider); + + const gchar* (*get_provider_type) (GtdProvider *provider); + + const gchar* (*get_description) (GtdProvider *provider); + + gboolean (*get_enabled) (GtdProvider *provider); + + void (*refresh) (GtdProvider *provider); + + /* Customs */ + GIcon* (*get_icon) (GtdProvider *provider); + + /* Tasks */ + void (*create_task) (GtdProvider *provider, + GtdTaskList *list, + const gchar *title, + GDateTime *due_date, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + GtdTask* (*create_task_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + void (*update_task) (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + gboolean (*update_task_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + void (*remove_task) (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + gboolean (*remove_task_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + /* Task lists */ + void (*create_task_list) (GtdProvider *provider, + const gchar *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + gboolean (*create_task_list_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + void (*update_task_list) (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + gboolean (*update_task_list_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + void (*remove_task_list) (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + gboolean (*remove_task_list_finish) (GtdProvider *self, + GAsyncResult *result, + GError **error); + + GList* (*get_task_lists) (GtdProvider *provider); + + GtdTaskList* (*get_inbox) (GtdProvider *provider); +}; + +const gchar* gtd_provider_get_id (GtdProvider *provider); + +const gchar* gtd_provider_get_name (GtdProvider *provider); + +const gchar* gtd_provider_get_provider_type (GtdProvider *provider); + +const gchar* gtd_provider_get_description (GtdProvider *provider); + +gboolean gtd_provider_get_enabled (GtdProvider *provider); + +void gtd_provider_refresh (GtdProvider *provider); + +GIcon* gtd_provider_get_icon (GtdProvider *provider); + +void gtd_provider_create_task (GtdProvider *provider, + GtdTaskList *list, + const gchar *title, + GDateTime *due_date, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GtdTask* gtd_provider_create_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +void gtd_provider_update_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtd_provider_update_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +void gtd_provider_remove_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtd_provider_remove_task_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +void gtd_provider_create_task_list (GtdProvider *provider, + const gchar *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtd_provider_create_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +void gtd_provider_update_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtd_provider_update_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +void gtd_provider_remove_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean gtd_provider_remove_task_list_finish (GtdProvider *self, + GAsyncResult *result, + GError **error); + +GList* gtd_provider_get_task_lists (GtdProvider *provider); + +GtdTaskList* gtd_provider_get_inbox (GtdProvider *provider); + +gint gtd_provider_compare (GtdProvider *a, + GtdProvider *b); + +G_END_DECLS + +#endif /* GTD_PROVIDER_H */ diff --git a/src/core/gtd-task-list.c b/src/core/gtd-task-list.c new file mode 100644 index 0000000..2e71a9a --- /dev/null +++ b/src/core/gtd-task-list.c @@ -0,0 +1,1161 @@ +/* gtd-task-list.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTaskList" + +#include "gtd-debug.h" +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" + +#include + +/** + * SECTION:gtd-task-list + * @short_description:a list of tasks + * @title:GtdTaskList + * @stability:Unstable + * @see_also:#GtdTask + * + * A #GtdTaskList represents a task list, and contains a list of tasks, a + * color, a name and the provider who generated it. + * + * Only a #GtdProvider can create a #GtdTaskList. Equally, a #GtdTaskList + * is only valid when associated with a #GtdProvider. + * + * It implements #GListModel, and can be used as the model for #GtkListBox. + */ + +typedef struct +{ + GtdTask *task; + GTask *gtask; +} ImportingTaskData; + +typedef struct +{ + GtdProvider *provider; + GdkRGBA *color; + + GHashTable *task_to_uid; + GHashTable *tasks; + GSequence *sorted_tasks; + guint n_tasks; + + guint freeze_counter; + + gchar *name; + gboolean removable; + gboolean archived; +} GtdTaskListPrivate; + + +static gint compare_tasks_cb (gconstpointer a, + gconstpointer b, + gpointer user_data); + +static void task_changed_cb (GtdTask *task, + GParamSpec *pspec, + GtdTaskList *self); + +static void g_list_model_iface_init (GListModelInterface *iface); + + +G_DEFINE_TYPE_WITH_CODE (GtdTaskList, gtd_task_list, GTD_TYPE_OBJECT, + G_ADD_PRIVATE (GtdTaskList) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init)) + +enum +{ + TASK_ADDED, + TASK_REMOVED, + TASK_UPDATED, + NUM_SIGNALS +}; + +enum +{ + PROP_0, + PROP_ARCHIVED, + PROP_COLOR, + PROP_IS_REMOVABLE, + PROP_NAME, + PROP_PROVIDER, + N_PROPS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; +static GParamSpec *properties[N_PROPS] = { NULL, }; + + +/* + * Auxiliary functions + */ + +static void +update_task_uid (GtdTaskList *self, + GtdTask *task) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + const gchar *old_uid; + gchar *new_uid; + + priv = gtd_task_list_get_instance_private (self); + + g_debug ("Updating uid of task '%s'", gtd_task_get_title (task)); + + new_uid = g_strdup (gtd_object_get_uid (GTD_OBJECT (task))); + + old_uid = g_hash_table_lookup (priv->task_to_uid, task); + iter = g_hash_table_lookup (priv->tasks, old_uid); + + g_assert (g_sequence_get (iter) == task); + + g_hash_table_remove (priv->tasks, old_uid); + + g_hash_table_insert (priv->task_to_uid, task, new_uid); + g_hash_table_insert (priv->tasks, new_uid, iter); +} + +static guint +add_task (GtdTaskList *self, + GtdTask *task) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + gchar *uid; + + priv = gtd_task_list_get_instance_private (self); + + uid = g_strdup (gtd_object_get_uid (GTD_OBJECT (task))); + iter = g_sequence_insert_sorted (priv->sorted_tasks, + g_object_ref (task), + compare_tasks_cb, + NULL); + + g_hash_table_insert (priv->task_to_uid, task, uid); + g_hash_table_insert (priv->tasks, uid, iter); + + g_signal_connect (task, "notify", G_CALLBACK (task_changed_cb), self); + + priv->n_tasks++; + + g_signal_emit (self, signals[TASK_ADDED], 0, task); + + return g_sequence_iter_get_position (iter); +} + +static guint +remove_task (GtdTaskList *self, + GtdTask *task) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + const gchar *uid; + guint position; + + priv = gtd_task_list_get_instance_private (self); + + g_signal_handlers_disconnect_by_func (task, task_changed_cb, self); + + uid = gtd_object_get_uid (GTD_OBJECT (task)); + iter = g_hash_table_lookup (priv->tasks, uid); + position = g_sequence_iter_get_position (iter); + + g_hash_table_remove (priv->task_to_uid, task); + g_hash_table_remove (priv->tasks, uid); + + g_sequence_remove (iter); + + priv->n_tasks--; + + g_signal_emit (self, signals[TASK_REMOVED], 0, task); + + return position; +} + + +/* + * Callbacks + */ + +static void +on_import_old_task_removed_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GtdTask *new_task; + GTask *gtask; + g_autoptr (GError) error = NULL; + + gtask = user_data; + + gtd_provider_remove_task_finish (GTD_PROVIDER (object), result, &error); + + if (error) + { + g_warning ("Error removing task: %s", error->message); + return; + } + + new_task = g_task_get_task_data (gtask); + g_task_return_pointer (gtask, g_object_ref (new_task), g_object_unref); +} + +static void +on_task_updated_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_update_task_finish (GTD_PROVIDER (object), result, &error); + + if (error) + { + g_warning ("Error updating task: %s", error->message); + return; + } +} + +static void +on_import_new_task_added_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GtdTask) new_task = NULL; + g_autoptr (GError) error = NULL; + GTask *gtask; + GtdProvider *provider; + GtdTask *importing_task; + GtdTaskList *list; + ImportingTaskData *data; + + GTD_ENTRY; + + data = user_data; + gtask = data->gtask; + importing_task = data->task; + + new_task = gtd_provider_create_task_finish (GTD_PROVIDER (object), result, &error); + g_task_set_task_data (gtask, g_object_ref (new_task), g_object_unref); + list = gtd_task_get_list (new_task); + + if (error) + { + g_warning ("Error creating task: %s", error->message); + GTD_RETURN (); + } + + gtd_task_set_complete (new_task, gtd_task_get_complete (importing_task)); + gtd_task_set_description (new_task, gtd_task_get_description (importing_task)); + gtd_task_set_due_date (new_task, gtd_task_get_due_date (importing_task)); + gtd_task_set_important (new_task, gtd_task_get_important (importing_task)); + gtd_task_set_title (new_task, gtd_task_get_title (importing_task)); + + gtd_task_list_update_task (list, new_task); + + provider = gtd_task_get_provider (importing_task); + + gtd_provider_remove_task (provider, + importing_task, + NULL, + on_import_old_task_removed_cb, + gtask); + + g_free (data); + + GTD_EXIT; +} + +static gint +compare_tasks_cb (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + return gtd_task_compare ((GtdTask*) a, (GtdTask*) b); +} + +static void +task_changed_cb (GtdTask *task, + GParamSpec *pspec, + GtdTaskList *self) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + guint old_position; + guint new_position; + + GTD_ENTRY; + + priv = gtd_task_list_get_instance_private (self); + + if (g_strcmp0 (g_param_spec_get_name (pspec), "loading") == 0) + GTD_RETURN (); + + if (g_strcmp0 (g_param_spec_get_name (pspec), "uid") == 0) + { + update_task_uid (self, task); + GTD_RETURN (); + } + + /* Don't update when the list is frozen */ + if (priv->freeze_counter > 0) + GTD_RETURN (); + + iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task))); + + old_position = g_sequence_iter_get_position (iter); + g_sequence_sort_changed (iter, compare_tasks_cb, NULL); + new_position = g_sequence_iter_get_position (iter); + + if (old_position != new_position) + { + GTD_TRACE_MSG ("Old position: %u, New position: %u", old_position, new_position); + + g_list_model_items_changed (G_LIST_MODEL (self), old_position, 1, 0); + g_list_model_items_changed (G_LIST_MODEL (self), new_position, 0, 1); + } + + GTD_EXIT; +} + + +/* + * GListModel iface + */ + +static GType +gtd_list_model_get_type (GListModel *model) +{ + return GTD_TYPE_TASK; +} + +static guint +gtd_list_model_get_n_items (GListModel *model) +{ + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (GTD_TASK_LIST (model)); + return priv->n_tasks; +} + +static gpointer +gtd_list_model_get_item (GListModel *model, + guint i) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + GtdTask *task; + + priv = gtd_task_list_get_instance_private (GTD_TASK_LIST (model)); + iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, i); + task = g_sequence_get (iter); + + return g_object_ref (task); +} + +static void +g_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = gtd_list_model_get_type; + iface->get_n_items = gtd_list_model_get_n_items; + iface->get_item = gtd_list_model_get_item; +} + + +/* + * GtdTaskList overrides + */ + +static gboolean +gtd_task_list_real_get_archived (GtdTaskList *self) +{ + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + + return priv->archived; +} + +static void +gtd_task_list_real_set_archived (GtdTaskList *self, + gboolean archived) +{ + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + + priv->archived = archived; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_finalize (GObject *object) +{ + GtdTaskList *self = (GtdTaskList*) object; + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + + g_clear_object (&priv->provider); + + g_clear_pointer (&priv->color, gdk_rgba_free); + g_clear_pointer (&priv->name, g_free); + g_clear_pointer (&priv->sorted_tasks, g_sequence_free); + g_clear_pointer (&priv->tasks, g_hash_table_destroy); + g_clear_pointer (&priv->task_to_uid, g_hash_table_destroy); + + G_OBJECT_CLASS (gtd_task_list_parent_class)->finalize (object); +} + +static void +gtd_task_list_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskList *self = GTD_TASK_LIST (object); + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + + switch (prop_id) + { + case PROP_ARCHIVED: + g_value_set_boolean (value, gtd_task_list_get_archived (self)); + break; + + case PROP_COLOR: + { + GdkRGBA *color = gtd_task_list_get_color (self); + g_value_set_boxed (value, color); + gdk_rgba_free (color); + break; + } + + case PROP_IS_REMOVABLE: + g_value_set_boolean (value, gtd_task_list_is_removable (self)); + break; + + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + + case PROP_PROVIDER: + g_value_set_object (value, priv->provider); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskList *self = GTD_TASK_LIST (object); + + switch (prop_id) + { + case PROP_ARCHIVED: + gtd_task_list_set_archived (self, g_value_get_boolean (value)); + break; + + case PROP_COLOR: + gtd_task_list_set_color (self, g_value_get_boxed (value)); + break; + + case PROP_IS_REMOVABLE: + gtd_task_list_set_is_removable (self, g_value_get_boolean (value)); + break; + + case PROP_NAME: + gtd_task_list_set_name (self, g_value_get_string (value)); + break; + + case PROP_PROVIDER: + gtd_task_list_set_provider (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_class_init (GtdTaskListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_task_list_finalize; + object_class->get_property = gtd_task_list_get_property; + object_class->set_property = gtd_task_list_set_property; + + klass->get_archived = gtd_task_list_real_get_archived; + klass->set_archived = gtd_task_list_real_set_archived; + + /** + * GtdTaskList::archived: + * + * Whether the task list is archived or not. + */ + properties[PROP_ARCHIVED] = g_param_spec_boolean ("archived", + "Whether the list is archived", + "Whether the list is archived or not", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdTaskList::color: + * + * The color of the list. + */ + properties[PROP_COLOR] = g_param_spec_boxed ("color", + "Color of the list", + "The color of the list", + GDK_TYPE_RGBA, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdTaskList::is-removable: + * + * Whether the task list can be removed from the system. + */ + properties[PROP_IS_REMOVABLE] = g_param_spec_boolean ("is-removable", + "Whether the task list is removable", + "Whether the task list can be removed from the system", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdTaskList::name: + * + * The display name of the list. + */ + properties[PROP_NAME] = g_param_spec_string ("name", + "Name of the list", + "The name of the list", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdTaskList::provider: + * + * The data provider of the list. + */ + properties[PROP_PROVIDER] = g_param_spec_object ("provider", + "Provider of the list", + "The provider that handles the list", + GTD_TYPE_PROVIDER, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * GtdTaskList::task-added: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * The ::task-added signal is emitted after a #GtdTask + * is added to the list. + */ + signals[TASK_ADDED] = g_signal_new ("task-added", + GTD_TYPE_TASK_LIST, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTaskListClass, task_added), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK); + + /** + * GtdTaskList::task-removed: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * The ::task-removed signal is emitted after a #GtdTask + * is removed from the list. + */ + signals[TASK_REMOVED] = g_signal_new ("task-removed", + GTD_TYPE_TASK_LIST, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTaskListClass, task_removed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK); + + /** + * GtdTaskList::task-updated: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * The ::task-updated signal is emitted after a #GtdTask + * in the list is updated. + */ + signals[TASK_UPDATED] = g_signal_new ("task-updated", + GTD_TYPE_TASK_LIST, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GtdTaskListClass, task_updated), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK); +} + +static void +gtd_task_list_init (GtdTaskList *self) +{ + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + + priv->task_to_uid = g_hash_table_new (g_str_hash, g_str_equal); + priv->tasks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + priv->sorted_tasks = g_sequence_new (g_object_unref); +} + +/** + * gtd_task_list_new: + * @provider: (nullable): a #GtdProvider + * + * Creates a new list. + * + * Returns: (transfer full): the new #GtdTaskList + */ +GtdTaskList * +gtd_task_list_new (GtdProvider *provider) +{ + return g_object_new (GTD_TYPE_TASK_LIST, + "provider", provider, + NULL); +} + +/** + * gtd_task_list_get_color: + * @list: a #GtdTaskList + * + * Retrieves the color of %list. It is guarantee that it always returns a + * color, given a valid #GtdTaskList. + * + * Returns: (transfer full): the color of %list. Free with %gdk_rgba_free after use. + */ +GdkRGBA* +gtd_task_list_get_color (GtdTaskList *list) +{ + GtdTaskListPrivate *priv; + GdkRGBA rgba; + + g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL); + + priv = gtd_task_list_get_instance_private (list); + + if (!priv->color) + { + gdk_rgba_parse (&rgba, "#ffffff"); + priv->color = gdk_rgba_copy (&rgba); + } + + return gdk_rgba_copy (priv->color); +} + +/** + * gtd_task_list_set_color: + * @list: a #GtdTaskList + * #color: a #GdkRGBA + * + * sets the color of @list. + */ +void +gtd_task_list_set_color (GtdTaskList *list, + const GdkRGBA *color) +{ + GtdTaskListPrivate *priv; + GdkRGBA *current_color; + + g_return_if_fail (GTD_IS_TASK_LIST (list)); + + priv = gtd_task_list_get_instance_private (list); + current_color = gtd_task_list_get_color (list); + + if (!gdk_rgba_equal (current_color, color)) + { + g_clear_pointer (&priv->color, gdk_rgba_free); + priv->color = gdk_rgba_copy (color); + + g_object_notify (G_OBJECT (list), "color"); + } + + gdk_rgba_free (current_color); +} + +/** + * gtd_task_list_get_name: + * @list: a #GtdTaskList + * + * Retrieves the user-visible name of @list, or %NULL. + * + * Returns: (transfer none): the internal name of @list. Do not free + * after use. + */ +const gchar* +gtd_task_list_get_name (GtdTaskList *list) +{ + GtdTaskListPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL); + + priv = gtd_task_list_get_instance_private (list); + + return priv->name; +} + +/** + * gtd_task_list_set_name: + * @list: a #GtdTaskList + * @name: (nullable): the name of @list + * + * Sets the @list name to @name. + */ +void +gtd_task_list_set_name (GtdTaskList *list, + const gchar *name) +{ + GtdTaskListPrivate *priv; + + g_assert (GTD_IS_TASK_LIST (list)); + + priv = gtd_task_list_get_instance_private (list); + + if (g_strcmp0 (priv->name, name) != 0) + { + g_free (priv->name); + priv->name = g_strdup (name); + + g_object_notify (G_OBJECT (list), "name"); + } +} + +/** + * gtd_task_list_get_provider: + * @list: a #GtdTaskList + * + * Retrieves the #GtdProvider who owns this list. + * + * Returns: (transfer none): a #GtdProvider + */ +GtdProvider* +gtd_task_list_get_provider (GtdTaskList *list) +{ + GtdTaskListPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL); + + priv = gtd_task_list_get_instance_private (list); + + return priv->provider; +} + +/** + * gtd_task_list_set_provider: + * @self: a #GtdTaskList + * @provider: (nullable): a #GtdProvider, or %NULL + * + * Sets the provider of this tasklist. + */ +void +gtd_task_list_set_provider (GtdTaskList *self, + GtdProvider *provider) +{ + GtdTaskListPrivate *priv; + + g_assert (GTD_IS_TASK_LIST (self)); + + priv = gtd_task_list_get_instance_private (self); + + if (g_set_object (&priv->provider, provider)) + g_object_notify (G_OBJECT (self), "provider"); +} + +/** + * gtd_task_list_add_task: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * Adds @task to @list. + */ +void +gtd_task_list_add_task (GtdTaskList *self, + GtdTask *task) +{ + guint position; + + g_assert (GTD_IS_TASK_LIST (self)); + g_assert (GTD_IS_TASK (task)); + g_assert (!gtd_task_list_contains (self, task)); + + position = add_task (self, task); + g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); +} + +/** + * gtd_task_list_update_task: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * Updates @task at @list. + */ +void +gtd_task_list_update_task (GtdTaskList *self, + GtdTask *task) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + + g_return_if_fail (GTD_IS_TASK_LIST (self)); + g_return_if_fail (GTD_IS_TASK (task)); + + priv = gtd_task_list_get_instance_private (self); + + g_return_if_fail (gtd_task_list_contains (self, task)); + + iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task))); + + g_list_model_items_changed (G_LIST_MODEL (self), + g_sequence_iter_get_position (iter), + 1, + 1); + + g_signal_emit (self, signals[TASK_UPDATED], 0, task); +} + +/** + * gtd_task_list_remove_task: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * Removes @task from @list if it's inside the list. + */ +void +gtd_task_list_remove_task (GtdTaskList *list, + GtdTask *task) +{ + guint position; + + g_assert (GTD_IS_TASK_LIST (list)); + g_assert (GTD_IS_TASK (task)); + g_assert (gtd_task_list_contains (list, task)); + + position = remove_task (list, task); + g_list_model_items_changed (G_LIST_MODEL (list), position, 1, 0); +} + +/** + * gtd_task_list_contains: + * @list: a #GtdTaskList + * @task: a #GtdTask + * + * Checks if @task is inside @list. + * + * Returns: %TRUE if @list contains @task, %FALSE otherwise + */ +gboolean +gtd_task_list_contains (GtdTaskList *list, + GtdTask *task) +{ + GtdTaskListPrivate *priv; + + g_assert (GTD_IS_TASK_LIST (list)); + g_assert (GTD_IS_TASK (task)); + + priv = gtd_task_list_get_instance_private (list); + + return g_hash_table_contains (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task))); +} + +/** + * gtd_task_list_get_is_removable: + * @list: a #GtdTaskList + * + * Retrieves whether @list can be removed or not. + * + * Returns: %TRUE if the @list can be removed, %FALSE otherwise + */ +gboolean +gtd_task_list_is_removable (GtdTaskList *list) +{ + GtdTaskListPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK_LIST (list), FALSE); + + priv = gtd_task_list_get_instance_private (list); + + return priv->removable; +} + +/** + * gtd_task_list_set_is_removable: + * @list: a #GtdTaskList + * @is_removable: %TRUE if @list can be deleted, %FALSE otherwise + * + * Sets whether @list can be deleted or not. + */ +void +gtd_task_list_set_is_removable (GtdTaskList *list, + gboolean is_removable) +{ + GtdTaskListPrivate *priv; + + g_return_if_fail (GTD_IS_TASK_LIST (list)); + + priv = gtd_task_list_get_instance_private (list); + + if (priv->removable != is_removable) + { + priv->removable = is_removable; + + g_object_notify (G_OBJECT (list), "is-removable"); + } +} + +/** + * gtd_task_list_get_task_by_id: + * @self: a #GtdTaskList + * @id: the id of the task + * + * Retrieves a task from @self with the given @id. + * + * Returns: (transfer none)(nullable): a #GtdTask, or %NULL + */ +GtdTask* +gtd_task_list_get_task_by_id (GtdTaskList *self, + const gchar *id) +{ + GtdTaskListPrivate *priv; + GSequenceIter *iter; + + g_return_val_if_fail (GTD_IS_TASK_LIST (self), NULL); + + priv = gtd_task_list_get_instance_private (self); + iter = g_hash_table_lookup (priv->tasks, id); + + if (!iter) + return NULL; + + return g_sequence_get (iter); +} + +/** + * gtd_task_list_move_task_to_position: + * @self: a #GtdTaskList + * @task: a #GtdTask + * @new_position: the new position of @task inside @self + * + * Moves @task to @new_position, and repositions the elements + * in between as well. + * + * @task must belong to @self. + */ +void +gtd_task_list_move_task_to_position (GtdTaskList *self, + GtdTask *task, + guint new_position) +{ + + GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self); + GSequenceIter *new_position_iter; + GSequenceIter *iter; + guint old_position; + guint length; + guint start; + guint i; + + + g_return_if_fail (GTD_IS_TASK_LIST (self)); + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (gtd_task_list_contains (self, task)); + g_return_if_fail (g_list_model_get_n_items (G_LIST_MODEL (self)) >= new_position); + + iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task))); + old_position = g_sequence_iter_get_position (iter); + + if (old_position == new_position) + return; + + GTD_TRACE_MSG ("Moving task '%s' (%s) from %u to %u", + gtd_task_get_title (task), + gtd_object_get_uid (GTD_OBJECT (task)), + old_position, + new_position); + + /* Update the GSequence */ + new_position_iter = new_position < old_position ? + g_sequence_get_iter_at_pos (priv->sorted_tasks, new_position) : + g_sequence_get_iter_at_pos (priv->sorted_tasks, new_position + 1); + g_sequence_move (iter, new_position_iter); + + /* Update the 'position' property of all tasks in between */ + priv->freeze_counter++; + + length = ABS ((gint) new_position - (gint64) old_position) + 1; + start = MIN (old_position, new_position); + iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, start); + + for (i = 0; i < length; i++) + { + GtdTask *aux = g_sequence_get (iter); + + g_signal_handlers_block_by_func (aux, task_changed_cb, self); + gtd_task_set_position (aux, start + i); + g_signal_handlers_unblock_by_func (aux, task_changed_cb, self); + + gtd_provider_update_task (priv->provider, aux, NULL, on_task_updated_cb, self); + + iter = g_sequence_iter_next (iter); + } + + g_list_model_items_changed (G_LIST_MODEL (self), old_position, 1, 0); + g_list_model_items_changed (G_LIST_MODEL (self), new_position, 0, 1); + + priv->freeze_counter--; +} + +/** + * gtd_task_list_get_archived: + * @self: a #GtdTaskList + * + * Retrieves whether @self is archived or not. Archived task lists + * are hidden by default, and new tasks cannot be added. + * + * Returns: %TRUE if @self is archived, %FALSE otherwise. + */ +gboolean +gtd_task_list_get_archived (GtdTaskList *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST (self), FALSE); + + return GTD_TASK_LIST_GET_CLASS (self)->get_archived (self); +} + +/** + * gtd_task_list_set_archived: + * @self: a #GtdTaskList + * @archived: whether @self is archived or not + * + * Sets the "archive" property of @self to @archived. + */ +void +gtd_task_list_set_archived (GtdTaskList *self, + gboolean archived) +{ + gboolean was_archived; + + g_return_if_fail (GTD_IS_TASK_LIST (self)); + + was_archived = gtd_task_list_get_archived (self); + + if (archived == was_archived) + return; + + GTD_TASK_LIST_GET_CLASS (self)->set_archived (self, archived); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ARCHIVED]); +} + +/** + * gtd_task_list_import_task: + * @self: a #GtdTaskList + * @task: a #GtdTask + * + * Imports task into @self + */ +void +gtd_task_list_import_task (GtdTaskList *self, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + GtdProvider *provider; + ImportingTaskData *data; + + g_return_if_fail (GTD_IS_TASK_LIST (self)); + + gtask = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_task_list_import_task); + + provider = gtd_task_get_provider (task); + + data = g_new0 (ImportingTaskData, 1); + data->gtask = g_object_ref (gtask); + data->task = g_object_ref (task); + + gtd_provider_create_task (provider, + self, + gtd_task_get_title (task), + gtd_task_get_due_date (task), + NULL, + on_import_new_task_added_cb, + data); +} + +/** + * gtd_task_list_import_task_finish: + * @self: a #GtdTaskList + * @result: a #GAsyncResult + * @error: a #GError + * + * Imports task into @self + * + * Returns: (transfer full): a #GTask + */ +GTask * +gtd_task_list_import_task_finish (GtdTaskList *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, self), NULL); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gtd_task_list_import_task, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + +/** + * gtd_task_list_is_inbox: + * @self: a #GtdTaskList + * + * Retrieves whether @self is the inbox task list of its provider. + * + * Returns: %TRUE if @self is the inbox of it's provider, %FALSE otherwise. + */ +gboolean +gtd_task_list_is_inbox (GtdTaskList *self) +{ + GtdTaskListPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK_LIST (self), FALSE); + + priv = gtd_task_list_get_instance_private (self); + + return self == gtd_provider_get_inbox (priv->provider); +} diff --git a/src/core/gtd-task-list.h b/src/core/gtd-task-list.h new file mode 100644 index 0000000..6f06490 --- /dev/null +++ b/src/core/gtd-task-list.h @@ -0,0 +1,118 @@ +/* gtd-task-list.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_LIST_H +#define GTD_TASK_LIST_H + +#include +#include +#include + +#include "gtd-object.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST (gtd_task_list_get_type()) + +G_DECLARE_DERIVABLE_TYPE (GtdTaskList, gtd_task_list, GTD, TASK_LIST, GtdObject) + +struct _GtdTaskListClass +{ + GtdObjectClass parent; + + /* Vfuncs */ + gboolean (*get_archived) (GtdTaskList *self); + + void (*set_archived) (GtdTaskList *self, + gboolean archived); + + /* Signal methods */ + void (*task_added) (GtdTaskList *list, + GtdTask *task); + + void (*task_updated) (GtdTaskList *list, + GtdTask *task); + + void (*task_removed) (GtdTaskList *list, + GtdTask *task); + + gpointer padding[10]; +}; + +GtdTaskList* gtd_task_list_new (GtdProvider *provider); + +GdkRGBA* gtd_task_list_get_color (GtdTaskList *list); + +void gtd_task_list_set_color (GtdTaskList *list, + const GdkRGBA *color); + +gboolean gtd_task_list_is_removable (GtdTaskList *list); + +void gtd_task_list_set_is_removable (GtdTaskList *list, + gboolean is_removable); + +const gchar* gtd_task_list_get_name (GtdTaskList *list); + +void gtd_task_list_set_name (GtdTaskList *list, + const gchar *name); + +GtdProvider* gtd_task_list_get_provider (GtdTaskList *list); + +void gtd_task_list_set_provider (GtdTaskList *self, + GtdProvider *provider); + +void gtd_task_list_add_task (GtdTaskList *list, + GtdTask *task); + +void gtd_task_list_update_task (GtdTaskList *list, + GtdTask *task); + +void gtd_task_list_remove_task (GtdTaskList *list, + GtdTask *task); + +gboolean gtd_task_list_contains (GtdTaskList *list, + GtdTask *task); + +GtdTask* gtd_task_list_get_task_by_id (GtdTaskList *self, + const gchar *id); + +void gtd_task_list_move_task_to_position (GtdTaskList *self, + GtdTask *task, + guint new_position); + +gboolean gtd_task_list_get_archived (GtdTaskList *self); + +void gtd_task_list_set_archived (GtdTaskList *self, + gboolean archived); + +void gtd_task_list_import_task (GtdTaskList *self, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GTask * gtd_task_list_import_task_finish (GtdTaskList *self, + GAsyncResult *result, + GError **error); + +gboolean gtd_task_list_is_inbox (GtdTaskList *self); + +G_END_DECLS + +#endif /* GTD_TASK_LIST_H */ diff --git a/src/core/gtd-task.c b/src/core/gtd-task.c new file mode 100644 index 0000000..6951dec --- /dev/null +++ b/src/core/gtd-task.c @@ -0,0 +1,993 @@ +/* gtd-task.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTask" + +#include "gtd-debug.h" +#include "gtd-task.h" +#include "gtd-task-list.h" + +#include + +/** + * SECTION:gtd-task + * @short_description: a task + * @title:GtdTask + * @stability:Unstable + * @see_also:#GtdTaskList + * + * A #GtdTask is an object that represents a task. All #GtdTasks + * must be inside a #GtdTaskList. + */ + +typedef struct +{ + gchar *description; + GtdTaskList *list; + + GDateTime *creation_date; + GDateTime *completion_date; + GDateTime *due_date; + + gchar *title; + + gint32 priority; + gint64 position; + gboolean complete; + gboolean important; +} GtdTaskPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdTask, gtd_task, GTD_TYPE_OBJECT) + +enum +{ + PROP_0, + PROP_COMPLETE, + PROP_DESCRIPTION, + PROP_CREATION_DATE, + PROP_DUE_DATE, + PROP_IMPORTANT, + PROP_LIST, + PROP_POSITION, + PROP_TITLE, + LAST_PROP +}; + +static void +task_list_weak_notified (gpointer data, + GObject *where_the_object_was) +{ + GtdTask *task = GTD_TASK (data); + GtdTaskPrivate *priv = gtd_task_get_instance_private (task); + priv->list = NULL; +} + +/* + * GtdTask default implementations + */ + +static GDateTime* +gtd_task_real_get_completion_date (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->completion_date ? g_date_time_ref (priv->completion_date) : NULL; +} + +static void +gtd_task_real_set_completion_date (GtdTask *self, + GDateTime *dt) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + g_clear_pointer (&priv->completion_date, g_date_time_unref); + priv->completion_date = dt ? g_date_time_ref (dt) : NULL; +} + +static gboolean +gtd_task_real_get_complete (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->complete; +} + +static void +gtd_task_real_set_complete (GtdTask *self, + gboolean complete) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + GDateTime *dt; + + dt = complete ? g_date_time_new_now_local () : NULL; + gtd_task_real_set_completion_date (self, dt); + + priv->complete = complete; +} + +static GDateTime* +gtd_task_real_get_creation_date (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->creation_date ? g_date_time_ref (priv->creation_date) : NULL; +} + +static void +gtd_task_real_set_creation_date (GtdTask *self, + GDateTime *dt) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + g_clear_pointer (&priv->creation_date, g_date_time_unref); + priv->creation_date = dt ? g_date_time_ref (dt) : NULL; +} + +static const gchar* +gtd_task_real_get_description (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->description ? priv->description : ""; +} + +static void +gtd_task_real_set_description (GtdTask *self, + const gchar *description) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + g_clear_pointer (&priv->description, g_free); + priv->description = g_strdup (description); +} + +static GDateTime* +gtd_task_real_get_due_date (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->due_date ? g_date_time_ref (priv->due_date) : NULL; +} + +static void +gtd_task_real_set_due_date (GtdTask *self, + GDateTime *due_date) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + g_clear_pointer (&priv->due_date, g_date_time_unref); + + if (due_date) + priv->due_date = g_date_time_ref (due_date); +} + +static gboolean +gtd_task_real_get_important (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->important; +} + +static void +gtd_task_real_set_important (GtdTask *self, + gboolean important) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + if (priv->important == important) + return; + + priv->important = important; +} + +static gint64 +gtd_task_real_get_position (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->position; +} + +static void +gtd_task_real_set_position (GtdTask *self, + gint64 position) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + priv->position = position; +} + +static const gchar* +gtd_task_real_get_title (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + return priv->title; +} + +static void +gtd_task_real_set_title (GtdTask *self, + const gchar *title) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + g_clear_pointer (&priv->title, g_free); + priv->title = title ? g_strdup (title) : NULL; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_finalize (GObject *object) +{ + GtdTask *self = (GtdTask*) object; + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + if (priv->list) + g_object_weak_unref (G_OBJECT (priv->list), task_list_weak_notified, self); + + priv->list = NULL; + g_free (priv->description); + + G_OBJECT_CLASS (gtd_task_parent_class)->finalize (object); +} + +static void +gtd_task_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTask *self = GTD_TASK (object); + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + GDateTime *date; + + switch (prop_id) + { + case PROP_COMPLETE: + g_value_set_boolean (value, gtd_task_get_complete (self)); + break; + + case PROP_CREATION_DATE: + g_value_set_boxed (value, gtd_task_get_creation_date (self)); + break; + + case PROP_DESCRIPTION: + g_value_set_string (value, gtd_task_get_description (self)); + break; + + case PROP_DUE_DATE: + date = gtd_task_get_due_date (self); + g_value_set_boxed (value, date); + g_clear_pointer (&date, g_date_time_unref); + break; + + case PROP_IMPORTANT: + g_value_set_boolean (value, gtd_task_get_important (self)); + break; + + case PROP_LIST: + g_value_set_object (value, priv->list); + break; + + case PROP_POSITION: + g_value_set_int64 (value, gtd_task_get_position (self)); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_task_get_title (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTask *self = GTD_TASK (object); + + switch (prop_id) + { + case PROP_COMPLETE: + gtd_task_set_complete (self, g_value_get_boolean (value)); + break; + + case PROP_CREATION_DATE: + gtd_task_set_creation_date (self, g_value_get_boxed (value)); + break; + + case PROP_DESCRIPTION: + gtd_task_set_description (self, g_value_get_string (value)); + break; + + case PROP_DUE_DATE: + gtd_task_set_due_date (self, g_value_get_boxed (value)); + break; + + case PROP_IMPORTANT: + gtd_task_set_important (self, g_value_get_boolean (value)); + break; + + case PROP_LIST: + gtd_task_set_list (self, g_value_get_object (value)); + break; + + case PROP_POSITION: + gtd_task_set_position (self, g_value_get_int64 (value)); + break; + + case PROP_TITLE: + gtd_task_set_title (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_class_init (GtdTaskClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + klass->get_complete = gtd_task_real_get_complete; + klass->set_complete = gtd_task_real_set_complete; + klass->get_creation_date = gtd_task_real_get_creation_date; + klass->set_creation_date = gtd_task_real_set_creation_date; + klass->get_completion_date = gtd_task_real_get_completion_date; + klass->set_completion_date = gtd_task_real_set_completion_date; + klass->get_description = gtd_task_real_get_description; + klass->set_description = gtd_task_real_set_description; + klass->get_due_date = gtd_task_real_get_due_date; + klass->set_due_date = gtd_task_real_set_due_date; + klass->get_important = gtd_task_real_get_important; + klass->set_important = gtd_task_real_set_important; + klass->get_position = gtd_task_real_get_position; + klass->set_position = gtd_task_real_set_position; + klass->get_title = gtd_task_real_get_title; + klass->set_title = gtd_task_real_set_title; + + object_class->finalize = gtd_task_finalize; + object_class->get_property = gtd_task_get_property; + object_class->set_property = gtd_task_set_property; + + /** + * GtdTask::complete: + * + * @TRUE if the task is marked as complete or @FALSE otherwise. Usually + * represented by a checkbox at user interfaces. + */ + g_object_class_install_property ( + object_class, + PROP_COMPLETE, + g_param_spec_boolean ("complete", + "Whether the task is completed or not", + "Whether the task is marked as completed by the user", + FALSE, + G_PARAM_READWRITE)); + + /** + * GtdTask::creation-date: + * + * The @GDateTime that represents the time in which the task was created. + */ + g_object_class_install_property ( + object_class, + PROP_CREATION_DATE, + g_param_spec_boxed ("creation-date", + "Creation date of the task", + "The day the task was created.", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdTask::description: + * + * Description of the task. + */ + g_object_class_install_property ( + object_class, + PROP_DESCRIPTION, + g_param_spec_string ("description", + "Description of the task", + "Optional string describing the task", + NULL, + G_PARAM_READWRITE)); + + /** + * GtdTask::due-date: + * + * The @GDateTime that represents the time in which the task should + * be completed before. + */ + g_object_class_install_property ( + object_class, + PROP_DUE_DATE, + g_param_spec_boxed ("due-date", + "End date of the task", + "The day the task is supposed to be completed", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE)); + + /** + * GtdTask::important: + * + * @TRUE if the task is important, @FALSE otherwise. + */ + g_object_class_install_property ( + object_class, + PROP_IMPORTANT, + g_param_spec_boolean ("important", + "Whether the task is important or not", + "Whether the task is important or not", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + /** + * GtdTask::list: + * + * The @GtdTaskList that contains this task. + */ + g_object_class_install_property ( + object_class, + PROP_LIST, + g_param_spec_object ("list", + "List of the task", + "The list that owns this task", + GTD_TYPE_TASK_LIST, + G_PARAM_READWRITE)); + + /** + * GtdTask::position: + * + * Position of the task, -1 if not set. + */ + g_object_class_install_property ( + object_class, + PROP_POSITION, + g_param_spec_int64 ("position", + "Position of the task", + "The position of the task. -1 means no position, and tasks will be sorted alphabetically.", + -1, + G_MAXINT64, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdTask::title: + * + * The title of the task, usually the task name. + */ + g_object_class_install_property ( + object_class, + PROP_TITLE, + g_param_spec_string ("title", + "Title of the task", + "The title of the task", + NULL, + G_PARAM_READWRITE)); +} + +static void +gtd_task_init (GtdTask *self) +{ + GtdTaskPrivate *priv = gtd_task_get_instance_private (self); + + priv->position = -1; +} + +/** + * gtd_task_new: + * + * Creates a new #GtdTask + * + * Returns: (transfer full): a #GtdTask + */ +GtdTask * +gtd_task_new (void) +{ + return g_object_new (GTD_TYPE_TASK, NULL); +} + +/** + * gtd_task_get_complete: + * @self: a #GtdTask + * + * Retrieves whether the task is complete or not. + * + * Returns: %TRUE if the task is complete, %FALSE otherwise + */ +gboolean +gtd_task_get_complete (GtdTask *self) +{ + g_return_val_if_fail (GTD_IS_TASK (self), FALSE); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->get_complete (self); +} + +/** + * gtd_task_set_complete: + * @self: a #GtdTask + * @complete: the new value + * + * Updates the complete state of @task. + */ +void +gtd_task_set_complete (GtdTask *task, + gboolean complete) +{ + g_return_if_fail (GTD_IS_TASK (task)); + + if (gtd_task_get_complete (task) == complete) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_complete (task, complete); + + g_object_notify (G_OBJECT (task), "complete"); +} + +/** + * gtd_task_get_creation_date: + * @self: a #GtdTask + * + * Returns the #GDateTime that represents the task's creation date. + * The value is referenced for thread safety. Returns %NULL if + * no date is set. + * + * Returns: (transfer full): the internal #GDateTime referenced + * for thread safety, or %NULL. Unreference it after use. + */ +GDateTime* +gtd_task_get_creation_date (GtdTask *task) +{ + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_creation_date (task); +} + +/** + * gtd_task_set_creation_date: + * @self: a #GtdTask + * + * Sets the creation date of @task. + */ +void +gtd_task_set_creation_date (GtdTask *task, + GDateTime *dt) +{ + g_return_if_fail (GTD_IS_TASK (task)); + + if (gtd_task_get_creation_date (task) == dt) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_creation_date (task, dt); + + g_object_notify (G_OBJECT (task), "complete"); +} + +/** + * gtd_task_get_completion_date: + * @self: a #GtdTask + * + * Returns the #GDateTime that represents the task's completion date. + * Returns %NULL if no date is set. + * + * Returns: (transfer full)(nullable): the internal #GDateTime or %NULL. + * Unreference it after use. + */ +GDateTime* +gtd_task_get_completion_date (GtdTask *task) +{ + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_completion_date (task); +} + +/** + * gtd_task_get_description: + * @self: a #GtdTask + * + * Retrieves the description of the task. + * + * Returns: (transfer none): the description of @task + */ +const gchar* +gtd_task_get_description (GtdTask *task) +{ + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_description (task); +} + +/** + * gtd_task_set_description: + * @self: a #GtdTask + * @description: (nullable): the new description, or %NULL + * + * Updates the description of @task. The string is not stripped off of + * spaces to preserve user data. + */ +void +gtd_task_set_description (GtdTask *task, + const gchar *description) +{ + GtdTaskPrivate *priv; + + g_assert (GTD_IS_TASK (task)); + g_assert (g_utf8_validate (description, -1, NULL)); + + priv = gtd_task_get_instance_private (task); + + if (g_strcmp0 (priv->description, description) == 0) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_description (task, description); + + g_object_notify (G_OBJECT (task), "description"); +} + +/** + * gtd_task_get_due_date: + * @self: a #GtdTask + * + * Returns the #GDateTime that represents the task's due date. + * The value is referenced for thread safety. Returns %NULL if + * no date is set. + * + * Returns: (transfer full) (nullable): the internal #GDateTime referenced + * for thread safety, or %NULL. Unreference it after use. + */ +GDateTime* +gtd_task_get_due_date (GtdTask *task) +{ + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_due_date (task); +} + +/** + * gtd_task_set_due_date: + * @self: a #GtdTask + * @dt: (nullable): a #GDateTime + * + * Updates the internal @GtdTask::due-date property. + */ +void +gtd_task_set_due_date (GtdTask *task, + GDateTime *dt) +{ + g_autoptr (GDateTime) current_dt = NULL; + + g_assert (GTD_IS_TASK (task)); + + current_dt = gtd_task_get_due_date (task); + + /* Don't do anything if the date is equal */ + if (current_dt == dt || (current_dt && dt && g_date_time_equal (current_dt, dt))) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_due_date (task, dt); + + g_object_notify (G_OBJECT (task), "due-date"); +} + +/** + * gtd_task_get_important: + * @self: a #GtdTask + * + * Retrieves whether @self is @important or not. + * + * Returns: %TRUE if @self is important, %FALSE otherwise + */ +gboolean +gtd_task_get_important (GtdTask *self) +{ + g_return_val_if_fail (GTD_IS_TASK (self), FALSE); + + return GTD_TASK_GET_CLASS (self)->get_important (self); +} + +/** + * gtd_task_set_important: + * @self: a #GtdTask + * @important: whether @self is important or not + * + * Sets whether @self is @important or not. + */ +void +gtd_task_set_important (GtdTask *self, + gboolean important) +{ + g_return_if_fail (GTD_IS_TASK (self)); + + important = !!important; + + GTD_TASK_GET_CLASS (self)->set_important (self, important); + g_object_notify (G_OBJECT (self), "important"); +} + +/** + * gtd_task_get_list: + * + * Returns a weak reference to the #GtdTaskList that + * owns the given @task. + * + * Returns: (transfer none): a weak reference to the + * #GtdTaskList that owns @task. Do not free after + * usage. + */ +GtdTaskList* +gtd_task_get_list (GtdTask *task) +{ + GtdTaskPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + priv = gtd_task_get_instance_private (task); + + return priv->list; +} + +/** + * gtd_task_set_list: + * @self: a #GtdTask + * @list: (nullable): a #GtdTaskList + * + * Sets the parent #GtdTaskList of @task. + */ +void +gtd_task_set_list (GtdTask *task, + GtdTaskList *list) +{ + GtdTaskPrivate *priv; + + g_assert (GTD_IS_TASK (task)); + g_assert (GTD_IS_TASK_LIST (list)); + + priv = gtd_task_get_instance_private (task); + + if (priv->list == list) + return; + + if (priv->list) + g_object_weak_unref (G_OBJECT (priv->list), task_list_weak_notified, task); + + priv->list = list; + g_object_weak_ref (G_OBJECT (priv->list), task_list_weak_notified, task); + g_object_notify (G_OBJECT (task), "list"); +} + +/** + * gtd_task_get_position: + * @self: a #GtdTask + * + * Returns the position of @task inside the parent #GtdTaskList, + * or -1 if not set. + * + * Returns: the position of the task, or -1 + */ +gint64 +gtd_task_get_position (GtdTask *self) +{ + g_return_val_if_fail (GTD_IS_TASK (self), -1); + + return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->get_position (self); +} + +/** + * gtd_task_set_position: + * @self: a #GtdTask + * @position: the priority of @task, or -1 + * + * Sets the @task position inside the parent #GtdTaskList. It + * is up to the interface to handle two or more #GtdTask with + * the same position value. + */ +void +gtd_task_set_position (GtdTask *self, + gint64 position) +{ + g_return_if_fail (GTD_IS_TASK (self)); + + if (gtd_task_get_position (self) == position) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->set_position (self, position); + + g_object_notify (G_OBJECT (self), "position"); +} + +/** + * gtd_task_get_title: + * @self: a #GtdTask + * + * Retrieves the title of the task, or %NULL. + * + * Returns: (transfer none): the title of @task, or %NULL + */ +const gchar* +gtd_task_get_title (GtdTask *task) +{ + const gchar *title; + + g_return_val_if_fail (GTD_IS_TASK (task), NULL); + + title = GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_title (task); + + return title ? title : ""; +} + +/** + * gtd_task_set_title: + * @self: a #GtdTask + * @title: (nullable): the new title, or %NULL + * + * Updates the title of @task. The string is stripped off of + * leading spaces. + */ +void +gtd_task_set_title (GtdTask *task, + const gchar *title) +{ + const gchar *current_title; + + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (g_utf8_validate (title, -1, NULL)); + + current_title = gtd_task_get_title (task); + + if (g_strcmp0 (current_title, title) == 0) + return; + + GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_title (task, title); + + g_object_notify (G_OBJECT (task), "title"); +} + +/** + * gtd_task_compare: + * @t1: (nullable): a #GtdTask + * @t2: (nullable): a #GtdTask + * + * Compare @t1 and @t2. + * + * Returns: %-1 if @t1 comes before @t2, %1 for the opposite, %0 if they're equal + */ +gint +gtd_task_compare (GtdTask *t1, + GtdTask *t2) +{ + GDateTime *dt1; + GDateTime *dt2; + gchar *txt1; + gchar *txt2; + gint retval; + + if (!t1 && !t2) + return 0; + if (!t1) + return 1; + if (!t2) + return -1; + + /* + * The custom position overrides any comparison we can make. To keep compatibility, + * for now, we only compare by position if both tasks have a custom position set. + */ + if (gtd_task_get_position (t1) != -1 && gtd_task_get_position (t2) != -1) + { + retval = gtd_task_get_position (t1) - gtd_task_get_position (t2); + + if (retval != 0) + return retval; + } + + /* Compare by due date */ + dt1 = gtd_task_get_due_date (t1); + dt2 = gtd_task_get_due_date (t2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + if (dt1) + g_date_time_unref (dt1); + if (dt2) + g_date_time_unref (dt2); + + if (retval != 0) + return retval; + + /* Compare by creation date */ + dt1 = gtd_task_get_creation_date (t1); + dt2 = gtd_task_get_creation_date (t2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* If they're equal up to now, compare by title */ + txt1 = txt2 = NULL; + + txt1 = g_utf8_casefold (gtd_task_get_title (t1), -1); + txt2 = g_utf8_casefold (gtd_task_get_title (t2), -1); + + retval = g_strcmp0 (txt1, txt2); + + g_free (txt1); + g_free (txt2); + + return retval; +} + +/** + * gtd_task_get_provider: + * @self: a #GtdTaskList + * + * Utility function to retrieve the data provider that backs this + * task. Notice that this is exactly the same as writing: + * + * |[ + * GtdTaskList *list; + * GtdProvider *provider; + * + * list = gtd_task_get_list (task); + * provider = gtd_task_list_get_provider (list); + * ]| + * + * Returns: (transfer none)(nullable): the #GtdProvider of this task's list. + */ +GtdProvider* +gtd_task_get_provider (GtdTask *self) +{ + GtdTaskPrivate *priv; + + g_return_val_if_fail (GTD_IS_TASK (self), NULL); + + priv = gtd_task_get_instance_private (self); + + if (priv->list) + return gtd_task_list_get_provider (priv->list); + + return NULL; +} diff --git a/src/core/gtd-task.h b/src/core/gtd-task.h new file mode 100644 index 0000000..0c2b5aa --- /dev/null +++ b/src/core/gtd-task.h @@ -0,0 +1,122 @@ +/* gtd-task.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_H +#define GTD_TASK_H + +#include "gtd-object.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK (gtd_task_get_type()) + +G_DECLARE_DERIVABLE_TYPE (GtdTask, gtd_task, GTD, TASK, GtdObject) + +struct _GtdTaskClass +{ + GtdObjectClass parent; + + gboolean (*get_complete) (GtdTask *self); + void (*set_complete) (GtdTask *self, + gboolean complete); + + GDateTime* (*get_creation_date) (GtdTask *self); + void (*set_creation_date) (GtdTask *self, + GDateTime *dt); + + GDateTime* (*get_completion_date) (GtdTask *self); + void (*set_completion_date) (GtdTask *self, + GDateTime *dt); + + const gchar* (*get_description) (GtdTask *self); + void (*set_description) (GtdTask *self, + const gchar *description); + + GDateTime* (*get_due_date) (GtdTask *self); + void (*set_due_date) (GtdTask *self, + GDateTime *dt); + + gboolean (*get_important) (GtdTask *self); + void (*set_important) (GtdTask *self, + gboolean important); + + gint64 (*get_position) (GtdTask *self); + void (*set_position) (GtdTask *self, + gint64 position); + + const gchar* (*get_title) (GtdTask *self); + void (*set_title) (GtdTask *self, + const gchar *title); + + gpointer padding[8]; +}; + +GtdTask* gtd_task_new (void); + +gboolean gtd_task_get_complete (GtdTask *self); + +void gtd_task_set_complete (GtdTask *self, + gboolean complete); + +GDateTime* gtd_task_get_creation_date (GtdTask *self); + +void gtd_task_set_creation_date (GtdTask *self, + GDateTime *dt); + +GDateTime* gtd_task_get_completion_date (GtdTask *self); + +const gchar* gtd_task_get_description (GtdTask *self); + +void gtd_task_set_description (GtdTask *self, + const gchar *description); + +GDateTime* gtd_task_get_due_date (GtdTask *self); + +void gtd_task_set_due_date (GtdTask *self, + GDateTime *dt); + +gboolean gtd_task_get_important (GtdTask *self); + +void gtd_task_set_important (GtdTask *self, + gboolean important); + +GtdTaskList* gtd_task_get_list (GtdTask *self); + +void gtd_task_set_list (GtdTask *self, + GtdTaskList *list); + +gint64 gtd_task_get_position (GtdTask *self); + +void gtd_task_set_position (GtdTask *self, + gint64 position); + +const gchar* gtd_task_get_title (GtdTask *self); + +void gtd_task_set_title (GtdTask *self, + const gchar *title); + +gint gtd_task_compare (GtdTask *t1, + GtdTask *t2); + +GtdProvider* gtd_task_get_provider (GtdTask *self); + +G_END_DECLS + +#endif /* GTD_TASK_H */ diff --git a/src/endeavour.h b/src/endeavour.h new file mode 100644 index 0000000..aa93aa9 --- /dev/null +++ b/src/endeavour.h @@ -0,0 +1,56 @@ +/* endeavour.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ENDEAVOUR_H +#define ENDEAVOUR_H + +#include + +#include "gtd-enum-types.h" + +#include "gtd-activatable.h" +#include "gtd-bin-layout.h" +#include "gtd-easing.h" +#include "gtd-keyframe-transition.h" +#include "gtd-list-model-filter.h" +#include "gtd-list-model-sort.h" +#include "gtd-list-store.h" +#include "gtd-manager.h" +#include "gtd-max-size-layout.h" +#include "gtd-menu-button.h" +#include "gtd-notification.h" +#include "gtd-object.h" +#include "gtd-omni-area.h" +#include "gtd-omni-area-addin.h" +#include "gtd-panel.h" +#include "gtd-property-transition.h" +#include "gtd-provider.h" +#include "gtd-provider-popover.h" +#include "gtd-star-widget.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-list-view.h" +#include "gtd-timeline.h" +#include "gtd-transition.h" +#include "gtd-types.h" +#include "gtd-utils.h" +#include "gtd-widget.h" +#include "gtd-window.h" +#include "gtd-workspace.h" + +#endif /* ENDEAVOUR_H */ diff --git a/src/gtd-debug.h.in b/src/gtd-debug.h.in new file mode 100644 index 0000000..ba5bcf8 --- /dev/null +++ b/src/gtd-debug.h.in @@ -0,0 +1,229 @@ +/* gtd-debug.h.in + * + * Copyright (C) 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +/** + * SECTION:gtd-debug + * @short_description: Debugging macros + * @title:Debugging + * @stability:stable + * + * Macros used for tracing and debugging code. These + * are only valid when Endeavour is compiled with tracing + * support (pass `--enable-tracing` to the configure + * script to do that). + */ + +G_BEGIN_DECLS + +#ifndef GTD_ENABLE_TRACE +# define GTD_ENABLE_TRACE @ENABLE_TRACING@ +#endif +#if GTD_ENABLE_TRACE != 1 +# undef GTD_ENABLE_TRACE +#endif + +/** + * GTD_LOG_LEVEL_TRACE: (skip) + */ +#ifndef GTD_LOG_LEVEL_TRACE +# define GTD_LOG_LEVEL_TRACE ((GLogLevelFlags)(1 << G_LOG_LEVEL_USER_SHIFT)) +#endif + +#ifdef GTD_ENABLE_TRACE + +/** + * GTD_TRACE_MSG: + * @fmt: printf-like format of the message + * @...: arguments for @fmt + * + * Prints a trace message. + */ +# define GTD_TRACE_MSG(fmt, ...) \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " MSG: %s():%d: " fmt, \ + G_STRFUNC, __LINE__, ##__VA_ARGS__) + +/** + * GTD_PROBE: + * + * Prints a probing message. Put this macro in the code when + * you want to check the program reaches a certain section + * of code. + */ +# define GTD_PROBE \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, "PROBE: %s():%d", \ + G_STRFUNC, __LINE__) + +/** + * GTD_TODO: + * @_msg: the message to print + * + * Prints a TODO message. + */ +# define GTD_TODO(_msg) \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " TODO: %s():%d: %s", \ + G_STRFUNC, __LINE__, _msg) + +/** + * GTD_ENTRY: + * + * Prints an entry message. This shouldn't be used in + * critical functions. Place this at the beggining of + * the function, before any assertion. + */ +# define GTD_ENTRY \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, "ENTRY: %s():%d", \ + G_STRFUNC, __LINE__) + +/** + * GTD_EXIT: + * + * Prints an exit message. This shouldn't be used in + * critical functions. Place this at the end of + * the function, after any relevant code. If the + * function returns something, use GTD_RETURN() + * instead. + */ +# define GTD_EXIT \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " EXIT: %s():%d", \ + G_STRFUNC, __LINE__); \ + return; \ + } G_STMT_END + +/** + * GTD_GOTO: + * @_l: goto tag + * + * Logs a goto jump. + */ +# define GTD_GOTO(_l) \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " GOTO: %s():%d ("#_l")",\ + G_STRFUNC, __LINE__); \ + goto _l; \ + } G_STMT_END + +/** + * GTD_RETURN: + * @_r: the return value. + * + * Prints an exit message, and returns @_r. See #GTD_EXIT. + */ +# define GTD_RETURN(_r) \ + G_STMT_START { \ + g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " EXIT: %s():%d ", \ + G_STRFUNC, __LINE__); \ + return _r; \ + } G_STMT_END + +#else + +/** + * GTD_TODO: + * @_msg: the message to print + * + * Prints a TODO message. + */ +# define GTD_TODO(_msg) + +/** + * GTD_PROBE: + * + * Prints a probing message. + */ +# define GTD_PROBE + +/** + * GTD_TRACE_MSG: + * @fmt: printf-like format of the message + * @...: arguments for @fmt + * + * Prints a trace message. + */ +# define GTD_TRACE_MSG(fmt, ...) + +/** + * GTD_ENTRY: + * + * Prints a probing message. This shouldn't be used in + * critical functions. Place this at the beggining of + * the function, before any assertion. + */ +# define GTD_ENTRY + +/** + * GTD_GOTO: + * @_l: goto tag + * + * Logs a goto jump. + */ +# define GTD_GOTO(_l) goto _l + +/** + * GTD_EXIT: + * + * Prints an exit message. This shouldn't be used in + * critical functions. Place this at the end of + * the function, after any relevant code. If the + * function returns somethin, use GTD_RETURN() + * instead. + */ +# define GTD_EXIT return + +/** + * GTD_RETURN: + * @_r: the return value. + * + * Prints an exit message, and returns @_r. See #GTD_EXIT. + */ +# define GTD_RETURN(_r) return _r +#endif + +/** + * _GTD_BUG: (skip) + */ +#define _GTD_BUG(Component, Description, File, Line, Func, ...) \ + G_STMT_START { \ + g_printerr ("-----------------------------------------------------------------\n"); \ + g_printerr ("You've found a bug in Endeavour or one of its dependent libraries.\n"); \ + g_printerr ("Please help us help you by filing a bug report at:\n"); \ + g_printerr ("\n"); \ + g_printerr ("@BUGREPORT_URL@&component=%s\n", Component); \ + g_printerr ("\n"); \ + g_printerr ("%s:%d in function %s()\n", File, Line, Func); \ + g_printerr ("\n"); \ + g_printerr (Description"\n", ##__VA_ARGS__); \ + g_printerr ("-----------------------------------------------------------------\n"); \ + } G_STMT_END + +/** + * GTD_BUG: + * @Component: the component + * @Description: the description + * @...: extra arguments + * + * Logs a bug-friendly message. + */ +#define GTD_BUG(Component, Description, ...) \ + _GTD_BUG(Component, Description, __FILE__, __LINE__, G_STRFUNC, ##__VA_ARGS__) + +G_END_DECLS diff --git a/src/gtd-enum-types.c.template b/src/gtd-enum-types.c.template new file mode 100644 index 0000000..ed83a71 --- /dev/null +++ b/src/gtd-enum-types.c.template @@ -0,0 +1,39 @@ +/*** BEGIN file-header ***/ +#include "gtd-enum-types.h" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ +#include "@filename@" + +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static GType the_type = 0; + + if (the_type == 0) + { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, + "@VALUENAME@", + "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + the_type = g_@type@_register_static ( + g_intern_static_string ("@EnumName@"), + values); + } + return the_type; +} + +/*** END value-tail ***/ diff --git a/src/gtd-enum-types.h.template b/src/gtd-enum-types.h.template new file mode 100644 index 0000000..f3d5156 --- /dev/null +++ b/src/gtd-enum-types.h.template @@ -0,0 +1,24 @@ +/*** BEGIN file-header ***/ +#pragma once + +#include + +G_BEGIN_DECLS + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* Enumerations from "@filename@" */ + +/*** END file-production ***/ + +/*** BEGIN enumeration-production ***/ +#define GTD_TYPE_@ENUMSHORT@ (@enum_name@_get_type()) +GType @enum_name@_get_type (void) G_GNUC_CONST; + +/*** END enumeration-production ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +/*** END file-tail ***/ diff --git a/src/gtd-types.h b/src/gtd-types.h new file mode 100644 index 0000000..a240cc9 --- /dev/null +++ b/src/gtd-types.h @@ -0,0 +1,57 @@ +/* gtd-types.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TYPES_H +#define GTD_TYPES_H + +#include + +G_BEGIN_DECLS + +typedef struct _GtdActivatable GtdActivatable; +typedef struct _GtdAnimatable GtdAnimatable; +typedef struct _GtdApplication GtdApplication; +typedef struct _GtdClock GtdClock; +typedef struct _GtdDoneButton GtdDoneButton; +typedef struct _GtdInterval GtdInterval; +typedef struct _GtdInitialSetupWindow GtdInitialSetupWindow; +typedef struct _GtdListView GtdListView; +typedef struct _GtdManager GtdManager; +typedef struct _GtdMarkdownRenderer GtdMarkdownRenderer; +typedef struct _GtdNotification GtdNotification; +typedef struct _GtdObject GtdObject; +typedef struct _GtdOmniArea GtdOmniArea; +typedef struct _GtdPanel GtdPanel; +typedef struct _GtdPluginManager GtdPluginManager; +typedef struct _GtdProvider GtdProvider; +typedef struct _GtdStorage GtdStorage; +typedef struct _GtdStoragePopover GtdStoragePopover; +typedef struct _GtdStorageRow GtdStorageRow; +typedef struct _GtdStorageSelector GtdStorageSelector; +typedef struct _GtdTask GtdTask; +typedef struct _GtdTaskList GtdTaskList; +typedef struct _GtdTaskListItem GtdTaskListItem; +typedef struct _GtdTaskRow GtdTaskRow; +typedef struct _GtdTransition GtdTransition; +typedef struct _GtdWidget GtdWidget; +typedef struct _GtdWindow GtdWindow; +typedef struct _GtdWorkspace GtdWorkspace; + +G_END_DECLS + +#endif /* GTD_TYPES_H */ diff --git a/src/gtd-utils-private.h b/src/gtd-utils-private.h new file mode 100644 index 0000000..f668307 --- /dev/null +++ b/src/gtd-utils-private.h @@ -0,0 +1,29 @@ +/* gtd-utils-private.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +void gtd_ensure_types (void); + +G_END_DECLS diff --git a/src/gtd-utils.c b/src/gtd-utils.c new file mode 100644 index 0000000..fd14cfd --- /dev/null +++ b/src/gtd-utils.c @@ -0,0 +1,158 @@ +/* gtd-utils.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-bin-layout.h" +#include "gtd-max-size-layout.h" +#include "gtd-task.h" +#include "gtd-utils.h" +#include "gtd-utils-private.h" +#include "gtd-widget.h" + +#include + +#include + + +/* Combining diacritical mark? + * Basic range: [0x0300,0x036F] + * Supplement: [0x1DC0,0x1DFF] + * For Symbols: [0x20D0,0x20FF] + * Half marks: [0xFE20,0xFE2F] + */ +#define IS_CDM_UCS4(c) (((c) >= 0x0300 && (c) <= 0x036F) || \ + ((c) >= 0x1DC0 && (c) <= 0x1DFF) || \ + ((c) >= 0x20D0 && (c) <= 0x20FF) || \ + ((c) >= 0xFE20 && (c) <= 0xFE2F)) + +#define IS_SOFT_HYPHEN(c) ((c) == 0x00AD) + + +/* Copied from tracker/src/libtracker-fts/tracker-parser-glib.c under the GPL + * And then from gnome-shell/src/shell-util.c + * + * Originally written by Aleksander Morgado + */ +gchar* +gtd_normalize_casefold_and_unaccent (const gchar *str) +{ + g_autofree gchar *normalized = NULL; + gchar *tmp; + gint i = 0; + gint j = 0; + gint ilen; + + if (str == NULL) + return NULL; + + normalized = g_utf8_normalize (str, -1, G_NORMALIZE_NFKD); + tmp = g_utf8_casefold (normalized, -1); + + ilen = strlen (tmp); + + while (i < ilen) + { + gunichar unichar; + gchar *next_utf8; + gint utf8_len; + + /* Get next character of the word as UCS4 */ + unichar = g_utf8_get_char_validated (&tmp[i], -1); + + /* Invalid UTF-8 character or end of original string. */ + if (unichar == (gunichar) -1 || + unichar == (gunichar) -2) + { + break; + } + + /* Find next UTF-8 character */ + next_utf8 = g_utf8_next_char (&tmp[i]); + utf8_len = next_utf8 - &tmp[i]; + + if (IS_CDM_UCS4 (unichar) || IS_SOFT_HYPHEN (unichar)) + { + /* If the given unichar is a combining diacritical mark, + * just update the original index, not the output one */ + i += utf8_len; + continue; + } + + /* If already found a previous combining + * diacritical mark, indexes are different so + * need to copy characters. As output and input + * buffers may overlap, need to use memmove + * instead of memcpy */ + if (i != j) + { + memmove (&tmp[j], &tmp[i], utf8_len); + } + + /* Update both indexes */ + i += utf8_len; + j += utf8_len; + } + + /* Force proper string end */ + tmp[j] = '\0'; + + return tmp; +} + +GdkPaintable* +gtd_create_circular_paintable (GdkRGBA *color, + gint size) +{ + g_autoptr (GtkSnapshot) snapshot = NULL; + GskRoundedRect rect; + + snapshot = gtk_snapshot_new (); + + gtk_snapshot_push_rounded_clip (snapshot, + gsk_rounded_rect_init_from_rect (&rect, + &GRAPHENE_RECT_INIT (0, 0, size, size), + size / 2.0)); + + gtk_snapshot_append_color (snapshot, color, &GRAPHENE_RECT_INIT (0, 0, size, size)); + + gtk_snapshot_pop (snapshot); + + return gtk_snapshot_to_paintable (snapshot, &GRAPHENE_SIZE_INIT (size, size)); +} + +gint +gtd_collate_compare_strings (const gchar *string_a, + const gchar *string_b) +{ + g_autofree gchar *collated_a = NULL; + g_autofree gchar *collated_b = NULL; + + collated_a = g_utf8_collate_key (string_a, -1); + collated_b = g_utf8_collate_key (string_b, -1); + + return g_strcmp0 (collated_a, collated_b); +} + +void +gtd_ensure_types (void) +{ + g_type_ensure (GTD_TYPE_BIN_LAYOUT); + g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT); + g_type_ensure (GTD_TYPE_WIDGET); +} diff --git a/src/gtd-utils.h b/src/gtd-utils.h new file mode 100644 index 0000000..bc03170 --- /dev/null +++ b/src/gtd-utils.h @@ -0,0 +1,35 @@ +/* gtd-utils.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +gchar* gtd_normalize_casefold_and_unaccent (const gchar *str); + +gint gtd_collate_compare_strings (const gchar *string_a, + const gchar *string_b); + +GdkPaintable* gtd_create_circular_paintable (GdkRGBA *color, + gint size); + +G_END_DECLS diff --git a/src/gtd-vcs.h.in b/src/gtd-vcs.h.in new file mode 100644 index 0000000..9c499df --- /dev/null +++ b/src/gtd-vcs.h.in @@ -0,0 +1,29 @@ +/* gtd-vcs.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_VCS_TAG "@VCS_TAG@" + +G_END_DECLS diff --git a/src/gui/assets/all-done.svg b/src/gui/assets/all-done.svg new file mode 100644 index 0000000..60f4194 --- /dev/null +++ b/src/gui/assets/all-done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/gui/gtd-application.c b/src/gui/gtd-application.c new file mode 100644 index 0000000..f89e25f --- /dev/null +++ b/src/gui/gtd-application.c @@ -0,0 +1,331 @@ +/* gtd-application.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdApplication" + +#include "config.h" + +#include "gtd-application.h" +#include "gtd-debug.h" +#include "gtd-initial-setup-window.h" +#include "gtd-log.h" +#include "gtd-manager.h" +#include "gtd-manager-protected.h" +#include "gtd-vcs.h" +#include "gtd-window.h" + +#include +#include +#include +#include +#include + + +struct _GtdApplication +{ + AdwApplication application; + + GtkWindow *window; + GtkWidget *initial_setup; +}; + +static void gtd_application_activate_action (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +static void gtd_application_show_about (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +static void gtd_application_quit (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +static void gtd_application_show_help (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +G_DEFINE_TYPE (GtdApplication, gtd_application, ADW_TYPE_APPLICATION) + +static GOptionEntry cmd_options[] = { + { "quit", 'q', 0, G_OPTION_ARG_NONE, NULL, N_("Quit Endeavour"), NULL }, + { "debug", 'd', 0, G_OPTION_ARG_NONE, NULL, N_("Enable debug messages"), NULL }, + { "version", 'v', 0, G_OPTION_ARG_NONE, NULL, N_("Print version information and exit"), NULL }, + { NULL } +}; + +static const GActionEntry gtd_application_entries[] = { + { "activate", gtd_application_activate_action }, + { "about", gtd_application_show_about }, + { "quit", gtd_application_quit }, + { "help", gtd_application_show_help } +}; + +static void +gtd_application_activate_action (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdApplication *self = GTD_APPLICATION (user_data); + + gtk_window_present (GTK_WINDOW (self->window)); +} + +static void +gtd_application_show_about (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdApplication *self; + + static const gchar *developers[] = { + "Emmanuele Bassi ", + "Georges Basile Stavracas Neto ", + "Isaque Galdino ", + "Patrick Griffis ", + "Jamie Murphy ", + "Saiful B. Khan ", + NULL + }; + + static const gchar *designers[] = { + "Allan Day ", + "Jakub Steiner ", + "Tobias Bernard ", + NULL + }; + + self = GTD_APPLICATION (user_data); + + adw_show_about_window (GTK_WINDOW (self->window), + "application-name", _("Endeavour"), + "application-icon", APPLICATION_ID, + "version", GTD_VCS_TAG, + "copyright", _("Copyright \xC2\xA9 2015–2022 The Endeavour authors"), + "issue-url", "https://gitlab.gnome.org/World/Endeavour/-/issues", + "website", "https://gitlab.gnome.org/World/Endeavour", + "license-type", GTK_LICENSE_GPL_3_0, + "developers", developers, + "designers", designers, + "translator-credits", _("translator-credits"), + NULL); +} + +static void +gtd_application_quit (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdApplication *self = GTD_APPLICATION (user_data); + + gtk_window_destroy (self->window); +} + +static void +gtd_application_show_help (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdApplication *self = GTD_APPLICATION (user_data); + gtk_show_uri (GTK_WINDOW (self->window) , "help:endeavour", GDK_CURRENT_TIME); +} + +GtdApplication * +gtd_application_new (void) +{ + return g_object_new (GTD_TYPE_APPLICATION, + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE, + "resource-base-path", "/org/gnome/todo", + NULL); +} + +static void +set_accel_for_action (GtdApplication *self, + const gchar *detailed_action_name, + const gchar *accel) +{ + const char *accels[] = { accel, NULL }; + + gtk_application_set_accels_for_action (GTK_APPLICATION (self), detailed_action_name, accels); +} + + +static void +run_window (GtdApplication *self) +{ + gtk_window_present (GTK_WINDOW (self->window)); +} + +/* +static void +finish_initial_setup (GtdApplication *application) +{ + g_return_if_fail (GTD_IS_APPLICATION (application)); + + run_window (application); + + gtd_manager_set_is_first_run (application->priv->manager, FALSE); + + g_clear_pointer (&application->priv->initial_setup, gtk_widget_destroy); +} + +static void +run_initial_setup (GtdApplication *application) +{ + GtdApplicationPrivate *priv; + + g_return_if_fail (GTD_IS_APPLICATION (application)); + + priv = application->priv; + + if (!priv->initial_setup) + { + priv->initial_setup = gtd_initial_setup_window_new (application); + + g_signal_connect (priv->initial_setup, + "cancel", + G_CALLBACK (gtk_widget_destroy), + application); + + g_signal_connect_swapped (priv->initial_setup, + "done", + G_CALLBACK (finish_initial_setup), + application); + } + + gtk_widget_show (priv->initial_setup); +} +*/ + +static void +gtd_application_finalize (GObject *object) +{ + G_OBJECT_CLASS (gtd_application_parent_class)->finalize (object); +} + +static void +gtd_application_activate (GApplication *application) +{ + GTD_ENTRY; + + /* FIXME: the initial setup is disabled for the 3.18 release because + * we can't create tasklists on GOA accounts. + */ + run_window (GTD_APPLICATION (application)); + + GTD_EXIT; +} + +static void +gtd_application_startup (GApplication *application) +{ + GtdApplication *self; + + GTD_ENTRY; + + self = GTD_APPLICATION (application); + + /* add actions */ + g_action_map_add_action_entries (G_ACTION_MAP (self), + gtd_application_entries, + G_N_ELEMENTS (gtd_application_entries), + self); + + set_accel_for_action (self, "app.quit", "q"); + set_accel_for_action (self, "app.help", "F1"); + + G_APPLICATION_CLASS (gtd_application_parent_class)->startup (application); + + /* window */ + gtk_window_set_default_icon_name (APPLICATION_ID); + self->window = GTK_WINDOW (gtd_window_new (self)); + + /* Load the plugins */ + gtd_manager_load_plugins (gtd_manager_get_default ()); + + GTD_EXIT; +} + +static gint +gtd_application_command_line (GApplication *app, + GApplicationCommandLine *command_line) +{ + GVariantDict *options; + + options = g_application_command_line_get_options_dict (command_line); + + if (g_variant_dict_contains (options, "quit")) + { + g_application_quit (app); + return 0; + } + + g_application_activate (app); + + return 0; +} + +static gboolean +gtd_application_local_command_line (GApplication *application, + gchar ***arguments, + gint *exit_status) +{ + g_application_add_option_group (application, g_irepository_get_option_group()); + + return G_APPLICATION_CLASS (gtd_application_parent_class)->local_command_line (application, + arguments, + exit_status); +} + +static gint +gtd_application_handle_local_options (GApplication *application, + GVariantDict *options) +{ + if (g_variant_dict_contains (options, "debug")) + gtd_log_init (); + + if (g_variant_dict_contains (options, "version")) + { + g_print ("%s - Version %s\n", g_get_application_name (), PACKAGE_VERSION); + return 0; + } + + return -1; +} + +static void +gtd_application_class_init (GtdApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *application_class = G_APPLICATION_CLASS (klass); + + object_class->finalize = gtd_application_finalize; + + application_class->activate = gtd_application_activate; + application_class->startup = gtd_application_startup; + application_class->command_line = gtd_application_command_line; + application_class->local_command_line = gtd_application_local_command_line; + application_class->handle_local_options = gtd_application_handle_local_options; +} + +static void +gtd_application_init (GtdApplication *self) +{ + g_application_add_main_option_entries (G_APPLICATION (self), cmd_options); +} diff --git a/src/gui/gtd-application.h b/src/gui/gtd-application.h new file mode 100644 index 0000000..864d72d --- /dev/null +++ b/src/gui/gtd-application.h @@ -0,0 +1,36 @@ +/* gtd-application.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_APPLICATION_H +#define GTD_APPLICATION_H + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_APPLICATION (gtd_application_get_type()) + +G_DECLARE_FINAL_TYPE (GtdApplication, gtd_application, GTD, APPLICATION, AdwApplication) + +GtdApplication* gtd_application_new (void); + +G_END_DECLS + +#endif /* GTD_APPLICATION_H */ diff --git a/src/gui/gtd-bin-layout.c b/src/gui/gtd-bin-layout.c new file mode 100644 index 0000000..7f081eb --- /dev/null +++ b/src/gui/gtd-bin-layout.c @@ -0,0 +1,112 @@ +/* gtd-bin-layout.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-bin-layout.h" +#include "gtd-widget.h" + +struct _GtdBinLayout +{ + GtkLayoutManager parent_instance; +}; + +G_DEFINE_TYPE (GtdBinLayout, gtd_bin_layout, GTK_TYPE_LAYOUT_MANAGER) + +static void +gtd_bin_layout_measure (GtkLayoutManager *layout_manager, + GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (widget); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + if (gtk_widget_should_layout (child)) + { + int child_min = 0; + int child_nat = 0; + int child_min_baseline = -1; + int child_nat_baseline = -1; + + gtk_widget_measure (child, orientation, for_size, + &child_min, &child_nat, + &child_min_baseline, &child_nat_baseline); + + *minimum = MAX (*minimum, child_min); + *natural = MAX (*natural, child_nat); + + if (child_min_baseline > -1) + *minimum_baseline = MAX (*minimum_baseline, child_min_baseline); + if (child_nat_baseline > -1) + *natural_baseline = MAX (*natural_baseline, child_nat_baseline); + } + } +} + +static void +gtd_bin_layout_allocate (GtkLayoutManager *layout_manager, + GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (widget); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + if (child && gtk_widget_should_layout (child)) + { + GskTransform *transform = NULL; + + if (GTD_IS_WIDGET (child)) + transform = gtd_widget_apply_transform (GTD_WIDGET (child), NULL); + + gtk_widget_allocate (child, width, height, baseline, transform); + } + } +} + +static void +gtd_bin_layout_class_init (GtdBinLayoutClass *klass) +{ + GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass); + + layout_manager_class->measure = gtd_bin_layout_measure; + layout_manager_class->allocate = gtd_bin_layout_allocate; +} + +static void +gtd_bin_layout_init (GtdBinLayout *self) +{ +} + +GtkLayoutManager* +gtd_bin_layout_new (void) +{ + return g_object_new (GTD_TYPE_BIN_LAYOUT, NULL); +} diff --git a/src/gui/gtd-bin-layout.h b/src/gui/gtd-bin-layout.h new file mode 100644 index 0000000..8d66ead --- /dev/null +++ b/src/gui/gtd-bin-layout.h @@ -0,0 +1,32 @@ +/* gtd-bin-layout.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_BIN_LAYOUT (gtd_bin_layout_get_type()) +G_DECLARE_FINAL_TYPE (GtdBinLayout, gtd_bin_layout, GTD, BIN_LAYOUT, GtkLayoutManager) + +GtkLayoutManager* gtd_bin_layout_new (void); + +G_END_DECLS diff --git a/src/gui/gtd-color-button.c b/src/gui/gtd-color-button.c new file mode 100644 index 0000000..4ff3e23 --- /dev/null +++ b/src/gui/gtd-color-button.c @@ -0,0 +1,273 @@ +/* gtd-color-button.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdColorButton" + +#include "gtd-color-button.h" +#include "gtd-utils.h" + +#define INTENSITY(r, g, b) ((r) * 0.30 + (g) * 0.59 + (b) * 0.11) + +struct _GtdColorButton +{ + GtkWidget parent; + + GdkRGBA color; + + GtkWidget *selected_icon; +}; + +G_DEFINE_TYPE (GtdColorButton, gtd_color_button, GTK_TYPE_WIDGET) + +enum +{ + PROP_0, + PROP_COLOR, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * GtkWidget overrides + */ + +static void +gtd_color_button_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + GtdColorButton *self; + gint height_request; + gint width_request; + + self = GTD_COLOR_BUTTON (widget); + + gtk_widget_get_size_request (widget, &width_request, &height_request); + + gtk_widget_measure (self->selected_icon, + orientation, + for_size, + minimum, + natural, + NULL, + NULL); + + if (orientation == GTK_ORIENTATION_VERTICAL) + { + *minimum = MAX (*minimum, height_request); + *natural = MAX (*natural, height_request); + } + else + { + *minimum = MAX (*minimum, width_request); + *natural = MAX (*natural, width_request); + } +} + +static void +gtd_color_button_size_allocate (GtkWidget *widget, + gint width, + gint height, + gint baseline) +{ + GtdColorButton *self = GTD_COLOR_BUTTON (widget); + + gtk_widget_size_allocate (self->selected_icon, + &(GtkAllocation) + { + 0, 0, + width, height, + }, + baseline); +} + +static void +gtd_color_button_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + GtdColorButton *self; + gint height; + gint width; + + self = GTD_COLOR_BUTTON (widget); + width = gtk_widget_get_width (widget); + height = gtk_widget_get_height (widget); + + gtk_snapshot_append_color (snapshot, &self->color, &GRAPHENE_RECT_INIT (0, 0, width, height)); + gtk_widget_snapshot_child (widget, self->selected_icon, snapshot); +} + +static void +gtd_color_button_state_flags_changed (GtkWidget *widget, + GtkStateFlags previous_state) +{ + GtdColorButton *self; + gboolean selected; + + self = GTD_COLOR_BUTTON (widget); + selected = gtk_widget_get_state_flags (widget) & GTK_STATE_FLAG_SELECTED; + + if (selected) + gtk_image_set_from_icon_name (GTK_IMAGE (self->selected_icon), "object-select-symbolic"); + else + gtk_image_clear (GTK_IMAGE (self->selected_icon)); + + GTK_WIDGET_CLASS (gtd_color_button_parent_class)->state_flags_changed (widget, previous_state); +} + + +/* + * GObject overrides + */ + +static void +gtd_color_button_finalize (GObject *object) +{ + GtdColorButton *self = (GtdColorButton *)object; + + g_clear_pointer (&self->selected_icon, gtk_widget_unparent); + + G_OBJECT_CLASS (gtd_color_button_parent_class)->finalize (object); +} + +static void +gtd_color_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdColorButton *self = GTD_COLOR_BUTTON (object); + + switch (prop_id) + { + case PROP_COLOR: + g_value_set_boxed (value, &self->color); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_color_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdColorButton *self = GTD_COLOR_BUTTON (object); + + switch (prop_id) + { + case PROP_COLOR: + gtd_color_button_set_color (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_color_button_class_init (GtdColorButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_color_button_finalize; + object_class->get_property = gtd_color_button_get_property; + object_class->set_property = gtd_color_button_set_property; + + widget_class->measure = gtd_color_button_measure; + widget_class->size_allocate = gtd_color_button_size_allocate; + widget_class->snapshot = gtd_color_button_snapshot; + widget_class->state_flags_changed = gtd_color_button_state_flags_changed; + + properties[PROP_COLOR] = g_param_spec_boxed ("color", + "Color", + "Color of the button", + GDK_TYPE_RGBA, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_css_name (widget_class, "colorbutton"); +} + +static void +gtd_color_button_init (GtdColorButton *self) +{ + self->selected_icon = gtk_image_new (); + gtk_widget_set_parent (self->selected_icon, GTK_WIDGET (self)); + + gtk_widget_set_can_focus (GTK_WIDGET (self), TRUE); +} + +GtkWidget* +gtd_color_button_new (const GdkRGBA *color) +{ + return g_object_new (GTD_TYPE_COLOR_BUTTON, + "color", color, + "overflow", GTK_OVERFLOW_HIDDEN, + NULL); +} + +const GdkRGBA* +gtd_color_button_get_color (GtdColorButton *self) +{ + g_return_val_if_fail (GTD_IS_COLOR_BUTTON (self), NULL); + + return &self->color; +} + +void +gtd_color_button_set_color (GtdColorButton *self, + const GdkRGBA *color) +{ + g_return_if_fail (GTD_IS_COLOR_BUTTON (self)); + + if (gdk_rgba_equal (&self->color, color)) + return; + + self->color = *color; + self->color.alpha = 1.0; + + /* ... and adjust the icon color */ + if (INTENSITY (self->color.red, self->color.green, self->color.blue) > 0.5) + { + gtk_widget_add_css_class (GTK_WIDGET (self), "light"); + gtk_widget_remove_css_class (GTK_WIDGET (self), "dark"); + } + else + { + gtk_widget_add_css_class (GTK_WIDGET (self), "dark"); + gtk_widget_remove_css_class (GTK_WIDGET (self), "light"); + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COLOR]); +} + diff --git a/src/gui/gtd-color-button.h b/src/gui/gtd-color-button.h new file mode 100644 index 0000000..db18854 --- /dev/null +++ b/src/gui/gtd-color-button.h @@ -0,0 +1,38 @@ +/* gtd-color-button.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_COLOR_BUTTON (gtd_color_button_get_type()) + +G_DECLARE_FINAL_TYPE (GtdColorButton, gtd_color_button, GTD, COLOR_BUTTON, GtkWidget) + +GtkWidget* gtd_color_button_new (const GdkRGBA *color); + +const GdkRGBA* gtd_color_button_get_color (GtdColorButton *self); + +void gtd_color_button_set_color (GtdColorButton *self, + const GdkRGBA *color); + +G_END_DECLS diff --git a/src/gui/gtd-edit-pane.c b/src/gui/gtd-edit-pane.c new file mode 100644 index 0000000..1de17ac --- /dev/null +++ b/src/gui/gtd-edit-pane.c @@ -0,0 +1,602 @@ +/* gtd-edit-pane.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * Copyright (C) 2022 Jamie Murphy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdEditPane" + +#include "gtd-debug.h" +#include "gtd-edit-pane.h" +#include "gtd-manager.h" +#include "gtd-markdown-renderer.h" +#include "gtd-task.h" +#include "gtd-task-list.h" + +#include + +struct _GtdEditPane +{ + GtkBox parent; + + GtkCalendar *calendar; + GtkMenuButton *date_button; + GtkTextView *notes_textview; + + GCancellable *cancellable; + + /* task bindings */ + GBinding *notes_binding; + + GtdTask *task; + GDateTime *new_date; +}; + +G_DEFINE_TYPE (GtdEditPane, gtd_edit_pane, GTK_TYPE_BOX) + +enum +{ + PROP_0, + PROP_TASK, + LAST_PROP +}; + +enum +{ + CHANGED, + REMOVE_TASK, + NUM_SIGNALS +}; + + +static guint signals[NUM_SIGNALS] = { 0, }; + + +static void on_date_selected_cb (GtkCalendar *calendar, + GtdEditPane *self); + +/* + * Auxiliary methods + */ + +static void +update_date_widgets (GtdEditPane *self, GDateTime *dt) +{ + gchar *text; + + g_return_if_fail (GTD_IS_EDIT_PANE (self)); + + text = dt ? g_date_time_format (dt, "%x") : NULL; + + g_signal_handlers_block_by_func (self->calendar, on_date_selected_cb, self); + + if (!dt) + dt = g_date_time_new_now_local (); + + gtk_calendar_select_day (self->calendar, dt); + + g_signal_handlers_unblock_by_func (self->calendar, on_date_selected_cb, self); + + gtk_menu_button_set_label (self->date_button, text ? text : _("No date set")); + + g_free (text); +} + + +/* + * Callbacks + */ + +static void +on_date_popover_closed_cb (GtdEditPane *self) +{ + g_autofree gchar *text = NULL; + + if (!self->new_date || !self->task) + return; + + text = g_date_time_format (self->new_date, "%x"); + gtk_menu_button_set_label (self->date_button, text); + + g_signal_emit (self, signals[CHANGED], 0); +} + +static void +on_date_selected_cb (GtkCalendar *calendar, + GtdEditPane *self) +{ + g_autofree gchar *text = NULL; + + GTD_ENTRY; + + g_clear_pointer (&self->new_date, g_date_time_unref); + self->new_date = gtk_calendar_get_date (calendar); + text = g_date_time_format (self->new_date, "%x"); + + gtk_menu_button_set_label (self->date_button, text); + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_delete_button_clicked_cb (GtkButton *button, + GtdEditPane *self) +{ + g_signal_emit (self, signals[REMOVE_TASK], 0, self->task); +} + +static void +on_no_date_button_clicked_cb (GtkButton *button, + GtdEditPane *self) +{ + GTD_ENTRY; + + g_clear_pointer (&self->new_date, g_date_time_unref); + self->new_date = NULL; + gtk_calendar_clear_marks (GTK_CALENDAR (self->calendar)); + update_date_widgets (self, self->new_date); + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_today_button_clicked_cb (GtkButton *button, + GtdEditPane *self) +{ + g_autoptr (GDateTime) new_dt = g_date_time_new_now_local (); + + GTD_ENTRY; + + self->new_date = g_date_time_ref (new_dt); + update_date_widgets (self, self->new_date); + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_tomorrow_button_clicked_cb (GtkButton *button, + GtdEditPane *self) +{ + g_autoptr (GDateTime) current_date = NULL; + g_autoptr (GDateTime) new_dt = NULL; + + GTD_ENTRY; + + current_date = g_date_time_new_now_local (); + new_dt = g_date_time_add_days (current_date, 1); + + self->new_date = g_date_time_ref (new_dt); + update_date_widgets (self, self->new_date); + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_priority_changed_cb (GtkComboBox *combobox, + GtdEditPane *self) +{ + GTD_ENTRY; + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_text_buffer_changed_cb (GtkTextBuffer *buffer, + GParamSpec *pspec, + GtdEditPane *self) +{ + GTD_ENTRY; + + g_signal_emit (self, signals[CHANGED], 0); + + GTD_EXIT; +} + +static void +on_hyperlink_hover_cb (GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + GtdEditPane *self) +{ + GtkTextView *text_view; + GtkTextIter iter; + gboolean hovering; + gint ex, ey; + + text_view = GTK_TEXT_VIEW (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (controller))); + + gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_WIDGET, x, y, &ex, &ey); + + hovering = FALSE; + + if (gtk_text_view_get_iter_at_location (text_view, &iter, ex, ey)) + { + GSList *tags = NULL; + GSList *l = NULL; + + tags = gtk_text_iter_get_tags (&iter); + + for (l = tags; l; l = l->next) + { + g_autofree gchar *tag_name = NULL; + GtkTextTag *tag; + + tag = l->data; + + g_object_get (tag, "name", &tag_name, NULL); + + if (g_strcmp0 (tag_name, "url") == 0) + { + hovering = TRUE; + break; + } + } + } + + gtk_widget_set_cursor_from_name (GTK_WIDGET (text_view), hovering ? "pointer" : "text"); +} + +static void +on_uri_shown_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtk_show_uri_full_finish (GTK_WINDOW (source), result, &error); + + if (error) + g_warning ("%s", error->message); +} + +static void +on_hyperlink_clicked_cb (GtkGestureClick *gesture, + gint n_press, + gdouble x, + gdouble y, + GtdEditPane *self) +{ + GdkEventSequence *sequence; + GtkTextBuffer *buffer; + GtkTextView *text_view; + GtkTextIter end_iter; + GtkTextIter iter; + GSList *tags = NULL; + GSList *l = NULL; + gint ex = 0; + gint ey = 0; + + GTD_ENTRY; + + /* Claim the sequence to prevent propagating to GtkListBox and closing the row */ + sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), sequence, GTK_EVENT_SEQUENCE_CLAIMED); + + text_view = GTK_TEXT_VIEW (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture))); + buffer = gtk_text_view_get_buffer (text_view); + + /* Ensure focus */ + gtk_widget_grab_focus (GTK_WIDGET (text_view)); + + /* We shouldn't follow a link if the user has selected something */ + if (gtk_text_buffer_get_has_selection (buffer)) + return; + + gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_WIDGET, x, y, &ex, &ey); + + if (!gtk_text_view_get_iter_at_location (text_view, &iter, ex, ey)) + return; + + tags = gtk_text_iter_get_tags (&iter); + + for (l = tags; l; l = l->next) + { + g_autofree gchar *tag_name = NULL; + g_autofree gchar *url = NULL; + GtkTextIter url_start; + GtkTextIter url_end; + GtkTextTag *tag; + GtkRoot *root; + + tag = l->data; + + g_object_get (tag, "name", &tag_name, NULL); + + if (g_strcmp0 (tag_name, "url") != 0) + continue; + + gtk_text_buffer_get_iter_at_line (buffer, &iter, gtk_text_iter_get_line (&iter)); + end_iter = iter; + gtk_text_iter_forward_to_line_end (&end_iter); + + /* Find the beginning... */ + if (!gtk_text_iter_forward_search (&iter, "(", GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &url_start, NULL)) + continue; + + /* ... and the end of the URL */ + if (!gtk_text_iter_forward_search (&iter, ")", GTK_TEXT_SEARCH_TEXT_ONLY, &url_end, NULL, &end_iter)) + continue; + + url = gtk_text_iter_get_text (&url_start, &url_end); + root = gtk_widget_get_root (GTK_WIDGET (text_view)); + + if (root && GTK_IS_WINDOW (root)) + gtk_show_uri_full (GTK_WINDOW (root), url, GDK_CURRENT_TIME, self->cancellable, on_uri_shown_cb, self); + } +} + + +/* + * GObject overrides + */ + +static void +gtd_edit_pane_finalize (GObject *object) +{ + GtdEditPane *self = (GtdEditPane *) object; + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + g_clear_object (&self->task); + + G_OBJECT_CLASS (gtd_edit_pane_parent_class)->finalize (object); +} + +static void +gtd_edit_pane_dispose (GObject *object) +{ + GtdEditPane *self = (GtdEditPane *) object; + + g_clear_pointer (&self->new_date, g_date_time_unref); + g_clear_object (&self->task); + + G_OBJECT_CLASS (gtd_edit_pane_parent_class)->dispose (object); +} + +static void +gtd_edit_pane_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdEditPane *self = GTD_EDIT_PANE (object); + + switch (prop_id) + { + case PROP_TASK: + g_value_set_object (value, self->task); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_edit_pane_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdEditPane *self = GTD_EDIT_PANE (object); + + switch (prop_id) + { + case PROP_TASK: + self->task = g_value_get_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_edit_pane_class_init (GtdEditPaneClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_edit_pane_finalize; + object_class->dispose = gtd_edit_pane_dispose; + object_class->get_property = gtd_edit_pane_get_property; + object_class->set_property = gtd_edit_pane_set_property; + + /** + * GtdEditPane::task: + * + * The task that is actually being edited. + */ + g_object_class_install_property ( + object_class, + PROP_TASK, + g_param_spec_object ("task", + "Task being edited", + "The task that is actually being edited", + GTD_TYPE_TASK, + G_PARAM_READWRITE)); + + /** + * GtdEditPane::changed: + * + * Emitted when the task was changed. + */ + signals[CHANGED] = g_signal_new ("changed", + GTD_TYPE_EDIT_PANE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdEditPane::task-removed: + * + * Emitted when the user wants to remove the task. + */ + signals[REMOVE_TASK] = g_signal_new ("remove-task", + GTD_TYPE_EDIT_PANE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK); + + /* template class */ + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-edit-pane.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdEditPane, calendar); + gtk_widget_class_bind_template_child (widget_class, GtdEditPane, date_button); + gtk_widget_class_bind_template_child (widget_class, GtdEditPane, notes_textview); + + gtk_widget_class_bind_template_callback (widget_class, on_date_popover_closed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_date_selected_cb); + gtk_widget_class_bind_template_callback (widget_class, on_delete_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_hyperlink_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_hyperlink_hover_cb); + gtk_widget_class_bind_template_callback (widget_class, on_no_date_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_priority_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_text_buffer_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_today_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_tomorrow_button_clicked_cb); + + gtk_widget_class_set_css_name (widget_class, "editpane"); +} + +static void +gtd_edit_pane_init (GtdEditPane *self) +{ + self->cancellable = g_cancellable_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_edit_pane_new (void) +{ + return g_object_new (GTD_TYPE_EDIT_PANE, NULL); +} + +/** + * gtd_edit_pane_get_task: + * @self: a #GtdEditPane + * + * Retrieves the currently edited #GtdTask of %pane, or %NULL if none is set. + * + * Returns: (transfer none): the current #GtdTask being edited from %pane. + */ +GtdTask* +gtd_edit_pane_get_task (GtdEditPane *self) +{ + g_return_val_if_fail (GTD_IS_EDIT_PANE (self), NULL); + + return self->task; +} + +/** + * gtd_edit_pane_set_task: + * @pane: a #GtdEditPane + * @task: a #GtdTask or %NULL + * + * Sets %task as the currently editing task of %pane. + */ +void +gtd_edit_pane_set_task (GtdEditPane *self, + GtdTask *task) +{ + g_return_if_fail (GTD_IS_EDIT_PANE (self)); + + if (self->task && self->new_date) + { + g_clear_pointer (&self->new_date, g_date_time_unref); + } + + if (!g_set_object (&self->task, task)) + return; + + if (task) + { + GtkTextBuffer *buffer; + + buffer = gtk_text_view_get_buffer (self->notes_textview); + + g_signal_handlers_block_by_func (buffer, on_text_buffer_changed_cb, self); + + /* due date */ + self->new_date = gtd_task_get_due_date (self->task); + update_date_widgets (self, gtd_task_get_due_date (self->task)); + + /* description */ + gtk_text_buffer_set_text (buffer, gtd_task_get_description (task), -1); + + self->notes_binding = g_object_bind_property (buffer, + "text", + task, + "description", + G_BINDING_BIDIRECTIONAL); + + g_signal_handlers_unblock_by_func (buffer, on_text_buffer_changed_cb, self); + + } + + g_object_notify (G_OBJECT (self), "task"); +} + +/** + * gtd_edit_pane_get_new_date_time: + * @self: a #GtdEditPane + * + * Retrieves the newly set #GDateTime of %self, or %NULL if none is set. + * + * Returns: (transfer none): the current #GDateTime set in %self. + */ +GDateTime* +gtd_edit_pane_get_new_date_time (GtdEditPane *self) +{ + g_return_val_if_fail (GTD_IS_EDIT_PANE (self), NULL); + + return self->new_date; +} + +void +gtd_edit_pane_set_markdown_renderer (GtdEditPane *self, + GtdMarkdownRenderer *renderer) +{ + GtkTextBuffer *buffer; + + g_assert (GTD_IS_EDIT_PANE (self)); + g_assert (GTD_IS_MARKDOWN_RENDERER (renderer)); + + buffer = gtk_text_view_get_buffer (self->notes_textview); + + gtd_markdown_renderer_add_buffer (renderer, buffer); +} + diff --git a/src/gui/gtd-edit-pane.h b/src/gui/gtd-edit-pane.h new file mode 100644 index 0000000..fefaf9c --- /dev/null +++ b/src/gui/gtd-edit-pane.h @@ -0,0 +1,47 @@ +/* gtd-edit-pane.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_EDIT_PANE_H +#define GTD_EDIT_PANE_H + +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_EDIT_PANE (gtd_edit_pane_get_type()) + +G_DECLARE_FINAL_TYPE (GtdEditPane, gtd_edit_pane, GTD, EDIT_PANE, GtkBox) + +GtkWidget* gtd_edit_pane_new (void); + +GtdTask* gtd_edit_pane_get_task (GtdEditPane *self); + +void gtd_edit_pane_set_task (GtdEditPane *self, + GtdTask *task); + +GDateTime* gtd_edit_pane_get_new_date_time (GtdEditPane *self); + +void gtd_edit_pane_set_markdown_renderer (GtdEditPane *self, + GtdMarkdownRenderer *renderer); + +G_END_DECLS + +#endif /* GTD_EDIT_PANE_H */ diff --git a/src/gui/gtd-edit-pane.ui b/src/gui/gtd-edit-pane.ui new file mode 100644 index 0000000..9a96fd5 --- /dev/null +++ b/src/gui/gtd-edit-pane.ui @@ -0,0 +1,201 @@ + + + + + + + + + bottom + + + + vertical + 12 + 12 + 12 + 12 + 12 + + + 1 + 1 + + + + + + horizontal + 12 + true + + + _Today + 1 + 1 + 1 + + + + + + To_morrow + 1 + 1 + 1 + + + + + + + + None + 1 + + + + + + + diff --git a/src/gui/gtd-initial-setup-window.c b/src/gui/gtd-initial-setup-window.c new file mode 100644 index 0000000..ccea90b --- /dev/null +++ b/src/gui/gtd-initial-setup-window.c @@ -0,0 +1,247 @@ +/* gtd-initial-setup-window.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdInitialSetupWindow" + +#include "gtd-provider.h" +#include "gtd-application.h" +#include "gtd-provider-selector.h" +#include "gtd-initial-setup-window.h" +#include "gtd-manager.h" + +#include + +typedef struct +{ + GtkWidget *cancel_button; + GtkWidget *done_button; + GtkWidget *storage_selector; + + GtdManager *manager; +} GtdInitialSetupWindowPrivate; + +struct _GtdInitialSetupWindow +{ + GtkApplicationWindow parent; + + /**/ + GtdInitialSetupWindowPrivate *priv; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdInitialSetupWindow, gtd_initial_setup_window, GTK_TYPE_APPLICATION_WINDOW) + +enum { + PROP_0, + PROP_MANAGER, + LAST_PROP +}; + +enum { + CANCEL, + DONE, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +static void +gtd_initial_setup_window__location_selected (GtdInitialSetupWindow *window, + GtdProvider *provider) +{ + GtdInitialSetupWindowPrivate *priv; + + g_return_if_fail (GTD_IS_INITIAL_SETUP_WINDOW (window)); + + priv = window->priv; + + gtk_widget_set_sensitive (priv->done_button, provider != NULL); + + if (provider) + gtd_manager_set_default_provider (priv->manager, provider); +} + +static void +gtd_initial_setup_window__button_clicked (GtdInitialSetupWindow *window, + GtkWidget *button) +{ + GtdInitialSetupWindowPrivate *priv; + + g_return_if_fail (GTD_IS_INITIAL_SETUP_WINDOW (window)); + + priv = window->priv; + + if (button == priv->cancel_button) + { + g_signal_emit (window, + signals[CANCEL], + 0); + } + else if (button == priv->done_button) + { + g_signal_emit (window, + signals[DONE], + 0); + } +} + +static void +gtd_initial_setup_window_finalize (GObject *object) +{ + G_OBJECT_CLASS (gtd_initial_setup_window_parent_class)->finalize (object); +} + +static void +gtd_initial_setup_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdInitialSetupWindow *self = GTD_INITIAL_SETUP_WINDOW (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, self->priv->manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_initial_setup_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdInitialSetupWindow *self = GTD_INITIAL_SETUP_WINDOW (object); + + switch (prop_id) + { + case PROP_MANAGER: + self->priv->manager = g_value_get_object (value); + g_object_notify (object, "manager"); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_initial_setup_window_constructed (GObject *object) +{ + GtdInitialSetupWindowPrivate *priv; + + G_OBJECT_CLASS (gtd_initial_setup_window_parent_class)->constructed (object); + + priv = GTD_INITIAL_SETUP_WINDOW (object)->priv; + + g_object_bind_property (object, + "manager", + priv->storage_selector, + "manager", + G_BINDING_DEFAULT); +} + +static void +gtd_initial_setup_window_class_init (GtdInitialSetupWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_initial_setup_window_finalize; + object_class->constructed = gtd_initial_setup_window_constructed; + object_class->get_property = gtd_initial_setup_window_get_property; + object_class->set_property = gtd_initial_setup_window_set_property; + + /** + * GtdInitialSetupWindow::cancel: + * + * Emitted when the Cancel button is clicked. Should quit the + * application. + */ + signals[CANCEL] = g_signal_new ("cancel", + GTD_TYPE_INITIAL_SETUP_WINDOW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdInitialSetupWindow::done: + * + * Emitted when the Done button is clicked. Should start the + * application. + */ + signals[DONE] = g_signal_new ("done", + GTD_TYPE_INITIAL_SETUP_WINDOW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdInitialSetupWindow::manager: + * + * Manager of the application. + */ + g_object_class_install_property ( + object_class, + PROP_MANAGER, + g_param_spec_object ("manager", + "Manager of the task", + "The singleton manager instance of the task", + GTD_TYPE_MANAGER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-initial-setup.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, cancel_button); + gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, done_button); + gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, storage_selector); + + gtk_widget_class_bind_template_callback (widget_class, gtd_initial_setup_window__button_clicked); + gtk_widget_class_bind_template_callback (widget_class, gtd_initial_setup_window__location_selected); +} + +static void +gtd_initial_setup_window_init (GtdInitialSetupWindow *self) +{ + self->priv = gtd_initial_setup_window_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_initial_setup_window_new (GtdApplication *application) +{ + g_return_val_if_fail (GTD_IS_APPLICATION (application), NULL); + + return g_object_new (GTD_TYPE_INITIAL_SETUP_WINDOW, + "application", application, + "manager", gtd_manager_get_default (), + NULL); +} diff --git a/src/gui/gtd-initial-setup-window.h b/src/gui/gtd-initial-setup-window.h new file mode 100644 index 0000000..8927581 --- /dev/null +++ b/src/gui/gtd-initial-setup-window.h @@ -0,0 +1,37 @@ +/* gtd-initial-setup-window.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_INITIAL_SETUP_WINDOW_H +#define GTD_INITIAL_SETUP_WINDOW_H + +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_INITIAL_SETUP_WINDOW (gtd_initial_setup_window_get_type()) + +G_DECLARE_FINAL_TYPE (GtdInitialSetupWindow, gtd_initial_setup_window, GTD, INITIAL_SETUP_WINDOW, GtkApplicationWindow) + +GtkWidget* gtd_initial_setup_window_new (GtdApplication *application); + +G_END_DECLS + +#endif /* GTD_INITIAL_SETUP_WINDOW_H */ diff --git a/src/gui/gtd-initial-setup-window.ui b/src/gui/gtd-initial-setup-window.ui new file mode 100644 index 0000000..01c4b87 --- /dev/null +++ b/src/gui/gtd-initial-setup-window.ui @@ -0,0 +1,82 @@ + + + + + diff --git a/src/gui/gtd-markdown-renderer.c b/src/gui/gtd-markdown-renderer.c new file mode 100644 index 0000000..b257149 --- /dev/null +++ b/src/gui/gtd-markdown-renderer.c @@ -0,0 +1,357 @@ +/* gtd-markdown-buffer.c + * + * Copyright © 2018 Vyas Giridharan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdMarkdownRenderer" + +#include "gtd-debug.h" +#include "gtd-markdown-renderer.h" + +#define ITALICS_1 "*" +#define ITALICS_2 "_" +#define BOLD_1 "__" +#define BOLD_2 "**" +#define STRIKE "~~" +#define HEAD_1 "#" +#define HEAD_2 "##" +#define HEAD_3 "###" +#define LIST "+" + +struct _GtdMarkdownRenderer +{ + GObject parent_instance; + + GHashTable *populated_buffers; +}; + + +static void on_text_buffer_weak_notified_cb (gpointer data, + GObject *where_the_object_was); + +static void on_text_changed_cb (GtkTextBuffer *buffer, + GParamSpec *pspec, + GtdMarkdownRenderer *self); + + +G_DEFINE_TYPE (GtdMarkdownRenderer, gtd_markdown_renderer, G_TYPE_OBJECT) + + +/* + * Auxiliary methods + */ + +static void +apply_link_tags (GtdMarkdownRenderer *self, + GtkTextBuffer *buffer, + GtkTextTag *link_tag, + GtkTextTag *url_tag, + GtkTextIter *text_start, + GtkTextIter *text_end) +{ + GtkTextIter symbol_start; + GtkTextIter symbol_end; + GtkTextIter target_start; + GtkTextIter target_end; + GtkTextIter iter; + + GTD_ENTRY; + + iter = *text_start; + + /* + * We advance in pairs of [...], and inside the loop we check if the very next character + * after ']' is '('. No spaces are allowed. Only if this condition is satisfied that we + * claim this to be a link, and render it as such. + */ + while (gtk_text_iter_forward_search (&iter, "[", GTK_TEXT_SEARCH_TEXT_ONLY, &symbol_start, &target_start, text_end) && + gtk_text_iter_forward_search (&target_start, "]", GTK_TEXT_SEARCH_TEXT_ONLY, &target_end, &symbol_end, text_end)) + { + GtkTextIter url_start; + GtkTextIter url_end; + + iter = symbol_end; + + /* Advance a single position */ + url_start = symbol_end; + + /* Only consider valid if the character after ']' is '(' */ + if (gtk_text_iter_get_char (&url_start) != '(') + continue; + + /* + * Try and find the matching (...), if it fails, iter is set to the previous ']' so + * we don't enter in an infinite loop + */ + if (!gtk_text_iter_forward_search (&iter, "(", GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &url_start, text_end) || + !gtk_text_iter_forward_search (&iter, ")", GTK_TEXT_SEARCH_TEXT_ONLY, &url_end, NULL, text_end)) + { + continue; + } + + /* Apply both the link and url tags */ + gtk_text_buffer_apply_tag (buffer, link_tag, &target_start, &target_end); + gtk_text_buffer_apply_tag (buffer, url_tag, &url_start, &url_end); + + iter = url_end; + } +} + +static void +apply_markdown_tag (GtdMarkdownRenderer *self, + GtkTextBuffer *buffer, + GtkTextTag *tag, + const gchar *symbol, + GtkTextIter *text_start, + GtkTextIter *text_end, + gboolean paired) +{ + GtkTextIter symbol_start; + GtkTextIter symbol_end; + GtkTextIter iter; + + iter = *text_start; + + while (gtk_text_iter_forward_search (&iter, symbol, GTK_TEXT_SEARCH_TEXT_ONLY, &symbol_start, &symbol_end, text_end)) + { + GtkTextIter tag_start; + GtkTextIter tag_end; + + tag_start = symbol_start; + tag_end = symbol_end; + + /* Iter is initially at the end of the found symbol, to avoid infinite loops */ + iter = symbol_end; + + if (paired) + { + /* + * If the markdown tag is in the form of pairs (e.g. **bold**, __italics__, etc), then we should + * search the symbol twice. The first marks the start and the second marks the end of the section + * of the text that needs the tag. + * + * We also ignore the tag if it's not contained in the same line of the start. + */ + if (!gtk_text_iter_forward_search (&tag_end, symbol, GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &tag_end, text_end) || + gtk_text_iter_get_line (&tag_end) != gtk_text_iter_get_line (&symbol_start)) + { + continue; + } + } + else + { + /* + * If the markdown tag is not paired (e.g. ## header), then it is just applied at the start of + * the line. As such, we must search for the symbol - and this is where the tag starts - but move + * straight to the end of the line. + */ + gtk_text_iter_forward_to_line_end (&tag_end); + + /* Only apply this tag if this is the start of the line */ + if (gtk_text_iter_get_line_offset (&tag_start) != 0) + continue; + } + + /* Apply the tag */ + gtk_text_buffer_apply_tag (buffer, tag, &tag_start, &tag_end); + + /* + * If we applied the tag, jump the iter to the end of the tag. We are already guaranteed + * to not run into infinite loops, but this skips a bigger section of the buffer too and + * can save a tiny few cycles + */ + iter = tag_end; + } +} + +static void +populate_tag_table (GtdMarkdownRenderer *self, + GtkTextBuffer *buffer) +{ + gtk_text_buffer_create_tag (buffer, + "italic", + "style", + PANGO_STYLE_ITALIC, + NULL); + + gtk_text_buffer_create_tag (buffer, + "bold", + "weight", + PANGO_WEIGHT_BOLD, + NULL); + + gtk_text_buffer_create_tag (buffer, + "head_1", + "weight", + PANGO_WEIGHT_BOLD, + "scale", + PANGO_SCALE_XX_LARGE, + NULL); + + gtk_text_buffer_create_tag (buffer, + "head_2", + "weight", + PANGO_WEIGHT_BOLD, + "scale", + PANGO_SCALE_SMALL, + NULL); + + gtk_text_buffer_create_tag (buffer, + "head_3", + "weight", + PANGO_WEIGHT_BOLD, + "scale", + PANGO_SCALE_SMALL, + NULL); + + gtk_text_buffer_create_tag (buffer, + "strikethrough", + "strikethrough", + TRUE, + NULL); + + gtk_text_buffer_create_tag (buffer, + "list-indent", + "indent", + 20, + NULL); + + gtk_text_buffer_create_tag (buffer, + "url", + "foreground", + "blue", + "underline", + PANGO_UNDERLINE_SINGLE, + NULL); + + gtk_text_buffer_create_tag (buffer, + "link-text", + "weight", + PANGO_WEIGHT_BOLD, + "foreground", + "#555F61", + NULL); + + /* + * Add a weak ref so we can remove from the map of populated buffers when it's + * finalized. + */ + g_object_weak_ref (G_OBJECT (buffer), on_text_buffer_weak_notified_cb, self); + + /* Add to the map of populated buffers */ + g_hash_table_add (self->populated_buffers, buffer); + g_signal_connect (buffer, "notify::text", G_CALLBACK (on_text_changed_cb), self); + + g_debug ("Added buffer %p to markdown renderer", buffer); +} + +static void +render_markdown (GtdMarkdownRenderer *self, + GtkTextBuffer *buffer) +{ + GtkTextTagTable *tag_table; + GtkTextIter start; + GtkTextIter end; + + GTD_ENTRY; + + /* TODO: render in idle */ + + /* Wipe out the previous tags */ + gtk_text_buffer_get_start_iter (buffer, &start); + gtk_text_buffer_get_end_iter (buffer, &end); + + gtk_text_buffer_remove_all_tags (buffer, &start, &end); + + /* Apply the tags */ + tag_table = gtk_text_buffer_get_tag_table (buffer); + +#define TAG(x) gtk_text_tag_table_lookup(tag_table, x) + + apply_markdown_tag (self, buffer, TAG ("bold"), BOLD_2, &start, &end, TRUE); + apply_markdown_tag (self, buffer, TAG ("bold"), BOLD_1, &start, &end, TRUE); + apply_markdown_tag (self, buffer, TAG ("italic"), ITALICS_2, &start, &end, TRUE); + apply_markdown_tag (self, buffer, TAG ("italic"), ITALICS_1, &start, &end, TRUE); + apply_markdown_tag (self, buffer, TAG ("head_3"), HEAD_3, &start, &end, FALSE); + apply_markdown_tag (self, buffer, TAG ("head_2"), HEAD_2, &start, &end, FALSE); + apply_markdown_tag (self, buffer, TAG ("head_1"), HEAD_1, &start, &end, FALSE); + apply_markdown_tag (self, buffer, TAG ("strikethrough"), STRIKE, &start, &end, TRUE); + apply_markdown_tag (self, buffer, TAG ("list_indent"), LIST, &start, &end, FALSE); + + apply_link_tags (self, buffer, TAG ("link-text"), TAG ("url"), &start, &end); + +#undef TAG + + GTD_EXIT; +} + +/* + * Callbacks + */ + +static void +on_text_buffer_weak_notified_cb (gpointer data, + GObject *where_the_object_was) +{ + GtdMarkdownRenderer *self = GTD_MARKDOWN_RENDERER (data); + + g_hash_table_remove (self->populated_buffers, where_the_object_was); + + g_debug ("Buffer %p died and was removed from markdown renderer", where_the_object_was); +} + + +static void +on_text_changed_cb (GtkTextBuffer *buffer, + GParamSpec *pspec, + GtdMarkdownRenderer *self) +{ + render_markdown (self, buffer); +} + +static void +gtd_markdown_renderer_class_init (GtdMarkdownRendererClass *klass) +{ +} + +void +gtd_markdown_renderer_init (GtdMarkdownRenderer *self) +{ + self->populated_buffers = g_hash_table_new (g_direct_hash, g_direct_equal); +} + +GtdMarkdownRenderer* +gtd_markdown_renderer_new (void) +{ + return g_object_new (GTD_TYPE_MARKDOWN_RENDERER, NULL); +} + +void +gtd_markdown_renderer_add_buffer (GtdMarkdownRenderer *self, + GtkTextBuffer *buffer) +{ + g_return_if_fail (GTD_IS_MARKDOWN_RENDERER (self)); + + GTD_ENTRY; + + /* If the text buffer is not poopulated yet, do it now */ + if (!g_hash_table_contains (self->populated_buffers, buffer)) + populate_tag_table (self, buffer); + + render_markdown (self, buffer); + + GTD_EXIT; +} diff --git a/src/gui/gtd-markdown-renderer.h b/src/gui/gtd-markdown-renderer.h new file mode 100644 index 0000000..1c2593b --- /dev/null +++ b/src/gui/gtd-markdown-renderer.h @@ -0,0 +1,35 @@ +/* gtd-markdown-buffer.h + * + * Copyright (C) 2018 Vyas Giridharan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_MARKDOWN_RENDERER (gtd_markdown_renderer_get_type()) + +G_DECLARE_FINAL_TYPE (GtdMarkdownRenderer, gtd_markdown_renderer, GTD, MARKDOWN_RENDERER, GObject) + +GtdMarkdownRenderer* gtd_markdown_renderer_new (void); + +void gtd_markdown_renderer_add_buffer (GtdMarkdownRenderer*self, + GtkTextBuffer *buffer); + +G_END_DECLS + diff --git a/src/gui/gtd-max-size-layout.c b/src/gui/gtd-max-size-layout.c new file mode 100644 index 0000000..348b2ed --- /dev/null +++ b/src/gui/gtd-max-size-layout.c @@ -0,0 +1,474 @@ +/* gtd-max-size-layout.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-max-size-layout.h" +#include "gtd-widget.h" + +struct _GtdMaxSizeLayout +{ + GtkLayoutManager parent; + + gint max_height; + gint max_width; + gint max_width_chars; + gint width_chars; +}; + +G_DEFINE_TYPE (GtdMaxSizeLayout, gtd_max_size_layout, GTK_TYPE_LAYOUT_MANAGER) + +enum +{ + PROP_0, + PROP_MAX_HEIGHT, + PROP_MAX_WIDTH_CHARS, + PROP_MAX_WIDTH, + PROP_WIDTH_CHARS, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * GtkLayoutManager overrides + */ + +static void +gtd_max_size_layout_measure (GtkLayoutManager *layout_manager, + GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + GtdMaxSizeLayout *self = (GtdMaxSizeLayout *)layout_manager; + GtkWidget *child; + + for (child = gtk_widget_get_first_child (widget); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + gint child_min_baseline = -1; + gint child_nat_baseline = -1; + gint child_min = 0; + gint child_nat = 0; + + if (!gtk_widget_should_layout (child)) + continue; + + gtk_widget_measure (child, orientation, for_size, + &child_min, &child_nat, + &child_min_baseline, &child_nat_baseline); + + *minimum = MAX (*minimum, child_min); + *natural = MAX (*natural, child_nat); + + if (child_min_baseline > -1) + *minimum_baseline = MAX (*minimum_baseline, child_min_baseline); + if (child_nat_baseline > -1) + *natural_baseline = MAX (*natural_baseline, child_nat_baseline); + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + PangoFontMetrics *metrics; + PangoContext *context; + gint char_width; + + context = gtk_widget_get_pango_context (widget); + metrics = pango_context_get_metrics (context, + pango_context_get_font_description (context), + pango_context_get_language (context)); + + char_width = MAX (pango_font_metrics_get_approximate_char_width (metrics), + pango_font_metrics_get_approximate_digit_width (metrics)); + + if (self->width_chars > 0) + { + gint width_chars; + + width_chars = char_width * self->width_chars / PANGO_SCALE; + *minimum = MAX (*minimum, width_chars); + *natural = MAX (*natural, width_chars); + } + + if (self->max_width_chars > 0) + { + gint max_width_chars; + + max_width_chars = char_width * self->max_width_chars / PANGO_SCALE; + *minimum = MIN (*minimum, max_width_chars); + *natural = MAX (*natural, max_width_chars); + } + + if (self->max_width > 0) + { + *minimum = MIN (*minimum, self->max_width); + *natural = MAX (*natural, self->max_width); + } + + pango_font_metrics_unref (metrics); + } + else if (self->max_height > 0) + { + *minimum = MIN (*minimum, self->max_height); + *natural = MAX (*natural, self->max_height); + } +} + +static void +gtd_max_size_layout_allocate (GtkLayoutManager *layout_manager, + GtkWidget *widget, + gint width, + gint height, + gint baseline) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (widget); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + if (child && gtk_widget_should_layout (child)) + { + GskTransform *transform = NULL; + + if (GTD_IS_WIDGET (child)) + transform = gtd_widget_apply_transform (GTD_WIDGET (child), NULL); + + gtk_widget_allocate (child, width, height, baseline, transform); + } + } +} + + +/* + * GObject overrides + */ + +static void +gtd_max_size_layout_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdMaxSizeLayout *self = GTD_MAX_SIZE_LAYOUT (object); + + switch (prop_id) + { + case PROP_MAX_HEIGHT: + g_value_set_int (value, self->max_height); + break; + + case PROP_MAX_WIDTH: + g_value_set_int (value, self->max_width); + break; + + case PROP_MAX_WIDTH_CHARS: + g_value_set_int (value, self->max_width_chars); + break; + + case PROP_WIDTH_CHARS: + g_value_set_int (value, self->width_chars); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_max_size_layout_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdMaxSizeLayout *self = GTD_MAX_SIZE_LAYOUT (object); + + switch (prop_id) + { + case PROP_MAX_HEIGHT: + gtd_max_size_layout_set_max_height (self, g_value_get_int (value)); + break; + + case PROP_MAX_WIDTH: + gtd_max_size_layout_set_max_width (self, g_value_get_int (value)); + break; + + case PROP_MAX_WIDTH_CHARS: + gtd_max_size_layout_set_max_width_chars (self, g_value_get_int (value)); + break; + + case PROP_WIDTH_CHARS: + gtd_max_size_layout_set_width_chars (self, g_value_get_int (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_max_size_layout_class_init (GtdMaxSizeLayoutClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass); + + layout_manager_class->measure = gtd_max_size_layout_measure; + layout_manager_class->allocate = gtd_max_size_layout_allocate; + + object_class->get_property = gtd_max_size_layout_get_property; + object_class->set_property = gtd_max_size_layout_set_property; + + /** + * GtdMaxSizeLayout:max-height: + * + * Sets the maximum height of the #GtkWidget. + */ + properties[PROP_MAX_HEIGHT] = g_param_spec_int ("max-height", + "Max Height", + "Max Height", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtdMaxSizeLayout:max-width: + * + * Sets the maximum width of the #GtkWidget. + */ + properties[PROP_MAX_WIDTH] = g_param_spec_int ("max-width", + "Max Width", + "Max Width", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtdMaxSizeLayout:max-width-chars: + * + * Sets the maximum size of the #GtkWidget in characters. + */ + properties[PROP_MAX_WIDTH_CHARS] = g_param_spec_int ("max-width-chars", + "Max Width Chars", + "Max Width Chars", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GtdMaxSizeLayout:width-chars: + * + * Sets the size of the #GtkWidget in characters. + */ + properties[PROP_WIDTH_CHARS] = g_param_spec_int ("width-chars", + "Width Chars", + "Width Chars", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_max_size_layout_init (GtdMaxSizeLayout *self) +{ + self->max_height = -1; + self->max_width = -1; + self->max_width_chars = -1; + self->width_chars = -1; +} + +/** + * gtd_max_size_layout_new: + * + * Creates a new #GtdMaxSizeLayout. + * + * Returns: (transfer full): a #GtdMaxSizeLayout + */ +GtkLayoutManager* +gtd_max_size_layout_new (void) +{ + return g_object_new (GTD_TYPE_MAX_SIZE_LAYOUT, NULL); +} + +/** + * gtd_max_size_layout_get_max_height: + * + * Retrieves the maximum height of @self. + * + * Returns: maximum height + */ +gint +gtd_max_size_layout_get_max_height (GtdMaxSizeLayout *self) +{ + g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1); + + return self->max_height; +} + +/** + * gtd_max_size_layout_set_max_height: + * @self: a #GtdMaxSizeLayout + * @max_height: maximum height of the widget @self is attached to + * + * Sets the maximum height @self has. + */ +void +gtd_max_size_layout_set_max_height (GtdMaxSizeLayout *self, + gint max_height) +{ + g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self)); + g_return_if_fail (max_height >= -1); + + if (self->max_height == max_height) + return; + + self->max_height = max_height; + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_HEIGHT]); +} + +/** + * gtd_max_size_layout_get_max_width: + * + * Retrieves the maximum width of @self. + * + * Returns: maximum width + */ +gint +gtd_max_size_layout_get_max_width (GtdMaxSizeLayout *self) +{ + + g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1); + + return self->max_width; +} + +/** + * gtd_max_size_layout_set_max_width: + * @self: a #GtdMaxSizeLayout + * @max_width: maximum width of the widget @self is attached to + * + * Sets the maximum width @self has. + */ +void +gtd_max_size_layout_set_max_width (GtdMaxSizeLayout *self, + gint max_width) +{ + g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self)); + g_return_if_fail (max_width >= -1); + + if (self->max_width == max_width) + return; + + self->max_width = max_width; + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_WIDTH]); +} + +/** + * gtd_max_size_layout_get_max_width_chars: + * + * Retrieves the maximum width in characters of @self. + * + * Returns: maximum width in characters + */ +gint +gtd_max_size_layout_get_max_width_chars (GtdMaxSizeLayout *self) +{ + g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1); + + return self->max_width_chars; +} + +/** + * gtd_max_size_layout_set_max_width_chars: + * @self: a #GtdMaxSizeLayout + * @max_width_chars: maximum width of the widget @self is attached to, in character length + * + * Sets the maximum width @self has, in characters length. It is a programming + * error to set a value smaller than #GtdMaxSizeLayout:width-layout. + */ +void +gtd_max_size_layout_set_max_width_chars (GtdMaxSizeLayout *self, + gint max_width_chars) +{ + g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self)); + g_return_if_fail (max_width_chars >= -1); + g_return_if_fail (self->width_chars == -1 || max_width_chars >= self->width_chars); + + if (self->max_width_chars == max_width_chars) + return; + + self->max_width_chars = max_width_chars; + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_WIDTH_CHARS]); +} + +/** + * gtd_max_size_layout_get_width_chars: + * + * Retrieves the minimum width in characters of @self. + * + * Returns: minimum width in characters + */ +gint +gtd_max_size_layout_get_width_chars (GtdMaxSizeLayout *self) +{ + g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1); + + return self->width_chars; +} + +/** + * gtd_max_size_layout_set_width_chars: + * @self: a #GtdMaxSizeLayout + * @width_chars: minimum width of the widget @self is attached to, in character length + * + * Sets the minimum width @self has, in characters length. It is a programming + * error to set a value bigger than #GtdMaxSizeLayout:max-width-layout. + */ +void +gtd_max_size_layout_set_width_chars (GtdMaxSizeLayout *self, + gint width_chars) +{ + g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self)); + g_return_if_fail (width_chars >= -1); + g_return_if_fail (self->max_width_chars == -1 || width_chars <= self->max_width_chars); + + if (self->width_chars == width_chars) + return; + + self->width_chars = width_chars; + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WIDTH_CHARS]); +} diff --git a/src/gui/gtd-max-size-layout.h b/src/gui/gtd-max-size-layout.h new file mode 100644 index 0000000..2859d31 --- /dev/null +++ b/src/gui/gtd-max-size-layout.h @@ -0,0 +1,52 @@ +/* gtd-max-size-layout.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_MAX_SIZE_LAYOUT (gtd_max_size_layout_get_type()) +G_DECLARE_FINAL_TYPE (GtdMaxSizeLayout, gtd_max_size_layout, GTD, MAX_SIZE_LAYOUT, GtkLayoutManager) + +GtkLayoutManager* gtd_max_size_layout_new (void); + +gint gtd_max_size_layout_get_max_height (GtdMaxSizeLayout *self); + +void gtd_max_size_layout_set_max_height (GtdMaxSizeLayout *self, + gint max_height); + +gint gtd_max_size_layout_get_max_width (GtdMaxSizeLayout *self); + +void gtd_max_size_layout_set_max_width (GtdMaxSizeLayout *self, + gint max_width); + +gint gtd_max_size_layout_get_max_width_chars (GtdMaxSizeLayout *self); + +void gtd_max_size_layout_set_max_width_chars (GtdMaxSizeLayout *self, + gint max_width_chars); + +gint gtd_max_size_layout_get_width_chars (GtdMaxSizeLayout *self); + +void gtd_max_size_layout_set_width_chars (GtdMaxSizeLayout *self, + gint width_chars); + +G_END_DECLS diff --git a/src/gui/gtd-menu-button.c b/src/gui/gtd-menu-button.c new file mode 100644 index 0000000..e9f12be --- /dev/null +++ b/src/gui/gtd-menu-button.c @@ -0,0 +1,1056 @@ +/* gtd-menu-button.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-menu-button.h" + +typedef struct +{ + GIcon *icon; + + GtkWidget *button; + GtkWidget *popover; /* Only one at a time can be set */ + GMenuModel *model; + + GtdMenuButtonCreatePopupFunc create_popup_func; + gpointer create_popup_user_data; + GDestroyNotify create_popup_destroy_notify; + + GtkWidget *label_widget; + GtkWidget *align_widget; + GtkWidget *arrow_widget; + GtkArrowType arrow_type; +} GtdMenuButtonPrivate; + +enum +{ + PROP_0, + PROP_MENU_MODEL, + PROP_ALIGN_WIDGET, + PROP_DIRECTION, + PROP_POPOVER, + PROP_GICON, + PROP_LABEL, + PROP_USE_UNDERLINE, + PROP_HAS_FRAME, + LAST_PROP +}; + +static GParamSpec *menu_button_props[LAST_PROP]; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdMenuButton, gtd_menu_button, GTK_TYPE_WIDGET) + +static void +update_sensitivity (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + gtk_widget_set_sensitive (priv->button, + priv->popover != NULL || + priv->create_popup_func != NULL); +} + +static void +update_popover_direction (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + if (!priv->popover) + return; + + switch (priv->arrow_type) + { + case GTK_ARROW_UP: + gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_TOP); + break; + case GTK_ARROW_DOWN: + case GTK_ARROW_NONE: + gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_BOTTOM); + break; + case GTK_ARROW_LEFT: + gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_LEFT); + break; + case GTK_ARROW_RIGHT: + gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_RIGHT); + break; + default: + break; + } +} + +static void +set_arrow_type (GtkImage *image, + GtkArrowType arrow_type) +{ + switch (arrow_type) + { + case GTK_ARROW_NONE: + gtk_image_set_from_icon_name (image, "open-menu-symbolic"); + break; + case GTK_ARROW_DOWN: + gtk_image_set_from_icon_name (image, "pan-down-symbolic"); + break; + case GTK_ARROW_UP: + gtk_image_set_from_icon_name (image, "pan-up-symbolic"); + break; + case GTK_ARROW_LEFT: + gtk_image_set_from_icon_name (image, "pan-start-symbolic"); + break; + case GTK_ARROW_RIGHT: + gtk_image_set_from_icon_name (image, "pan-end-symbolic"); + break; + default: + break; + } +} + +static void +add_arrow (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv; + GtkWidget *arrow; + + priv = gtd_menu_button_get_instance_private (self); + + arrow = gtk_image_new (); + set_arrow_type (GTK_IMAGE (arrow), priv->arrow_type); + gtk_button_set_child (GTK_BUTTON (priv->button), arrow); + priv->arrow_widget = arrow; +} + +static void +set_align_widget_pointer (GtdMenuButton *self, + GtkWidget *align_widget) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + if (priv->align_widget) + g_object_remove_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget); + + priv->align_widget = align_widget; + + if (priv->align_widget) + g_object_add_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget); +} + + +/* + * Callbacks + */ + +static gboolean +menu_deactivate_cb (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), FALSE); + + return TRUE; +} + +static void +popover_destroy_cb (GtdMenuButton *self) +{ + gtd_menu_button_set_popover (self, NULL); +} + + +/* + * GtkWidget overrides + */ + +static void +gtd_menu_button_state_flags_changed (GtkWidget *widget, + GtkStateFlags previous_state_flags) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (widget); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + if (!gtk_widget_is_sensitive (widget)) + { + if (priv->popover) + gtk_widget_hide (priv->popover); + } +} + +static void +gtd_menu_button_toggled (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + const gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->button)); + + /* Might set a new menu/popover */ + if (active && priv->create_popup_func) + { + priv->create_popup_func (self, priv->create_popup_user_data); + } + + if (priv->popover) + { + if (active) + gtk_popover_popup (GTK_POPOVER (priv->popover)); + else + gtk_popover_popdown (GTK_POPOVER (priv->popover)); + } +} + +static void +gtd_menu_button_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (widget); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + gtk_widget_measure (priv->button, + orientation, + for_size, + minimum, natural, + minimum_baseline, natural_baseline); + +} + +static void +gtd_menu_button_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtdMenuButton *self= GTD_MENU_BUTTON (widget); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + gtk_widget_size_allocate (priv->button, + &(GtkAllocation) { 0, 0, width, height }, + baseline); + if (priv->popover) + gtk_popover_present (GTK_POPOVER (priv->popover)); +} + +static gboolean +gtd_menu_button_focus (GtkWidget *widget, + GtkDirectionType direction) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (widget); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + if (priv->popover && gtk_widget_get_visible (priv->popover)) + return gtk_widget_child_focus (priv->popover, direction); + else + return gtk_widget_child_focus (priv->button, direction); +} + +static gboolean +gtd_menu_button_grab_focus (GtkWidget *widget) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (widget); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + return gtk_widget_grab_focus (priv->button); +} + + +/* + * GObject overrides + */ + +static void +gtd_menu_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (object); + + switch (property_id) + { + case PROP_MENU_MODEL: + gtd_menu_button_set_menu_model (self, g_value_get_object (value)); + break; + + case PROP_ALIGN_WIDGET: + gtd_menu_button_set_align_widget (self, g_value_get_object (value)); + break; + + case PROP_DIRECTION: + gtd_menu_button_set_direction (self, g_value_get_enum (value)); + break; + + case PROP_POPOVER: + gtd_menu_button_set_popover (self, g_value_get_object (value)); + break; + + case PROP_GICON: + gtd_menu_button_set_gicon (self, g_value_get_object (value)); + break; + + case PROP_LABEL: + gtd_menu_button_set_label (self, g_value_get_string (value)); + break; + + case PROP_USE_UNDERLINE: + gtd_menu_button_set_use_underline (self, g_value_get_boolean (value)); + break; + + case PROP_HAS_FRAME: + gtd_menu_button_set_has_frame (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtd_menu_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (object); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + switch (property_id) + { + case PROP_MENU_MODEL: + g_value_set_object (value, priv->model); + break; + + case PROP_ALIGN_WIDGET: + g_value_set_object (value, priv->align_widget); + break; + + case PROP_DIRECTION: + g_value_set_enum (value, priv->arrow_type); + break; + + case PROP_POPOVER: + g_value_set_object (value, priv->popover); + break; + + case PROP_GICON: + g_value_set_object (value, priv->icon); + break; + + case PROP_LABEL: + g_value_set_string (value, gtd_menu_button_get_label (GTD_MENU_BUTTON (object))); + break; + + case PROP_USE_UNDERLINE: + g_value_set_boolean (value, gtd_menu_button_get_use_underline (GTD_MENU_BUTTON (object))); + break; + + case PROP_HAS_FRAME: + g_value_set_boolean (value, gtd_menu_button_get_has_frame (GTD_MENU_BUTTON (object))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtd_menu_button_dispose (GObject *object) +{ + GtdMenuButton *self = GTD_MENU_BUTTON (object); + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + if (priv->popover) + { + g_signal_handlers_disconnect_by_func (priv->popover, menu_deactivate_cb, object); + g_signal_handlers_disconnect_by_func (priv->popover, popover_destroy_cb, object); + gtk_widget_unparent (priv->popover); + priv->popover = NULL; + } + + set_align_widget_pointer (GTD_MENU_BUTTON (object), NULL); + + g_clear_object (&priv->model); + g_clear_pointer (&priv->button, gtk_widget_unparent); + + if (priv->create_popup_destroy_notify) + priv->create_popup_destroy_notify (priv->create_popup_user_data); + + G_OBJECT_CLASS (gtd_menu_button_parent_class)->dispose (object); +} + +static void +gtd_menu_button_class_init (GtdMenuButtonClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->set_property = gtd_menu_button_set_property; + gobject_class->get_property = gtd_menu_button_get_property; + gobject_class->dispose = gtd_menu_button_dispose; + + widget_class->measure = gtd_menu_button_measure; + widget_class->size_allocate = gtd_menu_button_size_allocate; + widget_class->state_flags_changed = gtd_menu_button_state_flags_changed; + widget_class->focus = gtd_menu_button_focus; + widget_class->grab_focus = gtd_menu_button_grab_focus; + + /** + * GtdMenuButton:menu-model: + * + * The #GMenuModel from which the popup will be created. + * + * See gtd_menu_button_set_menu_model() for the interaction with the + * #GtdMenuButton:popup property. + */ + menu_button_props[PROP_MENU_MODEL] = + g_param_spec_object ("menu-model", + "Menu model", + "The model from which the popup is made.", + G_TYPE_MENU_MODEL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdMenuButton:align-widget: + * + * The #GtkWidget to use to align the menu with. + */ + menu_button_props[PROP_ALIGN_WIDGET] = + g_param_spec_object ("align-widget", + "Align with", + "The parent widget which the menu should align with.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdMenuButton:direction: + * + * The #GtkArrowType representing the direction in which the + * menu or popover will be popped out. + */ + menu_button_props[PROP_DIRECTION] = + g_param_spec_enum ("direction", + "Direction", + "The direction the arrow should point.", + GTK_TYPE_ARROW_TYPE, + GTK_ARROW_DOWN, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdMenuButton:popover: + * + * The #GtkPopover that will be popped up when the button is clicked. + */ + menu_button_props[PROP_POPOVER] = + g_param_spec_object ("popover", + "Popover", + "The popover", + GTK_TYPE_POPOVER, + G_PARAM_READWRITE); + + menu_button_props[PROP_GICON] = + g_param_spec_string ("icon-name", + "Icon Name", + "The name of the icon used to automatically populate the button", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtdMenuButton:gicon: + * + */ + menu_button_props[PROP_GICON] = + g_param_spec_object ("gicon", + "GIcon", + "A GIcon", + G_TYPE_ICON, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + menu_button_props[PROP_LABEL] = + g_param_spec_string ("label", + "Label", + "The label for the button", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + menu_button_props[PROP_USE_UNDERLINE] = + g_param_spec_boolean ("use-underline", + "Use underline", + "If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + menu_button_props[PROP_HAS_FRAME] = + g_param_spec_boolean ("has-frame", + "Has frame", + "Whether the button has a frame", + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, LAST_PROP, menu_button_props); + + gtk_widget_class_set_css_name (widget_class, "menubutton"); +} + +static void +gtd_menu_button_init (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + priv->arrow_type = GTK_ARROW_DOWN; + + priv->button = gtk_toggle_button_new (); + gtk_widget_set_parent (priv->button, GTK_WIDGET (self)); + g_signal_connect_swapped (priv->button, "toggled", G_CALLBACK (gtd_menu_button_toggled), self); + add_arrow (self); + + gtk_widget_set_sensitive (priv->button, FALSE); + + gtk_widget_add_css_class (GTK_WIDGET (self), "popup"); +} + +/** + * gtd_menu_button_new: + * + * Creates a new #GtdMenuButton widget with downwards-pointing + * arrow as the only child. You can replace the child widget + * with another #GtkWidget should you wish to. + * + * Returns: The newly created #GtdMenuButton widget + */ +GtkWidget * +gtd_menu_button_new (void) +{ + return g_object_new (GTD_TYPE_MENU_BUTTON, NULL); +} + +/** + * gtd_menu_button_set_menu_model: + * @self: a #GtdMenuButton + * @menu_model: (nullable): a #GMenuModel, or %NULL to unset and disable the + * button + * + * Sets the #GMenuModel from which the popup will be constructed, + * or %NULL to dissociate any existing menu model and disable the button. + * + * A #GtkPopover will be created from the menu model with gtk_popover_menu_new_from_model(). + * Actions will be connected as documented for this function. + * + * If #GtdMenuButton:popover is already set, it will be dissociated from the @menu_button, + * and the property is set to %NULL. + */ +void +gtd_menu_button_set_menu_model (GtdMenuButton *self, + GMenuModel *menu_model) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + g_return_if_fail (G_IS_MENU_MODEL (menu_model) || menu_model == NULL); + + g_object_freeze_notify (G_OBJECT (self)); + + if (menu_model) + g_object_ref (menu_model); + + if (menu_model) + { + GtkWidget *popover; + + popover = gtk_popover_menu_new_from_model (menu_model); + gtd_menu_button_set_popover (self, popover); + } + else + { + gtd_menu_button_set_popover (self, NULL); + } + + priv->model = menu_model; + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_MENU_MODEL]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtd_menu_button_get_menu_model: + * @self: a #GtdMenuButton + * + * Returns the #GMenuModel used to generate the popup. + * + * Returns: (nullable) (transfer none): a #GMenuModel or %NULL + */ +GMenuModel * +gtd_menu_button_get_menu_model (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL); + + return priv->model; +} + +/** + * gtd_menu_button_set_align_widget: + * @self: a #GtdMenuButton + * @align_widget: (allow-none): a #GtkWidget + * + * Sets the #GtkWidget to use to line the menu with when popped up. + * Note that the @align_widget must contain the #GtdMenuButton itself. + * + * Setting it to %NULL means that the menu will be aligned with the + * button itself. + * + * Note that this property is only used with menus currently, + * and not for popovers. + */ +void +gtd_menu_button_set_align_widget (GtdMenuButton *self, + GtkWidget *align_widget) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + g_return_if_fail (align_widget == NULL || gtk_widget_is_ancestor (GTK_WIDGET (self), align_widget)); + + if (priv->align_widget == align_widget) + return; + + set_align_widget_pointer (self, align_widget); + + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_ALIGN_WIDGET]); +} + +/** + * gtd_menu_button_get_align_widget: + * @self: a #GtdMenuButton + * + * Returns the parent #GtkWidget to use to line up with menu. + * + * Returns: (nullable) (transfer none): a #GtkWidget value or %NULL + */ +GtkWidget * +gtd_menu_button_get_align_widget (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL); + + return priv->align_widget; +} + +/** + * gtd_menu_button_set_direction: + * @self: a #GtdMenuButton + * @direction: a #GtkArrowType + * + * Sets the direction in which the popup will be popped up, as + * well as changing the arrow’s direction. The child will not + * be changed to an arrow if it was customized. + * + * If the does not fit in the available space in the given direction, + * GTK+ will its best to keep it inside the screen and fully visible. + * + * If you pass %GTK_ARROW_NONE for a @direction, the popup will behave + * as if you passed %GTK_ARROW_DOWN (although you won’t see any arrows). + */ +void +gtd_menu_button_set_direction (GtdMenuButton *self, + GtkArrowType direction) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + GtkWidget *child; + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + if (priv->arrow_type == direction) + return; + + priv->arrow_type = direction; + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_DIRECTION]); + + /* Is it custom content? We don't change that */ + child = gtk_button_get_child (GTK_BUTTON (priv->button)); + if (priv->arrow_widget != child) + return; + + set_arrow_type (GTK_IMAGE (child), priv->arrow_type); + update_popover_direction (self); +} + +/** + * gtd_menu_button_get_direction: + * @self: a #GtdMenuButton + * + * Returns the direction the popup will be pointing at when popped up. + * + * Returns: a #GtkArrowType value + */ +GtkArrowType +gtd_menu_button_get_direction (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), GTK_ARROW_DOWN); + + return priv->arrow_type; +} + +/** + * gtd_menu_button_set_popover: + * @self: a #GtdMenuButton + * @popover: (nullable): a #GtkPopover, or %NULL to unset and disable the button + * + * Sets the #GtkPopover that will be popped up when the @menu_button is clicked, + * or %NULL to dissociate any existing popover and disable the button. + * + * If #GtdMenuButton:menu-model is set, the menu model is dissociated from the + * @menu_button, and the property is set to %NULL. + */ +void +gtd_menu_button_set_popover (GtdMenuButton *self, + GtkWidget *popover) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + g_return_if_fail (GTK_IS_POPOVER (popover) || popover == NULL); + + g_object_freeze_notify (G_OBJECT (self)); + + g_clear_object (&priv->model); + + if (priv->popover) + { + if (gtk_widget_get_visible (priv->popover)) + gtk_widget_hide (priv->popover); + + g_signal_handlers_disconnect_by_func (priv->popover, menu_deactivate_cb, self); + g_signal_handlers_disconnect_by_func (priv->popover, popover_destroy_cb, self); + + gtk_widget_unparent (priv->popover); + } + + priv->popover = popover; + + if (popover) + { + gtk_widget_set_parent (priv->popover, GTK_WIDGET (self)); + g_signal_connect_swapped (priv->popover, "closed", G_CALLBACK (menu_deactivate_cb), self); + g_signal_connect_swapped (priv->popover, "destroy", G_CALLBACK (popover_destroy_cb), self); + update_popover_direction (self); + } + + update_sensitivity (self); + + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_POPOVER]); + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_MENU_MODEL]); + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * gtd_menu_button_get_popover: + * @self: a #GtdMenuButton + * + * Returns the #GtkPopover that pops out of the button. + * If the button is not using a #GtkPopover, this function + * returns %NULL. + * + * Returns: (nullable) (transfer none): a #GtkPopover or %NULL + */ +GtkPopover * +gtd_menu_button_get_popover (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL); + + return GTK_POPOVER (priv->popover); +} + +/** + * gtd_menu_button_set_icon_name: + * @self: a #GtdMenuButton + * @icon_name: the icon name + * + * Sets the name of an icon to show inside the menu button. + */ +void +gtd_menu_button_set_gicon (GtdMenuButton *self, + GIcon *icon) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + GtkWidget *box; + GtkWidget *icon_image; + GtkWidget *image; + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_margin_start (box, 6); + gtk_widget_set_margin_end (box, 6); + + icon_image = gtk_image_new_from_gicon (icon); + gtk_widget_set_hexpand (icon_image, TRUE); + + image = gtk_image_new_from_icon_name ("pan-down-symbolic"); + + gtk_box_append (GTK_BOX (box), icon_image); + gtk_box_append (GTK_BOX (box), image); + gtk_button_set_child (GTK_BUTTON (priv->button), box); + + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_GICON]); +} + +/** + * gtd_menu_button_get_gicon: + * @self: a #GtdMenuButton + * + * Gets the name of the icon shown in the button. + * + * Returns: (transfer full): the name of the icon shown in the button + */ +GIcon* +gtd_menu_button_get_gicon (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL); + + return priv->icon; +} + +/** + * gtd_menu_button_set_label: + * @self: a #GtdMenuButton + * @label: the label + * + * Sets the label to show inside the menu button. + */ +void +gtd_menu_button_set_label (GtdMenuButton *self, + const gchar *label) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + GtkWidget *box; + GtkWidget *label_widget; + GtkWidget *image; + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_margin_start (box, 6); + gtk_widget_set_margin_end (box, 6); + + label_widget = gtk_label_new (label); + gtk_label_set_xalign (GTK_LABEL (label_widget), 0); + gtk_label_set_use_underline (GTK_LABEL (label_widget), + gtk_button_get_use_underline (GTK_BUTTON (priv->button))); + gtk_widget_set_hexpand (label_widget, TRUE); + image = gtk_image_new_from_icon_name ("pan-down-symbolic"); + gtk_box_append (GTK_BOX (box), label_widget); + gtk_box_append (GTK_BOX (box), image); + gtk_button_set_child (GTK_BUTTON (priv->button), box); + priv->label_widget = label_widget; + + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_LABEL]); +} + +/** + * gtd_menu_button_get_label: + * @self: a #GtdMenuButton + * + * Gets the label shown in the button + * + * Returns: the label shown in the button + */ +const gchar* +gtd_menu_button_get_label (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + GtkWidget *child; + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL); + + child = gtk_button_get_child (GTK_BUTTON (priv->button)); + if (GTK_IS_BOX (child)) + { + child = gtk_widget_get_first_child (child); + return gtk_label_get_label (GTK_LABEL (child)); + } + + return NULL; +} + +/** + * gtd_menu_button_set_has_frame: + * @self: a #GtdMenuButton + * @has_frame: whether the button should have a visible frame + * + * Sets the style of the button. + */ +void +gtd_menu_button_set_has_frame (GtdMenuButton *self, + gboolean has_frame) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + if (gtk_button_get_has_frame (GTK_BUTTON (priv->button)) == has_frame) + return; + + gtk_button_set_has_frame (GTK_BUTTON (priv->button), has_frame); + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_HAS_FRAME]); +} + +/** + * gtd_menu_button_get_has_frame: + * @self: a #GtdMenuButton + * + * Returns whether the button has a frame. + * + * Returns: %TRUE if the button has a frame + */ +gboolean +gtd_menu_button_get_has_frame (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), TRUE); + + return gtk_button_get_has_frame (GTK_BUTTON (priv->button)); +} + +/** + * gtd_menu_button_popup: + * @self: a #GtdMenuButton + * + * Pop up the menu. + */ +void +gtd_menu_button_popup (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), TRUE); +} + +/** + * gtd_menu_button_popdown: + * @self: a #GtdMenuButton + * + * Dismiss the menu. + */ +void +gtd_menu_button_popdown (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), FALSE); +} + +/** + * gtd_menu_button_set_create_popup_func: + * @self: a #GtdMenuButton + * @func: (nullable): function to call when a popuop is about to + * be shown, but none has been provided via other means, or %NULL + * to reset to default behavior. + * @user_data: (closure): user data to pass to @func. + * @destroy_notify: (nullable): destroy notify for @user_data + * + * Sets @func to be called when a popup is about to be shown. + * @func should use one of + * + * - gtd_menu_button_set_popover() + * - gtd_menu_button_set_menu_model() + * + * to set a popup for @menu_button. + * If @func is non-%NULL, @menu_button will always be sensitive. + * + * Using this function will not reset the menu widget attached to @menu_button. + * Instead, this can be done manually in @func. + */ +void +gtd_menu_button_set_create_popup_func (GtdMenuButton *self, + GtdMenuButtonCreatePopupFunc func, + gpointer user_data, + GDestroyNotify destroy_notify) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + if (priv->create_popup_destroy_notify) + priv->create_popup_destroy_notify (priv->create_popup_user_data); + + priv->create_popup_func = func; + priv->create_popup_user_data = user_data; + priv->create_popup_destroy_notify = destroy_notify; + + update_sensitivity (self); +} + +/** + * gtd_menu_button_set_use_underline: + * @self: a #GtdMenuButton + * @use_underline: %TRUE if underlines in the text indicate mnemonics + * + * If true, an underline in the text indicates the next character should be + * used for the mnemonic accelerator key. + */ +void +gtd_menu_button_set_use_underline (GtdMenuButton *self, + gboolean use_underline) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_if_fail (GTD_IS_MENU_BUTTON (self)); + + if (gtk_button_get_use_underline (GTK_BUTTON (priv->button)) == use_underline) + return; + + gtk_button_set_use_underline (GTK_BUTTON (priv->button), use_underline); + if (priv->label_widget) + gtk_label_set_use_underline (GTK_LABEL (priv->label_widget), use_underline); + + g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_USE_UNDERLINE]); +} + +/** + * gtd_menu_button_get_use_underline: + * @self: a #GtdMenuButton + * + * Returns whether an embedded underline in the text indicates a + * mnemonic. See gtd_menu_button_set_use_underline(). + * + * Returns: %TRUE whether an embedded underline in the text indicates + * the mnemonic accelerator keys. + */ +gboolean +gtd_menu_button_get_use_underline (GtdMenuButton *self) +{ + GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), FALSE); + + return gtk_button_get_use_underline (GTK_BUTTON (priv->button)); +} diff --git a/src/gui/gtd-menu-button.h b/src/gui/gtd-menu-button.h new file mode 100644 index 0000000..191806a --- /dev/null +++ b/src/gui/gtd-menu-button.h @@ -0,0 +1,104 @@ +/* gtd-menu-button.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_MENU_BUTTON (gtd_menu_button_get_type()) +G_DECLARE_DERIVABLE_TYPE (GtdMenuButton, gtd_menu_button, GTD, MENU_BUTTON, GtkWidget) + +struct _GtdMenuButtonClass +{ + GtkWidgetClass parent_class; +}; + +/** + * GtdMenuButtonCreatePopupFunc: + * @self: the #GtdMenuButton + * + * User-provided callback function to create a popup for @menu_button on demand. + * This function is called when the popoup of @menu_button is shown, but none has + * been provided via gtd_menu_buton_set_popup(), gtd_menu_button_set_popover() + * or gtd_menu_button_set_menu_model(). + */ +typedef void (*GtdMenuButtonCreatePopupFunc) (GtdMenuButton *self, + gpointer user_data); + +GtkWidget* gtd_menu_button_new (void); + +void gtd_menu_button_set_popover (GtdMenuButton *self, + GtkWidget *popover); + +GtkPopover* gtd_menu_button_get_popover (GtdMenuButton *self); + +void gtd_menu_button_set_direction (GtdMenuButton *self, + GtkArrowType direction); + +GtkArrowType gtd_menu_button_get_direction (GtdMenuButton *self); + +void gtd_menu_button_set_menu_model (GtdMenuButton *self, + GMenuModel *menu_model); + +GMenuModel* gtd_menu_button_get_menu_model (GtdMenuButton *self); + + +void gtd_menu_button_set_align_widget (GtdMenuButton *self, + GtkWidget *align_widget); + +GtkWidget* gtd_menu_button_get_align_widget (GtdMenuButton *self); + + +void gtd_menu_button_set_gicon (GtdMenuButton *self, + GIcon *icon); + +GIcon* gtd_menu_button_get_gicon (GtdMenuButton *self); + + +void gtd_menu_button_set_label (GtdMenuButton *self, + const gchar *label); + +const gchar* gtd_menu_button_get_label (GtdMenuButton *self); + + +void gtd_menu_button_set_use_underline (GtdMenuButton *self, + gboolean use_underline); + +gboolean gtd_menu_button_get_use_underline (GtdMenuButton *self); + +void gtd_menu_button_set_has_frame (GtdMenuButton *self, + gboolean has_frame); + +gboolean gtd_menu_button_get_has_frame (GtdMenuButton *self); + +void gtd_menu_button_popup (GtdMenuButton *self); + +void gtd_menu_button_popdown (GtdMenuButton *self); + + +void gtd_menu_button_set_create_popup_func (GtdMenuButton *self, + GtdMenuButtonCreatePopupFunc func, + gpointer user_data, + GDestroyNotify destroy_notify); + + +G_END_DECLS diff --git a/src/gui/gtd-new-task-row.c b/src/gui/gtd-new-task-row.c new file mode 100644 index 0000000..956bbef --- /dev/null +++ b/src/gui/gtd-new-task-row.c @@ -0,0 +1,374 @@ +/* gtd-new-task-row.c + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdNewTaskRow" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-new-task-row.h" +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-list-popover.h" +#include "gtd-task-list-view.h" +#include "gtd-utils.h" + +#include +#include + +struct _GtdNewTaskRow +{ + GtkListBoxRow parent; + + GtkEntry *entry; + GtdTaskListPopover *tasklist_popover; + + gboolean show_list_selector; +}; + +G_DEFINE_TYPE (GtdNewTaskRow, gtd_new_task_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + ENTER, + EXIT, + NUM_SIGNALS +}; + +enum +{ + PROP_0, + N_PROPS +}; + +static guint signals [NUM_SIGNALS] = { 0, }; + +/* + * Auxiliary methods + */ + +static void +update_secondary_icon (GtdNewTaskRow *self) +{ + g_autoptr (GdkPaintable) paintable = NULL; + g_autoptr (GdkRGBA) color = NULL; + g_autofree gchar *tooltip = NULL; + GtdTaskList *selected_list; + + if (!self->show_list_selector) + { + gtk_entry_set_icon_from_paintable (self->entry, GTK_ENTRY_ICON_SECONDARY, NULL); + return; + } + + selected_list = gtd_task_list_popover_get_task_list (GTD_TASK_LIST_POPOVER (self->tasklist_popover)); + + if (!selected_list) + return; + + color = gtd_task_list_get_color (selected_list); + paintable = gtd_create_circular_paintable (color, 12); + + gtk_entry_set_icon_from_paintable (self->entry, GTK_ENTRY_ICON_SECONDARY, paintable); + + /* Translators: %1$s is the task list name, %2$s is the provider name */ + tooltip = g_strdup_printf (_("%1$s \t %2$s"), + gtd_task_list_get_name (selected_list), + gtd_provider_get_description (gtd_task_list_get_provider (selected_list))); + gtk_entry_set_icon_tooltip_markup (self->entry, GTK_ENTRY_ICON_SECONDARY, tooltip); +} + +static void +show_task_list_selector_popover (GtdNewTaskRow *self) +{ + GdkRectangle rect; + + gtk_entry_get_icon_area (self->entry, GTK_ENTRY_ICON_SECONDARY, &rect); + gtk_popover_set_pointing_to (GTK_POPOVER (self->tasklist_popover), &rect); + gtk_popover_popup (GTK_POPOVER (self->tasklist_popover)); +} + +/* + * Callbacks + */ + +static void +on_task_created_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdNewTaskRow *self; + GtdTask *new_task; + + self = GTD_NEW_TASK_ROW (user_data); + new_task = gtd_provider_create_task_finish (GTD_PROVIDER (object), result, &error); + + if (!new_task) + { + g_warning ("Error creating task: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("An error occurred while creating a task"), + error->message, + NULL, + NULL); + } + + gtk_editable_set_text (GTK_EDITABLE (self->entry), ""); + gtk_widget_set_sensitive (GTK_WIDGET (self->entry), TRUE); + gtk_widget_grab_focus (GTK_WIDGET (self->entry)); +} + +static void +entry_activated_cb (GtdNewTaskRow *self) +{ + GtdTaskListView *view; + GtdTaskList *list; + GListModel *model; + + /* Cannot create empty tasks */ + if (gtk_entry_get_text_length (self->entry) == 0) + return; + + view = GTD_TASK_LIST_VIEW (gtk_widget_get_ancestor (GTK_WIDGET (self), GTD_TYPE_TASK_LIST_VIEW)); + + /* If there's a task list set, always go for it */ + model = gtd_task_list_view_get_model (view); + list = GTD_IS_TASK_LIST (model) ? GTD_TASK_LIST (model) : NULL; + + /* + * If there is no current list set, use the default list from the + * default provider. + */ + if (!list) + list = gtd_task_list_popover_get_task_list (GTD_TASK_LIST_POPOVER (self->tasklist_popover)); + + if (!list) + list = gtd_manager_get_inbox (gtd_manager_get_default ()); + + g_return_if_fail (GTD_IS_TASK_LIST (list)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->entry), FALSE); + + gtd_provider_create_task (gtd_task_list_get_provider (list), + list, + gtk_editable_get_text (GTK_EDITABLE (self->entry)), + gtd_task_list_view_get_default_date (view), + NULL, + on_task_created_cb, + self); +} + +static void +on_entry_icon_released_cb (GtkEntry *entry, + GtkEntryIconPosition position, + GtdNewTaskRow *self) +{ + switch (position) + { + case GTK_ENTRY_ICON_PRIMARY: + entry_activated_cb (self); + break; + + case GTK_ENTRY_ICON_SECONDARY: + show_task_list_selector_popover (self); + break; + } +} + +static void +on_focus_enter_cb (GtkEventControllerKey *event_controller, + GtdNewTaskRow *self) +{ + GTD_ENTRY; + + g_signal_emit (self, signals[ENTER], 0); + + GTD_EXIT; +} + +static void +on_tasklist_popover_changed_cb (GtdTaskListPopover *popover, + GParamSpec *pspec, + GtdNewTaskRow *self) +{ + GTD_ENTRY; + + update_secondary_icon (self); + + GTD_EXIT; +} + +static void +on_tasklist_popover_closed_cb (GtdTaskListPopover *popover, + GtdNewTaskRow *self) +{ + GTD_ENTRY; + + //gtk_entry_grab_focus_without_selecting (self->entry); + gtk_widget_grab_focus (GTK_WIDGET (self->entry)); + + GTD_EXIT; +} + + +/* + * GObject overrides + */ + +static void +gtd_new_task_row_dispose (GObject *object) +{ + GtdNewTaskRow *self = (GtdNewTaskRow *) object; + + if (self->tasklist_popover) + { + gtk_widget_unparent (GTK_WIDGET (self->tasklist_popover)); + self->tasklist_popover = NULL; + } + + if (self->entry) + { + gtk_widget_unparent (GTK_WIDGET (self->entry)); + self->entry = NULL; + } + + G_OBJECT_CLASS (gtd_new_task_row_parent_class)->dispose (object); +} + +static void +gtd_new_task_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_new_task_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_new_task_row_class_init (GtdNewTaskRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gtd_new_task_row_dispose; + object_class->get_property = gtd_new_task_row_get_property; + object_class->set_property = gtd_new_task_row_set_property; + + /** + * GtdNewTaskRow::enter: + * + * Emitted when the row is focused and in the editing state. + */ + signals[ENTER] = g_signal_new ("enter", + GTD_TYPE_NEW_TASK_ROW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdNewTaskRow::exit: + * + * Emitted when the row is unfocused and leaves the editing state. + */ + signals[EXIT] = g_signal_new ("exit", + GTD_TYPE_NEW_TASK_ROW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-new-task-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdNewTaskRow, entry); + gtk_widget_class_bind_template_child (widget_class, GtdNewTaskRow, tasklist_popover); + + gtk_widget_class_bind_template_callback (widget_class, entry_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_entry_icon_released_cb); + gtk_widget_class_bind_template_callback (widget_class, on_focus_enter_cb); + gtk_widget_class_bind_template_callback (widget_class, on_tasklist_popover_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_tasklist_popover_closed_cb); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); + gtk_widget_class_set_css_name (widget_class, "newtaskrow"); + + g_type_ensure (GTD_TYPE_TASK_LIST_POPOVER); +} + +static void +gtd_new_task_row_init (GtdNewTaskRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_widget_set_parent (GTK_WIDGET (self->tasklist_popover), GTK_WIDGET (self->entry)); + update_secondary_icon (self); +} + +GtkWidget* +gtd_new_task_row_new (void) +{ + return g_object_new (GTD_TYPE_NEW_TASK_ROW, NULL); +} + +gboolean +gtd_new_task_row_get_active (GtdNewTaskRow *self) +{ + g_return_val_if_fail (GTD_IS_NEW_TASK_ROW (self), FALSE); + + return gtk_widget_has_focus (GTK_WIDGET (self->entry)); +} + +void +gtd_new_task_row_set_active (GtdNewTaskRow *self, + gboolean active) +{ + g_return_if_fail (GTD_IS_NEW_TASK_ROW (self)); + + if (active) + gtk_widget_grab_focus (GTK_WIDGET (self->entry)); +} + +void +gtd_new_task_row_set_show_list_selector (GtdNewTaskRow *self, + gboolean show_list_selector) +{ + g_return_if_fail (GTD_IS_NEW_TASK_ROW (self)); + + if (self->show_list_selector == show_list_selector) + return; + + self->show_list_selector = show_list_selector; + update_secondary_icon (self); +} diff --git a/src/gui/gtd-new-task-row.h b/src/gui/gtd-new-task-row.h new file mode 100644 index 0000000..cb97fe8 --- /dev/null +++ b/src/gui/gtd-new-task-row.h @@ -0,0 +1,43 @@ +/* gtd-new-task-row.h + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_NEW_TASK_ROW_H +#define GTD_NEW_TASK_ROW_H + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_NEW_TASK_ROW (gtd_new_task_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdNewTaskRow, gtd_new_task_row, GTD, NEW_TASK_ROW, GtkListBoxRow) + +GtkWidget* gtd_new_task_row_new (void); + +gboolean gtd_new_task_row_get_active (GtdNewTaskRow *self); + +void gtd_new_task_row_set_active (GtdNewTaskRow *self, + gboolean active); + +void gtd_new_task_row_set_show_list_selector (GtdNewTaskRow *self, + gboolean show_list_selector); + +G_END_DECLS + +#endif /* GTD_NEW_TASK_ROW_H */ + diff --git a/src/gui/gtd-new-task-row.ui b/src/gui/gtd-new-task-row.ui new file mode 100644 index 0000000..a0da71d --- /dev/null +++ b/src/gui/gtd-new-task-row.ui @@ -0,0 +1,31 @@ + + + + + + False + + + + diff --git a/src/gui/gtd-omni-area-addin.c b/src/gui/gtd-omni-area-addin.c new file mode 100644 index 0000000..3ceec50 --- /dev/null +++ b/src/gui/gtd-omni-area-addin.c @@ -0,0 +1,69 @@ +/* gtd-omni-area-addin.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-omni-area-addin.h" + +#include "gtd-omni-area.h" + +G_DEFINE_INTERFACE (GtdOmniAreaAddin, gtd_omni_area_addin, G_TYPE_OBJECT) + +static void +gtd_omni_area_addin_default_init (GtdOmniAreaAddinInterface *iface) +{ +} + +/** + * gtd_omni_area_addin_load: + * @self: an #GtdOmniAreaAddin + * @omni_bar: an #GtdOmniArea + * + * Requests that the #GtdOmniAreaAddin initialize, possibly modifying + * @omni_bar as necessary. + */ +void +gtd_omni_area_addin_load (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar) +{ + g_return_if_fail (GTD_IS_OMNI_AREA_ADDIN (self)); + g_return_if_fail (GTD_IS_OMNI_AREA (omni_bar)); + + if (GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->load) + GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->load (self, omni_bar); +} + +/** + * gtd_omni_area_addin_unload: + * @self: an #GtdOmniAreaAddin + * @omni_bar: an #GtdOmniArea + * + * Requests that the #GtdOmniAreaAddin shutdown, possibly modifying + * @omni_bar as necessary to return it to the original state before + * the addin was loaded. + */ +void +gtd_omni_area_addin_unload (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar) +{ + g_return_if_fail (GTD_IS_OMNI_AREA_ADDIN (self)); + g_return_if_fail (GTD_IS_OMNI_AREA (omni_bar)); + + if (GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->unload) + GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->unload (self, omni_bar); +} diff --git a/src/gui/gtd-omni-area-addin.h b/src/gui/gtd-omni-area-addin.h new file mode 100644 index 0000000..28e3bd9 --- /dev/null +++ b/src/gui/gtd-omni-area-addin.h @@ -0,0 +1,49 @@ +/* gtd-omni-area-addin.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_OMNI_AREA_ADDIN (gtd_omni_area_addin_get_type ()) +G_DECLARE_INTERFACE (GtdOmniAreaAddin, gtd_omni_area_addin, GTD, OMNI_AREA_ADDIN, GObject) + +struct _GtdOmniAreaAddinInterface +{ + GTypeInterface parent; + + void (*load) (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar); + + void (*unload) (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar); +}; + +void gtd_omni_area_addin_load (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar); + +void gtd_omni_area_addin_unload (GtdOmniAreaAddin *self, + GtdOmniArea *omni_bar); + +G_END_DECLS diff --git a/src/gui/gtd-omni-area.c b/src/gui/gtd-omni-area.c new file mode 100644 index 0000000..828f36c --- /dev/null +++ b/src/gui/gtd-omni-area.c @@ -0,0 +1,256 @@ +/* gtd-omni-area.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdOmniArea" + +#include "gtd-omni-area.h" + +#include "gtd-debug.h" +#include "gtd-max-size-layout.h" +#include "gtd-omni-area-addin.h" + +#include + +struct _GtdOmniArea +{ + GtdWidget parent; + + GtkStack *main_stack; + GtkStack *status_stack; + + PeasExtensionSet *addins; + + GQueue *messages; + guint current; + + guint switch_messages_timeout_id; +}; + +G_DEFINE_TYPE (GtdOmniArea, gtd_omni_area, GTD_TYPE_WIDGET) + + +/* + * Auxiliary methods + */ + +static void +show_message (GtdOmniArea *self, + guint message_index) +{ + const gchar *message_id; + + message_id = g_queue_peek_nth (self->messages, message_index); + + gtk_stack_set_visible_child_name (self->status_stack, message_id); + self->current = message_index; +} + + + +/* + * Callbacks + */ + +static gboolean +switch_message_cb (gpointer user_data) +{ + GtdOmniArea *self = GTD_OMNI_AREA (user_data); + gint next_message_index; + guint n_messages; + + n_messages = g_queue_get_length (self->messages); + gtk_stack_set_visible_child_name (self->main_stack, n_messages > 0 ? "messages" : "placeholder"); + + next_message_index = (self->current + 1) % n_messages; + show_message (self, next_message_index); + + return G_SOURCE_CONTINUE; +} + +static void +on_omni_area_addin_added_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdOmniAreaAddin *addin, + GtdOmniArea *self) +{ + gtd_omni_area_addin_load (addin, self); +} + +static void +on_omni_area_addin_removed_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdOmniAreaAddin *addin, + GtdOmniArea *self) +{ + gtd_omni_area_addin_unload (addin, self); +} + + +/* + * GObject overrides + */ + +static void +gtd_omni_area_dispose (GObject *object) +{ + GtdOmniArea *self = GTD_OMNI_AREA (object); + + g_clear_object (&self->addins); + + G_OBJECT_CLASS (gtd_omni_area_parent_class)->dispose (object); +} + +static void +gtd_omni_area_finalize (GObject *object) +{ + GtdOmniArea *self = (GtdOmniArea *)object; + + g_clear_handle_id (&self->switch_messages_timeout_id, g_source_remove); + g_queue_free_full (self->messages, g_free); + + G_OBJECT_CLASS (gtd_omni_area_parent_class)->finalize (object); +} + +static void +gtd_omni_area_class_init (GtdOmniAreaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gtd_omni_area_dispose; + object_class->finalize = gtd_omni_area_finalize; + + g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-omni-area.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdOmniArea, main_stack); + gtk_widget_class_bind_template_child (widget_class, GtdOmniArea, status_stack); + + gtk_widget_class_set_css_name (widget_class, "omniarea"); +} + +static void +gtd_omni_area_init (GtdOmniArea *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->messages = g_queue_new (); + self->addins = peas_extension_set_new (peas_engine_get_default (), + GTD_TYPE_OMNI_AREA_ADDIN, + NULL); + + peas_extension_set_foreach (self->addins, + (PeasExtensionSetForeachFunc) on_omni_area_addin_added_cb, + self); + + g_signal_connect (self->addins, "extension-added", G_CALLBACK (on_omni_area_addin_added_cb), self); + g_signal_connect (self->addins, "extension-removed", G_CALLBACK (on_omni_area_addin_removed_cb), self); +} + +/** + * gtd_omni_area_push_message: + * @self: a #GtdOmniArea + * @id: an identifier for this notification + * @text: user visible text of the notification + * @icon: (nullable): a #GIcon + * + * Pushes a new message to @self. + * + */ +void +gtd_omni_area_push_message (GtdOmniArea *self, + const gchar *id, + const gchar *text, + GIcon *icon) +{ + GtkWidget *label; + GtkWidget *box; + + g_return_if_fail (GTD_IS_OMNI_AREA (self)); + g_return_if_fail (id != NULL); + g_return_if_fail (text != NULL); + + GTD_ENTRY; + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 18); + + label = gtk_label_new (text); + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_box_append (GTK_BOX (box), label); + + if (icon) + { + GtkWidget *image = gtk_image_new_from_gicon (icon); + + gtk_widget_add_css_class (image, "dim-label"); + gtk_box_append (GTK_BOX (box), image); + } + + gtk_stack_add_named (self->status_stack, box, id); + + g_debug ("Adding message '%s' to Omni Area", id); + + g_queue_push_tail (self->messages, g_strdup (id)); + show_message (self, g_queue_get_length (self->messages) - 1); + + g_clear_handle_id (&self->switch_messages_timeout_id, g_source_remove); + self->switch_messages_timeout_id = g_timeout_add (7500, switch_message_cb, self); + + GTD_EXIT; +} + +/** + * gtd_omni_area_withdraw_message: + * @self: a #GtdOmniArea + * @id: an identifier for this notification + * + * Withdraws a message from @self. If a message with @id doesn't + * exist, nothing happens. + */ +void +gtd_omni_area_withdraw_message (GtdOmniArea *self, + const gchar *id) +{ + GtkWidget *widget; + GList *l; + + g_return_if_fail (GTD_IS_OMNI_AREA (self)); + g_return_if_fail (id != NULL); + + GTD_ENTRY; + + widget = gtk_stack_get_child_by_name (self->status_stack, id); + if (!widget) + return; + + g_debug ("Removing message '%s' from Omni Area", id); + + gtk_stack_remove (self->status_stack, widget); + + l = g_queue_find_custom (self->messages, id, (GCompareFunc) g_strcmp0); + g_queue_delete_link (self->messages, l); + + if (g_queue_get_length (self->messages) == 0) + gtk_stack_set_visible_child_name (self->main_stack, "placeholder"); + + GTD_EXIT; +} diff --git a/src/gui/gtd-omni-area.h b/src/gui/gtd-omni-area.h new file mode 100644 index 0000000..9d49c38 --- /dev/null +++ b/src/gui/gtd-omni-area.h @@ -0,0 +1,41 @@ +/* gtd-omni-area.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +#include "gtd-widget.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_OMNI_AREA (gtd_omni_area_get_type()) +G_DECLARE_FINAL_TYPE (GtdOmniArea, gtd_omni_area, GTD, OMNI_AREA, GtdWidget) + +void gtd_omni_area_push_message (GtdOmniArea *self, + const gchar *id, + const gchar *text, + GIcon *icon); + +void gtd_omni_area_withdraw_message (GtdOmniArea *self, + const gchar *id); + +G_END_DECLS diff --git a/src/gui/gtd-omni-area.ui b/src/gui/gtd-omni-area.ui new file mode 100644 index 0000000..b04b918 --- /dev/null +++ b/src/gui/gtd-omni-area.ui @@ -0,0 +1,85 @@ + + + + diff --git a/src/gui/gtd-panel.c b/src/gui/gtd-panel.c new file mode 100644 index 0000000..5c1b5df --- /dev/null +++ b/src/gui/gtd-panel.c @@ -0,0 +1,313 @@ +/* gtd-panel.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPanel" + +#include "gtd-panel.h" + +/** + * SECTION:gtd-panel + * @short_description: interface for panels + * @title:GtdPanel + * @stability:Unstable + * + * The #GtdPanel interface must be implemented by plugins that want + * a given widget to be shown as a panel in the main window. Examples + * of panels are the "Today" and "Scheduled" panels. + * + * A panel must have a unique name (see #GtdPanel:name) and a title. + * The title can change dynamically. Avoid long titles. + * + * The panel may also provide header widgets, which will be placed + * in the headerbar according to the #GtkWidget:halign property. See + * gtd_panel_get_header_widgets() for a detailed explanation. + * + * The #GtdPanel:icon and #GtdPanel:priority properties are used by + * the sidebar. The former is used to display the icon, and the latter + * is used to determine the position of the panel relative to the + * others panels. + * + * At last, a #GtdPanel implementation may provide a #GMenu that will + * be appended to the window menu. Alternatively, a #GtkPopover can + * also be set. Popovers are used when both a menu and a popover are + * provided. + */ + +G_DEFINE_INTERFACE (GtdPanel, gtd_panel, GTK_TYPE_WIDGET) + +static void +gtd_panel_default_init (GtdPanelInterface *iface) +{ + + /** + * GtdPanel::icon: + * + * The icon of the panel. + */ + g_object_interface_install_property (iface, + g_param_spec_object ("icon", + "Icon of the panel", + "The icon of the panel", + G_TYPE_ICON, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdPanel::name: + * + * The identifier name of the panel. It is used as the #GtkStack + * name, so be sure to use a specific name that won't collide with + * other plugins. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("name", + "The name of the panel", + "The identifier name of the panel", + NULL, + G_PARAM_READABLE)); + + /** + * GtdPanel::priority: + * + * The priority of the panel. + */ + g_object_interface_install_property (iface, + g_param_spec_uint ("priority", + "Priority of the panel", + "The priority of the panel", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdPanel::subtitle: + * + * The subtitle of the panel. It usually is the counter of not + * completed tasks. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("subtitle", + "The subtitle of the panel", + "The user-visible subtitle of the panel", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdPanel::title: + * + * The user-visible title of the panel. It is used as the #GtkStack + * title. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("title", + "The title of the panel", + "The user-visible title of the panel", + NULL, + G_PARAM_READABLE)); + + /** + * GtdPanel::menu: + * + * A #GMenu of entries of the window menu. + */ + g_object_interface_install_property (iface, + g_param_spec_object ("menu", + "The title of the panel", + "The user-visible title of the panel", + G_TYPE_MENU, + G_PARAM_READABLE)); + +} + +/** + * gtd_panel_get_panel_name: + * @panel: a #GtdPanel + * + * Retrieves the name of @panel + * + * Returns: (transfer none): the name of @panel + */ +const gchar* +gtd_panel_get_panel_name (GtdPanel *panel) +{ + g_return_val_if_fail (GTD_IS_PANEL (panel), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_panel_name, NULL); + + return GTD_PANEL_GET_IFACE (panel)->get_panel_name (panel); +} + +/** + * gtd_panel_get_panel_title: + * @panel: a #GtdPanel + * + * Retrieves the title of @panel + * + * Returns: (transfer none): the title of @panel + */ +const gchar* +gtd_panel_get_panel_title (GtdPanel *panel) +{ + g_return_val_if_fail (GTD_IS_PANEL (panel), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_panel_title, NULL); + + return GTD_PANEL_GET_IFACE (panel)->get_panel_title (panel); +} + +/** + * gtd_panel_get_header_widgets: + * @panel: a #GtdPanel + * + * Retrieves the list of widgets to be placed at headerbar. The + * position of the widget is determined by the #GtkWidget::halign + * property. + * + * Widgets with @GTK_ALIGN_START halign will be packed into the + * start of the headerbar, and @GTK_ALIGN_END at the end. Other + * values are silently ignored. + * + * Returns: (transfer container) (element-type Gtk.Widget): the list of #GtkWidget + */ +GList* +gtd_panel_get_header_widgets (GtdPanel *panel) +{ + g_return_val_if_fail (GTD_IS_PANEL (panel), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_header_widgets, NULL); + + return GTD_PANEL_GET_IFACE (panel)->get_header_widgets (panel); +} + +/** + * gtd_panel_get_menu: + * @panel: a #GtdPanel + * + * Retrieves the gear menu of @panel. + * + * Returns: (transfer none): a #GMenu + */ +const GMenu* +gtd_panel_get_menu (GtdPanel *panel) +{ + g_return_val_if_fail (GTD_IS_PANEL (panel), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_menu, NULL); + + return GTD_PANEL_GET_IFACE (panel)->get_menu (panel); +} + +/** + * gtd_panel_get_icon: + * @self: a #GtdPanel + * + * Retrieves the icon of @self. + * + * Returns: (transfer full)(nullable): a #GIcon + */ +GIcon* +gtd_panel_get_icon (GtdPanel *self) +{ + g_return_val_if_fail (GTD_IS_PANEL (self), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_icon, NULL); + + return GTD_PANEL_GET_IFACE (self)->get_icon (self); +} + +/** + * gtd_panel_get_popover: + * @self: a #GtdPanel + * + * Retrieves the popover of @self. It is used as the + * window menu when available. + * + * Returns: (nullable)(transfer none): a #GtkPopover + */ +GtkPopover* +gtd_panel_get_popover (GtdPanel *self) +{ + g_return_val_if_fail (GTD_IS_PANEL (self), 0); + + if (GTD_PANEL_GET_IFACE (self)->get_popover) + return GTD_PANEL_GET_IFACE (self)->get_popover (self); + + return NULL; +} + +/** + * gtd_panel_get_priority: + * @self: a #GtdPanel + * + * Retrieves the priority of @self. This value is used to + * determine the position of the panel in the sidebar. + * + * Returns: the priority of @self + */ +guint32 +gtd_panel_get_priority (GtdPanel *self) +{ + g_return_val_if_fail (GTD_IS_PANEL (self), 0); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_priority, 0); + + return GTD_PANEL_GET_IFACE (self)->get_priority (self); +} + +/** + * gtd_panel_get_subtitle: + * @self: a #GtdPanel + * + * Retrieves the subtitle of @panel + * + * Returns: (transfer full): the subtitle of @panel + */ +gchar* +gtd_panel_get_subtitle (GtdPanel *self) +{ + g_return_val_if_fail (GTD_IS_PANEL (self), NULL); + g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_icon, NULL); + + return GTD_PANEL_GET_IFACE (self)->get_subtitle (self); +} + +/** + * gtd_panel_activate: + * @self: a #GtdPanel + * @parameters: (nullable): parameters of the panel + * + * Activates the panel with @parameters. The passed parameters + * are in free form, to allow panels have any input they want. + * + * This is an optional vfunc. + */ +void +gtd_panel_activate (GtdPanel *self, + GVariant *parameters) +{ + + g_return_if_fail (GTD_IS_PANEL (self)); + + if (GTD_PANEL_GET_IFACE (self)->activate) + { + g_autofree gchar *formatted_params = NULL; + + if (parameters) + formatted_params = g_variant_print (parameters, TRUE); + + g_debug ("Activating %s with parameters %s", + G_OBJECT_TYPE_NAME (self), + formatted_params); + + GTD_PANEL_GET_IFACE (self)->activate (self, parameters); + } +} diff --git a/src/gui/gtd-panel.h b/src/gui/gtd-panel.h new file mode 100644 index 0000000..787a0e3 --- /dev/null +++ b/src/gui/gtd-panel.h @@ -0,0 +1,77 @@ +/* gtd-panel.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PANEL_H +#define GTD_PANEL_H + +#include +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PANEL (gtd_panel_get_type ()) + +G_DECLARE_INTERFACE (GtdPanel, gtd_panel, GTD, PANEL, GtkWidget) + +struct _GtdPanelInterface +{ + GTypeInterface parent; + + const gchar* (*get_panel_name) (GtdPanel *panel); + + const gchar* (*get_panel_title) (GtdPanel *panel); + + GList* (*get_header_widgets) (GtdPanel *panel); + + const GMenu* (*get_menu) (GtdPanel *panel); + + GIcon* (*get_icon) (GtdPanel *self); + + GtkPopover* (*get_popover) (GtdPanel *self); + + guint32 (*get_priority) (GtdPanel *self); + + gchar* (*get_subtitle) (GtdPanel *self); + + void (*activate) (GtdPanel *self, + GVariant *parameters); +}; + +const gchar* gtd_panel_get_panel_name (GtdPanel *panel); + +const gchar* gtd_panel_get_panel_title (GtdPanel *panel); + +GList* gtd_panel_get_header_widgets (GtdPanel *panel); + +const GMenu* gtd_panel_get_menu (GtdPanel *panel); + +GIcon* gtd_panel_get_icon (GtdPanel *self); + +GtkPopover* gtd_panel_get_popover (GtdPanel *self); + +guint32 gtd_panel_get_priority (GtdPanel *self); + +gchar* gtd_panel_get_subtitle (GtdPanel *self); + +void gtd_panel_activate (GtdPanel *self, + GVariant *parameters); + +G_END_DECLS + +#endif /* GTD_PANEL_H */ diff --git a/src/gui/gtd-provider-popover.c b/src/gui/gtd-provider-popover.c new file mode 100644 index 0000000..0fa580d --- /dev/null +++ b/src/gui/gtd-provider-popover.c @@ -0,0 +1,245 @@ +/* gtd-provider-popover.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderPopover" + +#include "gtd-manager.h" +#include "gtd-provider.h" +#include "gtd-provider-popover.h" +#include "gtd-provider-selector.h" +#include "gtd-task-list.h" + +#include + +struct _GtdProviderPopover +{ + GtkPopover parent; + + GtkWidget *change_location_button; + GtkWidget *location_provider_image; + GtkWidget *new_list_create_button; + GtkEditable *new_list_name_entry; + GtkWidget *stack; + GtkWidget *provider_selector; +}; + +G_DEFINE_TYPE (GtdProviderPopover, gtd_provider_popover, GTK_TYPE_POPOVER) + +static void +clear_and_hide (GtdProviderPopover *popover) +{ + GtdManager *manager; + GList *locations; + GList *l; + + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + + manager = gtd_manager_get_default (); + + /* Select the default source again */ + locations = gtd_manager_get_providers (manager); + + for (l = locations; l != NULL; l = l->next) + { + if (FALSE)//gtd_provider_get_is_default (l->data)) + { + gtd_provider_selector_set_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector), l->data); + break; + } + } + + g_list_free (locations); + + /* By clearing the text, Create button will get insensitive */ + gtk_editable_set_text (popover->new_list_name_entry, ""); + + /* Hide the popover at last */ + gtk_widget_hide (GTK_WIDGET (popover)); +} + +static void +gtd_provider_popover__closed (GtdProviderPopover *popover) +{ + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + + gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main"); +} + +static void +on_task_list_created_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_create_task_list_finish (GTD_PROVIDER (source), result, &error); + + if (error) + { + g_warning ("Error creating task list: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("An error occurred while creating a task list"), + error->message, + NULL, + NULL); + } +} + +static void +create_task_list (GtdProviderPopover *popover) +{ + GtdProvider *provider; + const gchar *name; + + provider = gtd_provider_selector_get_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector)); + name = gtk_editable_get_text (popover->new_list_name_entry); + + gtd_provider_create_task_list (provider, + name, + NULL, + on_task_list_created_cb, + popover); +} + +static void +gtd_provider_popover__entry_activate (GtdProviderPopover *popover, + GtkEntry *entry) +{ + if (gtk_entry_get_text_length (entry) > 0) + { + create_task_list (popover); + clear_and_hide (popover); + } +} + +static void +gtd_provider_popover__action_button_clicked (GtdProviderPopover *popover, + GtkWidget *button) +{ + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + + if (button == popover->new_list_create_button) + create_task_list (popover); + + clear_and_hide (popover); +} + +static void +gtd_provider_popover__text_changed_cb (GtdProviderPopover *popover, + GParamSpec *spec, + GtkEntry *entry) +{ + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + g_return_if_fail (GTK_IS_ENTRY (entry)); + + gtk_widget_set_sensitive (popover->new_list_create_button, gtk_entry_get_text_length (entry) > 0); +} + +static void +gtd_provider_popover__change_location_clicked (GtdProviderPopover *popover, + GtkWidget *button) +{ + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + + if (button == popover->change_location_button) + gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "selector"); + else + gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main"); +} + +static void +gtd_provider_popover__provider_selected (GtdProviderPopover *popover, + GtdProvider *provider) +{ + g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover)); + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + if (provider) + { + gtk_image_set_from_gicon (GTK_IMAGE (popover->location_provider_image), gtd_provider_get_icon (provider)); + gtk_widget_set_tooltip_text (popover->change_location_button, gtd_provider_get_name (provider)); + + /* Go back immediately after selecting a provider */ + gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main"); + + if (gtk_widget_is_visible (GTK_WIDGET (popover))) + gtk_widget_grab_focus (GTK_WIDGET (popover->new_list_name_entry)); + } +} + +static void +gtd_provider_popover_finalize (GObject *object) +{ + G_OBJECT_CLASS (gtd_provider_popover_parent_class)->finalize (object); +} + +static void +gtd_provider_popover_constructed (GObject *object) +{ + GtdProviderPopover *popover; + GtdProvider *provider; + + G_OBJECT_CLASS (gtd_provider_popover_parent_class)->constructed (object); + + popover = GTD_PROVIDER_POPOVER (object); + provider = gtd_provider_selector_get_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector)); + + if (provider) + gtk_image_set_from_gicon (GTK_IMAGE (popover->location_provider_image), gtd_provider_get_icon (provider)); +} + +static void +gtd_provider_popover_class_init (GtdProviderPopoverClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_provider_popover_finalize; + object_class->constructed = gtd_provider_popover_constructed; + + g_type_ensure (GTD_TYPE_PROVIDER_SELECTOR); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-popover.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, change_location_button); + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, location_provider_image); + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, new_list_create_button); + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, new_list_name_entry); + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, stack); + gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, provider_selector); + + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__action_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__change_location_clicked); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__closed); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__entry_activate); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__provider_selected); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__text_changed_cb); +} + +static void +gtd_provider_popover_init (GtdProviderPopover *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_provider_popover_new (void) +{ + return g_object_new (GTD_TYPE_PROVIDER_POPOVER, NULL); +} diff --git a/src/gui/gtd-provider-popover.h b/src/gui/gtd-provider-popover.h new file mode 100644 index 0000000..9b97583 --- /dev/null +++ b/src/gui/gtd-provider-popover.h @@ -0,0 +1,34 @@ +/* gtd-provider-popover.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PROVIDER_POPOVER_H +#define GTD_PROVIDER_POPOVER_H + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_POPOVER (gtd_provider_popover_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderPopover, gtd_provider_popover, GTD, PROVIDER_POPOVER, GtkPopover) + +GtkWidget* gtd_provider_popover_new (void); + +G_END_DECLS + +#endif /* GTD_PROVIDER_POPOVER_H */ diff --git a/src/gui/gtd-provider-popover.ui b/src/gui/gtd-provider-popover.ui new file mode 100644 index 0000000..75c033f --- /dev/null +++ b/src/gui/gtd-provider-popover.ui @@ -0,0 +1,164 @@ + + + + diff --git a/src/gui/gtd-provider-row.c b/src/gui/gtd-provider-row.c new file mode 100644 index 0000000..29aeebd --- /dev/null +++ b/src/gui/gtd-provider-row.c @@ -0,0 +1,240 @@ +/* gtd-provider-row.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderRow" + +#include "gtd-provider.h" +#include "gtd-provider-row.h" + +#include + +typedef struct +{ + GtkImage *icon; + GtkLabel *name; + GtkLabel *description; + GtkLabel *enabled; + GtkImage *selected; + + GtdProvider *provider; +} GtdProviderRowPrivate; + +struct _GtdProviderRow +{ + GtkListBoxRow parent; + + /*< private >*/ + GtdProviderRowPrivate *priv; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (GtdProviderRow, gtd_provider_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + PROP_0, + PROP_PROVIDER, + LAST_PROP +}; + +static void +gtd_provider_row_finalize (GObject *object) +{ + GtdProviderRow *self = (GtdProviderRow *)object; + GtdProviderRowPrivate *priv = gtd_provider_row_get_instance_private (self); + + g_clear_object (&priv->provider); + + G_OBJECT_CLASS (gtd_provider_row_parent_class)->finalize (object); +} + +static void +gtd_provider_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProviderRow *self = GTD_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + g_value_set_object (value, self->priv->provider); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderRow *self = GTD_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + self->priv->provider = g_value_get_object (value); + + if (!self->priv->provider) + break; + + g_object_ref (self->priv->provider); + + g_object_bind_property (self->priv->provider, + "name", + self->priv->name, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->priv->provider, + "description", + self->priv->description, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->priv->provider, + "enabled", + self->priv->enabled, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + g_object_bind_property (self->priv->provider, + "icon", + self->priv->icon, + "gicon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_row_class_init (GtdProviderRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_provider_row_finalize; + object_class->get_property = gtd_provider_row_get_property; + object_class->set_property = gtd_provider_row_set_property; + + /** + * GtdProviderRow::provider: + * + * #GtdProvider related to this row. + */ + g_object_class_install_property ( + object_class, + PROP_PROVIDER, + g_param_spec_object ("provider", + "Provider of the row", + "The provider that this row holds", + GTD_TYPE_PROVIDER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, icon); + gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, name); + gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, description); + gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, enabled); + gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, selected); +} + +static void +gtd_provider_row_init (GtdProviderRow *self) +{ + self->priv = gtd_provider_row_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * gtd_provider_row_new: + * @provider: a #GtdProvider + * + * Created a new #GtdProviderRow with @account information. + * + * Returns: (transfer full): a new #GtdProviderRow + */ +GtkWidget* +gtd_provider_row_new (GtdProvider *provider) +{ + return g_object_new (GTD_TYPE_PROVIDER_ROW, + "provider", provider, + NULL); +} + +/** + * gtd_provider_row_get_provider: + * @row: a #GtdProviderRow + * + * Retrieves the #GtdProvider associated with @row. + * + * Returns: (transfer none): the #GtdProvider associated with @row. + */ +GtdProvider* +gtd_provider_row_get_provider (GtdProviderRow *row) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_ROW (row), NULL); + + return row->priv->provider; +} + +/** + * gtd_provider_row_get_selected: + * @row: a #GtdProviderRow + * + * Whether @row is the currently selected row or not. + * + * Returns: %TRUE if the row is currently selected, %FALSE otherwise. + */ +gboolean +gtd_provider_row_get_selected (GtdProviderRow *row) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_ROW (row), FALSE); + + return gtk_widget_get_visible (GTK_WIDGET (row->priv->selected)); +} + +/** + * gtd_provider_row_set_selected: + * @row: a #GtdProviderRow + * @selected: %TRUE if row is selected (i.e. the selection + * mark is visible) + * + * Sets @row as the currently selected row. + */ +void +gtd_provider_row_set_selected (GtdProviderRow *row, + gboolean selected) +{ + g_return_if_fail (GTD_IS_PROVIDER_ROW (row)); + + if (selected != gtk_widget_get_visible (GTK_WIDGET (row->priv->selected))) + { + gtk_widget_set_visible (GTK_WIDGET (row->priv->selected), selected); + } +} diff --git a/src/gui/gtd-provider-row.h b/src/gui/gtd-provider-row.h new file mode 100644 index 0000000..8ccabeb --- /dev/null +++ b/src/gui/gtd-provider-row.h @@ -0,0 +1,44 @@ +/* gtd-provider-row.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_GOA_ROW_H +#define GTD_GOA_ROW_H + +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_ROW (gtd_provider_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderRow, gtd_provider_row, GTD, PROVIDER_ROW, GtkListBoxRow) + +GtkWidget* gtd_provider_row_new (GtdProvider *provider); + +gboolean gtd_provider_row_get_selected (GtdProviderRow *row); + +void gtd_provider_row_set_selected (GtdProviderRow *row, + gboolean selected); + +GtdProvider* gtd_provider_row_get_provider (GtdProviderRow *row); + +G_END_DECLS + +#endif /* GTD_GOA_ROW_H */ diff --git a/src/gui/gtd-provider-row.ui b/src/gui/gtd-provider-row.ui new file mode 100644 index 0000000..9c3ce60 --- /dev/null +++ b/src/gui/gtd-provider-row.ui @@ -0,0 +1,71 @@ + + + + + diff --git a/src/gui/gtd-provider-selector.c b/src/gui/gtd-provider-selector.c new file mode 100644 index 0000000..5f8f5cb --- /dev/null +++ b/src/gui/gtd-provider-selector.c @@ -0,0 +1,695 @@ +/* gtd-provider-selector.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderSelector" + +#include "gtd-application.h" +#include "gtd-manager.h" +#include "gtd-provider.h" +#include "gtd-provider-row.h" +#include "gtd-provider-selector.h" + +#include + +struct _GtdProviderSelector +{ + GtkBox parent; + + GtkListBox *listbox; + GtkWidget *local_check; + + /* stub rows */ + GtkWidget *exchange_stub_row; + GtkWidget *google_stub_row; + GtkWidget *owncloud_stub_row; + GtkWidget *local_row; + + gint show_local_provider : 1; + gint show_stub_rows : 1; +}; + +G_DEFINE_TYPE (GtdProviderSelector, gtd_provider_selector, GTK_TYPE_BOX) + +enum { + PROP_0, + PROP_SHOW_LOCAL, + PROP_SHOW_STUB_ROWS, + LAST_PROP +}; + +enum { + PROVIDER_SELECTED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +static void +spawn (const gchar *action, + const gchar *arg) +{ + const gchar *command[] = {"gnome-control-center", "online-accounts", action, arg, NULL}; + g_spawn_async (NULL, (gchar **) command, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, NULL, NULL); +} + +/** + * display_header_func: + * + * Shows a separator before each row. + */ +static void +display_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + if (before != NULL) + { + GtkWidget *header; + + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + + gtk_list_box_row_set_header (row, header); + gtk_widget_show (header); + } +} + +static void +unselect_rows (GtdProviderSelector *self) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (GTD_IS_PROVIDER_ROW (child)) + gtd_provider_row_set_selected (GTD_PROVIDER_ROW (child), FALSE); + } +} + +static void +default_provider_changed_cb (GtdProviderSelector *selector) +{ + GtdProvider *current; + GtdManager *manager; + GtkWidget *child; + + manager = gtd_manager_get_default (); + current = gtd_manager_get_default_provider (manager); + + for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtdProviderRow *provider_row; + GtdProvider *provider; + + if (!GTD_IS_PROVIDER_ROW (child)) + continue; + + provider_row = GTD_PROVIDER_ROW (child); + provider = gtd_provider_row_get_provider (provider_row); + + gtd_provider_row_set_selected (provider_row, provider == current); + } + + g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, current); +} + +static void +gtd_provider_selector__listbox_row_activated (GtdProviderSelector *selector, + GtkWidget *row) +{ + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + /* The row is either one of the stub rows, or a GtdGoaRow */ + if (row == selector->google_stub_row) + { + spawn ("add", "google"); + } + else if (row == selector->owncloud_stub_row) + { + spawn ("add", "owncloud"); + } + else if (row == selector->exchange_stub_row) + { + spawn ("add", "exchange"); + } + else + { + GtdProvider *provider; + + provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row)); + + unselect_rows (selector); + + /* + * If the account has it's calendars disabled, we cannot let it + * be a default provider location. Instead, open the Control Center + * to give the user the ability to change it. + */ + if (gtd_provider_get_enabled (provider)) + { + gtd_provider_row_set_selected (GTD_PROVIDER_ROW (row), TRUE); + g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, provider); + } + else + { + spawn ((gchar*) gtd_provider_get_id (provider), NULL); + } + } +} + +static void +on_local_check_toggled_cb (GtdProviderSelector *selector, + GtkCheckButton *check) +{ + + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + /* + * Unset the currently selected provider location row when the check button is + * activated. No need to do this when deactivated, since we already did. + */ + + if (gtk_check_button_get_active (check)) + { + GtdProvider *local_provider; + + local_provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (selector->local_row)); + + unselect_rows (selector); + + /* + * Sets the provider location to "local", and don't unset it if the + * check gets deactivated. + */ + g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, local_provider); + } + else + { + g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, NULL); + } +} + +static void +remove_provider (GtdProviderSelector *selector, + GtdProvider *provider) +{ + GtkWidget *child; + gint exchange; + gint google; + gint owncloud; + + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + exchange = google = owncloud = 0; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtdProviderRow *provider_row; + GtdProvider *row_provider; + const gchar *provider_id; + + if (!GTD_IS_PROVIDER_ROW (child)) + continue; + + provider_row = GTD_PROVIDER_ROW (child); + + row_provider = gtd_provider_row_get_provider (provider_row); + provider_id = gtd_provider_get_id (row_provider); + + if (row_provider == provider) + { + gtk_list_box_remove (selector->listbox, child); + } + else + { + if (g_strcmp0 (provider_id, "exchange") == 0) + exchange++; + else if (g_strcmp0 (provider_id, "google") == 0) + google++; + else if (g_strcmp0 (provider_id, "owncloud") == 0) + owncloud++; + } + } + + gtk_widget_set_visible (selector->exchange_stub_row, exchange == 0); + gtk_widget_set_visible (selector->google_stub_row, google == 0); + gtk_widget_set_visible (selector->owncloud_stub_row, owncloud == 0); +} + +static void +add_provider (GtdProviderSelector *selector, + GtdProvider *provider) +{ + GtkWidget *row; + const gchar *provider_id; + + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + g_return_if_fail (GTD_IS_PROVIDER (provider)); + + row = gtd_provider_row_new (provider); + provider_id = gtd_provider_get_id (provider); + + g_debug ("Adding provider %s", provider_id); + + gtk_list_box_insert (selector->listbox, row, -1); + + /* track the local provider row */ + if (g_str_has_prefix (provider_id, "local") == 0) + { + gtk_widget_set_visible (row, selector->show_local_provider); + selector->local_row = row; + } + + /* Auto selects the default provider row when needed */ + if (!gtd_provider_selector_get_selected_provider (selector)) + gtd_provider_selector_set_selected_provider (selector, provider); + + /* hide the related stub row */ + if (g_str_has_prefix (provider_id, "exchange") == 0) + gtk_widget_hide (selector->exchange_stub_row); + else if (g_str_has_prefix (provider_id, "google") == 0) + gtk_widget_hide (selector->google_stub_row); + else if (g_str_has_prefix (provider_id, "owncloud") == 0) + gtk_widget_hide (selector->owncloud_stub_row); +} + +static void +gtd_provider_selector__fill_accounts (GtdProviderSelector *selector) +{ + GtdManager *manager; + GList *providers; + GList *l; + + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + manager = gtd_manager_get_default (); + + /* load accounts */ + providers = gtd_manager_get_providers (manager); + + for (l = providers; l != NULL; l = l->next) + add_provider (selector, l->data); + + g_list_free (providers); +} + +static gint +sort_func (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + GtdProvider *provider1; + GtdProvider *provider2; + + if (!GTD_IS_PROVIDER_ROW (row1)) + return 1; + else if (!GTD_IS_PROVIDER_ROW (row2)) + return -1; + + provider1 = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row1)); + provider2 = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row2)); + + return provider2 != provider1;//gtd_provider_compare (provider1, provider2); +} + +static void +gtd_provider_selector_constructed (GObject *object) +{ + GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object); + + G_OBJECT_CLASS (gtd_provider_selector_parent_class)->constructed (object); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->listbox), + display_header_func, + NULL, + NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->listbox), + (GtkListBoxSortFunc) sort_func, + NULL, + NULL); +} + +static void +gtd_provider_selector_finalize (GObject *object) +{ + G_OBJECT_CLASS (gtd_provider_selector_parent_class)->finalize (object); +} + +static void +gtd_provider_selector_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object); + + switch (prop_id) + { + case PROP_SHOW_LOCAL: + g_value_set_boolean (value, self->show_local_provider); + break; + + case PROP_SHOW_STUB_ROWS: + g_value_set_boolean (value, self->show_stub_rows); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_selector_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object); + + switch (prop_id) + { + case PROP_SHOW_LOCAL: + gtd_provider_selector_show_local (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_STUB_ROWS: + gtd_provider_selector_set_show_stub_rows (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_selector_class_init (GtdProviderSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_provider_selector_finalize; + object_class->constructed = gtd_provider_selector_constructed; + object_class->get_property = gtd_provider_selector_get_property; + object_class->set_property = gtd_provider_selector_set_property; + + /** + * GtdProviderSelector::location-selected: + * + * Emitted when a provider location is selected. + */ + signals[PROVIDER_SELECTED] = g_signal_new ("provider-selected", + GTD_TYPE_PROVIDER_SELECTOR, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PROVIDER); + + + /** + * GtdProviderSelector::show-local-provider: + * + * Whether it should show a row for the local provider. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_LOCAL, + g_param_spec_boolean ("show-local", + "Show local provider row", + "Whether should show a local provider row instead of a checkbox", + FALSE, + G_PARAM_READWRITE)); + + /** + * GtdProviderSelector::show-stub-rows: + * + * Whether it should show stub rows for non-added accounts. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_STUB_ROWS, + g_param_spec_boolean ("show-stub-rows", + "Show stub rows", + "Whether should show stub rows for non-added accounts", + TRUE, + G_PARAM_READWRITE)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-selector.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, exchange_stub_row); + gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, google_stub_row); + gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, local_check); + gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, owncloud_stub_row); + + gtk_widget_class_bind_template_callback (widget_class, on_local_check_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, gtd_provider_selector__listbox_row_activated); +} + +static void +gtd_provider_selector_init (GtdProviderSelector *self) +{ + GtdManager *manager; + + self->show_stub_rows = TRUE; + + /* Setup the manager */ + manager = gtd_manager_get_default (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtd_provider_selector__fill_accounts (self); + + g_signal_connect_swapped (manager, + "notify::default-provider", + G_CALLBACK (default_provider_changed_cb), + self); + + g_signal_connect_swapped (manager, + "provider-added", + G_CALLBACK (add_provider), + self); + + g_signal_connect_swapped (manager, + "provider-removed", + G_CALLBACK (remove_provider), + self); + +} + +/** + * gtd_provider_selector_new: + * + * Creates a new #GtdProviderSelector. + * + * Returns: (transfer full): a new #GtdProviderSelector + */ +GtkWidget* +gtd_provider_selector_new (void) +{ + return g_object_new (GTD_TYPE_PROVIDER_SELECTOR, NULL); +} + +/** + * gtd_provider_selector_show_local: + * + * Shows a row for local provider item. + */ +void +gtd_provider_selector_show_local (GtdProviderSelector *selector, + gboolean show) +{ + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + if (selector->show_local_provider != show) + { + selector->show_local_provider = show; + + gtk_widget_set_visible (selector->local_check, !show); + + if (selector->local_row) + gtk_widget_set_visible (selector->local_row, show); + + g_object_notify (G_OBJECT (selector), "show-local"); + } +} + +/** + * gtd_provider_selector_get_selected_provider: + * @selector: a #GtdProviderSelector + * + * Retrieves the currently selected #GtdProvider, or %NULL if + * none is selected. + * + * Returns: (transfer none): the selected #GtdProvider + */ +GtdProvider* +gtd_provider_selector_get_selected_provider (GtdProviderSelector *selector) +{ + GtdProvider *provider; + GtkWidget *child; + + g_return_val_if_fail (GTD_IS_PROVIDER_SELECTOR (selector), NULL); + + provider = NULL; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtdProviderRow *provider_row; + + if (!GTD_IS_PROVIDER_ROW (child)) + continue; + + provider_row = GTD_PROVIDER_ROW (child); + if (gtd_provider_row_get_selected (provider_row)) + { + provider = gtd_provider_row_get_provider (provider_row); + break; + } + } + + return provider; +} + +/** + * gtd_provider_selector_set_selected_provider: + * @selector: a #GtdProviderSelector + * @provider: a #GtdProvider + * + * Selects @provider in the given #GtdProviderSelector. + */ +void +gtd_provider_selector_set_selected_provider (GtdProviderSelector *selector, + GtdProvider *provider) +{ + GtkWidget *child; + + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtdProviderRow *provider_row; + + if (!GTD_IS_PROVIDER_ROW (child)) + continue; + + provider_row = GTD_PROVIDER_ROW (child); + gtd_provider_row_set_selected (provider_row, + gtd_provider_row_get_provider (provider_row) == provider); + g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, provider); + } +} + +/** + * gtd_provider_selector_get_show_stub_rows: + * @selector: a #GtdProviderSelector + * + * Retrieves the ::show-stub-rows property. + * + * Returns: %TRUE if it shows stub rows, %FALSE if it hides them. + */ +gboolean +gtd_provider_selector_get_show_stub_rows (GtdProviderSelector *selector) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_SELECTOR (selector), FALSE); + + return selector->show_stub_rows; +} + +/** + * gtd_provider_selector_set_show_stub_rows: + * @selector: a #GtdProviderSelector + * @show_stub_rows: %TRUE to show stub rows, %FALSE to hide them. + * + * Sets the #GtdProviderSelector::show-stub-rows property. + * + * Returns: + */ +void +gtd_provider_selector_set_show_stub_rows (GtdProviderSelector *selector, + gboolean show_stub_rows) +{ + g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector)); + + if (selector->show_stub_rows != show_stub_rows) + { + selector->show_stub_rows = show_stub_rows; + + /* + * If we're showing the stub rows, it must check which ones should be shown. + * We don't want to show stub rows for + */ + if (show_stub_rows) + { + GtkWidget *child; + gint google_counter; + gint exchange_counter; + gint owncloud_counter; + + google_counter = 0; + exchange_counter = 0; + owncloud_counter = 0; + + + for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (GTD_IS_PROVIDER_ROW (child)) + { + GtdProvider *provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (child)); + const gchar *type; + + type = gtd_provider_get_id (provider); + + if (g_strcmp0 (type, "google") == 0) + google_counter++; + else if (g_strcmp0 (type, "exchange") == 0) + exchange_counter++; + else if (g_strcmp0 (type, "owncloud") == 0) + owncloud_counter++; + } + } + + gtk_widget_set_visible (selector->google_stub_row, google_counter == 0); + gtk_widget_set_visible (selector->exchange_stub_row, exchange_counter == 0); + gtk_widget_set_visible (selector->owncloud_stub_row, owncloud_counter == 0); + } + else + { + gtk_widget_hide (selector->exchange_stub_row); + gtk_widget_hide (selector->google_stub_row); + gtk_widget_hide (selector->owncloud_stub_row); + } + + g_object_notify (G_OBJECT (selector), "show-stub-rows"); + } +} diff --git a/src/gui/gtd-provider-selector.h b/src/gui/gtd-provider-selector.h new file mode 100644 index 0000000..4a29f70 --- /dev/null +++ b/src/gui/gtd-provider-selector.h @@ -0,0 +1,50 @@ +/* gtd-provider-selector.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PROVIDER_SELECTOR_H +#define GTD_PROVIDER_SELECTOR_H + +#include "gtd-types.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_SELECTOR (gtd_provider_selector_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderSelector, gtd_provider_selector, GTD, PROVIDER_SELECTOR, GtkBox) + +GtkWidget* gtd_provider_selector_new (void); + +void gtd_provider_selector_show_local (GtdProviderSelector *selector, + gboolean show); + +GtdProvider* gtd_provider_selector_get_selected_provider (GtdProviderSelector *selector); + +void gtd_provider_selector_set_selected_provider (GtdProviderSelector *selector, + GtdProvider *provider); + +gboolean gtd_provider_selector_get_show_stub_rows (GtdProviderSelector *selector); + +void gtd_provider_selector_set_show_stub_rows (GtdProviderSelector *selector, + gboolean show_stub_rows); + +G_END_DECLS + +#endif /* GTD_PROVIDER_SELECTOR_H */ diff --git a/src/gui/gtd-provider-selector.ui b/src/gui/gtd-provider-selector.ui new file mode 100644 index 0000000..006b546 --- /dev/null +++ b/src/gui/gtd-provider-selector.ui @@ -0,0 +1,109 @@ + + + + + diff --git a/src/gui/gtd-star-widget.c b/src/gui/gtd-star-widget.c new file mode 100644 index 0000000..cb6991b --- /dev/null +++ b/src/gui/gtd-star-widget.c @@ -0,0 +1,193 @@ +/* gtd-star-widget.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-star-widget.h" + +struct _GtdStarWidget +{ + GtdWidget parent; + + GtkWidget *filled_star; + GtkWidget *empty_star; + + gboolean active; +}; + +G_DEFINE_TYPE (GtdStarWidget, gtd_star_widget, GTD_TYPE_WIDGET) + +enum +{ + PROP_0, + PROP_ACTIVE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Callbacks + */ + +static void +on_star_widget_clicked_cb (GtkGestureClick *gesture, + gint n_press, + gdouble x, + gdouble y, + GtdStarWidget *self) +{ + gtd_star_widget_set_active (self, !self->active); + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); +} + + +/* + * GObject overrides + */ + +static void +gtd_star_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdStarWidget *self = GTD_STAR_WIDGET (object); + + switch (prop_id) + { + case PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_star_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdStarWidget *self = GTD_STAR_WIDGET (object); + + switch (prop_id) + { + case PROP_ACTIVE: + gtd_star_widget_set_active (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_star_widget_class_init (GtdStarWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gtd_star_widget_get_property; + object_class->set_property = gtd_star_widget_set_property; + + /** + * GtdStarWidget:active: + * + * Whether the star widget is active or not. When active, the + * star appears filled. + */ + properties[PROP_ACTIVE] = g_param_spec_boolean ("active", + "Active", + "Active", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); + gtk_widget_class_set_css_name (widget_class, "star"); +} + +static void +gtd_star_widget_init (GtdStarWidget *self) +{ + GtkGesture *click_gesture; + + click_gesture = gtk_gesture_click_new (); + gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (click_gesture), FALSE); + gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (click_gesture), TRUE); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click_gesture), GDK_BUTTON_PRIMARY); + gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (click_gesture), GTK_PHASE_CAPTURE); + g_signal_connect (click_gesture, "pressed", G_CALLBACK (on_star_widget_clicked_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (click_gesture)); + + gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "default"); + + self->empty_star = gtk_image_new_from_icon_name ("non-starred-symbolic"); + gtk_widget_set_parent (self->empty_star, GTK_WIDGET (self)); + + self->filled_star = gtk_image_new_from_icon_name ("starred-symbolic"); + gtk_widget_set_parent (self->filled_star, GTK_WIDGET (self)); + gtk_widget_hide (self->filled_star); + + g_object_bind_property (self->filled_star, + "visible", + self->empty_star, + "visible", + G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE); +} + +GtkWidget* +gtd_star_widget_new (void) +{ + return g_object_new (GTD_TYPE_STAR_WIDGET, NULL); +} + +gboolean +gtd_star_widget_get_active (GtdStarWidget *self) +{ + g_return_val_if_fail (GTD_IS_STAR_WIDGET (self), FALSE); + + return self->active; +} + +void +gtd_star_widget_set_active (GtdStarWidget *self, + gboolean active) +{ + g_return_if_fail (GTD_IS_STAR_WIDGET (self)); + + if (self->active == active) + return; + + self->active = active; + gtk_widget_set_visible (self->filled_star, active); + + if (active) + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED); + + /* TODO: explosion effect */ + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]); +} diff --git a/src/gui/gtd-star-widget.h b/src/gui/gtd-star-widget.h new file mode 100644 index 0000000..6f93b44 --- /dev/null +++ b/src/gui/gtd-star-widget.h @@ -0,0 +1,37 @@ +/* gtd-star-widget.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_STAR_WIDGET (gtd_star_widget_get_type()) +G_DECLARE_FINAL_TYPE (GtdStarWidget, gtd_star_widget, GTD, STAR_WIDGET, GtdWidget) + +GtkWidget* gtd_star_widget_new (void); + +gboolean gtd_star_widget_get_active (GtdStarWidget *self); + +void gtd_star_widget_set_active (GtdStarWidget *self, + gboolean active); + +G_END_DECLS diff --git a/src/gui/gtd-task-list-popover.c b/src/gui/gtd-task-list-popover.c new file mode 100644 index 0000000..7d556a0 --- /dev/null +++ b/src/gui/gtd-task-list-popover.c @@ -0,0 +1,269 @@ +/* gtd-task-list-popover.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskListPopover" + +#include "models/gtd-list-model-filter.h" +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-provider.h" +#include "gtd-task-list.h" +#include "gtd-task-list-popover.h" +#include "gtd-utils.h" + +struct _GtdTaskListPopover +{ + GtkPopover parent; + + GtkFilterListModel *filter_model; + + GtkSizeGroup *sizegroup; + GtkListBox *listbox; + GtkEditable *search_entry; + + GtdTaskList *selected_list; + GtdManager *manager; +}; + +G_DEFINE_TYPE (GtdTaskListPopover, gtd_task_list_popover, GTK_TYPE_POPOVER) + +enum +{ + PROP_0, + PROP_TASK_LIST, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +/* + * Auxiliary methods + */ + +static void +set_selected_tasklist (GtdTaskListPopover *self, + GtdTaskList *list) +{ + GtdManager *manager; + + manager = gtd_manager_get_default (); + + /* NULL list means the inbox */ + if (!list) + list = gtd_manager_get_inbox (manager); + + if (g_set_object (&self->selected_list, list)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TASK_LIST]); +} + + +/* + * Callbacks + */ + +static gboolean +filter_listbox_cb (gpointer item, + gpointer user_data) +{ + GtdTaskListPopover *self; + g_autofree gchar *normalized_list_name = NULL; + g_autofree gchar *normalized_search_query = NULL; + GtdTaskList *list; + + self = (GtdTaskListPopover*) user_data; + list = (GtdTaskList*) item; + + normalized_search_query = gtd_normalize_casefold_and_unaccent (gtk_editable_get_text (self->search_entry)); + normalized_list_name = gtd_normalize_casefold_and_unaccent (gtd_task_list_get_name (list)); + + return g_strrstr (normalized_list_name, normalized_search_query) != NULL; +} + +static GtkWidget* +create_list_row_cb (gpointer item, + gpointer data) +{ + g_autoptr (GdkPaintable) paintable = NULL; + g_autoptr (GdkRGBA) color = NULL; + GtdTaskListPopover *self; + GtkWidget *provider; + GtkWidget *icon; + GtkWidget *name; + GtkWidget *row; + GtkWidget *box; + + self = GTD_TASK_LIST_POPOVER (data); + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 12, + "margin-top", 6, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + NULL); + + /* Icon */ + color = gtd_task_list_get_color (item); + paintable = gtd_create_circular_paintable (color, 12); + icon = gtk_image_new_from_paintable (paintable); + gtk_widget_set_size_request (icon, 12, 12); + gtk_widget_set_halign (icon, GTK_ALIGN_CENTER); + gtk_widget_set_valign (icon, GTK_ALIGN_CENTER); + + gtk_box_append (GTK_BOX (box), icon); + + /* Tasklist name */ + name = g_object_new (GTK_TYPE_LABEL, + "label", gtd_task_list_get_name (item), + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_box_append (GTK_BOX (box), name); + + /* Provider name */ + provider = g_object_new (GTK_TYPE_LABEL, + "label", gtd_provider_get_description (gtd_task_list_get_provider (item)), + "xalign", 0.0, + NULL); + gtk_widget_add_css_class (provider, "dim-label"); + gtk_size_group_add_widget (self->sizegroup, provider); + gtk_box_append (GTK_BOX (box), provider); + + /* The row itself */ + row = gtk_list_box_row_new (); + gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box); + + g_object_set_data (G_OBJECT (row), "tasklist", item); + + return row; +} + +static void +on_listbox_row_activated_cb (GtkListBox *listbox, + GtkListBoxRow *row, + GtdTaskListPopover *self) +{ + GtdTaskList *list = g_object_get_data (G_OBJECT (row), "tasklist"); + + set_selected_tasklist (self, list); + gtk_popover_popdown (GTK_POPOVER (self)); + gtk_editable_set_text (self->search_entry, ""); +} + +static void +on_popover_closed_cb (GtkPopover *popover, + GtdTaskListPopover *self) +{ + gtk_editable_set_text (self->search_entry, ""); +} + +static void +on_search_entry_activated_cb (GtkEntry *entry, + GtdTaskListPopover *self) +{ + g_autoptr (GtdTaskList) list = g_list_model_get_item (G_LIST_MODEL (self->filter_model), 0); + + set_selected_tasklist (self, list); + gtk_popover_popdown (GTK_POPOVER (self)); + gtk_editable_set_text (self->search_entry, ""); +} + +static void +on_search_entry_search_changed_cb (GtkEntry *search_entry, + GtdTaskListPopover *self) +{ + GtkFilter *filter; + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_popover_finalize (GObject *object) +{ + GtdTaskListPopover *self = (GtdTaskListPopover *)object; + + g_clear_object (&self->selected_list); + + G_OBJECT_CLASS (gtd_task_list_popover_parent_class)->finalize (object); +} + +static void +gtd_task_list_popover_class_init (GtdTaskListPopoverClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_task_list_popover_finalize; + + properties[PROP_TASK_LIST] = g_param_spec_object ("task-list", + "Task list", + "Task list", + GTD_TYPE_TASK_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-popover.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, search_entry); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, sizegroup); + + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_popover_closed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_search_entry_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_search_entry_search_changed_cb); +} + +static void +gtd_task_list_popover_init (GtdTaskListPopover *self) +{ + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *filter; + + filter = gtk_custom_filter_new (filter_listbox_cb, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_task_lists_model (manager), + GTK_FILTER (filter)); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_bind_model (self->listbox, + G_LIST_MODEL (self->filter_model), + create_list_row_cb, + self, + NULL); + + self->manager = manager; + + set_selected_tasklist (self, NULL); +} + +GtdTaskList* +gtd_task_list_popover_get_task_list (GtdTaskListPopover *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_POPOVER (self), NULL); + + return self->selected_list; +} diff --git a/src/gui/gtd-task-list-popover.h b/src/gui/gtd-task-list-popover.h new file mode 100644 index 0000000..2a57ea9 --- /dev/null +++ b/src/gui/gtd-task-list-popover.h @@ -0,0 +1,34 @@ +/* gtd-task-list-popover.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_POPOVER (gtd_task_list_popover_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListPopover, gtd_task_list_popover, GTD, TASK_LIST_POPOVER, GtkPopover) + +GtdTaskList* gtd_task_list_popover_get_task_list (GtdTaskListPopover *self); + +G_END_DECLS diff --git a/src/gui/gtd-task-list-popover.ui b/src/gui/gtd-task-list-popover.ui new file mode 100644 index 0000000..9a8cc9e --- /dev/null +++ b/src/gui/gtd-task-list-popover.ui @@ -0,0 +1,39 @@ + + + + + diff --git a/src/gui/gtd-task-list-view.c b/src/gui/gtd-task-list-view.c new file mode 100644 index 0000000..6810c17 --- /dev/null +++ b/src/gui/gtd-task-list-view.c @@ -0,0 +1,1220 @@ +/* gtd-task-list-view.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTaskListView" + +#include "gtd-debug.h" +#include "gtd-edit-pane.h" +#include "gtd-task-list-view.h" +#include "gtd-task-list-view-model.h" +#include "gtd-manager.h" +#include "gtd-markdown-renderer.h" +#include "gtd-new-task-row.h" +#include "gtd-notification.h" +#include "gtd-provider.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-row.h" +#include "gtd-utils-private.h" +#include "gtd-widget.h" +#include "gtd-window.h" + +#include +#include +#include + +/** + * SECTION:gtd-task-list-view + * @Short_description: A widget to display tasklists + * @Title:GtdTaskListView + * + * The #GtdTaskListView widget shows the tasks of a #GtdTaskList with + * various options to fine-tune the appearance. Alternatively, one can + * pass a #GList of #GtdTask objects. + * + * It supports custom sorting and header functions, so the tasks can be + * sorted in various ways. See the "Today" and "Scheduled" panels for reference + * implementations. + * + * Example: + * |[ + * GtdTaskListView *view = gtd_task_list_view_new (); + * + * gtd_task_list_view_set_model (view, model); + * + * // Date which tasks will be automatically assigned + * gtd_task_list_view_set_default_date (view, now); + * ]| + * + */ + +struct _GtdTaskListView +{ + GtkBox parent; + + AdwStatusPage *empty_list_widget; + GtkListBox *listbox; + GtkStack *main_stack; + GtkWidget *scrolled_window; + + /* internal */ + gboolean can_toggle; + gboolean show_due_date; + gboolean show_list_name; + GtdTaskListViewModel *view_model; + GListModel *model; + GDateTime *default_date; + + GListModel *incomplete_tasks_model; + guint n_incomplete_tasks; + + guint scroll_to_bottom_handler_id; + + GHashTable *task_to_row; + + /* Markup renderer*/ + GtdMarkdownRenderer *renderer; + + /* DnD */ + guint scroll_timeout_id; + gboolean scroll_up; + + /* action */ + GActionGroup *action_group; + + /* Custom header function data */ + GtdTaskListViewHeaderFunc header_func; + gpointer header_user_data; + + GtdTaskRow *active_row; + GtkSizeGroup *due_date_sizegroup; + GtkSizeGroup *tasklist_name_sizegroup; +}; + +#define DND_SCROLL_OFFSET 24 //px +#define TASK_REMOVED_NOTIFICATION_ID "task-removed-id" + + +static gboolean filter_complete_func (gpointer item, + gpointer user_data); + +static void on_clear_completed_tasks_activated_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data); + +static void on_incomplete_tasks_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTaskListView *self); + +static void on_remove_task_row_cb (GtdTaskRow *row, + GtdTaskListView *self); + +static void on_task_row_entered_cb (GtdTaskListView *self, + GtdTaskRow *row); + +static void on_task_row_exited_cb (GtdTaskListView *self, + GtdTaskRow *row); + +static gboolean scroll_to_bottom_cb (gpointer data); + + +G_DEFINE_TYPE (GtdTaskListView, gtd_task_list_view, GTK_TYPE_BOX) + +static const GActionEntry gtd_task_list_view_entries[] = { + { "clear-completed-tasks", on_clear_completed_tasks_activated_cb }, +}; + +typedef struct +{ + GtdTaskListView *view; + GtdTask *task; +} RemoveTaskData; + +enum { + PROP_0, + PROP_SHOW_LIST_NAME, + PROP_SHOW_DUE_DATE, + LAST_PROP +}; + + +/* + * Auxiliary methods + */ + +static void +set_active_row (GtdTaskListView *self, + GtdTaskRow *row) +{ + if (self->active_row == row) + return; + + if (self->active_row) + gtd_task_row_set_active (self->active_row, FALSE); + + self->active_row = row; + + if (row) + { + gtd_task_row_set_active (row, TRUE); + gtk_widget_grab_focus (GTK_WIDGET (row)); + } +} + +static void +schedule_scroll_to_bottom (GtdTaskListView *self) +{ + if (self->scroll_to_bottom_handler_id > 0) + return; + + self->scroll_to_bottom_handler_id = g_timeout_add (250, scroll_to_bottom_cb, self); +} + +static void +update_empty_state (GtdTaskListView *self) +{ + gboolean show_empty_list_widget; + gboolean is_empty; + + g_assert (GTD_IS_TASK_LIST_VIEW (self)); + + if (!self->model) + return; + + is_empty = g_list_model_get_n_items (self->model) == 0; + + show_empty_list_widget = !GTD_IS_TASK_LIST (self->model) && + (is_empty || self->n_incomplete_tasks == 0); + gtk_stack_set_visible_child_name (self->main_stack, + show_empty_list_widget ? "empty-list" : "task-list"); +} + +static void +update_incomplete_tasks_model (GtdTaskListView *self) +{ + if (!self->incomplete_tasks_model) + { + g_autoptr (GtkFilterListModel) filter_model = NULL; + GtkCustomFilter *filter; + + filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + filter_model = gtk_filter_list_model_new (NULL, GTK_FILTER (filter)); + gtk_filter_list_model_set_incremental (filter_model, TRUE); + + self->incomplete_tasks_model = G_LIST_MODEL (g_steal_pointer (&filter_model)); + } + + gtk_filter_list_model_set_model (GTK_FILTER_LIST_MODEL (self->incomplete_tasks_model), + self->model); + self->n_incomplete_tasks = g_list_model_get_n_items (self->incomplete_tasks_model); + + g_signal_connect (self->incomplete_tasks_model, + "items-changed", + G_CALLBACK (on_incomplete_tasks_items_changed_cb), + self); +} + + +/* + * Callbacks + */ + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + return !gtd_task_get_complete (task); +} + +static void +on_incomplete_tasks_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTaskListView *self) +{ + self->n_incomplete_tasks -= n_removed; + self->n_incomplete_tasks += n_added; + + update_empty_state (self); +} + +static void +on_empty_list_widget_add_tasks_cb (AdwStatusPage *empty_list_widget, + GtdTaskListView *self) +{ + gtk_stack_set_visible_child_name (self->main_stack, "task-list"); +} + +static void +on_new_task_row_entered_cb (GtdTaskListView *self, + GtdNewTaskRow *row) +{ + set_active_row (self, NULL); +} + +static void +on_new_task_row_exited_cb (GtdTaskListView *self, + GtdNewTaskRow *row) +{ +} + +static GtkWidget* +create_row_for_task_cb (gpointer item, + gpointer user_data) +{ + GtdTaskListView *self; + GtkWidget *row; + + self = GTD_TASK_LIST_VIEW (user_data); + + set_active_row (self, NULL); + + if (GTD_IS_TASK (item)) + { + row = gtd_task_row_new (item, self->renderer); + + gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row), self->show_list_name); + gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row), self->show_due_date); + + g_signal_connect_swapped (row, "enter", G_CALLBACK (on_task_row_entered_cb), self); + g_signal_connect_swapped (row, "exit", G_CALLBACK (on_task_row_exited_cb), self); + + g_signal_connect (row, "remove-task", G_CALLBACK (on_remove_task_row_cb), self); + } + else + { + row = gtd_new_task_row_new (); + + g_signal_connect_swapped (row, "enter", G_CALLBACK (on_new_task_row_entered_cb), self); + g_signal_connect_swapped (row, "exit", G_CALLBACK (on_new_task_row_exited_cb), self); + } + + g_hash_table_insert (self->task_to_row, item, row); + + return row; +} + +static gboolean +scroll_to_bottom_cb (gpointer data) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (data); + GtkWidget *widget; + GtkRoot *root; + + widget = GTK_WIDGET (self); + root = gtk_widget_get_root (widget); + + if (!root) + return G_SOURCE_CONTINUE; + + self->scroll_to_bottom_handler_id = 0; + + /* + * Only focus the new task row if the current list is visible, + * and the focused widget isn't inside this list view. + */ + if (gtk_widget_get_visible (widget) && + gtk_widget_get_child_visible (widget) && + gtk_widget_get_mapped (widget) && + !gtk_widget_is_ancestor (gtk_window_get_focus (GTK_WINDOW (root)), widget)) + { + GtkWidget *new_task_row; + gboolean ignored; + + new_task_row = gtk_widget_get_last_child (GTK_WIDGET (self->listbox)); + gtk_widget_grab_focus (new_task_row); + g_signal_emit_by_name (self->scrolled_window, "scroll-child", GTK_SCROLL_END, FALSE, &ignored); + } + + return G_SOURCE_REMOVE; +} + +static void +on_task_removed_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_remove_task_finish (GTD_PROVIDER (source), result, &error); + + if (error) + g_warning ("Error removing task list: %s", error->message); +} + +static void +on_clear_completed_tasks_activated_cb (GSimpleAction *simple, + GVariant *parameter, + gpointer user_data) +{ + GtdTaskListView *self; + GListModel *model; + guint i; + + self = GTD_TASK_LIST_VIEW (user_data); + model = self->model; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + g_autoptr (GtdTask) task = g_list_model_get_item (model, i); + + if (!gtd_task_get_complete (task)) + continue; + + gtd_provider_remove_task (gtd_task_get_provider (task), + task, + NULL, + on_task_removed_cb, + self); + } +} + +static void +on_remove_task_action_cb (GtdNotification *notification, + gpointer user_data) +{ + RemoveTaskData *data = user_data; + + gtd_provider_remove_task (gtd_task_get_provider (data->task), + data->task, + NULL, + on_task_removed_cb, + data->view); + + g_clear_pointer (&data, g_free); +} + +static void +on_undo_remove_task_action_cb (GtdNotification *notification, + gpointer user_data) +{ + RemoveTaskData *data; + GtdTaskList *list; + + data = user_data; + + /* + * Re-add task to the list. This will emit GListModel:items-changed (since + * GtdTaskList implements GListModel) and the row will be added back. + */ + list = gtd_task_get_list (data->task); + gtd_task_list_add_task (list, data->task); + + g_free (data); +} + +static void +on_remove_task_row_cb (GtdTaskRow *row, + GtdTaskListView *self) +{ + GtdManager *manager = gtd_manager_get_default (); + g_autofree gchar *text = NULL; + GtdNotification *notification; + RemoveTaskData *data; + GtdTaskList *list; + GtdTask *task; + + task = gtd_task_row_get_task (row); + + text = g_strdup_printf (_("Task %s removed"), gtd_task_get_title (task)); + + data = g_new0 (RemoveTaskData, 1); + data->view = self; + data->task = task; + + /* Remove task from the list */ + list = gtd_task_get_list (task); + gtd_task_list_remove_task (list, task); + + /* Notify about the removal */ + notification = gtd_notification_new (text); + + gtd_notification_set_dismissal_action (notification, + (GtdNotificationActionFunc) on_remove_task_action_cb, + data); + + gtd_notification_set_secondary_action (notification, + _("Undo"), + (GtdNotificationActionFunc) on_undo_remove_task_action_cb, + data); + + gtd_manager_send_notification (manager, notification); + + /* Clear the active row */ + set_active_row (self, NULL); +} + +static void +on_task_row_entered_cb (GtdTaskListView *self, + GtdTaskRow *row) +{ + set_active_row (self, row); +} + +static void +on_task_row_exited_cb (GtdTaskListView *self, + GtdTaskRow *row) +{ + if (row == self->active_row) + set_active_row (self, NULL); +} + +static void +on_listbox_row_activated_cb (GtkListBox *listbox, + GtkListBoxRow *row, + GtdTaskListView *self) +{ + GtdTaskRow *task_row; + + GTD_ENTRY; + + if (!GTD_IS_TASK_ROW (row)) + GTD_RETURN (); + + task_row = GTD_TASK_ROW (row); + + /* Toggle the row */ + if (gtd_task_row_get_active (task_row)) + set_active_row (self, NULL); + else + set_active_row (self, task_row); + + GTD_EXIT; +} + + +/* + * Custom sorting functions + */ + +static void +internal_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + GtdTaskListView *self) +{ + GtkWidget *header; + GtdTask *row_task; + GtdTask *before_task; + + if (!self->header_func) + return; + + row_task = before_task = NULL; + + if (!GTD_IS_TASK_ROW (row)) + return; + + if (row) + row_task = gtd_task_row_get_task (GTD_TASK_ROW (row)); + + if (before) + before_task = gtd_task_row_get_task (GTD_TASK_ROW (before)); + + header = self->header_func (row_task, before_task, self->header_user_data); + + if (header) + { + GtkWidget *real_header = gtd_widget_new (); + gtk_widget_insert_before (header, real_header, NULL); + + header = real_header; + } + + gtk_list_box_row_set_header (row, header); +} + + +/* + * Drag n' Drop functions + */ + +static GtkListBoxRow* +get_drop_row_at_y (GtdTaskListView *self, + gdouble y) +{ + GtkAllocation row_allocation; + GtkListBoxRow *hovered_row; + GtkListBoxRow *task_row; + GtkListBoxRow *drop_row; + + hovered_row = gtk_list_box_get_row_at_y (self->listbox, y); + + /* Small optimization when hovering the first row */ + if (gtk_list_box_row_get_index (hovered_row) == 0) + return GTD_IS_TASK_ROW (hovered_row) ? hovered_row : NULL; + + drop_row = NULL; + task_row = hovered_row; + + gtk_widget_get_allocation (GTK_WIDGET (hovered_row), &row_allocation); + + /* + * If the pointer if in the top part of the row, move the DnD row to + * the previous row. + */ + if (y < row_allocation.y + row_allocation.height / 2) + { + GtkWidget *aux; + + /* Search for a valid task row */ + for (aux = gtk_widget_get_prev_sibling (GTK_WIDGET (hovered_row)); + aux; + aux = gtk_widget_get_prev_sibling (aux)) + { + /* Skip DnD, New task and hidden rows */ + if (!gtk_widget_get_visible (aux)) + continue; + + drop_row = GTK_LIST_BOX_ROW (aux); + break; + } + } + else + { + drop_row = task_row; + } + + return GTD_IS_TASK_ROW (drop_row) ? drop_row : NULL; +} + +static inline gboolean +scroll_to_dnd (gpointer user_data) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (user_data); + GtkAdjustment *vadjustment; + gint value; + + vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window)); + value = gtk_adjustment_get_value (vadjustment) + (self->scroll_up ? -6 : 6); + + gtk_adjustment_set_value (vadjustment, + CLAMP (value, 0, gtk_adjustment_get_upper (vadjustment))); + + return G_SOURCE_CONTINUE; +} + +static void +check_dnd_scroll (GtdTaskListView *self, + gboolean should_cancel, + gdouble y) +{ + gdouble current_y, height; + + if (should_cancel) + { + if (self->scroll_timeout_id > 0) + { + g_source_remove (self->scroll_timeout_id); + self->scroll_timeout_id = 0; + } + + return; + } + + height = gtk_widget_get_allocated_height (self->scrolled_window); + gtk_widget_translate_coordinates (GTK_WIDGET (self->listbox), + self->scrolled_window, + 0, y, + NULL, ¤t_y); + + if (current_y < DND_SCROLL_OFFSET || current_y > height - DND_SCROLL_OFFSET) + { + if (self->scroll_timeout_id > 0) + return; + + /* Start the autoscroll */ + self->scroll_up = current_y < DND_SCROLL_OFFSET; + self->scroll_timeout_id = g_timeout_add (25, + scroll_to_dnd, + self); + } + else + { + if (self->scroll_timeout_id == 0) + return; + + /* Cancel the autoscroll */ + g_source_remove (self->scroll_timeout_id); + self->scroll_timeout_id = 0; + } +} + +static GdkDragAction +on_drop_target_drag_enter_cb (GtkDropTarget *drop_target, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GTD_ENTRY; + + gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y)); + + GTD_RETURN (GDK_ACTION_MOVE); +} + +static void +on_drop_target_drag_leave_cb (GtkDropTarget *drop_target, + GtdTaskListView *self) +{ + GTD_ENTRY; + + gtk_list_box_drag_unhighlight_row (self->listbox); + check_dnd_scroll (self, TRUE, -1); + + GTD_EXIT; +} + +static GdkDragAction +on_drop_target_drag_motion_cb (GtkDropTarget *drop_target, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GdkDrop *drop; + GdkDrag *drag; + + GTD_ENTRY; + + /* Clear the currently active row */ + set_active_row (self, NULL); + + drop = gtk_drop_target_get_current_drop (drop_target); + drag = gdk_drop_get_drag (drop); + + if (!drag) + { + g_info ("Only dragging task rows is supported"); + GTD_GOTO (fail); + } + + gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y)); + check_dnd_scroll (self, FALSE, y); + GTD_RETURN (GDK_ACTION_MOVE); + +fail: + GTD_RETURN (0); +} + +static gboolean +on_drop_target_drag_drop_cb (GtkDropTarget *drop_target, + const GValue *value, + gdouble x, + gdouble y, + GtdTaskListView *self) +{ + GtkListBoxRow *drop_row; + GtkWidget *row; + GtdTask *hovered_task; + GtdTask *source_task; + GdkDrop *drop; + GdkDrag *drag; + gint64 current_position; + gint64 new_position; + + GTD_ENTRY; + + drop = gtk_drop_target_get_current_drop (drop_target); + drag = gdk_drop_get_drag (drop); + + if (!drag) + { + g_info ("Only dragging task rows is supported"); + GTD_RETURN (FALSE); + } + + gtk_list_box_drag_unhighlight_row (self->listbox); + + source_task = g_value_get_object (value); + g_assert (source_task != NULL); + + /* + * When the drag operation began, the source row was hidden. Now is the time + * to show it again. + */ + row = g_hash_table_lookup (self->task_to_row, source_task); + gtk_widget_show (row); + + drop_row = get_drop_row_at_y (self, y); + if (!drop_row) + { + check_dnd_scroll (self, TRUE, -1); + GTD_RETURN (FALSE); + } + + hovered_task = gtd_task_row_get_task (GTD_TASK_ROW (drop_row)); + new_position = gtd_task_get_position (hovered_task); + current_position = gtd_task_get_position (source_task); + + GTD_TRACE_MSG ("Dropping task %p at %ld", source_task, new_position); + + if (new_position != current_position) + { + gtd_task_list_move_task_to_position (GTD_TASK_LIST (self->model), + source_task, + new_position); + } + + check_dnd_scroll (self, TRUE, -1); + + GTD_RETURN (TRUE); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_view_finalize (GObject *object) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + g_clear_handle_id (&self->scroll_to_bottom_handler_id, g_source_remove); + g_clear_pointer (&self->task_to_row, g_hash_table_destroy); + g_clear_pointer (&self->default_date, g_date_time_unref); + g_clear_object (&self->incomplete_tasks_model); + g_clear_object (&self->renderer); + g_clear_object (&self->model); + + G_OBJECT_CLASS (gtd_task_list_view_parent_class)->finalize (object); +} + +static void +gtd_task_list_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + switch (prop_id) + { + case PROP_SHOW_DUE_DATE: + g_value_set_boolean (value, self->show_due_date); + break; + + case PROP_SHOW_LIST_NAME: + g_value_set_boolean (value, self->show_list_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + + switch (prop_id) + { + case PROP_SHOW_DUE_DATE: + gtd_task_list_view_set_show_due_date (self, g_value_get_boolean (value)); + break; + + case PROP_SHOW_LIST_NAME: + gtd_task_list_view_set_show_list_name (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_view_constructed (GObject *object) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (object); + GdkTexture *texture = gdk_texture_new_from_resource ("/org/gnome/todo/ui/assets/all-done.svg"); + G_OBJECT_CLASS (gtd_task_list_view_parent_class)->constructed (object); + + /* action_group */ + self->action_group = G_ACTION_GROUP (g_simple_action_group_new ()); + + adw_status_page_set_paintable (self->empty_list_widget, GDK_PAINTABLE (texture)); + + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + gtd_task_list_view_entries, + G_N_ELEMENTS (gtd_task_list_view_entries), + object); +} + + +/* + * GtkWidget overrides + */ + +static void +gtd_task_list_view_map (GtkWidget *widget) +{ + GtdTaskListView *self = GTD_TASK_LIST_VIEW (widget); + GtkRoot *root; + + + update_empty_state (self); + + GTK_WIDGET_CLASS (gtd_task_list_view_parent_class)->map (widget); + + root = gtk_widget_get_root (widget); + + /* Clear previously added "list" actions */ + gtk_widget_insert_action_group (GTK_WIDGET (root), "list", NULL); + + /* Add this instance's action group */ + gtk_widget_insert_action_group (GTK_WIDGET (root), "list", self->action_group); +} + +static void +gtd_task_list_view_class_init (GtdTaskListViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_task_list_view_finalize; + object_class->constructed = gtd_task_list_view_constructed; + object_class->get_property = gtd_task_list_view_get_property; + object_class->set_property = gtd_task_list_view_set_property; + + widget_class->map = gtd_task_list_view_map; + + g_type_ensure (GTD_TYPE_EDIT_PANE); + g_type_ensure (GTD_TYPE_NEW_TASK_ROW); + g_type_ensure (GTD_TYPE_TASK_ROW); + + /** + * GtdTaskListView::show-list-name: + * + * Whether the task rows should show the list name. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_LIST_NAME, + g_param_spec_boolean ("show-list-name", + "Whether task rows show the list name", + "Whether task rows show the list name at the end of the row", + FALSE, + G_PARAM_READWRITE)); + + /** + * GtdTaskListView::show-due-date: + * + * Whether due dates of the tasks are shown. + */ + g_object_class_install_property ( + object_class, + PROP_SHOW_DUE_DATE, + g_param_spec_boolean ("show-due-date", + "Whether due dates are shown", + "Whether due dates of the tasks are visible or not", + TRUE, + G_PARAM_READWRITE)); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, due_date_sizegroup); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, empty_list_widget); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, main_stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, tasklist_name_sizegroup); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, scrolled_window); + + gtk_widget_class_bind_template_callback (widget_class, on_empty_list_widget_add_tasks_cb); + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_entered_cb); + gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_exited_cb); + gtk_widget_class_bind_template_callback (widget_class, on_task_row_entered_cb); + gtk_widget_class_bind_template_callback (widget_class, on_task_row_exited_cb); + + gtk_widget_class_set_css_name (widget_class, "tasklistview"); +} + +static void +gtd_task_list_view_init (GtdTaskListView *self) +{ + GtkDropTarget *target; + + self->task_to_row = g_hash_table_new (NULL, NULL); + + self->can_toggle = TRUE; + self->show_due_date = TRUE; + self->show_due_date = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + target = gtk_drop_target_new (GTD_TYPE_TASK, GDK_ACTION_MOVE); + gtk_drop_target_set_preload (target, TRUE); + g_signal_connect (target, "drop", G_CALLBACK (on_drop_target_drag_drop_cb), self); + g_signal_connect (target, "enter", G_CALLBACK (on_drop_target_drag_enter_cb), self); + g_signal_connect (target, "leave", G_CALLBACK (on_drop_target_drag_leave_cb), self); + g_signal_connect (target, "motion", G_CALLBACK (on_drop_target_drag_motion_cb), self); + + gtk_widget_add_controller (GTK_WIDGET (self->listbox), GTK_EVENT_CONTROLLER (target)); + + self->renderer = gtd_markdown_renderer_new (); + + self->view_model = gtd_task_list_view_model_new (); + gtk_list_box_bind_model (self->listbox, + G_LIST_MODEL (self->view_model), + create_row_for_task_cb, + self, + NULL); +} + +/** + * gtd_task_list_view_new: + * + * Creates a new #GtdTaskListView + * + * Returns: (transfer full): a newly allocated #GtdTaskListView + */ +GtkWidget* +gtd_task_list_view_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LIST_VIEW, NULL); +} + +/** + * gtd_task_list_view_get_model: + * @view: a #GtdTaskListView + * + * Retrieves the #GtdTaskList from @view, or %NULL if none was set. + * + * Returns: (transfer none): the #GListModel of @view, or %NULL is + * none was set. + */ +GListModel* +gtd_task_list_view_get_model (GtdTaskListView *view) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), NULL); + + return view->model; +} + +/** + * gtd_task_list_view_set_model: + * @view: a #GtdTaskListView + * @model: a #GListModel + * + * Sets the internal #GListModel of @view. The model must have + * its element GType as @GtdTask. + */ +void +gtd_task_list_view_set_model (GtdTaskListView *view, + GListModel *model) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + g_return_if_fail (G_IS_LIST_MODEL (model)); + + if (view->model == model) + return; + + view->model = model; + + gtd_task_list_view_model_set_model (view->view_model, model); + schedule_scroll_to_bottom (view); + update_incomplete_tasks_model (view); + update_empty_state (view); +} + +/** + * gtd_task_list_view_get_show_list_name: + * @view: a #GtdTaskListView + * + * Whether @view shows the tasks' list names. + * + * Returns: %TRUE if @view show the tasks' list names, %FALSE otherwise + */ +gboolean +gtd_task_list_view_get_show_list_name (GtdTaskListView *view) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), FALSE); + + return view->show_list_name; +} + +/** + * gtd_task_list_view_set_show_list_name: + * @view: a #GtdTaskListView + * @show_list_name: %TRUE to show list names, %FALSE to hide it + * + * Whether @view should should it's tasks' list name. + */ +void +gtd_task_list_view_set_show_list_name (GtdTaskListView *view, + gboolean show_list_name) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + + if (view->show_list_name != show_list_name) + { + GtkWidget *child; + + view->show_list_name = show_list_name; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child)); + + if (!GTD_IS_TASK_ROW (row_child)) + continue; + + gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row_child), show_list_name); + } + + g_object_notify (G_OBJECT (view), "show-list-name"); + } +} + +/** + * gtd_task_list_view_get_show_due_date: + * @self: a #GtdTaskListView + * + * Retrieves whether the @self is showing the due dates of the tasks + * or not. + * + * Returns: %TRUE if due dates are visible, %FALSE otherwise. + */ +gboolean +gtd_task_list_view_get_show_due_date (GtdTaskListView *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), FALSE); + + return self->show_due_date; +} + +/** + * gtd_task_list_view_set_show_due_date: + * @self: a #GtdTaskListView + * @show_due_date: %TRUE to show due dates, %FALSE otherwise + * + * Sets whether @self shows the due dates of the tasks or not. + */ +void +gtd_task_list_view_set_show_due_date (GtdTaskListView *self, + gboolean show_due_date) +{ + GtkWidget *child; + + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self)); + + if (self->show_due_date == show_due_date) + return; + + self->show_due_date = show_due_date; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child)); + + if (!GTD_IS_TASK_ROW (row_child)) + continue; + + gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row_child), show_due_date); + } + + g_object_notify (G_OBJECT (self), "show-due-date"); +} + +/** + * gtd_task_list_view_set_header_func: + * @view: a #GtdTaskListView + * @func: (closure user_data) (scope call) (nullable): the header function + * @user_data: data passed to @func + * + * Sets @func as the header function of @view. You can safely call + * %gtk_list_box_row_set_header from within @func. + * + * Do not unref nor free any of the passed data. + */ +void +gtd_task_list_view_set_header_func (GtdTaskListView *view, + GtdTaskListViewHeaderFunc func, + gpointer user_data) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view)); + + if (func) + { + view->header_func = func; + view->header_user_data = user_data; + + gtk_list_box_set_header_func (view->listbox, + (GtkListBoxUpdateHeaderFunc) internal_header_func, + view, + NULL); + } + else + { + view->header_func = NULL; + view->header_user_data = NULL; + + gtk_list_box_set_header_func (view->listbox, + NULL, + NULL, + NULL); + } +} + +/** + * gtd_task_list_view_get_default_date: + * @self: a #GtdTaskListView + * + * Retrieves the current default date which new tasks are set to. + * + * Returns: (nullable): a #GDateTime, or %NULL + */ +GDateTime* +gtd_task_list_view_get_default_date (GtdTaskListView *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), NULL); + + return self->default_date; +} + +/** + * gtd_task_list_view_set_default_date: + * @self: a #GtdTaskListView + * @default_date: (nullable): the default_date, or %NULL + * + * Sets the current default date. + */ +void +gtd_task_list_view_set_default_date (GtdTaskListView *self, + GDateTime *default_date) +{ + g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self)); + + if (self->default_date == default_date) + return; + + g_clear_pointer (&self->default_date, g_date_time_unref); + self->default_date = default_date ? g_date_time_ref (default_date) : NULL; +} + diff --git a/src/gui/gtd-task-list-view.h b/src/gui/gtd-task-list-view.h new file mode 100644 index 0000000..bb863dc --- /dev/null +++ b/src/gui/gtd-task-list-view.h @@ -0,0 +1,73 @@ +/* gtd-task-list-view.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_LIST_VIEW_H +#define GTD_TASK_LIST_VIEW_H + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_VIEW (gtd_task_list_view_get_type()) +G_DECLARE_FINAL_TYPE (GtdTaskListView, gtd_task_list_view, GTD, TASK_LIST_VIEW, GtkBox) + +/** + * GtdTaskListViewHeaderFunc: + * @task: the #GtdTask that @row represents + * @previous_task: the #GtdTask that @before represents + * @user_data: (closure): user data + * + * The header function called on every task. + * + * Returns: (transfer full)(nullable): a #GtkWidget, or %NULL. + */ +typedef GtkWidget* (*GtdTaskListViewHeaderFunc) (GtdTask *task, + GtdTask *previous_task, + gpointer user_data); + +GtkWidget* gtd_task_list_view_new (void); + +GListModel* gtd_task_list_view_get_model (GtdTaskListView *view); + +void gtd_task_list_view_set_model (GtdTaskListView *view, + GListModel *model); + +gboolean gtd_task_list_view_get_show_list_name (GtdTaskListView *view); + +void gtd_task_list_view_set_show_list_name (GtdTaskListView *view, + gboolean show_list_name); + +gboolean gtd_task_list_view_get_show_due_date (GtdTaskListView *self); + +void gtd_task_list_view_set_show_due_date (GtdTaskListView *self, + gboolean show_due_date); + +void gtd_task_list_view_set_header_func (GtdTaskListView *view, + GtdTaskListViewHeaderFunc func, + gpointer user_data); + +GDateTime* gtd_task_list_view_get_default_date (GtdTaskListView *self); + +void gtd_task_list_view_set_default_date (GtdTaskListView *self, + GDateTime *default_date); + +G_END_DECLS + +#endif /* GTD_TASK_LIST_VIEW_H */ diff --git a/src/gui/gtd-task-list-view.ui b/src/gui/gtd-task-list-view.ui new file mode 100644 index 0000000..b283753 --- /dev/null +++ b/src/gui/gtd-task-list-view.ui @@ -0,0 +1,97 @@ + + + + + + diff --git a/src/gui/gtd-task-row.c b/src/gui/gtd-task-row.c new file mode 100644 index 0000000..dd43d2b --- /dev/null +++ b/src/gui/gtd-task-row.c @@ -0,0 +1,839 @@ +/* gtd-task-row.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTaskRow" + +#include "gtd-debug.h" +#include "gtd-edit-pane.h" +#include "gtd-manager.h" +#include "gtd-markdown-renderer.h" +#include "gtd-provider.h" +#include "gtd-star-widget.h" +#include "gtd-task-row.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-list-view.h" +#include "gtd-utils-private.h" +#include "gtd-widget.h" + +#include +#include +#include + +struct _GtdTaskRow +{ + GtkListBoxRow parent; + + /**/ + GtkWidget *content_box; + GtkWidget *main_box; + + GtkWidget *done_check; + GtkWidget *edit_panel_revealer; + GtkWidget *header_event_box; + GtdStarWidget *star_widget; + GtkWidget *title_entry; + + /* task widgets */ + GtkLabel *task_date_label; + GtkLabel *task_list_label; + + /* dnd widgets */ + GtkWidget *dnd_box; + GtkWidget *dnd_icon; + gint clicked_x; + gint clicked_y; + + /* data */ + GtdTask *task; + + GtdEditPane *edit_pane; + + GtdMarkdownRenderer *renderer; + GPtrArray *bindings; + + gboolean active; + gboolean changed; +}; + +#define PRIORITY_ICON_SIZE 8 + +static void on_star_widget_activated_cb (GtdStarWidget *star_widget, + GParamSpec *pspec, + GtdTaskRow *self); + +G_DEFINE_TYPE (GtdTaskRow, gtd_task_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + ENTER, + EXIT, + REMOVE_TASK, + NUM_SIGNALS +}; + +enum +{ + PROP_0, + PROP_RENDERER, + PROP_TASK, + LAST_PROP +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + + +/* + * Auxiliary methods + */ + +static gboolean +date_to_label_binding_cb (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + g_autofree gchar *new_label = NULL; + GDateTime *dt; + + g_return_val_if_fail (GTD_IS_TASK_ROW (user_data), FALSE); + + dt = g_value_get_boxed (from_value); + + if (dt) + { + g_autoptr (GDateTime) today = g_date_time_new_now_local (); + + if (g_date_time_get_year (dt) == g_date_time_get_year (today) && + g_date_time_get_month (dt) == g_date_time_get_month (today)) + { + if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today)) + { + new_label = g_strdup (_("Today")); + } + else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) + 1) + { + new_label = g_strdup (_("Tomorrow")); + } + else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) - 1) + { + new_label = g_strdup (_("Yesterday")); + } + else if (g_date_time_get_day_of_year (dt) > g_date_time_get_day_of_month (today) && + g_date_time_get_day_of_year (dt) < g_date_time_get_day_of_month (today) + 7) + { + new_label = g_date_time_format (dt, "%A"); + } + else + { + new_label = g_date_time_format (dt, "%x"); + } + } + else + { + new_label = g_date_time_format (dt, "%x"); + } + } + else + { + new_label = g_strdup (""); + } + + g_value_set_string (to_value, new_label); + + return TRUE; +} + +static GtkWidget* +create_transient_row (GtdTaskRow *self) +{ + GtdTaskRow *new_row; + + new_row = GTD_TASK_ROW (gtd_task_row_new (self->task, self->renderer)); + + gtk_widget_set_size_request (GTK_WIDGET (new_row), + gtk_widget_get_allocated_width (GTK_WIDGET (self)), + -1); + + gtk_revealer_set_reveal_child (GTK_REVEALER (new_row->edit_panel_revealer), self->active); + + gtk_widget_add_css_class (GTK_WIDGET (new_row), "background"); + gtk_widget_set_opacity (GTK_WIDGET (new_row), 0.66); + + return GTK_WIDGET (new_row); +} + + +/* + * Callbacks + */ + +static void +on_task_updated_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_update_task_finish (GTD_PROVIDER (object), result, &error); + + if (error) + { + g_warning ("Error updating task: %s", error->message); + return; + } +} + +static void +on_remove_task_cb (GtdEditPane *edit_panel, + GtdTask *task, + GtdTaskRow *self) +{ + g_signal_emit (self, signals[REMOVE_TASK], 0); +} + +static void +on_button_press_event_cb (GtkGestureClick *gesture, + gint n_press, + gdouble x, + gdouble y, + GtdTaskRow *self) +{ + GtkWidget *widget; + gdouble real_x; + gdouble real_y; + + widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); + + gtk_widget_translate_coordinates (widget, + GTK_WIDGET (self), + x, + y, + &real_x, + &real_y); + + self->clicked_x = real_x; + self->clicked_y = real_y; + + GTD_TRACE_MSG ("GtkGestureClick:pressed received from a %s at %.1lf,%.1lf (%.1lf,%.1lf)", + G_OBJECT_TYPE_NAME (widget), + x, + y, + real_x, + real_y); +} + +static GdkContentProvider* +on_drag_prepare_cb (GtkDragSource *source, + gdouble x, + gdouble y, + GtdTaskRow *self) +{ + GTD_ENTRY; + GTD_RETURN (gdk_content_provider_new_typed (GTD_TYPE_TASK, self->task)); +} + +static void +on_drag_begin_cb (GtkDragSource *source, + GdkDrag *drag, + GtdTaskRow *self) +{ + GtkWidget *drag_icon; + GtkWidget *new_row; + GtkWidget *widget; + + GTD_ENTRY; + + widget = GTK_WIDGET (self); + + gtk_widget_set_cursor_from_name (widget, "grabbing"); + + new_row = create_transient_row (self); + drag_icon = gtk_drag_icon_get_for_drag (drag); + gtk_drag_icon_set_child (GTK_DRAG_ICON (drag_icon), new_row); + gdk_drag_set_hotspot (drag, self->clicked_x, self->clicked_y); + + gtk_widget_hide (widget); + + GTD_EXIT; +} + +static gboolean +on_drag_cancelled_cb (GtkDragSource *source, + GdkDrag *drag, + GdkDragCancelReason result, + GtdTaskRow *self) +{ + GTD_ENTRY; + + gtk_widget_set_cursor_from_name (GTK_WIDGET (self), NULL); + gtk_widget_show (GTK_WIDGET (self)); + + GTD_RETURN (FALSE); +} + +static void +on_complete_check_toggled_cb (GtkCheckButton *button, + GtdTaskRow *self) +{ + GTD_ENTRY; + + g_assert (GTD_IS_TASK (self->task)); + + gtd_task_row_set_active (self, FALSE); + + gtd_task_set_complete (self->task, gtk_check_button_get_active (button)); + gtd_provider_update_task (gtd_task_get_provider (self->task), + self->task, + NULL, + on_task_updated_cb, + self); + + GTD_EXIT; +} + +static void +on_complete_changed_cb (GtdTaskRow *self, + GParamSpec *pspec, + GtdTask *task) +{ + gboolean complete; + + complete = gtd_task_get_complete (task); + + if (complete) + gtk_widget_add_css_class (GTK_WIDGET (self), "complete"); + else + gtk_widget_remove_css_class (GTK_WIDGET (self), "complete"); + + /* Update the toggle button as well */ + g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->done_check), complete); + g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self); +} + +static void +on_task_important_changed_cb (GtdTask *task, + GParamSpec *pspec, + GtdTaskRow *self) +{ + g_signal_handlers_block_by_func (self->star_widget, on_star_widget_activated_cb, self); + + gtd_star_widget_set_active (self->star_widget, gtd_task_get_important (task)); + + g_signal_handlers_unblock_by_func (self->star_widget, on_star_widget_activated_cb, self); +} + +static void +on_star_widget_activated_cb (GtdStarWidget *star_widget, + GParamSpec *pspec, + GtdTaskRow *self) +{ + g_signal_handlers_block_by_func (self->task, on_task_important_changed_cb, self); + + gtd_task_set_important (self->task, gtd_star_widget_get_active (star_widget)); + gtd_provider_update_task (gtd_task_get_provider (self->task), + self->task, + NULL, + on_task_updated_cb, + self); + + g_signal_handlers_unblock_by_func (self->task, on_task_important_changed_cb, self); +} + +static gboolean +on_key_pressed_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType modifiers, + GtdTaskRow *self) +{ + GTD_ENTRY; + + /* Exit when pressing Esc without modifiers */ + if (keyval == GDK_KEY_Escape && !(modifiers & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) + { + GTD_TRACE_MSG ("Escape pressed, closing task…"); + gtd_task_row_set_active (self, FALSE); + } + + GTD_RETURN (GDK_EVENT_PROPAGATE); +} + +static void +on_task_changed_cb (GtdTaskRow *self) +{ + g_debug ("Task changed"); + + self->changed = TRUE; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_row_finalize (GObject *object) +{ + GtdTaskRow *self = GTD_TASK_ROW (object); + + if (self->changed) + { + if (self->task) + { + gtd_provider_update_task (gtd_task_get_provider (self->task), + self->task, + NULL, + on_task_updated_cb, + self); + } + self->changed = FALSE; + } + + g_clear_object (&self->task); + + G_OBJECT_CLASS (gtd_task_row_parent_class)->finalize (object); +} + +static void +gtd_task_row_dispose (GObject *object) +{ + GtdTaskRow *self; + GtdTask *task; + + self = GTD_TASK_ROW (object); + task = self->task; + + g_clear_pointer (&self->bindings, g_ptr_array_unref); + + if (task) + g_signal_handlers_disconnect_by_func (task, on_complete_changed_cb, self); + + G_OBJECT_CLASS (gtd_task_row_parent_class)->dispose (object); +} + +static void +gtd_task_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskRow *self = GTD_TASK_ROW (object); + + switch (prop_id) + { + case PROP_RENDERER: + g_value_set_object (value, self->renderer); + break; + + case PROP_TASK: + g_value_set_object (value, self->task); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskRow *self = GTD_TASK_ROW (object); + + switch (prop_id) + { + case PROP_RENDERER: + self->renderer = g_value_get_object (value); + break; + + case PROP_TASK: + gtd_task_row_set_task (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_row_class_init (GtdTaskRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gtd_task_row_dispose; + object_class->finalize = gtd_task_row_finalize; + object_class->get_property = gtd_task_row_get_property; + object_class->set_property = gtd_task_row_set_property; + + /** + * GtdTaskRow::renderer: + * + * The internal markdown renderer. + */ + g_object_class_install_property ( + object_class, + PROP_RENDERER, + g_param_spec_object ("renderer", + "Renderer", + "Renderer", + GTD_TYPE_MARKDOWN_RENDERER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + /** + * GtdTaskRow::task: + * + * The task that this row represents, or %NULL. + */ + g_object_class_install_property ( + object_class, + PROP_TASK, + g_param_spec_object ("task", + "Task of the row", + "The task that this row represents", + GTD_TYPE_TASK, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + + /** + * GtdTaskRow::enter: + * + * Emitted when the row is focused and in the editing state. + */ + signals[ENTER] = g_signal_new ("enter", + GTD_TYPE_TASK_ROW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdTaskRow::exit: + * + * Emitted when the row is unfocused and leaves the editing state. + */ + signals[EXIT] = g_signal_new ("exit", + GTD_TYPE_TASK_ROW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + /** + * GtdTaskRow::remove-task: + * + * Emitted when the user wants to delete the task represented by this row. + */ + signals[REMOVE_TASK] = g_signal_new ("remove-task", + GTD_TYPE_TASK_ROW, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, content_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_icon); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, done_check); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, edit_panel_revealer); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, header_event_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, main_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, star_widget); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_date_label); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_list_label); + gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, title_entry); + + gtk_widget_class_bind_template_callback (widget_class, on_button_press_event_cb); + gtk_widget_class_bind_template_callback (widget_class, on_complete_check_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, on_key_pressed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_remove_task_cb); + gtk_widget_class_bind_template_callback (widget_class, on_task_changed_cb); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); + gtk_widget_class_set_css_name (widget_class, "taskrow"); +} + +static void +gtd_task_row_init (GtdTaskRow *self) +{ + GtkDragSource *drag_source; + + self->bindings = g_ptr_array_new_with_free_func ((GDestroyNotify) g_binding_unbind); + self->active = FALSE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + drag_source = gtk_drag_source_new (); + gtk_drag_source_set_actions (drag_source, GDK_ACTION_MOVE); + + g_signal_connect (drag_source, "prepare", G_CALLBACK (on_drag_prepare_cb), self); + g_signal_connect (drag_source, "drag-begin", G_CALLBACK (on_drag_begin_cb), self); + g_signal_connect (drag_source, "drag-cancel", G_CALLBACK (on_drag_cancelled_cb), self); + + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_source)); + + gtk_widget_set_cursor_from_name (self->dnd_icon, "grab"); + gtk_widget_set_cursor_from_name (self->header_event_box, "pointer"); +} + +GtkWidget* +gtd_task_row_new (GtdTask *task, + GtdMarkdownRenderer *renderer) +{ + return g_object_new (GTD_TYPE_TASK_ROW, + "task", task, + "renderer", renderer, + NULL); +} + +void +gtd_task_row_set_task (GtdTaskRow *self, + GtdTask *task) +{ + GBinding *binding; + GtdTask *old_task; + + g_return_if_fail (GTD_IS_TASK_ROW (self)); + + old_task = self->task; + + if (old_task) + { + g_signal_handlers_disconnect_by_func (old_task, on_complete_changed_cb, self); + g_signal_handlers_disconnect_by_func (old_task, on_task_important_changed_cb, self); + g_signal_handlers_disconnect_by_func (old_task, on_star_widget_activated_cb, self); + g_ptr_array_set_size (self->bindings, 0); + } + + g_set_object (&self->task, task); + + if (task) + { + gtk_label_set_label (self->task_list_label, + gtd_task_list_get_name (gtd_task_get_list (task))); + + g_signal_handlers_block_by_func (self->title_entry, on_task_changed_cb, self); + g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self); + + binding = g_object_bind_property (task, + "loading", + self, + "sensitive", + G_BINDING_DEFAULT | + G_BINDING_INVERT_BOOLEAN | + G_BINDING_SYNC_CREATE); + g_ptr_array_add (self->bindings, binding); + + binding = g_object_bind_property (task, + "title", + self->title_entry, + "text", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + g_ptr_array_add (self->bindings, binding); + + binding = g_object_bind_property_full (task, + "due-date", + self->task_date_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + date_to_label_binding_cb, + NULL, + self, + NULL); + g_ptr_array_add (self->bindings, binding); + + on_complete_changed_cb (self, NULL, task); + g_signal_connect_object (task, + "notify::complete", + G_CALLBACK (on_complete_changed_cb), + self, + G_CONNECT_SWAPPED); + + on_task_important_changed_cb (task, NULL, self); + g_signal_connect_object (task, + "notify::important", + G_CALLBACK (on_task_important_changed_cb), + self, + 0); + + g_signal_connect_object (self->star_widget, + "notify::active", + G_CALLBACK (on_star_widget_activated_cb), + self, + 0); + + g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self); + g_signal_handlers_unblock_by_func (self->title_entry, on_task_changed_cb, self); + } +} + +/** + * gtd_task_row_get_task: + * @row: a #GtdTaskRow + * + * Retrieves the #GtdTask that @row manages, or %NULL if none + * is set. + * + * Returns: (transfer none): the internal task of @row + */ +GtdTask* +gtd_task_row_get_task (GtdTaskRow *row) +{ + g_return_val_if_fail (GTD_IS_TASK_ROW (row), NULL); + + return row->task; +} + +/** + * gtd_task_row_set_list_name_visible: + * @row: a #GtdTaskRow + * @show_list_name: %TRUE to show the list name, %FALSE to hide it + * + * Sets @row's list name label visibility to @show_list_name. + */ +void +gtd_task_row_set_list_name_visible (GtdTaskRow *row, + gboolean show_list_name) +{ + g_return_if_fail (GTD_IS_TASK_ROW (row)); + + gtk_widget_set_visible (GTK_WIDGET (row->task_list_label), show_list_name); +} + +/** + * gtd_task_row_set_due_date_visible: + * @row: a #GtdTaskRow + * @show_due_date: %TRUE to show the due, %FALSE to hide it + * + * Sets @row's due date label visibility to @show_due_date. + */ +void +gtd_task_row_set_due_date_visible (GtdTaskRow *row, + gboolean show_due_date) +{ + g_return_if_fail (GTD_IS_TASK_ROW (row)); + + gtk_widget_set_visible (GTK_WIDGET (row->task_date_label), show_due_date); +} + +gboolean +gtd_task_row_get_active (GtdTaskRow *self) +{ + g_return_val_if_fail (GTD_IS_TASK_ROW (self), FALSE); + + return self->active; +} + +void +gtd_task_row_set_active (GtdTaskRow *self, + gboolean active) +{ + GDateTime *dt; + + g_return_if_fail (GTD_IS_TASK_ROW (self)); + + if (self->active == active) + return; + + self->active = active; + + /* Create or destroy the edit panel */ + if (active && !self->edit_pane) + { + GTD_TRACE_MSG ("Creating edit pane"); + + self->edit_pane = GTD_EDIT_PANE (gtd_edit_pane_new ()); + gtd_edit_pane_set_markdown_renderer (self->edit_pane, self->renderer); + gtd_edit_pane_set_task (self->edit_pane, self->task); + + gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), GTK_WIDGET (self->edit_pane)); + gtk_widget_show (GTK_WIDGET (self->edit_pane)); + + g_signal_connect_swapped (self->edit_pane, "changed", G_CALLBACK (on_task_changed_cb), self); + g_signal_connect (self->edit_pane, "remove-task", G_CALLBACK (on_remove_task_cb), self); + } + else if (!active && self->edit_pane) + { + /* For some reason, setting the due date before closing the Edit Row has + * the potential to cause the Task List to update, causing the active task row + * to become corrupted, and crash Endeavour. My guess is this is an upstream + * behaviour, so work around it by setting the task due date during close */ + dt = gtd_edit_pane_get_new_date_time (self->edit_pane); + + if (gtd_task_get_due_date (self->task) != dt) { + gtd_task_set_due_date (self->task, + gtd_edit_pane_get_new_date_time (self->edit_pane)); + } + + GTD_TRACE_MSG ("Destroying edit pane"); + + gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), NULL); + self->edit_pane = NULL; + } + + /* And reveal or hide it */ + gtk_revealer_set_reveal_child (GTK_REVEALER (self->edit_panel_revealer), active); + + /* Toggle title editability */ + gtk_editable_set_editable (GTK_EDITABLE (self->title_entry), active); + gtk_widget_set_sensitive (GTK_WIDGET (self->title_entry), active); + + /* Save the task if it is not being loaded */ + if (!active && !gtd_object_get_loading (GTD_OBJECT (self->task)) && self->changed) + { + g_debug ("Saving task…"); + + gtd_provider_update_task (gtd_task_get_provider (self->task), + self->task, + NULL, + on_task_updated_cb, + self); + self->changed = FALSE; + } + + if (active) { + gtk_widget_add_css_class (GTK_WIDGET (self), "card"); + } else { + gtk_widget_remove_css_class (GTK_WIDGET (self), "card"); + } + + g_signal_emit (self, active ? signals[ENTER] : signals[EXIT], 0); +} + +void +gtd_task_row_set_sizegroups (GtdTaskRow *self, + GtkSizeGroup *name_group, + GtkSizeGroup *date_group) +{ + gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_list_label)); + gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_date_label)); +} diff --git a/src/gui/gtd-task-row.h b/src/gui/gtd-task-row.h new file mode 100644 index 0000000..111fec3 --- /dev/null +++ b/src/gui/gtd-task-row.h @@ -0,0 +1,54 @@ +/* gtd-task-row.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_ROW_H +#define GTD_TASK_ROW_H + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_ROW (gtd_task_row_get_type()) +G_DECLARE_FINAL_TYPE (GtdTaskRow, gtd_task_row, GTD, TASK_ROW, GtkListBoxRow) + +GtkWidget* gtd_task_row_new (GtdTask *task, + GtdMarkdownRenderer *renderer); + +void gtd_task_row_set_task (GtdTaskRow *self, + GtdTask *task); + +GtdTask* gtd_task_row_get_task (GtdTaskRow *row); + +void gtd_task_row_set_list_name_visible (GtdTaskRow *row, + gboolean show_list_name); + +void gtd_task_row_set_due_date_visible (GtdTaskRow *row, + gboolean show_due_date); + +gboolean gtd_task_row_get_active (GtdTaskRow *self); + +void gtd_task_row_set_active (GtdTaskRow *self, + gboolean active); + +void gtd_task_row_set_sizegroups (GtdTaskRow *self, + GtkSizeGroup *name_group, + GtkSizeGroup *date_group); + +G_END_DECLS + +#endif /* GTD_TASK_ROW_H */ diff --git a/src/gui/gtd-task-row.ui b/src/gui/gtd-task-row.ui new file mode 100644 index 0000000..601464b --- /dev/null +++ b/src/gui/gtd-task-row.ui @@ -0,0 +1,127 @@ + + + + + diff --git a/src/gui/gtd-widget.c b/src/gui/gtd-widget.c new file mode 100644 index 0000000..e5bece4 --- /dev/null +++ b/src/gui/gtd-widget.c @@ -0,0 +1,1668 @@ +/* gtd-widget.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-animatable.h" +#include "gtd-bin-layout.h" +#include "gtd-debug.h" +#include "gtd-animation-enums.h" +#include "gtd-interval.h" +#include "gtd-timeline.h" +#include "gtd-property-transition.h" + +#include +#include + +enum +{ + X, + Y, + Z, +}; + +typedef struct +{ + guint easing_duration; + guint easing_delay; + GtdEaseMode easing_mode; +} AnimationState; + +typedef struct +{ + struct { + GHashTable *transitions; + GArray *states; + AnimationState *current_state; + } animation; + + graphene_point3d_t pivot_point; + gfloat rotation[3]; + gfloat scale[3]; + graphene_point3d_t translation; + + GskTransform *cached_transform; +} GtdWidgetPrivate; + +static void set_animatable_property (GtdWidget *self, + guint prop_id, + const GValue *value, + GParamSpec *pspec); + +static void gtd_animatable_iface_init (GtdAnimatableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdWidget, gtd_widget, GTK_TYPE_WIDGET, + G_ADD_PRIVATE (GtdWidget) + G_IMPLEMENT_INTERFACE (GTD_TYPE_ANIMATABLE, gtd_animatable_iface_init)) + +enum +{ + PROP_0, + PROP_PIVOT_POINT, + PROP_ROTATION_X, + PROP_ROTATION_Y, + PROP_ROTATION_Z, + PROP_SCALE_X, + PROP_SCALE_Y, + PROP_SCALE_Z, + PROP_TRANSLATION_X, + PROP_TRANSLATION_Y, + PROP_TRANSLATION_Z, + N_PROPS +}; + +enum +{ + TRANSITION_STOPPED, + TRANSITIONS_COMPLETED, + NUM_SIGNALS +}; + + +static guint signals[NUM_SIGNALS] = { 0, }; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + + +/* + * Auxiliary methods + */ + +typedef struct +{ + GtdWidget *widget; + GtdTransition *transition; + gchar *name; + gulong completed_id; +} TransitionClosure; + +static void +transition_closure_free (gpointer data) +{ + if (G_LIKELY (data != NULL)) + { + TransitionClosure *closure = data; + GtdTimeline *timeline; + + timeline = GTD_TIMELINE (closure->transition); + + /* we disconnect the signal handler before stopping the timeline, + * so that we don't end up inside on_transition_stopped() from + * a call to g_hash_table_remove(). + */ + g_clear_signal_handler (&closure->completed_id, closure->transition); + + if (gtd_timeline_is_playing (timeline)) + gtd_timeline_stop (timeline); + + g_object_unref (closure->transition); + + g_free (closure->name); + + g_slice_free (TransitionClosure, closure); + } +} + +static void +on_transition_stopped_cb (GtdTransition *transition, + gboolean is_finished, + TransitionClosure *closure) +{ + GtdWidget *self = closure->widget; + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + GQuark t_quark; + gchar *t_name; + + if (closure->name == NULL) + return; + + /* we need copies because we emit the signal after the + * TransitionClosure data structure has been freed + */ + t_quark = g_quark_from_string (closure->name); + t_name = g_strdup (closure->name); + + if (gtd_transition_get_remove_on_complete (transition)) + { + /* this is safe, because the timeline has now stopped, + * so we won't recurse; the reference on the Animatable + * will be dropped by the ::stopped signal closure in + * GtdTransition, which is RUN_LAST, and thus will + * be called after this handler + */ + g_hash_table_remove (priv->animation.transitions, closure->name); + } + + /* we emit the ::transition-stopped after removing the + * transition, so that we can chain up new transitions + * without interfering with the one that just finished + */ + g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, is_finished); + + g_free (t_name); + + /* if it's the last transition then we clean up */ + if (g_hash_table_size (priv->animation.transitions) == 0) + { + g_hash_table_unref (priv->animation.transitions); + priv->animation.transitions = NULL; + + GTD_TRACE_MSG ("[animation] Transitions for '%p' completed", self); + + g_signal_emit (self, signals[TRANSITIONS_COMPLETED], 0); + } +} + +static void +add_transition_to_widget (GtdWidget *self, + const gchar *name, + GtdTransition *transition) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + TransitionClosure *closure; + GtdTimeline *timeline; + + GTD_ENTRY; + + if (!priv->animation.transitions) + { + priv->animation.transitions = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + transition_closure_free); + } + + if (g_hash_table_lookup (priv->animation.transitions, name) != NULL) + { + g_warning ("A transition with name '%s' already exists for the widget '%p'", + name, + self); + GTD_RETURN (); + } + + gtd_transition_set_animatable (transition, GTD_ANIMATABLE (self)); + + timeline = GTD_TIMELINE (transition); + + closure = g_slice_new (TransitionClosure); + closure->widget = self; + closure->transition = g_object_ref (transition); + closure->name = g_strdup (name); + closure->completed_id = g_signal_connect (timeline, + "stopped", + G_CALLBACK (on_transition_stopped_cb), + closure); + + GTD_TRACE_MSG ("[animation] Adding transition '%s' [%p] to widget %p", + closure->name, + closure->transition, + self); + + g_hash_table_insert (priv->animation.transitions, closure->name, closure); + gtd_timeline_start (timeline); + + GTD_EXIT; +} + +static gboolean +should_skip_implicit_transition (GtdWidget *self, + GParamSpec *pspec) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + /* if the easing state has a non-zero duration we always want an + * implicit transition to occur + */ + if (priv->animation.current_state->easing_duration == 0) + return TRUE; + + /* if the widget is not mapped and is not part of a branch of the scene + * graph that is being cloned, then we always skip implicit transitions + * on the account of the fact that the widget is not going to be visible + * when those transitions happen + */ + if (!gtk_widget_get_mapped (GTK_WIDGET (self))) + return TRUE; + + return FALSE; +} + +static GtdTransition* +create_transition (GtdWidget *self, + GParamSpec *pspec, + ...) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + g_autofree gchar *error = NULL; + g_auto (GValue) initial = G_VALUE_INIT; + g_auto (GValue) final = G_VALUE_INIT; + TransitionClosure *closure; + GtdTimeline *timeline; + GtdInterval *interval; + GtdTransition *res = NULL; + va_list var_args; + GType ptype; + + g_assert (pspec != NULL); + + if (!priv->animation.transitions) + { + priv->animation.transitions = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + transition_closure_free); + } + + va_start (var_args, pspec); + + ptype = G_PARAM_SPEC_VALUE_TYPE (pspec); + + G_VALUE_COLLECT_INIT (&initial, ptype, var_args, 0, &error); + if (error != NULL) + { + g_critical ("%s: %s", G_STRLOC, error); + goto out; + } + + G_VALUE_COLLECT_INIT (&final, ptype, var_args, 0, &error); + if (error != NULL) + { + g_critical ("%s: %s", G_STRLOC, error); + goto out; + } + + if (should_skip_implicit_transition (self, pspec)) + { + GTD_TRACE_MSG ("[animation] Skipping implicit transition for '%p::%s'", + self, + pspec->name); + + /* remove a transition, if one exists */ + gtd_widget_remove_transition (self, pspec->name); + + /* we don't go through the Animatable interface because we + * already know we got here through an animatable property. + */ + set_animatable_property (self, pspec->param_id, &final, pspec); + + goto out; + } + + closure = g_hash_table_lookup (priv->animation.transitions, pspec->name); + if (closure == NULL) + { + res = gtd_property_transition_new (pspec->name); + + gtd_transition_set_remove_on_complete (res, TRUE); + + interval = gtd_interval_new_with_values (ptype, &initial, &final); + gtd_transition_set_interval (res, interval); + + timeline = GTD_TIMELINE (res); + gtd_timeline_set_delay (timeline, priv->animation.current_state->easing_delay); + gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration); + gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode); + + /* this will start the transition as well */ + add_transition_to_widget (self, pspec->name, res); + + /* the widget now owns the transition */ + g_object_unref (res); + } + else + { + GtdEaseMode cur_mode; + guint cur_duration; + + GTD_TRACE_MSG ("[animation] Existing transition for %p:%s", + self, + pspec->name); + + timeline = GTD_TIMELINE (closure->transition); + + cur_duration = gtd_timeline_get_duration (timeline); + if (cur_duration != priv->animation.current_state->easing_duration) + gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration); + + cur_mode = gtd_timeline_get_progress_mode (timeline); + if (cur_mode != priv->animation.current_state->easing_mode) + gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode); + + gtd_timeline_rewind (timeline); + + interval = gtd_transition_get_interval (closure->transition); + gtd_interval_set_initial_value (interval, &initial); + gtd_interval_set_final_value (interval, &final); + + res = closure->transition; + } + +out: + va_end (var_args); + + return res; +} + +static void +invalidate_cached_transform (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + g_clear_pointer (&priv->cached_transform, gsk_transform_unref); +} + +static void +calculate_transform (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + graphene_point3d_t pivot; + GskTransform *transform; + gboolean pivot_is_zero; + gint height; + gint width; + + transform = NULL; + width = gtk_widget_get_width (GTK_WIDGET (self)); + height = gtk_widget_get_height (GTK_WIDGET (self)); + + /* Pivot point */ + pivot_is_zero = graphene_point3d_equal (&priv->pivot_point, graphene_point3d_zero ()); + pivot = GRAPHENE_POINT3D_INIT (width * priv->pivot_point.x, + height * priv->pivot_point.y, + priv->pivot_point.z); + if (!pivot_is_zero) + transform = gsk_transform_translate_3d (transform, &pivot); + + /* Perspective */ + transform = gsk_transform_perspective (transform, 2 * MAX (width, height)); + + /* Translation */ + if (G_APPROX_VALUE (priv->translation.z, 0.f, FLT_EPSILON)) + { + transform = gsk_transform_translate (transform, + &GRAPHENE_POINT_INIT (priv->translation.x, + priv->translation.y)); + } + else + { + transform = gsk_transform_translate_3d (transform, &priv->translation); + } + + /* Scale */ + if (G_APPROX_VALUE (priv->scale[Z], 1.f, FLT_EPSILON)) + transform = gsk_transform_scale (transform, priv->scale[X], priv->scale[Y]); + else + transform = gsk_transform_scale_3d (transform, priv->scale[X], priv->scale[Y], priv->scale[Z]); + + /* Rotation */ + transform = gsk_transform_rotate_3d (transform, priv->rotation[X], graphene_vec3_x_axis ()); + transform = gsk_transform_rotate_3d (transform, priv->rotation[Y], graphene_vec3_y_axis ()); + transform = gsk_transform_rotate_3d (transform, priv->rotation[Z], graphene_vec3_z_axis ()); + + /* Rollback pivot point */ + if (!pivot_is_zero) + transform = gsk_transform_translate_3d (transform, + &GRAPHENE_POINT3D_INIT (-pivot.x, + -pivot.y, + -pivot.z)); + + priv->cached_transform = transform; +} + +static void +set_rotation_internal (GtdWidget *self, + gfloat rotation_x, + gfloat rotation_y, + gfloat rotation_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + invalidate_cached_transform (self); + + priv->rotation[X] = rotation_x; + priv->rotation[Y] = rotation_y; + priv->rotation[Z] = rotation_z; + + if (changed[X]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_X]); + + if (changed[Y]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Y]); + + if (changed[Z]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_scale_internal (GtdWidget *self, + gfloat scale_x, + gfloat scale_y, + gfloat scale_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + invalidate_cached_transform (self); + + priv->scale[X] = scale_x; + priv->scale[Y] = scale_y; + priv->scale[Z] = scale_z; + + if (changed[X]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_X]); + + if (changed[Y]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Y]); + + if (changed[Z]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_translation_internal (GtdWidget *self, + gfloat translation_x, + gfloat translation_y, + gfloat translation_z) +{ + graphene_point3d_t old_translation, translation; + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z); + + if (graphene_point3d_equal (&priv->translation, &translation)) + GTD_RETURN (); + + old_translation = priv->translation; + + invalidate_cached_transform (self); + priv->translation = translation; + + if (!G_APPROX_VALUE (old_translation.x, translation.x, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_X]); + + if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Y]); + + if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_animatable_property (GtdWidget *self, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + GObject *object = G_OBJECT (self); + + g_object_freeze_notify (object); + + switch (prop_id) + { + case PROP_ROTATION_X: + set_rotation_internal (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]); + break; + + case PROP_ROTATION_Y: + set_rotation_internal (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]); + break; + + case PROP_ROTATION_Z: + set_rotation_internal (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value)); + break; + + case PROP_SCALE_X: + set_scale_internal (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]); + break; + + case PROP_SCALE_Y: + set_scale_internal (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]); + break; + + case PROP_SCALE_Z: + set_scale_internal (self, priv->scale[X], priv->scale[Y], g_value_get_float (value)); + break; + + case PROP_TRANSLATION_X: + set_translation_internal (self, g_value_get_float (value), priv->translation.y, priv->translation.z); + break; + + case PROP_TRANSLATION_Y: + set_translation_internal (self, priv->translation.x, g_value_get_float (value), priv->translation.z); + break; + + case PROP_TRANSLATION_Z: + set_translation_internal (self, priv->translation.x, priv->translation.y, g_value_get_float (value)); + break; + + default: + g_object_set_property (object, pspec->name, value); + break; + } + + g_object_thaw_notify (object); +} + +/* + * GtdAnimatable interface + */ + +static GParamSpec * +gtd_widget_find_property (GtdAnimatable *animatable, + const gchar *property_name) +{ + return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable), property_name); +} + +static void +gtd_widget_get_initial_state (GtdAnimatable *animatable, + const gchar *property_name, + GValue *initial) +{ + g_object_get_property (G_OBJECT (animatable), property_name, initial); +} + +static void +gtd_widget_set_final_state (GtdAnimatable *animatable, + const gchar *property_name, + const GValue *final) +{ + GObjectClass *obj_class = G_OBJECT_GET_CLASS (animatable); + GParamSpec *pspec; + + pspec = g_object_class_find_property (obj_class, property_name); + + if (pspec) + set_animatable_property (GTD_WIDGET (animatable), pspec->param_id, final, pspec); +} + +static GtdWidget* +gtd_widget_get_widget (GtdAnimatable *animatable) +{ + return GTD_WIDGET (animatable); +} + +static void +gtd_animatable_iface_init (GtdAnimatableInterface *iface) +{ + iface->find_property = gtd_widget_find_property; + iface->get_initial_state = gtd_widget_get_initial_state; + iface->set_final_state = gtd_widget_set_final_state; + iface->get_widget = gtd_widget_get_widget; +} + + +/* + * GObject overrides + */ + +static void +gtd_widget_dispose (GObject *object) +{ + GtdWidget *self = GTD_WIDGET (object); + GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (object)); + + while (child) + { + GtkWidget *next = gtk_widget_get_next_sibling (child); + + gtk_widget_unparent (child); + child = next; + } + + invalidate_cached_transform (self); + gtd_widget_remove_all_transitions (self); + + G_OBJECT_CLASS (gtd_widget_parent_class)->dispose (object); +} + +static void +gtd_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdWidget *self = GTD_WIDGET (object); + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + switch (prop_id) + { + case PROP_PIVOT_POINT: + g_value_set_boxed (value, &priv->pivot_point); + break; + + case PROP_ROTATION_X: + g_value_set_float (value, priv->rotation[X]); + break; + + case PROP_ROTATION_Y: + g_value_set_float (value, priv->rotation[Y]); + break; + + case PROP_ROTATION_Z: + g_value_set_float (value, priv->rotation[Z]); + break; + + case PROP_SCALE_X: + g_value_set_float (value, priv->scale[X]); + break; + + case PROP_SCALE_Y: + g_value_set_float (value, priv->scale[Y]); + break; + + case PROP_SCALE_Z: + g_value_set_float (value, priv->scale[Z]); + break; + + case PROP_TRANSLATION_X: + g_value_set_float (value, priv->translation.x); + break; + + case PROP_TRANSLATION_Y: + g_value_set_float (value, priv->translation.y); + break; + + case PROP_TRANSLATION_Z: + g_value_set_float (value, priv->translation.z); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdWidget *self = GTD_WIDGET (object); + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + switch (prop_id) + { + case PROP_PIVOT_POINT: + gtd_widget_set_pivot_point (self, g_value_get_boxed (value)); + break; + + case PROP_ROTATION_X: + gtd_widget_set_rotation (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]); + break; + + case PROP_ROTATION_Y: + gtd_widget_set_rotation (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]); + break; + + case PROP_ROTATION_Z: + gtd_widget_set_rotation (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value)); + break; + + case PROP_SCALE_X: + gtd_widget_set_scale (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]); + break; + + case PROP_SCALE_Y: + gtd_widget_set_scale (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]); + break; + + case PROP_SCALE_Z: + gtd_widget_set_scale (self, priv->scale[X], priv->scale[Y], g_value_get_float (value)); + break; + + case PROP_TRANSLATION_X: + gtd_widget_set_translation (self, g_value_get_float (value), priv->translation.y, priv->translation.z); + break; + + case PROP_TRANSLATION_Y: + gtd_widget_set_translation (self, priv->translation.x, g_value_get_float (value), priv->translation.z); + break; + + case PROP_TRANSLATION_Z: + gtd_widget_set_translation (self, priv->translation.x, priv->translation.y, g_value_get_float (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_widget_class_init (GtdWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gtd_widget_dispose; + object_class->get_property = gtd_widget_get_property; + object_class->set_property = gtd_widget_set_property; + + /** + * GtdWidget: + */ + properties[PROP_PIVOT_POINT] = g_param_spec_boxed ("pivot-point", + "Pivot point", + "Pivot point", + GRAPHENE_TYPE_POINT3D, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-x + */ + properties[PROP_ROTATION_X] = g_param_spec_float ("rotation-x", + "Rotation in the X axis", + "Rotation in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-y + */ + properties[PROP_ROTATION_Y] = g_param_spec_float ("rotation-y", + "Rotation in the Y axis", + "Rotation in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-z + */ + properties[PROP_ROTATION_Z] = g_param_spec_float ("rotation-z", + "Rotation in the Z axis", + "Rotation in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-x + */ + properties[PROP_SCALE_X] = g_param_spec_float ("scale-x", + "Scale in the X axis", + "Scale in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-y + */ + properties[PROP_SCALE_Y] = g_param_spec_float ("scale-y", + "Scale in the Y axis", + "Scale in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-z + */ + properties[PROP_SCALE_Z] = g_param_spec_float ("scale-z", + "Scale in the Z axis", + "Scale in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-x + */ + properties[PROP_TRANSLATION_X] = g_param_spec_float ("translation-x", + "Translation in the X axis", + "Translation in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-y + */ + properties[PROP_TRANSLATION_Y] = g_param_spec_float ("translation-y", + "Translation in the Y axis", + "Translation in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-z + */ + properties[PROP_TRANSLATION_Z] = g_param_spec_float ("translation-z", + "Translation in the Z axis", + "Translation in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * GtdWidget::transitions-completed: + * @actor: a #GtdWidget + * + * The ::transitions-completed signal is emitted once all transitions + * involving @actor are complete. + * + * Since: 1.10 + */ + signals[TRANSITIONS_COMPLETED] = + g_signal_new ("transitions-completed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * GtdWidget::transition-stopped: + * @actor: a #GtdWidget + * @name: the name of the transition + * @is_finished: whether the transition was finished, or stopped + * + * The ::transition-stopped signal is emitted once a transition + * is stopped; a transition is stopped once it reached its total + * duration (including eventual repeats), it has been stopped + * using gtd_timeline_stop(), or it has been removed from the + * transitions applied on @actor, using gtd_actor_remove_transition(). + * + * Since: 1.12 + */ + signals[TRANSITION_STOPPED] = + g_signal_new ("transition-stopped", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | + G_SIGNAL_NO_HOOKS | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_BOOLEAN); + + gtk_widget_class_set_layout_manager_type (widget_class, GTD_TYPE_BIN_LAYOUT); +} + +static void +gtd_widget_init (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + priv->scale[X] = 1.f; + priv->scale[Y] = 1.f; + priv->scale[Z] = 1.f; + + priv->pivot_point = GRAPHENE_POINT3D_INIT (0.5, 0.5, 0.f); + + gtd_widget_save_easing_state (self); + gtd_widget_set_easing_duration (self, 0); +} + +GtkWidget* +gtd_widget_new (void) +{ + return g_object_new (GTD_TYPE_WIDGET, NULL); +} + +/** + */ +void +gtd_widget_get_pivot_point (GtdWidget *self, + graphene_point3d_t *out_pivot_point) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (out_pivot_point != NULL); + + priv = gtd_widget_get_instance_private (self); + *out_pivot_point = priv->pivot_point; +} + +/** + */ +void +gtd_widget_set_pivot_point (GtdWidget *self, + const graphene_point3d_t *pivot_point) +{ + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (pivot_point != NULL); + g_return_if_fail (pivot_point->x >= 0.f && pivot_point->x <= 1.0); + g_return_if_fail (pivot_point->y >= 0.f && pivot_point->y <= 1.0); + + priv = gtd_widget_get_instance_private (self); + + if (graphene_point3d_equal (&priv->pivot_point, pivot_point)) + GTD_RETURN (); + + invalidate_cached_transform (self); + priv->pivot_point = *pivot_point; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_rotation (GtdWidget *self, + gfloat *rotation_x, + gfloat *rotation_y, + gfloat *rotation_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (rotation_x) + *rotation_x = priv->rotation[X]; + + if (rotation_y) + *rotation_y = priv->rotation[Y]; + + if (rotation_z) + *rotation_z = priv->rotation[Z]; +} + +/** + */ +void +gtd_widget_set_rotation (GtdWidget *self, + gfloat rotation_x, + gfloat rotation_y, + gfloat rotation_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + if (changed[X]) + create_transition (self, properties[PROP_ROTATION_X], priv->rotation[X], rotation_x); + + if (changed[Y]) + create_transition (self, properties[PROP_ROTATION_Y], priv->rotation[Y], rotation_y); + + if (changed[Z]) + create_transition (self, properties[PROP_ROTATION_Z], priv->rotation[Z], rotation_z); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_scale (GtdWidget *self, + gfloat *scale_x, + gfloat *scale_y, + gfloat *scale_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (scale_x) + *scale_x = priv->scale[0]; + + if (scale_y) + *scale_y = priv->scale[1]; + + if (scale_z) + *scale_z = priv->scale[2]; +} + +/** + */ +void +gtd_widget_set_scale (GtdWidget *self, + gfloat scale_x, + gfloat scale_y, + gfloat scale_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + if (changed[X]) + create_transition (self, properties[PROP_SCALE_X], priv->scale[X], scale_x); + + if (changed[Y]) + create_transition (self, properties[PROP_SCALE_Y], priv->scale[Y], scale_y); + + if (changed[Z]) + create_transition (self, properties[PROP_SCALE_Z], priv->scale[Z], scale_z); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_translation (GtdWidget *self, + gfloat *translation_x, + gfloat *translation_y, + gfloat *translation_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (translation_x) + *translation_x = priv->translation.x; + + if (translation_y) + *translation_y = priv->translation.y; + + if (translation_z) + *translation_z = priv->translation.z; +} + +/** + */ +void +gtd_widget_set_translation (GtdWidget *self, + gfloat translation_x, + gfloat translation_y, + gfloat translation_z) +{ + graphene_point3d_t translation; + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z); + + if (graphene_point3d_equal (&priv->translation, &translation)) + GTD_RETURN (); + + if (!G_APPROX_VALUE (priv->translation.x, translation.x, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_X], priv->translation.x, translation_x); + + if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_Y], priv->translation.y, translation_y); + + if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_Z], priv->translation.z, translation_z); + + GTD_EXIT; +} + +/** + */ +GskTransform* +gtd_widget_apply_transform (GtdWidget *self, + GskTransform *transform) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), NULL); + + priv = gtd_widget_get_instance_private (self); + + if (!priv->cached_transform) + calculate_transform (self); + + if (!transform) + return gsk_transform_ref (priv->cached_transform); + + return gsk_transform_transform (transform, priv->cached_transform); +} + +/** + * gtd_widget_add_transition: + * @self: a #GtdWidget + * @name: the name of the transition to add + * @transition: the #GtdTransition to add + * + * Adds a @transition to the #GtdWidget's list of animations. + * + * The @name string is a per-widget unique identifier of the @transition: only + * one #GtdTransition can be associated to the specified @name. + * + * The @transition will be started once added. + * + * This function will take a reference on the @transition. + * + * This function is usually called implicitly when modifying an animatable + * property. + * + * Since: 1.10 + */ +void +gtd_widget_add_transition (GtdWidget *self, + const gchar *name, + GtdTransition *transition) +{ + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (name != NULL); + g_return_if_fail (GTD_IS_TRANSITION (transition)); + + add_transition_to_widget (self, name, transition); +} + +/** + * gtd_widget_remove_transition: + * @self: a #GtdWidget + * @name: the name of the transition to remove + * + * Removes the transition stored inside a #GtdWidget using @name + * identifier. + * + * If the transition is currently in progress, it will be stopped. + * + * This function releases the reference acquired when the transition + * was added to the #GtdWidget. + * + * Since: 1.10 + */ +void +gtd_widget_remove_transition (GtdWidget *self, + const gchar *name) +{ + GtdWidgetPrivate *priv; + TransitionClosure *closure; + gboolean was_playing; + GQuark t_quark; + gchar *t_name; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (name != NULL); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.transitions == NULL) + return; + + closure = g_hash_table_lookup (priv->animation.transitions, name); + if (closure == NULL) + return; + + was_playing = + gtd_timeline_is_playing (GTD_TIMELINE (closure->transition)); + t_quark = g_quark_from_string (closure->name); + t_name = g_strdup (closure->name); + + g_hash_table_remove (priv->animation.transitions, name); + + /* we want to maintain the invariant that ::transition-stopped is + * emitted after the transition has been removed, to allow replacing + * or chaining; removing the transition from the hash table will + * stop it, but transition_closure_free() will disconnect the signal + * handler we install in add_transition_internal(), to avoid loops + * or segfaults. + * + * since we know already that a transition will stop once it's removed + * from an widget, we can simply emit the ::transition-stopped here + * ourselves, if the timeline was playing (if it wasn't, then the + * signal was already emitted at least once). + */ + if (was_playing) + g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, FALSE); + + g_free (t_name); +} + +/** + * gtd_widget_remove_all_transitions: + * @self: a #GtdWidget + * + * Removes all transitions associated to @self. + * + * Since: 1.10 + */ +void +gtd_widget_remove_all_transitions (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return; + + g_hash_table_remove_all (priv->animation.transitions); +} + +/** + * gtd_widget_set_easing_duration: + * @self: a #GtdWidget + * @msecs: the duration of the easing, or %NULL + * + * Sets the duration of the tweening for animatable properties + * of @self for the current easing state. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_duration (GtdWidget *self, + guint msecs) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_duration()."); + return; + } + + if (priv->animation.current_state->easing_duration != msecs) + priv->animation.current_state->easing_duration = msecs; +} + +/** + * gtd_widget_get_easing_duration: + * @self: a #GtdWidget + * + * Retrieves the duration of the tweening for animatable + * properties of @self for the current easing state. + * + * Return value: the duration of the tweening, in milliseconds + * + * Since: 1.10 + */ +guint +gtd_widget_get_easing_duration (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), 0); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_duration; + + return 0; +} + +/** + * gtd_widget_set_easing_mode: + * @self: a #GtdWidget + * @mode: an easing mode, excluding %GTD_CUSTOM_MODE + * + * Sets the easing mode for the tweening of animatable properties + * of @self. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_mode (GtdWidget *self, + GtdEaseMode mode) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (mode != GTD_CUSTOM_MODE); + g_return_if_fail (mode < GTD_EASE_LAST); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_mode()."); + return; + } + + if (priv->animation.current_state->easing_mode != mode) + priv->animation.current_state->easing_mode = mode; +} + +/** + * gtd_widget_get_easing_mode: + * @self: a #GtdWidget + * + * Retrieves the easing mode for the tweening of animatable properties + * of @self for the current easing state. + * + * Return value: an easing mode + * + * Since: 1.10 + */ +GtdEaseMode +gtd_widget_get_easing_mode (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), GTD_EASE_OUT_CUBIC); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_mode; + + return GTD_EASE_OUT_CUBIC; +} + +/** + * gtd_widget_set_easing_delay: + * @self: a #GtdWidget + * @msecs: the delay before the start of the tweening, in milliseconds + * + * Sets the delay that should be applied before tweening animatable + * properties. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_delay (GtdWidget *self, + guint msecs) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_delay()."); + return; + } + + if (priv->animation.current_state->easing_delay != msecs) + priv->animation.current_state->easing_delay = msecs; +} + +/** + * gtd_widget_get_easing_delay: + * @self: a #GtdWidget + * + * Retrieves the delay that should be applied when tweening animatable + * properties. + * + * Return value: a delay, in milliseconds + * + * Since: 1.10 + */ +guint +gtd_widget_get_easing_delay (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), 0); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_delay; + + return 0; +} + +/** + * gtd_widget_get_transition: + * @self: a #GtdWidget + * @name: the name of the transition + * + * Retrieves the #GtdTransition of a #GtdWidget by using the + * transition @name. + * + * Transitions created for animatable properties use the name of the + * property itself, for instance the code below: + * + * |[ + * gtd_widget_set_easing_duration (widget, 1000); + * gtd_widget_set_rotation_angle (widget, GTD_Y_AXIS, 360.0); + * + * transition = gtd_widget_get_transition (widget, "rotation-angle-y"); + * g_signal_connect (transition, "stopped", + * G_CALLBACK (on_transition_stopped), + * widget); + * ]| + * + * will call the `on_transition_stopped` callback when the transition + * is finished. + * + * If you just want to get notifications of the completion of a transition, + * you should use the #GtdWidget::transition-stopped signal, using the + * transition name as the signal detail. + * + * Return value: (transfer none): a #GtdTransition, or %NULL is none + * was found to match the passed name; the returned instance is owned + * by Gtd and it should not be freed + */ +GtdTransition * +gtd_widget_get_transition (GtdWidget *self, + const gchar *name) +{ + TransitionClosure *closure; + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return NULL; + + closure = g_hash_table_lookup (priv->animation.transitions, name); + if (closure == NULL) + return NULL; + + return closure->transition; +} + +/** + * gtd_widget_has_transitions: (skip) + */ +gboolean +gtd_widget_has_transitions (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), FALSE); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return FALSE; + + return g_hash_table_size (priv->animation.transitions) > 0; +} + +/** + * gtd_widget_save_easing_state: + * @self: a #GtdWidget + * + * Saves the current easing state for animatable properties, and creates + * a new state with the default values for easing mode and duration. + * + * New transitions created after calling this function will inherit the + * duration, easing mode, and delay of the new easing state; this also + * applies to transitions modified in flight. + */ +void +gtd_widget_save_easing_state (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + AnimationState new_state; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.states == NULL) + priv->animation.states = g_array_new (FALSE, FALSE, sizeof (AnimationState)); + + new_state.easing_mode = GTD_EASE_OUT_CUBIC; + new_state.easing_duration = 250; + new_state.easing_delay = 0; + + g_array_append_val (priv->animation.states, new_state); + + priv->animation.current_state = &g_array_index (priv->animation.states, + AnimationState, + priv->animation.states->len - 1); +} + +/** + * gtd_widget_restore_easing_state: + * @self: a #GtdWidget + * + * Restores the easing state as it was prior to a call to + * gtd_widget_save_easing_state(). + * + * Since: 1.10 + */ +void +gtd_widget_restore_easing_state (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.states == NULL) + { + g_critical ("The function gtd_widget_restore_easing_state() has " + "been called without a previous call to " + "gtd_widget_save_easing_state()."); + return; + } + + g_array_remove_index (priv->animation.states, priv->animation.states->len - 1); + + if (priv->animation.states->len > 0) + priv->animation.current_state = &g_array_index (priv->animation.states, AnimationState, priv->animation.states->len - 1); + else + { + g_array_unref (priv->animation.states); + priv->animation.states = NULL; + priv->animation.current_state = NULL; + } +} diff --git a/src/gui/gtd-widget.h b/src/gui/gtd-widget.h new file mode 100644 index 0000000..c7b4ad2 --- /dev/null +++ b/src/gui/gtd-widget.h @@ -0,0 +1,110 @@ +/* gtd-'gtd-widget.c',.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gtd-animation-enums.h" +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_WIDGET (gtd_widget_get_type ()) +G_DECLARE_DERIVABLE_TYPE (GtdWidget, gtd_widget, GTD, WIDGET, GtkWidget) + +struct _GtdWidgetClass +{ + GtkWidgetClass parent; +}; + +GtkWidget* gtd_widget_new (void); + +void gtd_widget_get_pivot_point (GtdWidget *self, + graphene_point3d_t *out_pivot_point); + +void gtd_widget_set_pivot_point (GtdWidget *self, + const graphene_point3d_t *pivot_point); + +void gtd_widget_get_rotation (GtdWidget *self, + gfloat *rotation_x, + gfloat *rotation_y, + gfloat *rotation_z); + +void gtd_widget_set_rotation (GtdWidget *self, + gfloat rotation_x, + gfloat rotation_y, + gfloat rotation_z); + +void gtd_widget_get_scale (GtdWidget *self, + gfloat *scale_x, + gfloat *scale_y, + gfloat *scale_z); + +void gtd_widget_set_scale (GtdWidget *self, + gfloat scale_x, + gfloat scale_y, + gfloat scale_z); + +void gtd_widget_get_translation (GtdWidget *self, + gfloat *translation_x, + gfloat *translation_y, + gfloat *translation_z); + +void gtd_widget_set_translation (GtdWidget *self, + gfloat translation_x, + gfloat translation_y, + gfloat translation_z); + +GskTransform* gtd_widget_apply_transform (GtdWidget *self, + GskTransform *transform); + +void gtd_widget_save_easing_state (GtdWidget *self); + +void gtd_widget_restore_easing_state (GtdWidget *self); + +void gtd_widget_set_easing_mode (GtdWidget *self, + GtdEaseMode mode); + +GtdEaseMode gtd_widget_get_easing_mode (GtdWidget *self); + +void gtd_widget_set_easing_duration (GtdWidget *self, + guint msecs); + +guint gtd_widget_get_easing_duration (GtdWidget *self); + +void gtd_widget_set_easing_delay (GtdWidget *self, + guint msecs); + +guint gtd_widget_get_easing_delay (GtdWidget *self); + +GtdTransition* gtd_widget_get_transition (GtdWidget *self, + const gchar *name); + +void gtd_widget_add_transition (GtdWidget *self, + const gchar *name, + GtdTransition *transition); + +void gtd_widget_remove_transition (GtdWidget *self, + const gchar *name); + +void gtd_widget_remove_all_transitions (GtdWidget *self); + +G_END_DECLS diff --git a/src/gui/gtd-window.c b/src/gui/gtd-window.c new file mode 100644 index 0000000..029280c --- /dev/null +++ b/src/gui/gtd-window.c @@ -0,0 +1,413 @@ +/* gtd-window.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdWindow" + +#include "config.h" + +#include "gtd-activatable.h" +#include "gtd-application.h" +#include "gtd-debug.h" +#include "gtd-task-list-view.h" +#include "gtd-manager.h" +#include "gtd-manager-protected.h" +#include "gtd-menu-button.h" +#include "gtd-notification.h" +#include "gtd-omni-area.h" +#include "gtd-plugin-manager.h" +#include "gtd-provider.h" +#include "gtd-panel.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-window.h" +#include "gtd-workspace.h" + +#include +#include + +/** + * SECTION:gtd-window + * @short_description:main window + * @title:GtdWindow + * @stability:Unstable + * + * The #GtdWindow is the main application window of Endeavour. Objects should + * use this class to change between selection and normal mode and + * fine-tune the headerbar. + */ + +struct _GtdWindow +{ + AdwApplicationWindow application; + + GtkStack *stack; + + GtdWorkspace *current_workspace; + GListStore *workspaces; + GVariant *parameters; + + PeasExtensionSet *workspaces_set; +}; + +enum +{ + PROP_0, + PROP_CURRENT_WORKSPACE, + N_PROPS, +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +G_DEFINE_TYPE (GtdWindow, gtd_window, ADW_TYPE_APPLICATION_WINDOW) + +static gint compare_workspaced_func (gconstpointer a, + gconstpointer b, + gpointer user_data); + +static void +setup_development_build (GtdWindow *self) +{ + g_message (_("This is a development build of Endeavour. You may experience errors, wrong behaviors, " + "and data loss.")); + + gtk_widget_add_css_class (GTK_WIDGET (self), "devel"); +} + +static gboolean +is_development_build (void) +{ +#ifdef DEVELOPMENT_BUILD + return TRUE; +#else + return FALSE; +#endif +} + +static void +load_geometry (GtdWindow *self) +{ + GSettings *settings; + GtkWindow *window; + gboolean maximized; + gint height; + gint width; + + window = GTK_WINDOW (self); + settings = gtd_manager_get_settings (gtd_manager_get_default ()); + + maximized = g_settings_get_boolean (settings, "window-maximized"); + g_settings_get (settings, "window-size", "(ii)", &width, &height); + + gtk_window_set_default_size (window, width, height); + + if (maximized) + gtk_window_maximize (window); +} + +static void +add_workspace (GtdWindow *self, + GtdWorkspace *workspace) +{ + const gchar *workspace_id; + + workspace_id = gtd_workspace_get_id (workspace); + + gtk_stack_add_named (self->stack, GTK_WIDGET (workspace), workspace_id); + g_list_store_insert_sorted (self->workspaces, workspace, compare_workspaced_func, self); +} + +static void +remove_workspace (GtdWindow *self, + GtdWorkspace *workspace) +{ + guint position; + + if (!g_list_store_find (self->workspaces, workspace, &position)) + return; + + gtk_stack_remove (self->stack, GTK_WIDGET (workspace)); + g_list_store_remove (self->workspaces, position); +} + + +/* + * Callbacks + */ + + +static void +on_action_activate_workspace_activated_cb (GSimpleAction *simple, + GVariant *state, + gpointer user_data) +{ + g_autofree gchar *workspace_id = NULL; + GtdWindow *self; + + self = GTD_WINDOW (user_data); + g_variant_get (state, + "(sv)", + &workspace_id, + &self->parameters, + NULL); + + gtk_stack_set_visible_child_name (self->stack, workspace_id); +} + +static gint +compare_workspaced_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + gint a_priority; + gint b_priority; + + a_priority = gtd_workspace_get_priority ((GtdWorkspace *)a); + b_priority = gtd_workspace_get_priority ((GtdWorkspace *)b); + + return b_priority - a_priority; +} + +static void +on_stack_visible_child_cb (GtkStack *stack, + GParamSpec *pspec, + GtdWindow *self) +{ + g_autoptr (GVariant) parameters = NULL; + g_autoptr (GIcon) workspace_icon = NULL; + GtdWorkspace *new_workspace; + + GTD_ENTRY; + + if (self->current_workspace) + gtd_workspace_deactivate (self->current_workspace); + + new_workspace = GTD_WORKSPACE (gtk_stack_get_visible_child (stack)); + self->current_workspace = new_workspace; + + if (!new_workspace) + GTD_GOTO (out); + + parameters = g_steal_pointer (&self->parameters); + gtd_workspace_activate (new_workspace, parameters); + +out: + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CURRENT_WORKSPACE]); + GTD_EXIT; +} + +static void +on_workspace_added_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdWorkspace *workspace, + GtdWindow *self) +{ + GTD_ENTRY; + + add_workspace (self, g_object_ref_sink (workspace)); + + GTD_EXIT; +} + +static void +on_workspace_removed_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdWorkspace *workspace, + GtdWindow *self) +{ + GTD_ENTRY; + + remove_workspace (self, workspace); + + GTD_EXIT; +} + + +/* + * GtkWindow overrides + */ + +static void +gtd_window_unmap (GtkWidget *widget) +{ + GSettings *settings; + GtkWindow *window; + gboolean maximized; + + window = GTK_WINDOW (widget); + settings = gtd_manager_get_settings (gtd_manager_get_default ()); + maximized = gtk_window_is_maximized (window); + + g_settings_set_boolean (settings, "window-maximized", maximized); + + if (!maximized) + { + gint height; + gint width; + + gtk_window_get_default_size (window, &width, &height); + g_settings_set (settings, "window-size", "(ii)", width, height); + } + + GTK_WIDGET_CLASS (gtd_window_parent_class)->unmap (widget); +} + +/* + * GObject overrides + */ + +static void +gtd_window_dispose (GObject *object) +{ + GtdWindow *self = GTD_WINDOW (object); + + g_clear_object (&self->workspaces_set); + + G_OBJECT_CLASS (gtd_window_parent_class)->dispose (object); +} + +static void +gtd_window_finalize (GObject *object) +{ + GtdWindow *self = GTD_WINDOW (object); + + g_clear_object (&self->workspaces); + + G_OBJECT_CLASS (gtd_window_parent_class)->finalize (object); +} + +static void +gtd_window_constructed (GObject *object) +{ + GtdWindow *self; + + self = GTD_WINDOW (object); + + G_OBJECT_CLASS (gtd_window_parent_class)->constructed (object); + + /* Load stored size */ + load_geometry (GTD_WINDOW (object)); + + /* Workspaces */ + self->workspaces_set = peas_extension_set_new (peas_engine_get_default (), + GTD_TYPE_WORKSPACE, + NULL); + + peas_extension_set_foreach (self->workspaces_set, + (PeasExtensionSetForeachFunc) on_workspace_added_cb, + self); + + g_object_connect (self->workspaces_set, + "signal::extension-added", G_CALLBACK (on_workspace_added_cb), self, + "signal::extension-removed", G_CALLBACK (on_workspace_removed_cb), self, + NULL); +} + +static void +gtd_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdWindow *self = (GtdWindow *) object; + + switch (prop_id) + { + case PROP_CURRENT_WORKSPACE: + g_value_set_object (value, self->current_workspace); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_window_class_init (GtdWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gtd_window_dispose; + object_class->finalize = gtd_window_finalize; + object_class->constructed = gtd_window_constructed; + object_class->get_property = gtd_window_get_property; + + widget_class->unmap = gtd_window_unmap; + + properties[PROP_CURRENT_WORKSPACE] = g_param_spec_object ("current-workspace", + "Current workspace", + "Current workspace", + GTD_TYPE_WORKSPACE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + g_type_ensure (GTD_TYPE_MENU_BUTTON); + g_type_ensure (GTD_TYPE_OMNI_AREA); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-window.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdWindow, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_stack_visible_child_cb); +} + +static void +gtd_window_init (GtdWindow *self) +{ + static const GActionEntry entries[] = { + { "activate-workspace", on_action_activate_workspace_activated_cb, "(sv)" }, + }; + + g_action_map_add_action_entries (G_ACTION_MAP (self), + entries, + G_N_ELEMENTS (entries), + self); + + self->workspaces = g_list_store_new (GTD_TYPE_WORKSPACE); + + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Development build */ + if (is_development_build ()) + setup_development_build (self); +} + +GtkWidget* +gtd_window_new (GtdApplication *application) +{ + return g_object_new (GTD_TYPE_WINDOW, + "application", application, + NULL); +} + +/** + * gtd_window_get_current_workspace: + * @self: a #GtdWindow + * + * Retrieves the currently active workspace + * + * Returns: (transfer full): a #GtdWorkspace + */ +GtdWorkspace* +gtd_window_get_current_workspace (GtdWindow *self) +{ + g_return_val_if_fail (GTD_IS_WINDOW (self), NULL); + + return self->current_workspace; +} diff --git a/src/gui/gtd-window.h b/src/gui/gtd-window.h new file mode 100644 index 0000000..3cfe0d8 --- /dev/null +++ b/src/gui/gtd-window.h @@ -0,0 +1,43 @@ +/* gtd-window.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#ifndef GTD_WINDOW_H +#define GTD_WINDOW_H + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_WINDOW (gtd_window_get_type()) + +G_DECLARE_FINAL_TYPE (GtdWindow, gtd_window, GTD, WINDOW, AdwApplicationWindow) + +GtkWidget* gtd_window_new (GtdApplication *application); + +void gtd_window_embed_widget_in_header (GtdWindow *self, + GtkWidget *widget, + GtkPositionType position); + +GtdWorkspace* gtd_window_get_current_workspace (GtdWindow *self); + +G_END_DECLS + +#endif /* GTD_WINDOW_H */ diff --git a/src/gui/gtd-window.ui b/src/gui/gtd-window.ui new file mode 100644 index 0000000..147054b --- /dev/null +++ b/src/gui/gtd-window.ui @@ -0,0 +1,33 @@ + + + + + diff --git a/src/gui/gtd-workspace.c b/src/gui/gtd-workspace.c new file mode 100644 index 0000000..ad1b1c0 --- /dev/null +++ b/src/gui/gtd-workspace.c @@ -0,0 +1,158 @@ +/* gtd-workspace.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-workspace.h" + +G_DEFINE_INTERFACE (GtdWorkspace, gtd_workspace, GTK_TYPE_WIDGET) + +static void +gtd_workspace_default_init (GtdWorkspaceInterface *iface) +{ + /** + * GtdWorkspace::icon: + * + * The icon of the panel. + */ + g_object_interface_install_property (iface, + g_param_spec_object ("icon", + "Icon of the workspace", + "The icon of the workspace", + G_TYPE_ICON, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + /** + * GtdWorkspace::title: + * + * The user-visible title of the workspace. + */ + g_object_interface_install_property (iface, + g_param_spec_string ("title", + "The title of the workspace", + "The title of the workspace", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + +/** + * gtd_workspace_get_id: + * @self: a #GtdWorkspace + * + * Retrieves the id of @self. It is mandatory to implement + * this. + * + * Returns: the id of @self + */ +const gchar* +gtd_workspace_get_id (GtdWorkspace *self) +{ + g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL); + g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_id, NULL); + + return GTD_WORKSPACE_GET_IFACE (self)->get_id (self); +} + +/** + * gtd_workspace_get_title: + * @self: a #GtdWorkspace + * + * Retrieves the title of @self. It is mandatory to implement + * this. + * + * Returns: the title of @self + */ +const gchar* +gtd_workspace_get_title (GtdWorkspace *self) +{ + g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL); + g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_title, NULL); + + return GTD_WORKSPACE_GET_IFACE (self)->get_title (self); +} + +/** + * gtd_workspace_get_priority: + * @self: a #GtdWorkspace + * + * Retrieves the priority of @self. It is mandatory to implement + * this. + * + * Returns: the priority of @self + */ +gint +gtd_workspace_get_priority (GtdWorkspace *self) +{ + g_return_val_if_fail (GTD_IS_WORKSPACE (self), 0); + g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_priority, 0); + + return GTD_WORKSPACE_GET_IFACE (self)->get_priority (self); +} + +/** + * gtd_workspace_get_icon: + * @self: a #GtdWorkspace + * + * Retrieves the icon of @self. It is mandatory to implement + * this. + * + * Returns: (transfer full): a #GIcon + */ +GIcon* +gtd_workspace_get_icon (GtdWorkspace *self) +{ + g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL); + g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_icon, NULL); + + return GTD_WORKSPACE_GET_IFACE (self)->get_icon (self); +} + +/** + * gtd_workspace_activate: + * @self: a #GtdWorkspace + * @parameters: (nullable): workspace-specific parameters + * + * Activates @self. This happens when the workspace + * becomes the active workspace in the main window. + */ +void +gtd_workspace_activate (GtdWorkspace *self, + GVariant *parameters) +{ + g_return_if_fail (GTD_IS_WORKSPACE (self)); + + if (GTD_WORKSPACE_GET_IFACE (self)->activate) + GTD_WORKSPACE_GET_IFACE (self)->activate (self, parameters); +} + +/** + * gtd_workspace_deactivate: + * @self: a #GtdWorkspace + * + * Deactivates @self. This happens when the workspace + * is switched away in the main window. + */ +void +gtd_workspace_deactivate (GtdWorkspace *self) +{ + g_return_if_fail (GTD_IS_WORKSPACE (self)); + + if (GTD_WORKSPACE_GET_IFACE (self)->deactivate) + GTD_WORKSPACE_GET_IFACE (self)->deactivate (self); +} + diff --git a/src/gui/gtd-workspace.h b/src/gui/gtd-workspace.h new file mode 100644 index 0000000..d5bbaa5 --- /dev/null +++ b/src/gui/gtd-workspace.h @@ -0,0 +1,61 @@ +/* gtd-workspace.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_WORKSPACE (gtd_workspace_get_type ()) +G_DECLARE_INTERFACE (GtdWorkspace, gtd_workspace, GTD, WORKSPACE, GtkWidget) + +struct _GtdWorkspaceInterface +{ + GTypeInterface parent; + + const gchar* (*get_id) (GtdWorkspace *self); + + const gchar* (*get_title) (GtdWorkspace *self); + + gint (*get_priority) (GtdWorkspace *self); + + GIcon* (*get_icon) (GtdWorkspace *self); + + void (*activate) (GtdWorkspace *self, + GVariant *parameters); + + void (*deactivate) (GtdWorkspace *self); +}; + +const gchar* gtd_workspace_get_id (GtdWorkspace *self); + +const gchar* gtd_workspace_get_title (GtdWorkspace *self); + +gint gtd_workspace_get_priority (GtdWorkspace *self); + +GIcon* gtd_workspace_get_icon (GtdWorkspace *self); + +void gtd_workspace_activate (GtdWorkspace *self, + GVariant *parameters); + +void gtd_workspace_deactivate (GtdWorkspace *self); + +G_END_DECLS diff --git a/src/gui/gui.gresource.xml b/src/gui/gui.gresource.xml new file mode 100644 index 0000000..746902c --- /dev/null +++ b/src/gui/gui.gresource.xml @@ -0,0 +1,25 @@ + + + + gtd-edit-pane.ui + gtd-initial-setup-window.ui + gtd-new-task-row.ui + gtd-omni-area.ui + gtd-provider-popover.ui + gtd-provider-row.ui + gtd-provider-selector.ui + gtd-task-list-popover.ui + gtd-task-list-view.ui + gtd-task-row.ui + gtd-window.ui + + + assets/all-done.svg + + + + + shortcuts-dialog.ui + menus.ui + + diff --git a/src/gui/menus.ui b/src/gui/menus.ui new file mode 100644 index 0000000..cfd12e8 --- /dev/null +++ b/src/gui/menus.ui @@ -0,0 +1,21 @@ + + + + +
+ + _Help + app.help + + + _Keyboard Shortcuts + win.show-help-overlay + + + _About Endeavour + app.about + +
+ +
+
diff --git a/src/gui/meson.build b/src/gui/meson.build new file mode 100644 index 0000000..5e67748 --- /dev/null +++ b/src/gui/meson.build @@ -0,0 +1,6 @@ +sources += gnome.compile_resources( + 'gtd-gui-resources', + 'gui.gresource.xml', + c_name: 'gtd_gui', + export: true, +) diff --git a/src/gui/shortcuts-dialog.ui b/src/gui/shortcuts-dialog.ui new file mode 100644 index 0000000..38a1a85 --- /dev/null +++ b/src/gui/shortcuts-dialog.ui @@ -0,0 +1,72 @@ + + + + True + + + + True + shortcuts + 10 + + + + + True + General + + + + True + Quit + <Control>Q + + + + + True + Help + F1 + + + + + + + True + Move to the panel/view up + <Control>Page_Up + + + + + + True + Move to the panel/view up + <Alt>Up + + + + + + True + Move to the panel/view below + <Control>Page_Down + + + + + + True + Move to the panel/view below + <Alt>Down + + + + + + + + + + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..c25123e --- /dev/null +++ b/src/main.c @@ -0,0 +1,44 @@ +/* + * main.c + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * endeavour is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * endeavour is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "gtd-application.h" +#include "gtd-utils-private.h" + +#include +#include + +gint +main (gint argc, + gchar **argv) +{ + g_autoptr (GtdApplication) app = NULL; + + gtd_ensure_types (); + + bindtextdomain (GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + g_set_application_name (_("Endeavour")); + + app = gtd_application_new (); + g_application_set_default (G_APPLICATION (app)); + + return g_application_run (G_APPLICATION (app), argc, argv); +} + diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..57119c6 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,339 @@ +sources = [] + +####### +# VCS # +####### + +configure_file(output: 'config.h', configuration: config_h) + +vcs_tag = vcs_tag( + input: 'gtd-vcs.h.in', + output: 'gtd-vcs.h' +) + +vcs_identifier_h = declare_dependency(sources: vcs_tag) + +endeavour_deps += vcs_identifier_h + +######### +# Enums # +######### + +enum_headers = files( + join_paths('animation', 'gtd-animation-enums.h'), +) + +enum_types = 'gtd-enum-types' + +gtd_enum_types = gnome.mkenums( + enum_types, + sources: enum_headers, + c_template: enum_types + '.c.template', + h_template: enum_types + '.h.template' +) + +sources += gtd_enum_types + +########### +# Plugins # +########### + +incs = include_directories( + 'animation', + 'core', + 'gui', + 'models', +) + +########### +# Subdirs # +########### + +subdir('gui') +subdir('plugins') + +################ +# Dependencies # +################ + +gtd_deps = endeavour_deps + [ + plugins_dep, +] + +################ +# Header files # +################ + +headers = files( + 'animation/gtd-animatable.h', + 'animation/gtd-animation-utils.h', + 'animation/gtd-easing.h', + 'animation/gtd-interval.h', + 'animation/gtd-keyframe-transition.h', + 'animation/gtd-property-transition.h', + 'animation/gtd-timeline.h', + 'animation/gtd-transition.h', + 'core/gtd-activatable.h', + 'core/gtd-clock.h', + 'core/gtd-manager.h', + 'core/gtd-notification.h', + 'core/gtd-object.h', + 'core/gtd-provider.h', + 'core/gtd-task.h', + 'core/gtd-task-list.h', + 'gui/gtd-bin-layout.h', + 'gui/gtd-max-size-layout.h', + 'gui/gtd-menu-button.h', + 'gui/gtd-omni-area.h', + 'gui/gtd-omni-area-addin.h', + 'gui/gtd-panel.h', + 'gui/gtd-provider-popover.h', + 'gui/gtd-star-widget.h', + 'gui/gtd-task-list-view.h', + 'gui/gtd-widget.h', + 'gui/gtd-window.h', + 'gui/gtd-workspace.h', + 'models/gtd-list-model-filter.h', + 'models/gtd-list-store.h', + 'gtd-types.h', + 'gtd-utils.h', + 'endeavour.h' +) + +install_headers(headers, subdir: meson.project_name()) + + +################ +# Source files # +################ + +sources += files( + 'animation/gtd-animatable.c', + 'animation/gtd-animation-utils.c', + 'animation/gtd-easing.c', + 'animation/gtd-interval.c', + 'animation/gtd-keyframe-transition.c', + 'animation/gtd-property-transition.c', + 'animation/gtd-timeline.c', + 'animation/gtd-transition.c', + 'core/gtd-activatable.c', + 'core/gtd-clock.c', + 'core/gtd-log.c', + 'core/gtd-manager.c', + 'core/gtd-notification.c', + 'core/gtd-object.c', + 'core/gtd-plugin-manager.c', + 'core/gtd-provider.c', + 'core/gtd-task.c', + 'core/gtd-task-list.c', + 'gui/gtd-bin-layout.c', + 'gui/gtd-panel.c', + 'gui/gtd-workspace.c', + 'gui/gtd-provider-popover.c', + 'gui/gtd-provider-row.c', + 'gui/gtd-provider-selector.c', + 'gui/gtd-edit-pane.c', + 'gui/gtd-markdown-renderer.c', + 'gui/gtd-new-task-row.c', + 'gui/gtd-task-list-popover.c', + 'gui/gtd-task-list-view.c', + 'gui/gtd-task-row.c', + 'gui/gtd-color-button.c', + 'gui/gtd-menu-button.c', + 'gui/gtd-star-widget.c', + 'gui/gtd-application.c', + 'gui/gtd-initial-setup-window.c', + 'gui/gtd-max-size-layout.c', + 'gui/gtd-omni-area.c', + 'gui/gtd-omni-area-addin.c', + 'gui/gtd-widget.c', + 'gui/gtd-window.c', + 'models/gtd-list-model-filter.c', + 'models/gtd-list-model-sort.c', + 'models/gtd-list-store.c', + 'models/gtd-task-list-view-model.c', + 'models/gtd-task-model.c', + 'gtd-utils.c', + 'main.c' +) + + +############# +# Resources # +############# + +sources += compile_schemas + +sources += gnome.compile_resources( + 'gtd-resources', + 'todo.gresource.xml', + c_name: 'todo', + export: true, +) + +sources += gnome.compile_resources( + 'gtd-icon-resources', + join_paths(icons_dir, 'icons.gresource.xml'), + source_dir: [ icons_dir ], + c_name: 'todo_icons', + export: true, +) + +cflags = [ + '-DPACKAGE_DATA_DIR="@0@"'.format(endeavour_pkgdatadir), + '-DPACKAGE_LIB_DIR="@0@"'.format(endeavour_pkglibdir), + '-DPACKAGE_LOCALE_DIR="@0@"'.format(endeavour_localedir), + '-DPACKAGE_SRC_DIR="@0@"'.format(meson.current_source_dir()), + '-DUI_DATA_DIR="@0@"'.format(join_paths(endeavour_pkgdatadir, 'style')) +] + +ldflags = [ '-Wl,--export-dynamic' ] + + +######### +# Debug # +######### + +debug_conf = configuration_data() +debug_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/World/Endeavour/issues/new') +debug_conf.set10('ENABLE_TRACING', endeavour_tracing) + + +sources += configure_file( + input: 'gtd-debug.h.in', + output: 'gtd-debug.h', + configuration: debug_conf, +) + + +############## +# endeavour # +############## + +endeavour = executable( + meson.project_name(), + sources, + include_directories: incs, + dependencies: [ vcs_identifier_h, gtd_deps ], + c_args: cflags, + link_args: ldflags, + install: true, + install_dir: endeavour_bindir +) + + +################### +# Private library # +################### + +libgtd = shared_library( + 'gtd', + sources: sources, + version: libversion, + soversion: soversion, + include_directories: incs, + dependencies: gtd_deps, + c_args: cflags +) + +libgtd_dep = declare_dependency( + link_with: libgtd, + dependencies: gtd_deps +) + + +############## +# pkg-config # +############## + +pkg.generate( + libraries: libgtd, + version: endeavour_version, + name: 'Endeavour', + description: 'Header and path for Endeavour Plugins', + filebase: meson.project_name(), + subdirs: meson.project_name(), + variables: 'exec_prefix=' + endeavour_libexecdir, + install_dir: join_paths(endeavour_libdir, 'pkgconfig') +) + + +######################### +# GObject-Introspection # +######################### + +if get_option('introspection') + gir_sources = files( + 'core/gtd-activatable.c', + 'core/gtd-activatable.h', + 'core/gtd-clock.c', + 'core/gtd-clock.h', + 'core/gtd-manager.c', + 'core/gtd-manager.h', + 'core/gtd-notification.c', + 'core/gtd-notification.h', + 'core/gtd-object.c', + 'core/gtd-object.h', + 'core/gtd-provider.c', + 'core/gtd-provider.h', + 'core/gtd-task.c', + 'core/gtd-task.h', + 'core/gtd-task-list.c', + 'core/gtd-task-list.h', + 'gui/gtd-bin-layout.c', + 'gui/gtd-bin-layout.h', + 'gui/gtd-max-size-layout.c', + 'gui/gtd-max-size-layout.h', + 'gui/gtd-menu-button.c', + 'gui/gtd-menu-button.h', + 'gui/gtd-omni-area.c', + 'gui/gtd-omni-area.h', + 'gui/gtd-omni-area-addin.c', + 'gui/gtd-omni-area-addin.h', + 'gui/gtd-panel.c', + 'gui/gtd-panel.h', + 'gui/gtd-provider-popover.c', + 'gui/gtd-provider-popover.h', + 'gui/gtd-star-widget.c', + 'gui/gtd-star-widget.h', + 'gui/gtd-task-list-view.c', + 'gui/gtd-task-list-view.h', + 'gui/gtd-widget.c', + 'gui/gtd-widget.h', + 'gui/gtd-window.c', + 'gui/gtd-window.h', + 'gui/gtd-workspace.c', + 'gui/gtd-workspace.h', + 'models/gtd-list-model-filter.c', + 'models/gtd-list-model-filter.h', + 'models/gtd-list-store.c', + 'models/gtd-list-store.h', + 'gtd-types.h', + ) + + gir_incs = [ + 'Gio-2.0', + 'GObject-2.0', + 'Gtk-4.0' + ] + + gir_extra_args = '--warn-all' + + gir_dir = join_paths(endeavour_datadir, '@0@-@1@'.format('gir', endeavour_gir_version)) + typelib_dir = join_paths(endeavour_libdir, '@0@-@1@'.format('girepository', endeavour_gir_version)) + + gnome.generate_gir( + libgtd, + sources: gir_sources, + namespace: endeavour_gir_namespace, + nsversion: endeavour_gir_version, + identifier_prefix: endeavour_gir_namespace, + symbol_prefix: endeavour_gir_namespace.to_lower(), + includes: gir_incs, + include_directories: incs, + extra_args: gir_extra_args, + install: true, + install_dir_gir: gir_dir, + install_dir_typelib: typelib_dir, + ) +endif diff --git a/src/models/gtd-list-model-filter.c b/src/models/gtd-list-model-filter.c new file mode 100644 index 0000000..632c227 --- /dev/null +++ b/src/models/gtd-list-model-filter.c @@ -0,0 +1,569 @@ +/* gtd-list-model-filter.c + * + * Copyright (C) 2016 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdListModelFilter" + +#include "gtd-debug.h" +#include "gtd-list-model-filter.h" +#include "gtd-task-list.h" + +typedef struct +{ + GSequenceIter *child_iter; + GSequenceIter *filter_iter; +} GtdListModelFilterItem; + +typedef struct +{ + /* The list we are filtering */ + GListModel *child_model; + + /* + * Both sequences point to the same GtdListModelFilterItem which + * contains cross-referencing stable GSequenceIter pointers. + * The child_seq is considered the "owner" and used to release + * allocated resources. + */ + GSequence *child_seq; + GSequence *filter_seq; + + /* + * Typical set of callback/closure/free function pointers and data. + * Called for child items to determine visibility state. + */ + GtdListModelFilterFunc filter_func; + gpointer filter_func_data; + GDestroyNotify filter_func_data_destroy; + + /* cache */ + gint64 length; + gint64 last_position; + GSequenceIter *last_iter; + + /* + * If set, we will not emit items-changed. This is useful during + * invalidation so that we can do a single emission for all items + * that have changed. + */ + gboolean supress_items_changed : 1; +} GtdListModelFilterPrivate; + +struct _GtdListModelFilter +{ + GObject parent_instance; +}; + +static void list_model_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_EXTENDED (GtdListModelFilter, gtd_list_model_filter, G_TYPE_OBJECT, 0, + G_ADD_PRIVATE (GtdListModelFilter) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + list_model_iface_init)) + +enum +{ + PROP_0, + PROP_CHILD_MODEL, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; +static guint signal_id; + +static void +gtd_list_model_filter_item_free (gpointer data) +{ + GtdListModelFilterItem *item = data; + + g_clear_pointer (&item->filter_iter, g_sequence_remove); + item->child_iter = NULL; + g_slice_free (GtdListModelFilterItem, item); +} + +static gboolean +gtd_list_model_filter_default_filter_func (GObject *item, + gpointer user_data) +{ + return TRUE; +} + +/* + * Locates the next item in the filter sequence starting from + * the cross-reference found at @iter. If none are found, the + * end_iter for the filter sequence is returned. + * + * This returns an iter in the filter_sequence, not the child_seq. + * + * Returns: a #GSequenceIter from the filter sequence. + */ +static GSequenceIter * +find_next_visible_filter_iter (GtdListModelFilter *self, + GSequenceIter *iter) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_assert (GTD_IS_LIST_MODEL_FILTER (self)); + g_assert (iter != NULL); + + for (; !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter)) + { + GtdListModelFilterItem *item = g_sequence_get (iter); + + g_assert (item->child_iter == iter); + g_assert (item->filter_iter == NULL || + g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq); + + if (item->filter_iter != NULL) + return item->filter_iter; + } + + return g_sequence_get_end_iter (priv->filter_seq); +} + +static void +invalidate_cache (GtdListModelFilter *self) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + GTD_TRACE_MSG ("Invalidating cache"); + + priv->last_iter = NULL; + priv->last_position = -1u; +} + +static void +emit_items_changed (GtdListModelFilter *self, + guint position, + guint n_removed, + guint n_added) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + if (position <= priv->last_position) + invalidate_cache (self); + + priv->length -= n_removed; + priv->length += n_added; + + GTD_TRACE_MSG ("Emitting items-changed(%u, %u, %u)", position, n_removed, n_added); + + g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added); +} + +static void +child_model_items_changed (GtdListModelFilter *self, + guint position, + guint n_removed, + guint n_added, + GListModel *child_model) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + gboolean unblocked; + guint i; + + GTD_ENTRY; + + g_assert (GTD_IS_LIST_MODEL_FILTER (self)); + g_assert (G_IS_LIST_MODEL (child_model)); + g_assert (priv->child_model == child_model); + g_assert (position <= (guint)g_sequence_get_length (priv->child_seq)); + g_assert ((g_sequence_get_length (priv->child_seq) - n_removed + n_added) == + g_list_model_get_n_items (child_model)); + + GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added); + + unblocked = !priv->supress_items_changed; + + if (n_removed > 0) + { + GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position); + gint64 first_position = -1; + guint count = 0; + + g_assert (!g_sequence_iter_is_end (iter)); + + /* Small shortcut when all items are removed */ + if (n_removed == (guint)g_sequence_get_length (priv->child_seq)) + { + g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq), + g_sequence_get_end_iter (priv->child_seq)); + g_assert (g_sequence_is_empty (priv->child_seq)); + g_assert (g_sequence_is_empty (priv->filter_seq)); + + if (unblocked) + emit_items_changed (self, 0, priv->length, 0); + + GTD_TRACE_MSG ("Removed all items"); + + GTD_GOTO (add_new_items); + } + + for (i = 0; i < n_removed; i++) + { + GSequenceIter *to_remove = iter; + GtdListModelFilterItem *item = g_sequence_get (iter); + + g_assert (item != NULL); + g_assert (item->child_iter == iter); + g_assert (item->filter_iter == NULL || + g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq); + + /* If this is visible, we need to notify about removal */ + if (unblocked && item->filter_iter != NULL) + { + if (first_position < 0) + first_position = g_sequence_iter_get_position (item->filter_iter); + + count++; + } + + /* Fetch the next while the iter is still valid */ + iter = g_sequence_iter_next (iter); + + /* Cascades into also removing from filter_seq. */ + g_sequence_remove (to_remove); + } + + GTD_TRACE_MSG ("Removed %u items", count); + + if (unblocked && first_position >= 0) + emit_items_changed (self, first_position, count, 0); + } + +add_new_items: + + if (n_added > 0) + { + GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position); + GSequenceIter *filter_iter = find_next_visible_filter_iter (self, iter); + guint filter_position = g_sequence_iter_get_position (filter_iter); + guint count = 0; + + /* Walk backwards to insert items into the filter list so that + * we can use the same filter_position for each items-changed + * signal emission. + */ + for (i = position + n_added; i > position; i--) + { + GtdListModelFilterItem *item; + g_autoptr (GObject) instance = NULL; + + item = g_slice_new0 (GtdListModelFilterItem); + item->filter_iter = NULL; + item->child_iter = g_sequence_insert_before (iter, item); + + instance = g_list_model_get_item (child_model, i - 1); + g_assert (G_IS_OBJECT (instance)); + + /* Check if this item is visible */ + if (priv->filter_func (instance, priv->filter_func_data)) + { + item->filter_iter = g_sequence_insert_before (filter_iter, item); + count++; + + /* Use this in the future for relative positioning */ + filter_iter = item->filter_iter; + } + + /* Insert next item before this */ + iter = item->child_iter; + } + + GTD_TRACE_MSG ("Added %u items (%u were filtered)", count, n_added - count); + + if (unblocked && count) + emit_items_changed (self, filter_position, 0, count); + } + + g_assert ((guint)g_sequence_get_length (priv->child_seq) == g_list_model_get_n_items (child_model)); + + GTD_EXIT; +} + +static void +gtd_list_model_filter_finalize (GObject *object) +{ + GtdListModelFilter *self = (GtdListModelFilter *)object; + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_clear_pointer (&priv->child_seq, g_sequence_free); + g_clear_pointer (&priv->filter_seq, g_sequence_free); + + if (priv->filter_func_data_destroy) + { + g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy); + priv->filter_func_data_destroy = NULL; + } + + g_clear_object (&priv->child_model); + + G_OBJECT_CLASS (gtd_list_model_filter_parent_class)->finalize (object); +} + +static void +gtd_list_model_filter_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdListModelFilter *self = GTD_LIST_MODEL_FILTER (object); + + switch (prop_id) + { + case PROP_CHILD_MODEL: + g_value_set_object (value, gtd_list_model_filter_get_child_model (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_list_model_filter_class_init (GtdListModelFilterClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_list_model_filter_finalize; + object_class->get_property = gtd_list_model_filter_get_property; + + properties [PROP_CHILD_MODEL] = + g_param_spec_object ("child-model", + "Child Model", + "The child model being filtered.", + G_TYPE_LIST_MODEL, + (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signal_id = g_signal_lookup ("items-changed", GTD_TYPE_LIST_MODEL_FILTER); +} + +static void +gtd_list_model_filter_init (GtdListModelFilter *self) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + priv->filter_func = gtd_list_model_filter_default_filter_func; + priv->child_seq = g_sequence_new (gtd_list_model_filter_item_free); + priv->filter_seq = g_sequence_new (NULL); + priv->last_position = -1; +} + +static GType +gtd_list_model_filter_get_item_type (GListModel *model) +{ + GtdListModelFilter *self = (GtdListModelFilter *)model; + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_assert (GTD_IS_LIST_MODEL_FILTER (self)); + + return g_list_model_get_item_type (priv->child_model); +} + +static guint +gtd_list_model_filter_get_n_items (GListModel *model) +{ + GtdListModelFilter *self = (GtdListModelFilter *)model; + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_assert (GTD_IS_LIST_MODEL_FILTER (self)); + g_assert (priv->filter_seq != NULL); + + return priv->length; +} + +static gpointer +gtd_list_model_filter_get_item (GListModel *model, + guint position) +{ + GtdListModelFilter *self = (GtdListModelFilter *)model; + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + GtdListModelFilterItem *item; + GSequenceIter *iter; + guint child_position; + + g_assert (GTD_IS_LIST_MODEL_FILTER (self)); + + iter = NULL; + + if (priv->last_position != -1) + { + if (priv->last_position == position + 1) + iter = g_sequence_iter_prev (priv->last_iter); + else if (priv->last_position == position - 1) + iter = g_sequence_iter_next (priv->last_iter); + else if (priv->last_position == position) + iter = priv->last_iter; + } + + if (!iter) + iter = g_sequence_get_iter_at_pos (priv->filter_seq, position); + + if (g_sequence_iter_is_end (iter)) + return NULL; + + item = g_sequence_get (iter); + g_assert (item != NULL); + g_assert (item->filter_iter == iter); + g_assert (item->child_iter != NULL); + g_assert (g_sequence_iter_get_sequence (item->child_iter) == priv->child_seq); + + child_position = g_sequence_iter_get_position (item->child_iter); + + return g_list_model_get_item (priv->child_model, child_position); +} + +static void +list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = gtd_list_model_filter_get_item_type; + iface->get_n_items = gtd_list_model_filter_get_n_items; + iface->get_item = gtd_list_model_filter_get_item; +} + +GtdListModelFilter * +gtd_list_model_filter_new (GListModel *child_model) +{ + GtdListModelFilter *ret; + GtdListModelFilterPrivate *priv; + + g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL); + + ret = g_object_new (GTD_TYPE_LIST_MODEL_FILTER, NULL); + priv = gtd_list_model_filter_get_instance_private (ret); + priv->child_model = g_object_ref (child_model); + + g_signal_connect_object (child_model, + "items-changed", + G_CALLBACK (child_model_items_changed), + ret, + G_CONNECT_SWAPPED); + + gtd_list_model_filter_invalidate (ret); + + return ret; +} + +/** + * gtd_list_model_filter_get_child_model: + * @self: A #GtdListModelFilter + * + * Gets the child model that is being filtered. + * + * Returns: (transfer none): A #GListModel. + */ +GListModel * +gtd_list_model_filter_get_child_model (GtdListModelFilter *self) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_return_val_if_fail (GTD_IS_LIST_MODEL_FILTER (self), NULL); + + return priv->child_model; +} + +void +gtd_list_model_filter_invalidate (GtdListModelFilter *self) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + guint n_items; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self)); + + /* We block emission while in invalidate so that we can use + * a single larger items-changed rather lots of small emissions. + */ + priv->supress_items_changed = TRUE; + + /* First determine how many items we need to synthesize as a removal */ + n_items = g_sequence_get_length (priv->filter_seq); + + /* + * If we have a child store, we want to rebuild our list of items + * from scratch, so just remove everything. + */ + if (!g_sequence_is_empty (priv->child_seq)) + g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq), + g_sequence_get_end_iter (priv->child_seq)); + + g_assert (g_sequence_is_empty (priv->child_seq)); + g_assert (g_sequence_is_empty (priv->filter_seq)); + g_assert (!priv->child_model || G_IS_LIST_MODEL (priv->child_model)); + + /* + * Now add the new items by synthesizing the addition of all the + * items in the list. + */ + if (priv->child_model != NULL) + { + guint child_n_items; + + /* + * Now add all the items as one shot to our list so that + * we get populate our sequence and filter sequence. + */ + child_n_items = g_list_model_get_n_items (priv->child_model); + child_model_items_changed (self, 0, 0, child_n_items, priv->child_model); + + g_assert ((guint)g_sequence_get_length (priv->child_seq) == child_n_items); + g_assert ((guint)g_sequence_get_length (priv->filter_seq) <= child_n_items); + } + + priv->supress_items_changed = FALSE; + + /* Now that we've updated our sequences, notify of all the changes + * as a single series of updates to the consumers. + */ + if (n_items > 0 || !g_sequence_is_empty (priv->filter_seq)) + emit_items_changed (self, 0, n_items, g_sequence_get_length (priv->filter_seq)); + + GTD_EXIT; +} + +void +gtd_list_model_filter_set_filter_func (GtdListModelFilter *self, + GtdListModelFilterFunc filter_func, + gpointer filter_func_data, + GDestroyNotify filter_func_data_destroy) +{ + GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self); + + g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self)); + g_return_if_fail (filter_func || (!filter_func_data && !filter_func_data_destroy)); + + if (priv->filter_func_data_destroy != NULL) + g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy); + + if (filter_func != NULL) + { + priv->filter_func = filter_func; + priv->filter_func_data = filter_func_data; + priv->filter_func_data_destroy = filter_func_data_destroy; + } + else + { + priv->filter_func = gtd_list_model_filter_default_filter_func; + priv->filter_func_data = NULL; + priv->filter_func_data_destroy = NULL; + } + + gtd_list_model_filter_invalidate (self); +} diff --git a/src/models/gtd-list-model-filter.h b/src/models/gtd-list-model-filter.h new file mode 100644 index 0000000..f5c2a42 --- /dev/null +++ b/src/models/gtd-list-model-filter.h @@ -0,0 +1,44 @@ +/* gtd-list-model-filter.h + * + * Copyright © 2016 Christian Hergert + * 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_LIST_MODEL_FILTER (gtd_list_model_filter_get_type()) + +typedef gboolean (*GtdListModelFilterFunc) (GObject *object, + gpointer user_data); + +G_DECLARE_FINAL_TYPE (GtdListModelFilter, gtd_list_model_filter, GTD, LIST_MODEL_FILTER, GObject) + +GtdListModelFilter* gtd_list_model_filter_new (GListModel *child_model); + +GListModel* gtd_list_model_filter_get_child_model (GtdListModelFilter *self); + +void gtd_list_model_filter_invalidate (GtdListModelFilter *self); + +void gtd_list_model_filter_set_filter_func (GtdListModelFilter *self, + GtdListModelFilterFunc filter_func, + gpointer filter_func_data, + GDestroyNotify filter_func_data_destroy); + +G_END_DECLS diff --git a/src/models/gtd-list-model-sort.c b/src/models/gtd-list-model-sort.c new file mode 100644 index 0000000..dedbb6d --- /dev/null +++ b/src/models/gtd-list-model-sort.c @@ -0,0 +1,500 @@ +/* gtd-list-model-sort.c + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdListModelSort" + +#include "gtd-debug.h" +#include "gtd-list-model-sort.h" +#include "gtd-task.h" +#include "gtd-task-list.h" + +struct _GtdListModelSort +{ + GObject parent; + + GListModel *child_model; + + GSequence *child_seq; + GSequence *sort_seq; + + GtdListModelCompareFunc compare_func; + gpointer compare_func_data; + GDestroyNotify compare_func_data_destroy; + + gboolean supress_items_changed : 1; + + /* cache */ + gint64 length; + gint64 last_position; + GSequenceIter *last_iter; +}; + +static void list_model_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdListModelSort, gtd_list_model_sort, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) + +enum +{ + PROP_0, + PROP_CHILD_MODEL, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +gtd_list_model_sort_item_free (gpointer data) +{ + GSequenceIter *iter = data; + + g_clear_pointer (&iter, g_sequence_remove); +} + +static gint +seq_compare_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GtdListModelSort *self = (GtdListModelSort*) user_data; + + return self->compare_func (G_OBJECT (a), G_OBJECT (b), self->compare_func_data); +} + +static gint +default_compare_func (GObject *a, + GObject *b, + gpointer user_data) +{ + return 0; +} + + +static void +invalidate_cache (GtdListModelSort *self) +{ + GTD_TRACE_MSG ("Invalidating cache"); + + self->last_iter = NULL; + self->last_position = -1; +} + +static void +emit_items_changed (GtdListModelSort *self, + guint position, + guint n_removed, + guint n_added) +{ + if (position <= self->last_position) + invalidate_cache (self); + + self->length -= n_removed; + self->length += n_added; + + GTD_TRACE_MSG ("Emitting items-changed(%u, %u, %u)", position, n_removed, n_added); + + g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added); +} + + +static void +child_model_items_changed (GtdListModelSort *self, + guint position, + guint n_removed, + guint n_added, + GListModel *child_model) +{ + guint i; + + GTD_ENTRY; + + g_assert (GTD_IS_LIST_MODEL_SORT (self)); + g_assert (G_IS_LIST_MODEL (child_model)); + g_assert (self->child_model == child_model); + g_assert (position <= (guint)g_sequence_get_length (self->child_seq)); + g_assert (g_sequence_get_length (self->child_seq) - n_removed + n_added == g_list_model_get_n_items (child_model)); + + GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added); + + if (n_removed > 0) + { + GSequenceIter *iter; + gint64 previous_sort_position = -1; + gint64 first_position = -1; + gint64 counter = 0; + + /* Small shortcut when all items are removed */ + if (n_removed == (guint)g_sequence_get_length (self->child_seq)) + { + g_sequence_remove_range (g_sequence_get_begin_iter (self->child_seq), + g_sequence_get_end_iter (self->child_seq)); + + g_assert (g_sequence_is_empty (self->child_seq)); + g_assert (g_sequence_is_empty (self->sort_seq)); + + emit_items_changed (self, 0, n_removed, 0); + + goto add_new_items; + } + + iter = g_sequence_get_iter_at_pos (self->child_seq, position); + g_assert (!g_sequence_iter_is_end (iter)); + + for (i = 0; i < n_removed; i++) + { + GSequenceIter *sort_iter = g_sequence_get (iter); + GSequenceIter *to_remove = iter; + guint sort_position; + + g_assert (g_sequence_iter_get_sequence (sort_iter) == self->sort_seq); + + sort_position = g_sequence_iter_get_position (sort_iter); + + /* Fetch the next while the iter is still valid */ + iter = g_sequence_iter_next (iter); + + /* Cascades into also removing from sort_seq. */ + g_sequence_remove (to_remove); + + if (first_position == -1) + first_position = sort_position; + + /* + * This happens when the position in the sorted sequence is different + * from the position in the child sequence. We try to accumulate as many + * items-changed() signals as possible, but we can't do that when the + * order doesn't match. + */ + if (previous_sort_position != -1 && sort_position != previous_sort_position + 1) + { + emit_items_changed (self, first_position, counter, 0); + + first_position = sort_position; + counter = 0; + } + + previous_sort_position = sort_position; + counter++; + } + + /* + * Last items-changed() - if the child model is already sorted, + * only this one will be fired. + */ + if (counter > 0) + emit_items_changed (self, first_position, counter, 0); + } + +add_new_items: + + if (n_added > 0) + { + GSequenceIter *iter = g_sequence_get_iter_at_pos (self->child_seq, position); + gint64 previous_sort_position = -1; + gint64 first_position = -1; + gint64 counter = 0; + + for (i = 0; i < n_added; i++) + { + g_autoptr (GObject) instance = NULL; + GSequenceIter *sort_iter; + guint new_sort_position; + + instance = g_list_model_get_item (child_model, position + i); + g_assert (G_IS_OBJECT (instance)); + + sort_iter = g_sequence_insert_sorted (self->sort_seq, + g_steal_pointer (&instance), + seq_compare_func, + self); + + new_sort_position = g_sequence_iter_get_position (sort_iter); + + /* Insert next item before this */ + iter = g_sequence_insert_before (iter, sort_iter); + iter = g_sequence_iter_next (iter); + + if (first_position == -1) + first_position = new_sort_position; + + /* + * This happens when the position in the sorted sequence is different + * from the position in the child sequence. We try to accumulate as many + * items-changed() signals as possible, but we can't do that when the + * order doesn't match. + */ + if (previous_sort_position != -1 && new_sort_position != previous_sort_position + 1) + { + emit_items_changed (self, first_position, 0, counter); + + first_position = new_sort_position; + counter = 0; + } + + previous_sort_position = new_sort_position; + counter++; + } + + /* + * Last items-changed() - if the child model is already sorted, + * only this one will be fired. + */ + if (counter > 0) + emit_items_changed (self, first_position, 0, counter); + } + + g_assert ((guint)g_sequence_get_length (self->child_seq) == g_list_model_get_n_items (child_model)); + g_assert ((guint)g_sequence_get_length (self->sort_seq) == g_list_model_get_n_items (child_model)); + + GTD_EXIT; +} + + +/* + * GListModel iface + */ + +static GType +gtd_list_model_sort_get_item_type (GListModel *model) +{ + GtdListModelSort *self = (GtdListModelSort*) model; + + g_assert (GTD_IS_LIST_MODEL_SORT (self)); + + return g_list_model_get_item_type (self->child_model); +} + +static guint +gtd_list_model_sort_get_n_items (GListModel *model) +{ + GtdListModelSort *self = (GtdListModelSort*) model; + + g_assert (GTD_IS_LIST_MODEL_SORT (self)); + g_assert (self->sort_seq != NULL); + + return self->length; +} + +static gpointer +gtd_list_model_sort_get_item (GListModel *model, + guint position) +{ + GtdListModelSort *self; + GSequenceIter *iter; + gpointer item; + + g_assert (GTD_IS_LIST_MODEL_SORT (model)); + + self = (GtdListModelSort*) model; + iter = NULL; + + if (self->last_position != -1) + { + if (self->last_position == position + 1) + iter = g_sequence_iter_prev (self->last_iter); + else if (self->last_position == position - 1) + iter = g_sequence_iter_next (self->last_iter); + else if (self->last_position == position) + iter = self->last_iter; + } + + if (!iter) + iter = g_sequence_get_iter_at_pos (self->sort_seq, position); + + if (g_sequence_iter_is_end (iter)) + return NULL; + + self->last_iter = iter; + self->last_position = position; + + item = g_sequence_get (iter); + g_assert (item != NULL); + + return g_object_ref (G_OBJECT (item)); +} + +static void +list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = gtd_list_model_sort_get_item_type; + iface->get_n_items = gtd_list_model_sort_get_n_items; + iface->get_item = gtd_list_model_sort_get_item; +} + + +/* + * GObject overrides + */ + +static void +gtd_list_model_sort_finalize (GObject *object) +{ + GtdListModelSort *self = (GtdListModelSort*) object; + + g_clear_pointer (&self->child_seq, g_sequence_free); + g_clear_pointer (&self->sort_seq, g_sequence_free); + + if (self->compare_func_data_destroy) + { + g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy); + self->compare_func_data_destroy = NULL; + } + + g_clear_object (&self->child_model); + + G_OBJECT_CLASS (gtd_list_model_sort_parent_class)->finalize (object); +} + +static void +gtd_list_model_sort_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdListModelSort *self = GTD_LIST_MODEL_SORT (object); + + switch (prop_id) + { + case PROP_CHILD_MODEL: + g_value_set_object (value, gtd_list_model_sort_get_child_model (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_list_model_sort_class_init (GtdListModelSortClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_list_model_sort_finalize; + object_class->get_property = gtd_list_model_sort_get_property; + + properties [PROP_CHILD_MODEL] = g_param_spec_object ("child-model", + "Child Model", + "The child model being sorted.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_list_model_sort_init (GtdListModelSort *self) +{ + self->compare_func = default_compare_func; + self->child_seq = g_sequence_new (gtd_list_model_sort_item_free); + self->sort_seq = g_sequence_new (g_object_unref); + self->last_position = -1; +} + +GtdListModelSort * +gtd_list_model_sort_new (GListModel *child_model) +{ + GtdListModelSort *self; + + g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL); + + self = g_object_new (GTD_TYPE_LIST_MODEL_SORT, NULL); + self->child_model = g_object_ref (child_model); + + g_signal_connect_object (child_model, + "items-changed", + G_CALLBACK (child_model_items_changed), + self, + G_CONNECT_SWAPPED); + + child_model_items_changed (self, 0, 0, g_list_model_get_n_items (child_model), child_model); + + return self; +} + +/** + * gtd_list_model_sort_get_child_model: + * @self: A #GtdListModelSort + * + * Gets the child model that is being sorted. + * + * Returns: (transfer none): A #GListModel. + */ +GListModel * +gtd_list_model_sort_get_child_model (GtdListModelSort *self) +{ + g_return_val_if_fail (GTD_IS_LIST_MODEL_SORT (self), NULL); + + return self->child_model; +} + +void +gtd_list_model_sort_invalidate (GtdListModelSort *self) +{ + guint n_items; + + g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self)); + + /* First determine how many items we need to synthesize as a removal */ + n_items = g_sequence_get_length (self->child_seq); + + g_assert (G_IS_LIST_MODEL (self->child_model)); + + invalidate_cache (self); + + g_sequence_sort (self->sort_seq, seq_compare_func, self); + + g_assert ((guint)g_sequence_get_length (self->child_seq) == n_items); + g_assert ((guint)g_sequence_get_length (self->sort_seq) == n_items); + + if (n_items > 0) + emit_items_changed (self, 0, n_items, n_items); +} + +void +gtd_list_model_sort_set_sort_func (GtdListModelSort *self, + GtdListModelCompareFunc compare_func, + gpointer compare_func_data, + GDestroyNotify compare_func_data_destroy) +{ + g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self)); + g_return_if_fail (compare_func || (!compare_func_data && !compare_func_data_destroy)); + + GTD_ENTRY; + + if (self->compare_func_data_destroy != NULL) + g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy); + + if (compare_func != NULL) + { + self->compare_func = compare_func; + self->compare_func_data = compare_func_data; + self->compare_func_data_destroy = compare_func_data_destroy; + } + else + { + self->compare_func = default_compare_func; + self->compare_func_data = NULL; + self->compare_func_data_destroy = NULL; + } + + gtd_list_model_sort_invalidate (self); + + GTD_EXIT; +} diff --git a/src/models/gtd-list-model-sort.h b/src/models/gtd-list-model-sort.h new file mode 100644 index 0000000..c95db29 --- /dev/null +++ b/src/models/gtd-list-model-sort.h @@ -0,0 +1,46 @@ +/* gtd-list-model-sort.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_LIST_MODEL_SORT (gtd_list_model_sort_get_type()) + +typedef gboolean (*GtdListModelCompareFunc) (GObject *a, + GObject *b, + gpointer user_data); + +G_DECLARE_FINAL_TYPE (GtdListModelSort, gtd_list_model_sort, GTD, LIST_MODEL_SORT, GObject) + +GtdListModelSort* gtd_list_model_sort_new (GListModel *child_model); + +GListModel* gtd_list_model_sort_get_child_model (GtdListModelSort *self); + +void gtd_list_model_sort_invalidate (GtdListModelSort *self); + +void gtd_list_model_sort_set_sort_func (GtdListModelSort *self, + GtdListModelCompareFunc compare_func, + gpointer compare_func_data, + GDestroyNotify compare_func_data_destroy); + +G_END_DECLS diff --git a/src/models/gtd-list-store.c b/src/models/gtd-list-store.c new file mode 100644 index 0000000..bd30bcc --- /dev/null +++ b/src/models/gtd-list-store.c @@ -0,0 +1,567 @@ +/* gtd-list-store.c + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdListStore" + +#include "gtd-list-store.h" + +#include + +/** + * SECTION:gtdliststore + * @title: GtdListStore + * @short_description: A simple implementation of #GListModel + * @include: gio/gio.h + * + * #GtdListStore is a simple implementation of #GListModel that stores all + * items in memory. + * + * It provides insertions, deletions, and lookups in logarithmic time + * with a fast path for the common case of iterating the list linearly. + */ + +/** + * GtdListStore: + * + * #GtdListStore is an opaque data structure and can only be accessed + * using the following functions. + **/ + +struct _GtdListStore +{ + GObject parent; + + GType item_type; + GSequence *items; + + GHashTable *item_to_iter; + + /* cache */ + gint64 length; + gint64 last_position; + GSequenceIter *last_iter; +}; + +enum +{ + PROP_0, + PROP_ITEM_TYPE, + N_PROPERTIES +}; + +static void gtd_list_store_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdListStore, gtd_list_store, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtd_list_store_iface_init)); + +static void +remove_item_from_sequence_cb (gpointer item, + gpointer user_data) +{ + GSequenceIter *it; + GtdListStore *store; + + store = (GtdListStore *)user_data; + it = g_hash_table_lookup (store->item_to_iter, item); + + g_hash_table_remove (store->item_to_iter, item); + g_sequence_remove (it); +} + +static void +gtd_list_store_items_changed (GtdListStore *store, + guint position, + guint removed, + guint added) +{ + /* check if the iter cache may have been invalidated */ + if (position <= store->last_position) + { + store->last_iter = NULL; + store->last_position = -1; + } + + store->length -= removed; + store->length += added; + + g_list_model_items_changed (G_LIST_MODEL (store), position, removed, added); +} + +static void +gtd_list_store_dispose (GObject *object) +{ + GtdListStore *store = GTD_LIST_STORE (object); + + g_clear_pointer (&store->item_to_iter, g_hash_table_destroy); + g_clear_pointer (&store->items, g_sequence_free); + + G_OBJECT_CLASS (gtd_list_store_parent_class)->dispose (object); +} + +static void +gtd_list_store_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtdListStore *store = GTD_LIST_STORE (object); + + switch (property_id) + { + case PROP_ITEM_TYPE: + g_value_set_gtype (value, store->item_type); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtd_list_store_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdListStore *store = GTD_LIST_STORE (object); + + switch (property_id) + { + case PROP_ITEM_TYPE: /* construct-only */ + store->item_type = g_value_get_gtype (value); + if (!g_type_is_a (store->item_type, G_TYPE_OBJECT)) + g_critical ("GtdListStore cannot store items of type '%s'. Items must be GObjects.", + g_type_name (store->item_type)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtd_list_store_class_init (GtdListStoreClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gtd_list_store_dispose; + object_class->get_property = gtd_list_store_get_property; + object_class->set_property = gtd_list_store_set_property; + + /** + * GtdListStore:item-type: + * + * The type of items contained in this list store. Items must be + * subclasses of #GObject. + **/ + g_object_class_install_property (object_class, PROP_ITEM_TYPE, + g_param_spec_gtype ("item-type", "", "", G_TYPE_OBJECT, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +static GType +gtd_list_store_get_item_type (GListModel *list) +{ + GtdListStore *store = GTD_LIST_STORE (list); + + return store->item_type; +} + +static guint +gtd_list_store_get_n_items (GListModel *list) +{ + GtdListStore *store = GTD_LIST_STORE (list); + + return store->length; +} + +static gpointer +gtd_list_store_get_item (GListModel *list, + guint position) +{ + GtdListStore *store = GTD_LIST_STORE (list); + GSequenceIter *it = NULL; + + if (store->last_position != -1) + { + if (store->last_position == position + 1) + it = g_sequence_iter_prev (store->last_iter); + else if (store->last_position == position - 1) + it = g_sequence_iter_next (store->last_iter); + else if (store->last_position == position) + it = store->last_iter; + } + + if (it == NULL) + it = g_sequence_get_iter_at_pos (store->items, position); + + store->last_iter = it; + store->last_position = position; + + if (g_sequence_iter_is_end (it)) + return NULL; + else + return g_object_ref (g_sequence_get (it)); +} + +static void +gtd_list_store_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = gtd_list_store_get_item_type; + iface->get_n_items = gtd_list_store_get_n_items; + iface->get_item = gtd_list_store_get_item; +} + +static void +gtd_list_store_init (GtdListStore *store) +{ + store->item_to_iter = g_hash_table_new (g_direct_hash, g_direct_equal); + store->items = g_sequence_new (g_object_unref); + store->last_position = -1; +} + +/** + * gtd_list_store_new: + * @item_type: the #GType of items in the list + * + * Creates a new #GtdListStore with items of type @item_type. @item_type + * must be a subclass of #GObject. + * + * Returns: a new #GtdListStore + */ +GtdListStore * +gtd_list_store_new (GType item_type) +{ + /* We only allow GObjects as item types right now. This might change + * in the future. + */ + g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL); + + return g_object_new (GTD_TYPE_LIST_STORE, + "item-type", item_type, + NULL); +} + +/** + * gtd_list_store_insert: + * @store: a #GtdListStore + * @position: the position at which to insert the new item + * @item: (type GObject): the new item + * + * Inserts @item into @store at @position. @item must be of type + * #GtdListStore:item-type or derived from it. @position must be smaller + * than the length of the list, or equal to it to append. + * + * This function takes a ref on @item. + * + * Use gtd_list_store_splice() to insert multiple items at the same time + * efficiently. + */ +void +gtd_list_store_insert (GtdListStore *store, + guint position, + gpointer item) +{ + GSequenceIter *new_it; + GSequenceIter *it; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type)); + g_return_if_fail (position <= (guint) g_sequence_get_length (store->items)); + + it = g_sequence_get_iter_at_pos (store->items, position); + new_it = g_sequence_insert_before (it, g_object_ref (item)); + + g_hash_table_insert (store->item_to_iter, item, new_it); + + gtd_list_store_items_changed (store, position, 0, 1); +} + +/** + * gtd_list_store_insert_sorted: + * @store: a #GtdListStore + * @item: (type GObject): the new item + * @compare_func: (scope call): pairwise comparison function for sorting + * @user_data: (closure): user data for @compare_func + * + * Inserts @item into @store at a position to be determined by the + * @compare_func. + * + * The list must already be sorted before calling this function or the + * result is undefined. Usually you would approach this by only ever + * inserting items by way of this function. + * + * This function takes a ref on @item. + * + * Returns: the position at which @item was inserted + */ +guint +gtd_list_store_insert_sorted (GtdListStore *store, + gpointer item, + GCompareDataFunc compare_func, + gpointer user_data) +{ + GSequenceIter *it; + guint position; + + g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0); + g_return_val_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type), 0); + g_return_val_if_fail (compare_func != NULL, 0); + + it = g_sequence_insert_sorted (store->items, g_object_ref (item), compare_func, user_data); + position = g_sequence_iter_get_position (it); + + g_hash_table_insert (store->item_to_iter, item, it); + + gtd_list_store_items_changed (store, position, 0, 1); + + return position; +} + +/** + * gtd_list_store_sort: + * @store: a #GtdListStore + * @compare_func: (scope call): pairwise comparison function for sorting + * @user_data: (closure): user data for @compare_func + * + * Sort the items in @store according to @compare_func. + */ +void +gtd_list_store_sort (GtdListStore *store, + GCompareDataFunc compare_func, + gpointer user_data) +{ + gint n_items; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + g_return_if_fail (compare_func != NULL); + + g_sequence_sort (store->items, compare_func, user_data); + + n_items = g_sequence_get_length (store->items); + gtd_list_store_items_changed (store, 0, n_items, n_items); +} + +/** + * gtd_list_store_append: + * @store: a #GtdListStore + * @item: (type GObject): the new item + * + * Appends @item to @store. @item must be of type #GtdListStore:item-type. + * + * This function takes a ref on @item. + * + * Use gtd_list_store_splice() to append multiple items at the same time + * efficiently. + */ +void +gtd_list_store_append (GtdListStore *store, + gpointer item) +{ + GSequenceIter *it; + guint n_items; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type)); + + n_items = g_sequence_get_length (store->items); + it = g_sequence_append (store->items, g_object_ref (item)); + + g_hash_table_insert (store->item_to_iter, item, it); + + gtd_list_store_items_changed (store, n_items, 0, 1); +} + +/** + * gtd_list_store_remove: + * @store: a #GtdListStore + * @item: the item that is to be removed + * + * Removes the item from @store. + * + * Use gtd_list_store_splice() to remove multiple items at the same time + * efficiently. + */ +void +gtd_list_store_remove (GtdListStore *store, + gpointer item) +{ + GSequenceIter *it; + guint position; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + + it = g_hash_table_lookup (store->item_to_iter, item); + if (!it) + return; + + g_return_if_fail (!g_sequence_iter_is_end (it)); + + position = g_sequence_iter_get_position (it); + + g_hash_table_remove (store->item_to_iter, item); + g_sequence_remove (it); + gtd_list_store_items_changed (store, position, 1, 0); +} + +/** + * gtd_list_store_remove_at_position: + * @store: a #GtdListStore + * @position: the position of the item that is to be removed + * + * Removes the item from @store that is at @position. @position must be + * smaller than the current length of the list. + * + * Use gtd_list_store_splice() to remove multiple items at the same time + * efficiently. + */ +void +gtd_list_store_remove_at_position (GtdListStore *store, + guint position) +{ + GSequenceIter *it; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + + it = g_sequence_get_iter_at_pos (store->items, position); + g_return_if_fail (!g_sequence_iter_is_end (it)); + + g_hash_table_remove (store->item_to_iter, g_sequence_get (it)); + g_sequence_remove (it); + gtd_list_store_items_changed (store, position, 1, 0); +} + +/** + * gtd_list_store_remove_all: + * @store: a #GtdListStore + * + * Removes all items from @store. + */ +void +gtd_list_store_remove_all (GtdListStore *store) +{ + guint n_items; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + + n_items = g_sequence_get_length (store->items); + g_sequence_remove_range (g_sequence_get_begin_iter (store->items), + g_sequence_get_end_iter (store->items)); + g_hash_table_remove_all (store->item_to_iter); + + gtd_list_store_items_changed (store, 0, n_items, 0); +} + +/** + * gtd_list_store_splice: + * @store: a #GtdListStore + * @position: the position at which to make the change + * @n_removals: the number of items to remove + * @additions: (array length=n_additions) (element-type GObject): the items to add + * @n_additions: the number of items to add + * + * Changes @store by removing @n_removals items and adding @n_additions + * items to it. @additions must contain @n_additions items of type + * #GtdListStore:item-type. %NULL is not permitted. + * + * This function is more efficient than gtd_list_store_insert() and + * gtd_list_store_remove(), because it only emits + * #GListModel::items-changed once for the change. + * + * This function takes a ref on each item in @additions. + * + * The parameters @position and @n_removals must be correct (ie: + * @position + @n_removals must be less than or equal to the length of + * the list at the time this function is called). + */ +void +gtd_list_store_splice (GtdListStore *store, + guint position, + guint n_removals, + gpointer *additions, + guint n_additions) +{ + GSequenceIter *it; + guint n_items; + + g_return_if_fail (GTD_IS_LIST_STORE (store)); + g_return_if_fail (position + n_removals >= position); /* overflow */ + + n_items = g_sequence_get_length (store->items); + g_return_if_fail (position + n_removals <= n_items); + + it = g_sequence_get_iter_at_pos (store->items, position); + + if (n_removals) + { + GSequenceIter *end; + + end = g_sequence_iter_move (it, n_removals); + g_sequence_foreach_range (it, end, remove_item_from_sequence_cb, store); + + it = end; + } + + if (n_additions) + { + guint i; + + for (i = 0; i < n_additions; i++) + { + if G_UNLIKELY (!g_type_is_a (G_OBJECT_TYPE (additions[i]), store->item_type)) + { + g_critical ("%s: item %d is a %s instead of a %s. GtdListStore is now in an undefined state.", + G_STRFUNC, i, G_OBJECT_TYPE_NAME (additions[i]), g_type_name (store->item_type)); + return; + } + + it = g_sequence_insert_before (it, g_object_ref (additions[i])); + g_hash_table_insert (store->item_to_iter, additions[i], it); + + it = g_sequence_iter_next (it); + } + } + + gtd_list_store_items_changed (store, position, n_removals, n_additions); +} + +/** + * gtd_list_store_get_item_position: + * @store: a #GtdListStore + * @item: the item to retrieve the position + * + * Retrieves the position of @items inside @store. It is a programming + * error to pass an @item that is not contained in @store. + * + * Returns: the position of @item in @store. + */ +guint +gtd_list_store_get_item_position (GtdListStore *store, + gpointer item) +{ + GSequenceIter *iter; + + g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0); + + iter = g_hash_table_lookup (store->item_to_iter, item); + g_assert (iter != NULL); + + return g_sequence_iter_get_position (iter); +} diff --git a/src/models/gtd-list-store.h b/src/models/gtd-list-store.h new file mode 100644 index 0000000..84e8882 --- /dev/null +++ b/src/models/gtd-list-store.h @@ -0,0 +1,66 @@ +/* gtd-list-store.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_LIST_STORE (gtd_list_store_get_type()) + +G_DECLARE_FINAL_TYPE (GtdListStore, gtd_list_store, GTD, LIST_STORE, GObject) + +GtdListStore* gtd_list_store_new (GType item_type); + +void gtd_list_store_insert (GtdListStore *store, + guint position, + gpointer item); + +guint gtd_list_store_insert_sorted (GtdListStore *store, + gpointer item, + GCompareDataFunc compare_func, + gpointer user_data); + +void gtd_list_store_sort (GtdListStore *store, + GCompareDataFunc compare_func, + gpointer user_data); + +void gtd_list_store_append (GtdListStore *store, + gpointer item); + +void gtd_list_store_remove (GtdListStore *store, + gpointer item); + +void gtd_list_store_remove_at_position (GtdListStore *store, + guint position); + +void gtd_list_store_remove_all (GtdListStore *store); + +void gtd_list_store_splice (GtdListStore *store, + guint position, + guint n_removals, + gpointer *additions, + guint n_additions); + +guint gtd_list_store_get_item_position (GtdListStore *store, + gpointer item); + +G_END_DECLS diff --git a/src/models/gtd-task-list-view-model.c b/src/models/gtd-task-list-view-model.c new file mode 100644 index 0000000..d569d29 --- /dev/null +++ b/src/models/gtd-task-list-view-model.c @@ -0,0 +1,216 @@ +/* gtd-task-list-view-model.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-task-list.h" +#include "gtd-task-list-view-model.h" + + +/* + * Sentinel + */ + +struct _GtdSentinel +{ + GObject parent_instance; +}; + +G_DEFINE_TYPE (GtdSentinel, gtd_sentinel, G_TYPE_OBJECT); + +static void +gtd_sentinel_init (GtdSentinel *self) +{ +} + +static void +gtd_sentinel_class_init (GtdSentinelClass *klass) +{ +} + + + +struct _GtdTaskListViewModel +{ + GObject parent_instance; + + GtdSentinel *sentinel; + + GListModel *model; + guint n_items; +}; + +static void g_list_model_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTaskListViewModel, gtd_task_list_view_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init)) + + +/* + * Auxiliary methods + */ + +static void +update_n_items (GtdTaskListViewModel *self, + guint position, + guint removed, + guint added) +{ + self->n_items = self->n_items - removed + added; + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + + +/* + * Callbacks + */ + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtdTaskListViewModel *self) +{ + update_n_items (self, position, removed, added); +} + + +/* + * GListModel interface + */ + +static guint +gtd_task_list_view_model_get_n_items (GListModel *model) +{ + GtdTaskListViewModel *self = (GtdTaskListViewModel *)model; + + return self->n_items + 1; +} + +static GType +gtd_task_list_view_model_get_item_type (GListModel *model) +{ + return G_TYPE_OBJECT; +} + +static gpointer +gtd_task_list_view_model_get_item (GListModel *model, + guint position) +{ + GtdTaskListViewModel *self = (GtdTaskListViewModel *)model; + + if (gtd_task_list_view_model_is_sentinel (self, position)) + return g_object_ref (self->sentinel); + + return g_list_model_get_item (self->model, position); +} + +static void +g_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_n_items = gtd_task_list_view_model_get_n_items; + iface->get_item_type = gtd_task_list_view_model_get_item_type; + iface->get_item = gtd_task_list_view_model_get_item; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_view_model_finalize (GObject *object) +{ + GtdTaskListViewModel *self = (GtdTaskListViewModel *)object; + + g_clear_object (&self->model); + g_clear_object (&self->sentinel); + + G_OBJECT_CLASS (gtd_task_list_view_model_parent_class)->finalize (object); +} + +static void +gtd_task_list_view_model_class_init (GtdTaskListViewModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_task_list_view_model_finalize; +} + +static void +gtd_task_list_view_model_init (GtdTaskListViewModel *self) +{ + self->sentinel = g_object_new (GTD_TYPE_SENTINEL, NULL); +} + +GtdTaskListViewModel * +gtd_task_list_view_model_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LIST_VIEW_MODEL, NULL); +} + +GListModel * +gtd_task_list_view_model_get_model (GtdTaskListViewModel *self) +{ + return self->model; +} + +void +gtd_task_list_view_model_set_model (GtdTaskListViewModel *self, + GListModel *model) +{ + guint old_n_items = 0; + guint new_n_items = 0; + + if (G_UNLIKELY (self->model == model)) + return; + + old_n_items = self->n_items; + + if (self->model) + { + g_signal_handlers_disconnect_by_func (self->model, + on_model_items_changed_cb, + self); + } + + g_clear_object (&self->model); + + if (model) + { + self->model = g_object_ref (model); + + g_signal_connect_object (model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + new_n_items = g_list_model_get_n_items (model); + } + + update_n_items (self, 0, old_n_items, new_n_items); +} + +gboolean +gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self, + guint position) +{ + return position == self->n_items; +} diff --git a/src/models/gtd-task-list-view-model.h b/src/models/gtd-task-list-view-model.h new file mode 100644 index 0000000..7323f3a --- /dev/null +++ b/src/models/gtd-task-list-view-model.h @@ -0,0 +1,43 @@ +/* gtd-task-list-view-model.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_SENTINEL (gtd_sentinel_get_type ()) +G_DECLARE_FINAL_TYPE (GtdSentinel, gtd_sentinel, GTD, SENTINEL, GObject) + +#define GTD_TYPE_TASK_LIST_VIEW_MODEL (gtd_task_list_view_model_get_type()) +G_DECLARE_FINAL_TYPE (GtdTaskListViewModel, gtd_task_list_view_model, GTD, TASK_LIST_VIEW_MODEL, GObject) + +GtdTaskListViewModel* gtd_task_list_view_model_new (void); + +GListModel* gtd_task_list_view_model_get_model (GtdTaskListViewModel *self); + +void gtd_task_list_view_model_set_model (GtdTaskListViewModel *self, + GListModel *model); + +gboolean gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self, + guint position); + +G_END_DECLS diff --git a/src/models/gtd-task-model-private.h b/src/models/gtd-task-model-private.h new file mode 100644 index 0000000..7bc1c7d --- /dev/null +++ b/src/models/gtd-task-model-private.h @@ -0,0 +1,29 @@ +/* gtd-task-model-private.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-task-model.h" + +G_BEGIN_DECLS + +GtdTaskModel* _gtd_task_model_new (GtdManager *manager); + +G_END_DECLS diff --git a/src/models/gtd-task-model.c b/src/models/gtd-task-model.c new file mode 100644 index 0000000..62984e7 --- /dev/null +++ b/src/models/gtd-task-model.c @@ -0,0 +1,216 @@ +/* gtd-task-model.c + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskModel" + +#include "gtd-debug.h" +#include "gtd-list-store.h" +#include "gtd-manager.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-task-model.h" +#include "gtd-task-model-private.h" + +struct _GtdTaskModel +{ + GObject parent; + + GtkFlattenListModel *model; + + guint number_of_tasks; + + GtdManager *manager; +}; + +static void g_list_model_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTaskModel, gtd_task_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init)) + +enum +{ + PROP_0, + PROP_MANAGER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Callbacks + */ + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTaskModel *self) +{ + GTD_TRACE_MSG ("Child model changed with position=%u, n_removed=%u, n_added=%u", position, n_removed, n_added); + + g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added); +} + + +/* + * GListModel iface + */ + +static gpointer +gtd_task_model_get_item (GListModel *model, + guint position) +{ + GtdTaskModel *self = (GtdTaskModel*) model; + + return g_list_model_get_item (G_LIST_MODEL (self->model), position); +} + +static guint +gtd_task_model_get_n_items (GListModel *model) +{ + GtdTaskModel *self = (GtdTaskModel*) model; + + return g_list_model_get_n_items (G_LIST_MODEL (self->model)); +} + +static GType +gtd_task_model_get_item_type (GListModel *model) +{ + return GTD_TYPE_TASK; +} + +static void +g_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item = gtd_task_model_get_item; + iface->get_n_items = gtd_task_model_get_n_items; + iface->get_item_type = gtd_task_model_get_item_type; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_model_finalize (GObject *object) +{ + GtdTaskModel *self = (GtdTaskModel *)object; + + g_clear_object (&self->manager); + g_clear_object (&self->model); + + G_OBJECT_CLASS (gtd_task_model_parent_class)->finalize (object); +} + + +static void +gtd_task_model_constructed (GObject *object) +{ + GtdTaskModel *self = (GtdTaskModel *)object; + GListModel *model; + + g_assert (self->manager != NULL); + + model = gtd_manager_get_task_lists_model (self->manager); + + g_signal_connect_object (self->model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + gtk_flatten_list_model_set_model (self->model, model); + + G_OBJECT_CLASS (gtd_task_model_parent_class)->constructed (object); +} + +static void +gtd_task_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskModel *self = GTD_TASK_MODEL (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, self->manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskModel *self = GTD_TASK_MODEL (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_assert (self->manager == NULL); + self->manager = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_model_class_init (GtdTaskModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_task_model_finalize; + object_class->constructed = gtd_task_model_constructed; + object_class->get_property = gtd_task_model_get_property; + object_class->set_property = gtd_task_model_set_property; + + properties[PROP_MANAGER] = g_param_spec_object ("manager", + "Manager", + "Manager", + GTD_TYPE_MANAGER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_task_model_init (GtdTaskModel *self) +{ + self->model = gtk_flatten_list_model_new (NULL); +} + +GtdTaskModel* +_gtd_task_model_new (GtdManager *manager) +{ + return g_object_new (GTD_TYPE_TASK_MODEL, + "manager", manager, + NULL); +} diff --git a/src/models/gtd-task-model.h b/src/models/gtd-task-model.h new file mode 100644 index 0000000..2dc7aee --- /dev/null +++ b/src/models/gtd-task-model.h @@ -0,0 +1,33 @@ +/* gtd-task-model.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gtd-types.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_MODEL (gtd_task_model_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskModel, gtd_task_model, GTD, TASK_MODEL, GObject) + +G_END_DECLS diff --git a/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c b/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c new file mode 100644 index 0000000..93aedfe --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c @@ -0,0 +1,34 @@ +/* gtd-plugin-all-tasks-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdPluginAllTasksPanel" + +#include "endeavour.h" + +#include "gtd-all-tasks-panel.h" + +G_MODULE_EXPORT void +all_tasks_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_ALL_TASKS_PANEL); +} diff --git a/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml b/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml new file mode 100644 index 0000000..608338f --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml @@ -0,0 +1,6 @@ + + + + all-tasks-panel.plugin + + diff --git a/src/plugins/all-tasks-panel/all-tasks-panel.plugin b/src/plugins/all-tasks-panel/all-tasks-panel.plugin new file mode 100644 index 0000000..ac184ca --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = All Tasks +Module = all-tasks-panel +Description = A panel to show all open tasks +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = all_tasks_panel_plugin_register_types +Depends = diff --git a/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c new file mode 100644 index 0000000..a4b4ad3 --- /dev/null +++ b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c @@ -0,0 +1,491 @@ +/* gtd-all-tasks-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdAllTasksPanel" + +#include "gtd-all-tasks-panel.h" + +#include "endeavour.h" + +#include "gtd-debug.h" + +#include +#include + + +#define GTD_ALL_TASKS_PANEL_NAME "all-tasks-panel" +#define GTD_ALL_TASKS_PANEL_PRIORITY 000 + +struct _GtdAllTasksPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkSortListModel *sort_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdAllTasksPanel, gtd_all_tasks_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + + +static void +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < -1) + { + /* Translators: This message will never be used with '1 day ago' + * but the singular form is required because some languages do not + * have plurals, some languages reuse the singular form for numbers + * like 21, 31, 41, etc. + */ + str = g_strdup_printf (g_dngettext (NULL, "%d day ago", "%d days ago", -days_diff), -days_diff); + } + else if (days_diff == -1) + { + str = g_strdup (_("Yesterday")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-top", first_header ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (!d1 && !d2) + return 0; + else if (!d1) + return 1; + else if (!d2) + return -1; + + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + + +/* + * Callbacks + */ + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdAllTasksPanel *self) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint diff; + + before_dt = gtd_task_get_due_date (previous_task); + diff = compare_by_date (before_dt, dt); + + if (diff != 0) + text = get_string_for_date (dt, &span); + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + g_autoptr (GDateTime) dt1 = NULL; + g_autoptr (GDateTime) dt2 = NULL; + GtdTask *task1; + GtdTask *task2; + GDate dates[2]; + gint result; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + return gtd_task_compare (task1, task2); + else if (!dt1) + return 1; + else if (!dt2) + return -1; + + g_date_clear (dates, 2); + + g_date_set_dmy (&dates[0], + g_date_time_get_day_of_month (dt1), + g_date_time_get_month (dt1), + g_date_time_get_year (dt1)); + + g_date_set_dmy (&dates[1], + g_date_time_get_day_of_month (dt2), + g_date_time_get_month (dt2), + g_date_time_get_year (dt2)); + + result = g_date_days_between (&dates[1], &dates[0]); + + if (result != 0) + return result; + + return gtd_task_compare (task1, task2); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdAllTasksPanel *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added); + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdAllTasksPanel *self) +{ + g_autoptr (GDateTime) now = NULL; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_all_tasks_get_panel_name (GtdPanel *panel) +{ + return GTD_ALL_TASKS_PANEL_NAME; +} + +static const gchar* +gtd_panel_all_tasks_get_panel_title (GtdPanel *panel) +{ + return _("All"); +} + +static GList* +gtd_panel_all_tasks_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_all_tasks_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_all_tasks_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_ALL_TASKS_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_all_tasks_get_priority (GtdPanel *panel) +{ + return GTD_ALL_TASKS_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_all_tasks_get_subtitle (GtdPanel *panel) +{ + GtdAllTasksPanel *self = GTD_ALL_TASKS_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_all_tasks_get_panel_name; + iface->get_panel_title = gtd_panel_all_tasks_get_panel_title; + iface->get_header_widgets = gtd_panel_all_tasks_get_header_widgets; + iface->get_menu = gtd_panel_all_tasks_get_menu; + iface->get_icon = gtd_panel_all_tasks_get_icon; + iface->get_priority = gtd_panel_all_tasks_get_priority; + iface->get_subtitle = gtd_panel_all_tasks_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_all_tasks_panel_finalize (GObject *object) +{ + GtdAllTasksPanel *self = (GtdAllTasksPanel *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_all_tasks_panel_parent_class)->finalize (object); +} + +static void +gtd_all_tasks_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdAllTasksPanel *self = GTD_ALL_TASKS_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_ALL_TASKS_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_ALL_TASKS_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_all_tasks_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_all_tasks_panel_class_init (GtdAllTasksPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_all_tasks_panel_finalize; + object_class->get_property = gtd_all_tasks_panel_get_property; + object_class->set_property = gtd_all_tasks_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_all_tasks_panel_init (GtdAllTasksPanel *self) +{ + GtdManager *manager = gtd_manager_get_default (); + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("view-tasks-all-symbolic"); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_SORTER (sorter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), TRUE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (GTD_TASK_LIST_VIEW (self->view), + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->sort_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_all_tasks_panel_new (void) +{ + return g_object_new (GTD_TYPE_ALL_TASKS_PANEL, NULL); +} diff --git a/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h new file mode 100644 index 0000000..f6efec0 --- /dev/null +++ b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h @@ -0,0 +1,34 @@ +/* gtd-all-tasks-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_ALL_TASKS_PANEL (gtd_all_tasks_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdAllTasksPanel, gtd_all_tasks_panel, GTD, ALL_TASKS_PANEL, GtkBox) + +GtkWidget* gtd_all_tasks_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/all-tasks-panel/meson.build b/src/plugins/all-tasks-panel/meson.build new file mode 100644 index 0000000..9b4954f --- /dev/null +++ b/src/plugins/all-tasks-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=all_tasks_panel_plugin_register_types'] + +plugins_sources += files( + 'all-tasks-panel-plugin.c', + 'gtd-all-tasks-panel.c' +) + +plugins_sources += gnome.compile_resources( + 'all-tasks-panel-resources', + 'all-tasks-panel.gresource.xml', + c_name: 'all_tasks_panel_plugin', +) diff --git a/src/plugins/eds/e-source-endeavour.c b/src/plugins/eds/e-source-endeavour.c new file mode 100644 index 0000000..3ca8846 --- /dev/null +++ b/src/plugins/eds/e-source-endeavour.c @@ -0,0 +1,128 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "e-source-endeavour.h" + +struct _ESourceEndeavour +{ + ESourceExtension parent; + + guint api_version; +}; + +G_DEFINE_TYPE (ESourceEndeavour, e_source_endeavour, E_TYPE_SOURCE_EXTENSION) + +enum +{ + PROP_0, + PROP_API_VERSION, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + + +/* + * GObject overrides + */ + +static void +e_source_endeavour_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ESourceEndeavour *self = E_SOURCE_ENDEAVOUR (object); + + switch (prop_id) + { + case PROP_API_VERSION: + g_value_set_uint (value, self->api_version); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +e_source_endeavour_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ESourceEndeavour *self = E_SOURCE_ENDEAVOUR (object); + + switch (prop_id) + { + case PROP_API_VERSION: + self->api_version = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +e_source_endeavour_class_init (ESourceEndeavourClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ESourceExtensionClass *extension_class = E_SOURCE_EXTENSION_CLASS (klass); + + object_class->get_property = e_source_endeavour_get_property; + object_class->set_property = e_source_endeavour_set_property; + + extension_class->name = E_SOURCE_EXTENSION_ENDEAVOUR; + + properties[PROP_API_VERSION] = g_param_spec_uint ("api-version", + "API Version", + "API Version", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | E_SOURCE_PARAM_SETTING | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +e_source_endeavour_init (ESourceEndeavour *self) +{ + self->api_version = 0; +} + +guint +e_source_endeavour_get_api_version (ESourceEndeavour *self) +{ + g_return_val_if_fail (E_IS_SOURCE_ENDEAVOUR (self), 0); + + return self->api_version; +} + +void +e_source_endeavour_set_api_version (ESourceEndeavour *self, + guint api_version) +{ + g_return_if_fail (E_IS_SOURCE_ENDEAVOUR (self)); + + e_source_extension_property_lock (E_SOURCE_EXTENSION (self)); + self->api_version = api_version; + e_source_extension_property_unlock (E_SOURCE_EXTENSION (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_API_VERSION]); +} diff --git a/src/plugins/eds/e-source-endeavour.h b/src/plugins/eds/e-source-endeavour.h new file mode 100644 index 0000000..ca70c18 --- /dev/null +++ b/src/plugins/eds/e-source-endeavour.h @@ -0,0 +1,35 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "gtd-eds.h" + +G_BEGIN_DECLS + +#define E_SOURCE_EXTENSION_ENDEAVOUR "Endeavour" + +#define E_TYPE_SOURCE_ENDEAVOUR (e_source_endeavour_get_type()) +G_DECLARE_FINAL_TYPE (ESourceEndeavour, e_source_endeavour, E, SOURCE_ENDEAVOUR, ESourceExtension) + +guint e_source_endeavour_get_api_version (ESourceEndeavour *self); + +void e_source_endeavour_set_api_version (ESourceEndeavour *self, + guint api_version); + +G_END_DECLS diff --git a/src/plugins/eds/eds-plugin.c b/src/plugins/eds/eds-plugin.c new file mode 100644 index 0000000..df172d0 --- /dev/null +++ b/src/plugins/eds/eds-plugin.c @@ -0,0 +1,30 @@ +/* eds-plugin.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "endeavour.h" +#include "gtd-plugin-eds.h" + +G_MODULE_EXPORT void +gtd_plugin_eds_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_ACTIVATABLE, + GTD_TYPE_PLUGIN_EDS); +} diff --git a/src/plugins/eds/eds.gresource.xml b/src/plugins/eds/eds.gresource.xml new file mode 100644 index 0000000..4b578f9 --- /dev/null +++ b/src/plugins/eds/eds.gresource.xml @@ -0,0 +1,6 @@ + + + + eds.plugin + + diff --git a/src/plugins/eds/eds.plugin b/src/plugins/eds/eds.plugin new file mode 100644 index 0000000..b7f28cf --- /dev/null +++ b/src/plugins/eds/eds.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Core +Module = eds +Description = Evolution-data-server plugin for Endeavour +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = gtd_plugin_eds_register_types +Depends = diff --git a/src/plugins/eds/gtd-eds-autoptr.h b/src/plugins/eds/gtd-eds-autoptr.h new file mode 100644 index 0000000..99c6129 --- /dev/null +++ b/src/plugins/eds/gtd-eds-autoptr.h @@ -0,0 +1,27 @@ +/* gtd-eds-autoptr.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-eds.h" + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalComponent, g_object_unref); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalComponentId, e_cal_component_id_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalClient, g_object_unref); diff --git a/src/plugins/eds/gtd-eds.h b/src/plugins/eds/gtd-eds.h new file mode 100644 index 0000000..a48827c --- /dev/null +++ b/src/plugins/eds/gtd-eds.h @@ -0,0 +1,32 @@ +/* gtd-eds.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + +#include +#include + +G_GNUC_END_IGNORE_DEPRECATIONS diff --git a/src/plugins/eds/gtd-plugin-eds.c b/src/plugins/eds/gtd-plugin-eds.c new file mode 100644 index 0000000..6200916 --- /dev/null +++ b/src/plugins/eds/gtd-plugin-eds.c @@ -0,0 +1,323 @@ +/* gtd-plugin-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPluginEds" + +#include "gtd-plugin-eds.h" +#include "gtd-provider-goa.h" +#include "gtd-provider-local.h" + +#include +#include + +/** + * The #GtdPluginEds is a class that loads all the + * essential providers of Endeavour. + * + * It basically loads #ESourceRegistry which provides + * #GtdProviderLocal. Immediately after that, it loads + * #GoaClient which provides one #GtdProviderGoa per + * supported account. + * + * The currently supported Online Accounts are Google, + * ownCloud and Microsoft Exchange ones. + */ + +struct _GtdPluginEds +{ + GObject parent; + + ESourceRegistry *registry; + + /* Providers */ + GList *providers; +}; + +enum +{ + PROP_0, + PROP_PREFERENCES_PANEL, + LAST_PROP +}; + +const gchar *supported_accounts[] = { + "exchange", + "google", + "owncloud", + NULL +}; + +static void gtd_activatable_iface_init (GtdActivatableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdPluginEds, gtd_plugin_eds, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_ACTIVATABLE, gtd_activatable_iface_init)) + +/* + * GtdActivatable interface implementation + */ +static void +gtd_plugin_eds_activate (GtdActivatable *activatable) +{ + ; +} + +static void +gtd_plugin_eds_deactivate (GtdActivatable *activatable) +{ + ; +} + +static GtkWidget* +gtd_plugin_eds_get_preferences_panel (GtdActivatable *activatable) +{ + return NULL; +} + +static void +gtd_activatable_iface_init (GtdActivatableInterface *iface) +{ + iface->activate = gtd_plugin_eds_activate; + iface->deactivate = gtd_plugin_eds_deactivate; + iface->get_preferences_panel = gtd_plugin_eds_get_preferences_panel; +} + + +/* + * Init + */ + +static void +gtd_plugin_eds_goa_account_removed_cb (GoaClient *client, + GoaObject *object, + GtdPluginEds *self) +{ + GtdManager *manager; + GoaAccount *account; + GList *l; + + account = goa_object_peek_account (object); + manager = gtd_manager_get_default (); + + if (!g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + return; + + for (l = self->providers; l != NULL; l = l->next) + { + if (!GTD_IS_PROVIDER_GOA (l->data)) + continue; + + if (account == gtd_provider_goa_get_account (l->data)) + { + GtdProviderGoa *provider = GTD_PROVIDER_GOA (l->data); + + self->providers = g_list_remove (self->providers, l->data); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + break; + } + } +} + +static void +gtd_plugin_eds_goa_account_added_cb (GoaClient *client, + GoaObject *object, + GtdPluginEds *self) +{ + GtdManager *manager; + GoaAccount *account; + + account = goa_object_get_account (object); + manager = gtd_manager_get_default (); + + if (g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + { + GtdProviderGoa *provider; + + provider = gtd_provider_goa_new (self->registry, account); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + } +} + +static void +gtd_plugin_eds_goa_client_finish_cb (GObject *client, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdPluginEds *self; + GtdManager *manager; + GoaClient *goa_client; + GList *accounts; + GList *l; + + self = GTD_PLUGIN_EDS (user_data); + goa_client = goa_client_new_finish (result, &error); + manager = gtd_manager_get_default (); + + if (error) + { + g_warning ("%s: %s: %s", + G_STRFUNC, + "Error loading GNOME Online Accounts", + error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Error loading GNOME Online Accounts"), + error->message, + NULL, + NULL); + g_clear_error (&error); + } + + /* Load each supported GoaAccount into a GtdProviderGoa */ + accounts = goa_client_get_accounts (goa_client); + + for (l = accounts; l != NULL; l = l->next) + { + GtdProviderGoa *provider; + GoaAccount *account; + GoaObject *object; + + object = l->data; + account = goa_object_get_account (object); + + if (!g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + { + g_object_unref (account); + continue; + } + + g_debug ("Creating new provider for account '%s'", goa_account_get_identity (account)); + + /* Create the new GOA provider */ + provider = gtd_provider_goa_new (self->registry, account); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + + g_object_unref (account); + } + + /* Connect GoaClient signals */ + g_signal_connect (goa_client, + "account-added", + G_CALLBACK (gtd_plugin_eds_goa_account_added_cb), + user_data); + + g_signal_connect (goa_client, + "account-removed", + G_CALLBACK (gtd_plugin_eds_goa_account_removed_cb), + user_data); + + g_list_free_full (accounts, g_object_unref); +} + + + +static void +gtd_plugin_eds_source_registry_finish_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GtdPluginEds *self = GTD_PLUGIN_EDS (user_data); + GtdProviderLocal *provider; + ESourceRegistry *registry; + GtdManager *manager; + GError *error = NULL; + + manager = gtd_manager_get_default (); + registry = e_source_registry_new_finish (result, &error); + self->registry = registry; + + /* Abort on error */ + if (error) + { + g_warning ("%s: %s", + "Error loading Evolution-Data-Server backend", + error->message); + + g_clear_error (&error); + return; + } + + /* Load the local provider */ + provider = gtd_provider_local_new (registry); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + + /* We only start loading Goa accounts after + * ESourceRegistry is get, since it'd be way + * too hard to synchronize these two asynchronous + * calls. + */ + goa_client_new (NULL, + (GAsyncReadyCallback) gtd_plugin_eds_goa_client_finish_cb, + self); +} + +static void +gtd_plugin_eds_finalize (GObject *object) +{ + GtdPluginEds *self = (GtdPluginEds *)object; + + g_list_free_full (self->providers, g_object_unref); + self->providers = NULL; + + G_OBJECT_CLASS (gtd_plugin_eds_parent_class)->finalize (object); +} + +static void +gtd_plugin_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_PREFERENCES_PANEL: + g_value_set_object (value, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_plugin_eds_class_init (GtdPluginEdsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_plugin_eds_finalize; + object_class->get_property = gtd_plugin_eds_get_property; + + g_object_class_override_property (object_class, + PROP_PREFERENCES_PANEL, + "preferences-panel"); +} + +static void +gtd_plugin_eds_init (GtdPluginEds *self) +{ + /* load the source registry */ + e_source_registry_new (NULL, + (GAsyncReadyCallback) gtd_plugin_eds_source_registry_finish_cb, + self); +} diff --git a/src/plugins/eds/gtd-plugin-eds.h b/src/plugins/eds/gtd-plugin-eds.h new file mode 100644 index 0000000..6eea2c4 --- /dev/null +++ b/src/plugins/eds/gtd-plugin-eds.h @@ -0,0 +1,28 @@ +/* gtd-eds-plugin.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PLUGIN_EDS (gtd_plugin_eds_get_type()) +G_DECLARE_FINAL_TYPE (GtdPluginEds, gtd_plugin_eds, GTD, PLUGIN_EDS, PeasExtensionBase) + +G_END_DECLS diff --git a/src/plugins/eds/gtd-provider-eds.c b/src/plugins/eds/gtd-provider-eds.c new file mode 100644 index 0000000..d46d70e --- /dev/null +++ b/src/plugins/eds/gtd-provider-eds.c @@ -0,0 +1,1157 @@ +/* gtd-provider-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderEds" + +#include "gtd-debug.h" +#include "gtd-eds-autoptr.h" +#include "gtd-provider-eds.h" +#include "gtd-task-eds.h" +#include "gtd-task-list-eds.h" + +#include + +/** + * #GtdProviderEds is the base class of #GtdProviderLocal + * and #GtdProviderGoa. It provides the common functionality + * shared between these two providers. + * + * The subclasses basically have to implement GtdProviderEds->should_load_source + * which decides whether a given #ESource should be loaded (and added to the + * sources list) or not. #GtdProviderLocal for example would filter out + * sources whose backend is not "local". + */ + +typedef struct +{ + GtdTaskList *list; + GDateTime *due_date; + gchar *title; + ESource *source; + + /* Update Task */ + ECalComponent *component; + GtdTask *task; +} AsyncData; + +typedef struct +{ + GHashTable *task_lists; + + ESourceRegistry *source_registry; + + GCancellable *cancellable; + + gint lazy_load_id; +} GtdProviderEdsPrivate; + + +static void gtd_provider_iface_init (GtdProviderInterface *iface); + + +G_DEFINE_TYPE_WITH_CODE (GtdProviderEds, gtd_provider_eds, GTD_TYPE_OBJECT, + G_ADD_PRIVATE (GtdProviderEds) + G_IMPLEMENT_INTERFACE (GTD_TYPE_PROVIDER, gtd_provider_iface_init)) + + +enum +{ + PROP_0, + PROP_ENABLED, + PROP_DESCRIPTION, + PROP_ICON, + PROP_ID, + PROP_NAME, + PROP_PROVIDER_TYPE, + PROP_REGISTRY, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +async_data_free (gpointer data) +{ + AsyncData *async_data = data; + + g_clear_pointer (&async_data->due_date, g_date_time_unref); + g_clear_pointer (&async_data->title, g_free); + g_clear_object (&async_data->source); + g_clear_object (&async_data->list); + g_clear_object (&async_data->task); + g_clear_object (&async_data->component); + g_free (async_data); +} + +static void +set_default_list (GtdProviderEds *self, + GtdTaskList *list) +{ + GtdProviderEdsPrivate *priv; + GtdManager *manager; + ESource *source; + + priv = gtd_provider_eds_get_instance_private (self); + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + manager = gtd_manager_get_default (); + + e_source_registry_set_default_task_list (priv->source_registry, source); + + if (gtd_manager_get_default_provider (manager) != (GtdProvider*) self) + gtd_manager_set_default_provider (manager, GTD_PROVIDER (self)); +} + +static void +ensure_offline_sync (GtdProviderEds *self, + ESource *source) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + ESourceOffline *extension; + + extension = e_source_get_extension (source, E_SOURCE_EXTENSION_OFFLINE); + e_source_offline_set_stay_synchronized (extension, TRUE); + + e_source_registry_commit_source (priv->source_registry, source, NULL, NULL, NULL); +} + + +/* + * Callbacks + */ + +static void +on_task_list_eds_loaded_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + GtdTaskListEds *list; + ESource *source; + + self = GTD_PROVIDER_EDS (user_data); + priv = gtd_provider_eds_get_instance_private (self); + list = gtd_task_list_eds_new_finish (result, &error); + + if (error) + { + g_warning ("Error creating task list: %s", error->message); + return; + } + + source = gtd_task_list_eds_get_source (list); + + g_hash_table_insert (priv->task_lists, e_source_dup_uid (source), g_object_ref (list)); + g_object_set_data (G_OBJECT (source), "task-list", list); + + g_debug ("Task list '%s' successfully connected", e_source_get_display_name (source)); +} + +static void +on_client_connected_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + ECalClient *client; + ESource *source; + + self = GTD_PROVIDER_EDS (user_data); + priv = gtd_provider_eds_get_instance_private (self); + source = e_client_get_source (E_CLIENT (source_object)); + client = E_CAL_CLIENT (e_cal_client_connect_finish (result, &error)); + + if (error) + { + g_warning ("Failed to connect to task list '%s': %s", e_source_get_uid (source), error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Failed to connect to task list"), + error->message, + NULL, + NULL); + gtd_object_pop_loading (GTD_OBJECT (self)); + return; + } + + ensure_offline_sync (self, source); + + /* creates a new task list */ + gtd_task_list_eds_new (GTD_PROVIDER (self), + source, + client, + on_task_list_eds_loaded_cb, + priv->cancellable, + self); +} + +static void +on_source_added_cb (GtdProviderEds *provider, + ESource *source) +{ + /* Don't load the source if it's not a tasklist */ + if (!e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST) || + !GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->should_load_source (provider, source)) + { + GTD_TRACE_MSG ("Ignoring source %s (%s)", + e_source_get_display_name (source), + e_source_get_uid (source)); + return; + } + + /* + * The pop_loading() is actually emited by GtdTaskListEds, after the + * ECalClientView sends the :complete signal. + */ + gtd_object_push_loading (GTD_OBJECT (provider)); + gtd_object_push_loading (GTD_OBJECT (gtd_manager_get_default ())); + + e_cal_client_connect (source, + E_CAL_CLIENT_SOURCE_TYPE_TASKS, + 15, /* seconds to wait */ + NULL, + on_client_connected_cb, + provider); +} + +static void +on_source_removed_cb (GtdProviderEds *provider, + ESource *source) +{ + GtdProviderEdsPrivate *priv; + GtdTaskList *list; + + GTD_ENTRY; + + priv = gtd_provider_eds_get_instance_private (provider); + list = g_object_get_data (G_OBJECT (source), "task-list"); + + if (!g_hash_table_remove (priv->task_lists, gtd_object_get_uid (GTD_OBJECT (list)))) + GTD_RETURN (); + + /* + * Since all subclasses will have this signal given that they + * are all GtdProvider implementations, it's not that bad + * to let it stay here. + */ + g_signal_emit_by_name (provider, "list-removed", list); + + GTD_EXIT; +} + +static void +on_source_refreshed_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (user_data); + g_autoptr (GError) error = NULL; + + GTD_ENTRY; + + e_source_registry_refresh_backend_finish (priv->source_registry, result, &error); + + if (error) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error refreshing source: %s", error->message); + GTD_RETURN (); + } + + GTD_EXIT; +} + +static void +create_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (ECalComponent) component = NULL; + g_autoptr (GError) error = NULL; + g_autofree gchar *new_uid = NULL; + ECalComponentText *new_summary; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + GtdTask *new_task; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (data->list); + client = gtd_task_list_eds_get_client (tasklist); + + /* Create the new task */ + component = e_cal_component_new (); + e_cal_component_set_new_vtype (component, E_CAL_COMPONENT_TODO); + + new_summary = e_cal_component_text_new (data->title, NULL); + e_cal_component_set_summary (component, new_summary); + + if (data->due_date) + { + ECalComponentDateTime *comp_dt; + ICalTime *idt; + + idt = i_cal_time_new_null_time (); + i_cal_time_set_date (idt, + g_date_time_get_year (data->due_date), + g_date_time_get_month (data->due_date), + g_date_time_get_day_of_month (data->due_date)); + i_cal_time_set_time (idt, + g_date_time_get_hour (data->due_date), + g_date_time_get_minute (data->due_date), + g_date_time_get_seconds (data->due_date)); + i_cal_time_set_is_date (idt, + i_cal_time_get_hour (idt) == 0 && + i_cal_time_get_minute (idt) == 0 && + i_cal_time_get_second (idt) == 0); + + comp_dt = e_cal_component_datetime_new_take (idt, g_strdup ("UTC")); + e_cal_component_set_due (component, comp_dt); + e_cal_component_commit_sequence (component); + + e_cal_component_datetime_free (comp_dt); + } + + e_cal_client_create_object_sync (client, + e_cal_component_get_icalcomponent (component), + E_CAL_OPERATION_FLAG_NONE, + &new_uid, + cancellable, + &error); + + e_cal_component_text_free (new_summary); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + new_task = gtd_task_eds_new (component); + gtd_task_set_position (new_task, g_list_model_get_n_items (G_LIST_MODEL (tasklist))); + + /* + * In the case the task UID changes because of creation proccess, + * reapply it to the task. + */ + if (new_uid) + gtd_object_set_uid (GTD_OBJECT (new_task), new_uid); + + /* Effectively apply the updated component */ + gtd_task_eds_apply (GTD_TASK_EDS (new_task)); + + g_task_return_pointer (task, g_object_ref (new_task), g_object_unref); + + GTD_EXIT; +} + +static void +update_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (gtd_task_get_list (data->task)); + client = gtd_task_list_eds_get_client (tasklist); + + e_cal_client_modify_object_sync (client, + e_cal_component_get_icalcomponent (data->component), + E_CAL_OBJ_MOD_THIS, + E_CAL_OPERATION_FLAG_NONE, + cancellable, + &error); + + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + +static void +remove_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (ECalComponentId) id = NULL; + g_autoptr (GError) error = NULL; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (gtd_task_get_list (data->task)); + client = gtd_task_list_eds_get_client (tasklist); + id = e_cal_component_get_id (data->component); + + e_cal_client_remove_object_sync (client, + e_cal_component_id_get_uid (id), + e_cal_component_id_get_rid (id), + E_CAL_OBJ_MOD_THIS, + E_CAL_OPERATION_FLAG_NONE, + cancellable, + &error); + + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + +static void +create_or_update_task_list_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GtdProviderEdsPrivate *priv; + g_autoptr (GError) error = NULL; + GtdProviderEds *self; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + self = GTD_PROVIDER_EDS (source_object); + priv = gtd_provider_eds_get_instance_private (self); + + e_source_registry_commit_source_sync (priv->source_registry, + data->source, + cancellable, + &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + + +static void +remove_task_list_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + + e_source_remove_sync (data->source, cancellable, &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + + +/* + * GtdProvider iface + */ + +static const gchar* +gtd_provider_eds_get_id (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_id (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_name (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_name (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_provider_type (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_provider_type (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_description (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_description (GTD_PROVIDER_EDS (provider)); +} + + +static gboolean +gtd_provider_eds_get_enabled (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), FALSE); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_enabled (GTD_PROVIDER_EDS (provider)); +} + +static void +gtd_provider_eds_refresh (GtdProvider *provider) +{ + g_autoptr (GHashTable) collections = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + GHashTableIter iter; + GtdTaskListEds *list; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_PROVIDER_EDS (provider)); + + self = GTD_PROVIDER_EDS (provider); + priv = gtd_provider_eds_get_instance_private (self); + collections = g_hash_table_new (g_direct_hash, g_direct_equal); + + g_hash_table_iter_init (&iter, priv->task_lists); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &list)) + { + g_autoptr (ESource) collection = NULL; + ESource *source; + + source = gtd_task_list_eds_get_source (list); + collection = e_source_registry_find_extension (priv->source_registry, + source, + E_SOURCE_EXTENSION_COLLECTION); + + if (!collection || g_hash_table_contains (collections, collection)) + continue; + + GTD_TRACE_MSG ("Refreshing collection %s", e_source_get_uid (collection)); + + e_source_registry_refresh_backend (priv->source_registry, + e_source_get_uid (collection), + priv->cancellable, + on_source_refreshed_cb, + g_object_ref (self)); + + g_hash_table_add (collections, collection); + } + + GTD_EXIT; +} + +static GIcon* +gtd_provider_eds_get_icon (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_icon (GTD_PROVIDER_EDS (provider)); +} + +static void +gtd_provider_eds_create_task (GtdProvider *provider, + GtdTaskList *list, + const gchar *title, + GDateTime *due_date, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + GtdProviderEds *self; + AsyncData *data; + + g_return_if_fail (GTD_IS_TASK_LIST_EDS (list)); + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + + data = g_new0 (AsyncData, 1); + data->list = g_object_ref (list); + data->title = g_strdup (title); + data->due_date = due_date ? g_date_time_ref (due_date) : NULL; + + gtd_object_push_loading (GTD_OBJECT (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_create_task); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_task_in_thread_cb); + + GTD_EXIT; +} + +static GtdTask* +gtd_provider_eds_create_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + g_autoptr (GtdTask) new_task = NULL; + GtdProviderEds *self; + GtdTaskList *list; + AsyncData *data; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + list = data->list; + + gtd_object_pop_loading (GTD_OBJECT (self)); + + new_task = g_task_propagate_pointer (G_TASK (result), error); + + if (new_task) + { + gtd_task_set_list (new_task, list); + gtd_task_list_add_task (list, new_task); + set_default_list (self, list); + } + + GTD_RETURN (new_task); +} + +static void +gtd_provider_eds_update_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + ECalComponent *component; + AsyncData *data; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (GTD_IS_TASK_LIST_EDS (gtd_task_get_list (task))); + + component = gtd_task_eds_get_component (GTD_TASK_EDS (task)); + + e_cal_component_commit_sequence (component); + + /* The task is not ready until we finish the operation */ + gtd_object_push_loading (GTD_OBJECT (task)); + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->task = g_object_ref (task); + data->component = e_cal_component_clone (component); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_update_task); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, update_task_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_update_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + AsyncData *data; + GtdTask *task; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + task = data->task; + + gtd_object_pop_loading (GTD_OBJECT (self)); + gtd_object_pop_loading (GTD_OBJECT (task)); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + { + gtd_task_eds_revert (GTD_TASK_EDS (task)); + GTD_RETURN (FALSE); + } + + gtd_task_eds_apply (GTD_TASK_EDS (task)); + gtd_task_list_update_task (gtd_task_get_list (task), task); + + GTD_RETURN (TRUE); +} + +static void +gtd_provider_eds_remove_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + ECalComponent *component; + AsyncData *data; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (GTD_IS_TASK_LIST_EDS (gtd_task_get_list (task))); + + component = gtd_task_eds_get_component (GTD_TASK_EDS (task)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->task = g_object_ref (task); + data->component = e_cal_component_clone (component); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_remove_task); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, remove_task_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_remove_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GTD_ENTRY; + + gtd_object_pop_loading (GTD_OBJECT (provider)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_create_task_list (GtdProvider *provider, + const gchar *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + GtdProviderEds *self; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + source = NULL; + + /* Create an ESource */ + if (!GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->create_source) { + g_debug ("Can't create task list: not supported by %s", G_OBJECT_TYPE_NAME (provider)); + return; + } + + source = GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->create_source (self); + if (!source) { + g_debug ("Can't create task list: create_source() returned NULL"); + return; + } + + /* EDS properties */ + e_source_set_display_name (source, name); + + data = g_new0 (AsyncData, 1); + data->title = g_strdup (name); + data->source = g_object_ref (source); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_create_task_list); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_or_update_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_create_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + gtd_object_pop_loading (GTD_OBJECT (self)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_update_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + g_assert (GTD_IS_TASK_LIST_EDS (list)); + g_assert (gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)) != NULL); + + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + gtd_object_push_loading (GTD_OBJECT (list)); + + data = g_new0 (AsyncData, 1); + data->list = g_object_ref (list); + data->source = g_object_ref (source); + + task = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_update_task_list); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_or_update_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_update_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + AsyncData *data; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + + gtd_object_pop_loading (GTD_OBJECT (data->list)); + gtd_object_pop_loading (GTD_OBJECT (self)); + + g_signal_emit_by_name (self, "list-changed", data->list); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_remove_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + g_assert (GTD_IS_TASK_LIST_EDS (list)); + g_assert (gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)) != NULL); + + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->source = g_object_ref (source); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_remove_task_list); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, remove_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_remove_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GTD_ENTRY; + + gtd_object_pop_loading (GTD_OBJECT (provider)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static GList* +gtd_provider_eds_get_task_lists (GtdProvider *provider) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (provider)); + + return g_hash_table_get_values (priv->task_lists); +} + +static GtdTaskList* +gtd_provider_eds_get_inbox (GtdProvider *provider) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (provider)); + + return g_hash_table_lookup (priv->task_lists, GTD_PROVIDER_EDS_INBOX_ID); +} + +static void +gtd_provider_iface_init (GtdProviderInterface *iface) +{ + iface->get_id = gtd_provider_eds_get_id; + iface->get_name = gtd_provider_eds_get_name; + iface->get_provider_type = gtd_provider_eds_get_provider_type; + iface->get_description = gtd_provider_eds_get_description; + iface->get_enabled = gtd_provider_eds_get_enabled; + iface->refresh = gtd_provider_eds_refresh; + iface->get_icon = gtd_provider_eds_get_icon; + iface->create_task = gtd_provider_eds_create_task; + iface->create_task_finish = gtd_provider_eds_create_task_finish; + iface->update_task = gtd_provider_eds_update_task; + iface->update_task_finish = gtd_provider_eds_update_task_finish; + iface->remove_task = gtd_provider_eds_remove_task; + iface->remove_task_finish = gtd_provider_eds_remove_task_finish; + iface->create_task_list = gtd_provider_eds_create_task_list; + iface->create_task_list_finish = gtd_provider_eds_create_task_list_finish; + iface->update_task_list = gtd_provider_eds_update_task_list; + iface->update_task_list_finish = gtd_provider_eds_update_task_list_finish; + iface->remove_task_list = gtd_provider_eds_remove_task_list; + iface->remove_task_list_finish = gtd_provider_eds_remove_task_list_finish; + iface->get_task_lists = gtd_provider_eds_get_task_lists; + iface->get_inbox = gtd_provider_eds_get_inbox; +} + + +/* + * GObject overrides + */ + +static void +gtd_provider_eds_finalize (GObject *object) +{ + GtdProviderEds *self = (GtdProviderEds *)object; + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->cancellable); + g_clear_object (&priv->source_registry); + g_clear_pointer (&priv->task_lists, g_hash_table_destroy); + + G_OBJECT_CLASS (gtd_provider_eds_parent_class)->finalize (object); +} + +static void +gtd_provider_eds_constructed (GObject *object) +{ + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + g_autoptr (GError) error = NULL; + GList *sources; + GList *l; + + self = GTD_PROVIDER_EDS (object); + priv = gtd_provider_eds_get_instance_private (self); + + if (error) + { + g_warning ("%s: %s", "Error loading task manager", error->message); + return; + } + + /* Load task list sources */ + sources = e_source_registry_list_sources (priv->source_registry, E_SOURCE_EXTENSION_TASK_LIST); + + for (l = sources; l != NULL; l = l->next) + on_source_added_cb (self, l->data); + + g_list_free_full (sources, g_object_unref); + + /* listen to the signals, so new sources don't slip by */ + g_signal_connect_swapped (priv->source_registry, + "source-added", + G_CALLBACK (on_source_added_cb), + self); + + g_signal_connect_swapped (priv->source_registry, + "source-removed", + G_CALLBACK (on_source_removed_cb), + self); +} + +static void +gtd_provider_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProvider *provider = GTD_PROVIDER (object); + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (object)); + + + switch (prop_id) + { + case PROP_DESCRIPTION: + g_value_set_string (value, gtd_provider_eds_get_description (provider)); + break; + + case PROP_ENABLED: + g_value_set_boolean (value, gtd_provider_eds_get_enabled (provider)); + break; + + case PROP_ICON: + g_value_set_object (value, gtd_provider_eds_get_icon (provider)); + break; + + case PROP_ID: + g_value_set_string (value, gtd_provider_eds_get_id (provider)); + break; + + case PROP_NAME: + g_value_set_string (value, gtd_provider_eds_get_name (provider)); + break; + + case PROP_PROVIDER_TYPE: + g_value_set_string (value, gtd_provider_eds_get_provider_type (provider)); + break; + + case PROP_REGISTRY: + g_value_set_object (value, priv->source_registry); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_eds_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderEds *self = GTD_PROVIDER_EDS (object); + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + switch (prop_id) + { + case PROP_REGISTRY: + if (g_set_object (&priv->source_registry, g_value_get_object (value))) + g_object_notify (object, "registry"); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_eds_class_init (GtdProviderEdsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_provider_eds_finalize; + object_class->constructed = gtd_provider_eds_constructed; + object_class->get_property = gtd_provider_eds_get_property; + object_class->set_property = gtd_provider_eds_set_property; + + g_object_class_override_property (object_class, PROP_DESCRIPTION, "description"); + g_object_class_override_property (object_class, PROP_ENABLED, "enabled"); + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_ID, "id"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PROVIDER_TYPE, "provider-type"); + + g_object_class_install_property (object_class, + PROP_REGISTRY, + g_param_spec_object ("registry", + "Source registry", + "The EDS source registry object", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + +static void +gtd_provider_eds_init (GtdProviderEds *self) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + priv->cancellable = g_cancellable_new (); + priv->task_lists = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); +} + +GtdProviderEds* +gtd_provider_eds_new (ESourceRegistry *registry) +{ + return g_object_new (GTD_TYPE_PROVIDER_EDS, + "registry", registry, + NULL); +} + +ESourceRegistry* +gtd_provider_eds_get_registry (GtdProviderEds *provider) +{ + GtdProviderEdsPrivate *priv; + + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + priv = gtd_provider_eds_get_instance_private (provider); + + return priv->source_registry; +} diff --git a/src/plugins/eds/gtd-provider-eds.h b/src/plugins/eds/gtd-provider-eds.h new file mode 100644 index 0000000..5ffe4ff --- /dev/null +++ b/src/plugins/eds/gtd-provider-eds.h @@ -0,0 +1,61 @@ +/* gtd-provider-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "endeavour.h" + +#include "gtd-eds.h" + +#include + +G_BEGIN_DECLS + +#define GTD_PROVIDER_EDS_INBOX_ID "system-task-list" + +#define GTD_TYPE_PROVIDER_EDS (gtd_provider_eds_get_type()) + +G_DECLARE_DERIVABLE_TYPE (GtdProviderEds, gtd_provider_eds, GTD, PROVIDER_EDS, GtdObject) + +struct _GtdProviderEdsClass +{ + GtdObjectClass parent; + + const gchar* (*get_id) (GtdProviderEds *self); + + const gchar* (*get_name) (GtdProviderEds *self); + + const gchar* (*get_provider_type) (GtdProviderEds *self); + + const gchar* (*get_description) (GtdProviderEds *self); + + gboolean (*get_enabled) (GtdProviderEds *self); + + GIcon* (*get_icon) (GtdProviderEds *self); + + ESource* (*create_source) (GtdProviderEds *self); + + gboolean (*should_load_source) (GtdProviderEds *provider, + ESource *source); +}; + +GtdProviderEds* gtd_provider_eds_new (ESourceRegistry *registry); + +ESourceRegistry* gtd_provider_eds_get_registry (GtdProviderEds *local); + +G_END_DECLS diff --git a/src/plugins/eds/gtd-provider-goa.c b/src/plugins/eds/gtd-provider-goa.c new file mode 100644 index 0000000..05cacd0 --- /dev/null +++ b/src/plugins/eds/gtd-provider-goa.c @@ -0,0 +1,262 @@ +/* gtd-provider-goa.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderGoa" + +#include "gtd-eds-autoptr.h" +#include "gtd-provider-eds.h" +#include "gtd-provider-goa.h" + +#include + +struct _GtdProviderGoa +{ + GtdProviderEds parent; + + GoaAccount *account; + GIcon *icon; + + gchar *id; +}; + +G_DEFINE_TYPE (GtdProviderGoa, gtd_provider_goa, GTD_TYPE_PROVIDER_EDS) + +enum +{ + PROP_0, + PROP_ACCOUNT, + N_PROPS +}; + + +/* + * GtdProviderEds overrides + */ + +static const gchar* +gtd_provider_goa_get_id (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return self->id; +} + +static const gchar* +gtd_provider_goa_get_name (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_provider_name (self->account); +} + +static const gchar* +gtd_provider_goa_get_provider_type (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_provider_type (self->account); +} + +static const gchar* +gtd_provider_goa_get_description (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_identity (self->account); +} + +static gboolean +gtd_provider_goa_get_enabled (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return !goa_account_get_calendar_disabled (self->account); +} + +static GIcon* +gtd_provider_goa_get_icon (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return self->icon; +} + +static void +gtd_provider_goa_set_account (GtdProviderGoa *provider, + GoaAccount *account) +{ + g_autofree gchar *icon_name = NULL; + + if (provider->account == account) + return; + + g_set_object (&provider->account, account); + g_object_notify (G_OBJECT (provider), "account"); + + g_debug ("Setting up Online Account: %s (%s)", + goa_account_get_identity (account), + goa_account_get_id (account)); + + /* Update icon */ + icon_name = g_strdup_printf ("goa-account-%s", goa_account_get_provider_type (provider->account)); + g_set_object (&provider->icon, g_themed_icon_new (icon_name)); + g_object_notify (G_OBJECT (provider), "icon"); + + /* Provider id */ + provider->id = g_strdup_printf ("%s@%s", + goa_account_get_provider_type (provider->account), + goa_account_get_id (provider->account)); +} + + +/* + * GObject overrides + */ + +static void +gtd_provider_goa_finalize (GObject *object) +{ + GtdProviderGoa *self = (GtdProviderGoa *)object; + + g_clear_pointer (&self->id, g_free); + + g_clear_object (&self->account); + g_clear_object (&self->icon); + + G_OBJECT_CLASS (gtd_provider_goa_parent_class)->finalize (object); +} + +static void +gtd_provider_goa_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (object); + + switch (prop_id) + { + + case PROP_ACCOUNT: + g_value_set_object (value, self->account); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_goa_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (object); + + switch (prop_id) + { + case PROP_ACCOUNT: + gtd_provider_goa_set_account (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +gtd_provider_goa_should_load_source (GtdProviderEds *provider, + ESource *source) +{ + g_autoptr (ESource) ancestor = NULL; + GtdProviderGoa *self; + gboolean retval; + + self = GTD_PROVIDER_GOA (provider); + retval = FALSE; + + ancestor = e_source_registry_find_extension (gtd_provider_eds_get_registry (provider), + source, + E_SOURCE_EXTENSION_GOA); + + /* If we detect that the given source is provided by a GOA account, check the account id */ + if (ancestor) + { + ESourceExtension *extension; + const gchar *ancestor_id; + const gchar *account_id; + + extension = e_source_get_extension (ancestor, E_SOURCE_EXTENSION_GOA); + ancestor_id = e_source_goa_get_account_id (E_SOURCE_GOA (extension)); + account_id = goa_account_get_id (self->account); + + /* When the ancestor's GOA id matches the current account's id, we shall load this list */ + retval = g_strcmp0 (ancestor_id, account_id) == 0; + } + + return retval; +} + +static void +gtd_provider_goa_class_init (GtdProviderGoaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtdProviderEdsClass *eds_class = GTD_PROVIDER_EDS_CLASS (klass); + + eds_class->get_id = gtd_provider_goa_get_id; + eds_class->get_name = gtd_provider_goa_get_name; + eds_class->get_provider_type = gtd_provider_goa_get_provider_type; + eds_class->get_description = gtd_provider_goa_get_description; + eds_class->get_enabled = gtd_provider_goa_get_enabled; + eds_class->get_icon = gtd_provider_goa_get_icon; + eds_class->should_load_source = gtd_provider_goa_should_load_source; + + object_class->finalize = gtd_provider_goa_finalize; + object_class->get_property = gtd_provider_goa_get_property; + object_class->set_property = gtd_provider_goa_set_property; + + g_object_class_install_property (object_class, + PROP_ACCOUNT, + g_param_spec_object ("account", + "Account of the provider", + "The Online Account of the provider", + GOA_TYPE_ACCOUNT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +gtd_provider_goa_init (GtdProviderGoa *self) +{ +} + +GtdProviderGoa* +gtd_provider_goa_new (ESourceRegistry *registry, + GoaAccount *account) +{ + return g_object_new (GTD_TYPE_PROVIDER_GOA, + "account", account, + "registry", registry, + NULL); +} + +GoaAccount* +gtd_provider_goa_get_account (GtdProviderGoa *provider) +{ + return provider->account; +} diff --git a/src/plugins/eds/gtd-provider-goa.h b/src/plugins/eds/gtd-provider-goa.h new file mode 100644 index 0000000..42625cd --- /dev/null +++ b/src/plugins/eds/gtd-provider-goa.h @@ -0,0 +1,43 @@ +/* gtd-provider-goa.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PROVIDER_GOA_H +#define GTD_PROVIDER_GOA_H + +#define GOA_API_IS_SUBJECT_TO_CHANGE 1 + +#include "endeavour.h" +#include "gtd-provider-eds.h" + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_GOA (gtd_provider_goa_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderGoa, gtd_provider_goa, GTD, PROVIDER_GOA, GtdProviderEds) + +GtdProviderGoa* gtd_provider_goa_new (ESourceRegistry *registry, + GoaAccount *account); + +GoaAccount* gtd_provider_goa_get_account (GtdProviderGoa *provider); + +G_END_DECLS + +#endif /* GTD_PROVIDER_GOA_H */ diff --git a/src/plugins/eds/gtd-provider-local.c b/src/plugins/eds/gtd-provider-local.c new file mode 100644 index 0000000..c5651ab --- /dev/null +++ b/src/plugins/eds/gtd-provider-local.c @@ -0,0 +1,150 @@ +/* gtd-provider-local.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdProviderLocal" + +#include "gtd-provider-local.h" +#include "gtd-task-list-eds.h" + +#include + +struct _GtdProviderLocal +{ + GtdProviderEds parent; + + GIcon *icon; + GList *tasklists; +}; + +G_DEFINE_TYPE (GtdProviderLocal, gtd_provider_local, GTD_TYPE_PROVIDER_EDS) + + +/* + * GtdProviderEds overrides + */ + +static const gchar* +gtd_provider_local_get_id (GtdProviderEds *provider) +{ + return "local"; +} + +static const gchar* +gtd_provider_local_get_name (GtdProviderEds *provider) +{ + return _("On This Computer"); +} + +static const gchar* +gtd_provider_local_get_provider_type (GtdProviderEds *provider) +{ + return "local"; +} + +static const gchar* +gtd_provider_local_get_description (GtdProviderEds *provider) +{ + return _("Local"); +} + +static gboolean +gtd_provider_local_get_enabled (GtdProviderEds *provider) +{ + return TRUE; +} + +static GIcon* +gtd_provider_local_get_icon (GtdProviderEds *provider) +{ + GtdProviderLocal *self = GTD_PROVIDER_LOCAL (provider); + + return self->icon; +} + +static ESource* +gtd_provider_local_create_source (GtdProviderEds *provider) +{ + ESourceExtension *extension; + ESource *source; + + /* Create the source */ + source = e_source_new (NULL, NULL, NULL); + + if (!source) + return NULL; + + /* Make it a local source */ + extension = e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST); + + e_source_set_parent (source, "local-stub"); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (extension), "local"); + + return source; +} + +static void +gtd_provider_local_finalize (GObject *object) +{ + GtdProviderLocal *self = (GtdProviderLocal *)object; + + g_clear_object (&self->icon); + + G_OBJECT_CLASS (gtd_provider_local_parent_class)->finalize (object); +} + +static gboolean +gtd_provider_local_should_load_source (GtdProviderEds *provider, + ESource *source) +{ + if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + return g_strcmp0 (e_source_get_parent (source), "local-stub") == 0; + + return FALSE; +} + +static void +gtd_provider_local_class_init (GtdProviderLocalClass *klass) +{ + GtdProviderEdsClass *eds_class = GTD_PROVIDER_EDS_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + eds_class->get_id = gtd_provider_local_get_id; + eds_class->get_name = gtd_provider_local_get_name; + eds_class->get_provider_type = gtd_provider_local_get_provider_type; + eds_class->get_description = gtd_provider_local_get_description; + eds_class->get_enabled = gtd_provider_local_get_enabled; + eds_class->get_icon = gtd_provider_local_get_icon; + eds_class->create_source = gtd_provider_local_create_source; + eds_class->should_load_source = gtd_provider_local_should_load_source; + + object_class->finalize = gtd_provider_local_finalize; +} + +static void +gtd_provider_local_init (GtdProviderLocal *self) +{ + self->icon = G_ICON (g_themed_icon_new_with_default_fallbacks ("computer-symbolic")); +} + +GtdProviderLocal* +gtd_provider_local_new (ESourceRegistry *registry) +{ + return g_object_new (GTD_TYPE_PROVIDER_LOCAL, + "registry", registry, + NULL); +} diff --git a/src/plugins/eds/gtd-provider-local.h b/src/plugins/eds/gtd-provider-local.h new file mode 100644 index 0000000..90ff8a6 --- /dev/null +++ b/src/plugins/eds/gtd-provider-local.h @@ -0,0 +1,37 @@ +/* gtd-provider-local.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PROVIDER_LOCAL_H +#define GTD_PROVIDER_LOCAL_H + +#include "endeavour.h" +#include "gtd-provider-eds.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_LOCAL (gtd_provider_local_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderLocal, gtd_provider_local, GTD, PROVIDER_LOCAL, GtdProviderEds) + +GtdProviderLocal* gtd_provider_local_new (ESourceRegistry *source_registry); + +G_END_DECLS + +#endif /* GTD_PROVIDER_LOCAL_H */ diff --git a/src/plugins/eds/gtd-task-eds.c b/src/plugins/eds/gtd-task-eds.c new file mode 100644 index 0000000..5dc667f --- /dev/null +++ b/src/plugins/eds/gtd-task-eds.c @@ -0,0 +1,650 @@ +/* gtd-task-eds.c + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTaskEds" + +#include "gtd-eds-autoptr.h" +#include "gtd-task-eds.h" + +#define ICAL_X_ENDEAVOUR_POSITION "X-ENDEAVOUR-POSITION" + +struct _GtdTaskEds +{ + GtdTask parent; + + ECalComponent *component; + ECalComponent *new_component; + + gchar *description; +}; + +G_DEFINE_TYPE (GtdTaskEds, gtd_task_eds, GTD_TYPE_TASK) + +enum +{ + PROP_0, + PROP_COMPONENT, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static GDateTime* +convert_icaltime (const ICalTime *date) +{ + GDateTime *dt; + + if (!date) + return NULL; + + dt = g_date_time_new_utc (i_cal_time_get_year (date), + i_cal_time_get_month (date), + i_cal_time_get_day (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_hour (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_minute (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_second (date)); + + return dt; +} + +static void +set_description (GtdTaskEds *self, + const gchar *description) +{ + ECalComponentText *text; + GSList note; + + text = e_cal_component_text_new (description ? description : "", NULL); + + note.data = text; + note.next = NULL; + + g_clear_pointer (&self->description, g_free); + self->description = g_strdup (description); + + e_cal_component_set_descriptions (self->new_component, (description && *description) ? ¬e : NULL); + + e_cal_component_text_free (text); +} + +static void +setup_description (GtdTaskEds *self) +{ + g_autofree gchar *desc = NULL; + GSList *text_list; + GSList *l; + + /* concatenates the multiple descriptions a task may have */ + text_list = e_cal_component_get_descriptions (self->new_component); + + for (l = text_list; l != NULL; l = l->next) + { + if (l->data != NULL) + { + ECalComponentText *text; + gchar *carrier; + + text = l->data; + + if (desc) + { + carrier = g_strconcat (desc, + "\n", + e_cal_component_text_get_value (text), + NULL); + g_free (desc); + desc = carrier; + } + else + { + desc = g_strdup (e_cal_component_text_get_value (text)); + } + } + } + + set_description (self, desc); + + g_slist_free_full (text_list, e_cal_component_text_free); +} + + +/* + * GtdObject overrides + */ + +static const gchar* +gtd_task_eds_get_uid (GtdObject *object) +{ + GtdTaskEds *self; + const gchar *uid; + + g_return_val_if_fail (GTD_IS_TASK (object), NULL); + + self = GTD_TASK_EDS (object); + + if (self->new_component) + uid = e_cal_component_get_uid (self->new_component); + else + uid = NULL; + + return uid; +} + +static void +gtd_task_eds_set_uid (GtdObject *object, + const gchar *uid) +{ + GtdTaskEds *self; + const gchar *current_uid; + + g_return_if_fail (GTD_IS_TASK (object)); + + self = GTD_TASK_EDS (object); + + if (!self->new_component) + return; + + current_uid = e_cal_component_get_uid (self->new_component); + + if (g_strcmp0 (current_uid, uid) != 0) + { + e_cal_component_set_uid (self->new_component, uid); + + g_object_notify (G_OBJECT (object), "uid"); + } +} + + +/* + * GtdTask overrides + */ + +static GDateTime* +gtd_task_eds_get_completion_date (GtdTask *task) +{ + ICalTime *idt; + GtdTaskEds *self; + GDateTime *dt; + + self = GTD_TASK_EDS (task); + dt = NULL; + + idt = e_cal_component_get_completed (self->new_component); + + if (idt) + dt = convert_icaltime (idt); + + g_clear_object (&idt); + + return dt; +} + +static void +gtd_task_eds_set_completion_date (GtdTask *task, + GDateTime *dt) +{ + GtdTaskEds *self; + ICalTime *idt; + + self = GTD_TASK_EDS (task); + + idt = i_cal_time_new_null_time (); + i_cal_time_set_date (idt, + g_date_time_get_year (dt), + g_date_time_get_month (dt), + g_date_time_get_day_of_month (dt)); + i_cal_time_set_time (idt, + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_seconds (dt)); + i_cal_time_set_timezone (idt, i_cal_timezone_get_utc_timezone ()); + + /* convert timezone + * + * FIXME: This does not do anything until we have an ical + * timezone associated with the task + */ + i_cal_time_convert_timezone (idt, NULL, i_cal_timezone_get_utc_timezone ()); + + e_cal_component_set_completed (self->new_component, idt); + + g_object_unref (idt); +} + +static gboolean +gtd_task_eds_get_complete (GtdTask *task) +{ + ICalPropertyStatus status; + GtdTaskEds *self; + gboolean completed; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), FALSE); + + self = GTD_TASK_EDS (task); + + status = e_cal_component_get_status (self->new_component); + completed = status == I_CAL_STATUS_COMPLETED; + + return completed; +} + +static void +gtd_task_eds_set_complete (GtdTask *task, + gboolean complete) +{ + ICalPropertyStatus status; + GtdTaskEds *self; + gint percent; + g_autoptr (GDateTime) now = NULL; + + self = GTD_TASK_EDS (task); + now = complete ? g_date_time_new_now_local () : NULL; + + if (complete) + { + percent = 100; + status = I_CAL_STATUS_COMPLETED; + } + else + { + percent = 0; + status = I_CAL_STATUS_NEEDSACTION; + } + + e_cal_component_set_percent_complete (self->new_component, percent); + e_cal_component_set_status (self->new_component, status); + gtd_task_eds_set_completion_date (task, now); +} + +static GDateTime* +gtd_task_eds_get_creation_date (GtdTask *task) +{ + ICalTime *idt; + GtdTaskEds *self; + GDateTime *dt; + + self = GTD_TASK_EDS (task); + dt = NULL; + + idt = e_cal_component_get_created (self->new_component); + + if (idt) + dt = convert_icaltime (idt); + + g_clear_object (&idt); + + return dt; +} + +static void +gtd_task_eds_set_creation_date (GtdTask *task, + GDateTime *dt) +{ + g_assert_not_reached (); +} + +static const gchar* +gtd_task_eds_get_description (GtdTask *task) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + return self->description ? self->description : ""; +} + +static void +gtd_task_eds_set_description (GtdTask *task, + const gchar *description) +{ + set_description (GTD_TASK_EDS (task), description); +} + +static GDateTime* +gtd_task_eds_get_due_date (GtdTask *task) +{ + ECalComponentDateTime *comp_dt; + GtdTaskEds *self; + GDateTime *date; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), NULL); + + self = GTD_TASK_EDS (task); + + comp_dt = e_cal_component_get_due (self->new_component); + if (!comp_dt) + return NULL; + + date = convert_icaltime (e_cal_component_datetime_get_value (comp_dt)); + e_cal_component_datetime_free (comp_dt); + + return date; +} + +static void +gtd_task_eds_set_due_date (GtdTask *task, + GDateTime *dt) +{ + GtdTaskEds *self; + GDateTime *current_dt; + + g_assert (GTD_IS_TASK_EDS (task)); + + self = GTD_TASK_EDS (task); + + current_dt = gtd_task_get_due_date (task); + + if (dt != current_dt) + { + ECalComponentDateTime *comp_dt; + ICalTime *idt; + + comp_dt = NULL; + idt = NULL; + + if (!current_dt || + (current_dt && + dt && + g_date_time_compare (current_dt, dt) != 0)) + { + idt = i_cal_time_new_null_time (); + + g_date_time_ref (dt); + + /* Copy the given dt */ + i_cal_time_set_date (idt, + g_date_time_get_year (dt), + g_date_time_get_month (dt), + g_date_time_get_day_of_month (dt)); + i_cal_time_set_time (idt, + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_seconds (dt)); + i_cal_time_set_is_date (idt, + i_cal_time_get_hour (idt) == 0 && + i_cal_time_get_minute (idt) == 0 && + i_cal_time_get_second (idt) == 0); + + comp_dt = e_cal_component_datetime_new_take (idt, g_strdup ("UTC")); + + e_cal_component_set_due (self->new_component, comp_dt); + + e_cal_component_datetime_free (comp_dt); + + g_date_time_unref (dt); + } + else if (!dt) + { + e_cal_component_set_due (self->new_component, NULL); + } + } + + g_clear_pointer (¤t_dt, g_date_time_unref); +} + +static gboolean +gtd_task_eds_get_important (GtdTask *task) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + return e_cal_component_get_priority (self->new_component) > 0; +} + +static void +gtd_task_eds_set_important (GtdTask *task, + gboolean important) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + e_cal_component_set_priority (self->new_component, important ? 3 : -1); +} + +static gint64 +gtd_task_eds_get_position (GtdTask *task) +{ + g_autofree gchar *value = NULL; + ICalComponent *ical_comp; + gint64 position = -1; + GtdTaskEds *self; + + self = GTD_TASK_EDS (task); + ical_comp = e_cal_component_get_icalcomponent (self->new_component); + + value = e_cal_util_component_dup_x_property (ical_comp, ICAL_X_ENDEAVOUR_POSITION); + if (value) + position = g_ascii_strtoll (value, NULL, 10); + + return position; +} + +void +gtd_task_eds_set_position (GtdTask *task, + gint64 position) +{ + g_autofree gchar *value = NULL; + ICalComponent *ical_comp; + GtdTaskEds *self; + + self = GTD_TASK_EDS (task); + if (position != -1) + value = g_strdup_printf ("%" G_GINT64_FORMAT, position); + ical_comp = e_cal_component_get_icalcomponent (self->new_component); + + e_cal_util_component_set_x_property (ical_comp, ICAL_X_ENDEAVOUR_POSITION, value); +} + +static const gchar* +gtd_task_eds_get_title (GtdTask *task) +{ + GtdTaskEds *self; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), NULL); + + self = GTD_TASK_EDS (task); + + return i_cal_component_get_summary (e_cal_component_get_icalcomponent (self->new_component)); +} + +static void +gtd_task_eds_set_title (GtdTask *task, + const gchar *title) +{ + ECalComponentText *new_summary; + GtdTaskEds *self; + + g_return_if_fail (GTD_IS_TASK_EDS (task)); + g_return_if_fail (g_utf8_validate (title, -1, NULL)); + + self = GTD_TASK_EDS (task); + + new_summary = e_cal_component_text_new (title, NULL); + + e_cal_component_set_summary (self->new_component, new_summary); + + e_cal_component_text_free (new_summary); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_eds_finalize (GObject *object) +{ + GtdTaskEds *self = (GtdTaskEds *)object; + + g_clear_object (&self->component); + g_clear_object (&self->new_component); + + G_OBJECT_CLASS (gtd_task_eds_parent_class)->finalize (object); +} + +static void +gtd_task_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskEds *self = GTD_TASK_EDS (object); + + switch (prop_id) + { + case PROP_COMPONENT: + g_value_set_object (value, self->component); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_eds_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskEds *self = GTD_TASK_EDS (object); + + switch (prop_id) + { + case PROP_COMPONENT: + gtd_task_eds_set_component (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_eds_class_init (GtdTaskEdsClass *klass) +{ + GtdObjectClass *gtd_object_class = GTD_OBJECT_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtdTaskClass *task_class = GTD_TASK_CLASS (klass); + + object_class->finalize = gtd_task_eds_finalize; + object_class->get_property = gtd_task_eds_get_property; + object_class->set_property = gtd_task_eds_set_property; + + task_class->get_complete = gtd_task_eds_get_complete; + task_class->set_complete = gtd_task_eds_set_complete; + task_class->get_creation_date = gtd_task_eds_get_creation_date; + task_class->set_creation_date = gtd_task_eds_set_creation_date; + task_class->get_completion_date = gtd_task_eds_get_completion_date; + task_class->set_completion_date = gtd_task_eds_set_completion_date; + task_class->get_description = gtd_task_eds_get_description; + task_class->set_description = gtd_task_eds_set_description; + task_class->get_due_date = gtd_task_eds_get_due_date; + task_class->set_due_date = gtd_task_eds_set_due_date; + task_class->get_important = gtd_task_eds_get_important; + task_class->set_important = gtd_task_eds_set_important; + task_class->get_position = gtd_task_eds_get_position; + task_class->set_position = gtd_task_eds_set_position; + task_class->get_title = gtd_task_eds_get_title; + task_class->set_title = gtd_task_eds_set_title; + + gtd_object_class->get_uid = gtd_task_eds_get_uid; + gtd_object_class->set_uid = gtd_task_eds_set_uid; + + properties[PROP_COMPONENT] = g_param_spec_object ("component", + "Component", + "Component", + E_TYPE_CAL_COMPONENT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_task_eds_init (GtdTaskEds *self) +{ +} + +GtdTask* +gtd_task_eds_new (ECalComponent *component) +{ + return g_object_new (GTD_TYPE_TASK_EDS, + "component", component, + NULL); +} + +ECalComponent* +gtd_task_eds_get_component (GtdTaskEds *self) +{ + g_return_val_if_fail (GTD_IS_TASK_EDS (self), NULL); + + return self->new_component; +} + +void +gtd_task_eds_set_component (GtdTaskEds *self, + ECalComponent *component) +{ + GObject *object; + + g_return_if_fail (GTD_IS_TASK_EDS (self)); + g_return_if_fail (E_IS_CAL_COMPONENT (component)); + + if (!g_set_object (&self->component, component)) + return; + + object = G_OBJECT (self); + + g_clear_object (&self->new_component); + self->new_component = e_cal_component_clone (component); + + setup_description (self); + + g_object_notify (object, "complete"); + g_object_notify (object, "creation-date"); + g_object_notify (object, "description"); + g_object_notify (object, "due-date"); + g_object_notify (object, "important"); + g_object_notify (object, "position"); + g_object_notify (object, "title"); + g_object_notify_by_pspec (object, properties[PROP_COMPONENT]); +} + +void +gtd_task_eds_apply (GtdTaskEds *self) +{ + g_return_if_fail (GTD_IS_TASK_EDS (self)); + + e_cal_component_commit_sequence (self->new_component); + + /* Make new_component the actual component */ + gtd_task_eds_set_component (self, self->new_component); +} + +void +gtd_task_eds_revert (GtdTaskEds *self) +{ + g_autoptr (ECalComponent) component = NULL; + + g_return_if_fail (GTD_IS_TASK_EDS (self)); + + component = e_cal_component_clone (self->component); + + gtd_task_eds_set_component (self, component); +} diff --git a/src/plugins/eds/gtd-task-eds.h b/src/plugins/eds/gtd-task-eds.h new file mode 100644 index 0000000..078f585 --- /dev/null +++ b/src/plugins/eds/gtd-task-eds.h @@ -0,0 +1,46 @@ +/* gtd-task-eds.h + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_EDS_H +#define GTD_TASK_EDS_H + +#include "endeavour.h" + +#include "gtd-eds.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_EDS (gtd_task_eds_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskEds, gtd_task_eds, GTD, TASK_EDS, GtdTask) + +GtdTask* gtd_task_eds_new (ECalComponent *component); + +ECalComponent* gtd_task_eds_get_component (GtdTaskEds *self); + +void gtd_task_eds_set_component (GtdTaskEds *self, + ECalComponent *component); + +void gtd_task_eds_apply (GtdTaskEds *self); + +void gtd_task_eds_revert (GtdTaskEds *self); + +G_END_DECLS + +#endif /* GTD_TASK_EDS_H */ + diff --git a/src/plugins/eds/gtd-task-list-eds.c b/src/plugins/eds/gtd-task-list-eds.c new file mode 100644 index 0000000..19bcdab --- /dev/null +++ b/src/plugins/eds/gtd-task-list-eds.c @@ -0,0 +1,875 @@ +/* gtd-task-list-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdTaskListEds" + +#include "e-source-endeavour.h" +#include "gtd-debug.h" +#include "gtd-eds-autoptr.h" +#include "gtd-provider-eds.h" +#include "gtd-task-eds.h" +#include "gtd-task-list-eds.h" + +#include + +struct _GtdTaskListEds +{ + GtdTaskList parent; + + ECalClient *client; + ECalClientView *client_view; + ESource *source; + + GCancellable *cancellable; +}; + +typedef struct +{ + GtdProvider *provider; + ESource *source; + ECalClient *client; +} NewTaskListData; + +static void on_client_objects_modified_for_migration_cb (GObject *object, + GAsyncResult *result, + gpointer user_data); + +G_DEFINE_TYPE (GtdTaskListEds, gtd_task_list_eds, GTD_TYPE_TASK_LIST) + +enum +{ + PROP_0, + PROP_CLIENT, + PROP_SOURCE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +new_task_list_data_free (gpointer data) +{ + NewTaskListData *list_data = data; + + if (!list_data) + return; + + g_clear_object (&list_data->provider); + g_clear_object (&list_data->source); + g_clear_object (&list_data->client); + g_free (list_data); +} + +static void +update_changed_tasks (GtdTaskListEds *self, + GHashTable *changed_tasks) +{ + g_autoptr (GSList) components = NULL; + GHashTableIter iter; + GtdProvider *provider; + GtdTask *task; + guint n_changed_tasks; + + GTD_ENTRY; + + n_changed_tasks = g_hash_table_size (changed_tasks); + provider = gtd_task_list_get_provider (GTD_TASK_LIST (self)); + + /* Nothing changed, list is ready */ + if (n_changed_tasks == 0) + { + gtd_object_pop_loading (GTD_OBJECT (provider)); + g_signal_emit_by_name (provider, "list-added", self); + GTD_RETURN (); + } + + GTD_TRACE_MSG ("%u task(s) changed", n_changed_tasks); + + g_hash_table_iter_init (&iter, changed_tasks); + while (g_hash_table_iter_next (&iter, (gpointer *) &task, NULL)) + { + ICalComponent *ical_comp; + ECalComponent *comp; + + comp = gtd_task_eds_get_component (GTD_TASK_EDS (task)); + ical_comp = e_cal_component_get_icalcomponent (comp); + + components = g_slist_prepend (components, ical_comp); + } + + e_cal_client_modify_objects (self->client, + components, + E_CAL_OBJ_MOD_THIS, + E_CAL_OPERATION_FLAG_NONE, + self->cancellable, + on_client_objects_modified_for_migration_cb, + self); + + GTD_EXIT; +} + +static void +migrate_to_v1 (GtdTaskListEds *self, + GHashTable *changed_tasks) +{ + GListModel *model; + guint n_tasks; + guint i; + + model = G_LIST_MODEL (self); + n_tasks = g_list_model_get_n_items (model); + + for (i = 0; i < n_tasks; i++) + { + g_autoptr (GtdTask) task = NULL; + + task = g_list_model_get_item (model, i); + + /* Don't notify to avoid carpet-bombing GtdTaskList */ + g_object_freeze_notify (G_OBJECT (task)); + + gtd_task_set_position (task, i); + + g_hash_table_add (changed_tasks, task); + } + + for (i = 0; i < n_tasks; i++) + { + g_autoptr (GtdTask) task = NULL; + + task = g_list_model_get_item (model, i); + g_object_thaw_notify (G_OBJECT (task)); + } +} + +struct +{ + guint api_version; + void (* migrate) (GtdTaskListEds *self, + GHashTable *changed_tasks); +} +migration_vtable[] = +{ + { 0, migrate_to_v1 }, +}; + +static void +maybe_migrate_todo_api_version (GtdTaskListEds *self) +{ + g_autoptr (GHashTable) changed_tasks = NULL; + ESourceEndeavour *endeavour_extension; + gboolean api_version_changed; + guint api_version; + guint i; + + GTD_ENTRY; + + /* + * Ensure the type so that it is available for introspection when + * calling e_source_get_extension(). + */ + g_type_ensure (E_TYPE_SOURCE_ENDEAVOUR); + + api_version_changed = FALSE; + endeavour_extension = e_source_get_extension (self->source, E_SOURCE_EXTENSION_ENDEAVOUR); + api_version = e_source_endeavour_get_api_version (endeavour_extension); + changed_tasks = g_hash_table_new (g_direct_hash, g_direct_equal); + + g_debug ("%s: Endeavour API version %u", + gtd_task_list_get_name (GTD_TASK_LIST (self)), + api_version); + + for (i = 0; i < G_N_ELEMENTS (migration_vtable); i++) + { + guint new_api_version = i + 1; + + if (api_version > migration_vtable[i].api_version) + continue; + + g_debug (" Migrating task list to Endeavour API v%u", new_api_version); + + migration_vtable[i].migrate (self, changed_tasks); + + e_source_endeavour_set_api_version (endeavour_extension, new_api_version); + api_version_changed = TRUE; + } + + if (api_version_changed) + { + g_debug ("Saving new API version"); + + e_source_write (self->source, NULL, NULL, NULL); + } + + update_changed_tasks (self, changed_tasks); + + GTD_EXIT; +} + + +/* + * Callbacks + */ + +static gboolean +new_task_list_in_idle_cb (gpointer data) +{ + g_autoptr (GtdTaskListEds) list_eds = NULL; + g_autoptr (GTask) task = data; + NewTaskListData *list_data; + + list_data = g_task_get_task_data (task); + list_eds = g_object_new (GTD_TYPE_TASK_LIST_EDS, + "provider", list_data->provider, + "source", list_data->source, + "client", list_data->client, + NULL); + + g_task_return_pointer (task, g_steal_pointer (&list_eds), NULL); + + return G_SOURCE_REMOVE; +} + +static void +on_client_objects_modified_for_migration_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdProvider *provider; + GtdTaskListEds *self; + + GTD_ENTRY; + + self = GTD_TASK_LIST_EDS (user_data); + provider = gtd_task_list_get_provider (GTD_TASK_LIST (self)); + + e_cal_client_modify_objects_finish (self->client, result, &error); + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error migrating tasks to new API version: %s", error->message); + + gtd_object_pop_loading (GTD_OBJECT (provider)); + g_signal_emit_by_name (provider, "list-added", self); + + GTD_EXIT; +} + +static void +on_view_objects_added_cb (ECalClientView *view, + const GSList *objects, + GtdTaskList *self) +{ + g_autoptr (ECalClient) client = NULL; + GSList *l; + + GTD_ENTRY; + + client = e_cal_client_view_ref_client (view); + + for (l = (GSList*) objects; l; l = l->next) + { + g_autoptr (ECalComponent) component = NULL; + GtdTask *task; + const gchar *uid; + + component = e_cal_component_new_from_icalcomponent (i_cal_component_clone (l->data)); + uid = e_cal_component_get_uid (component); + + task = gtd_task_list_get_task_by_id (self, uid); + + /* If the task already exists, we must instead update its component */ + if (task) + { + gtd_task_eds_set_component (GTD_TASK_EDS (task), component); + + gtd_task_list_update_task (self, task); + + GTD_TRACE_MSG ("Updated task '%s' to tasklist '%s'", + gtd_task_get_title (task), + gtd_task_list_get_name (self)); + + continue; + } + + /* Add the new task */ + task = gtd_task_eds_new (component); + gtd_task_set_list (task, self); + + gtd_task_list_add_task (self, task); + + GTD_TRACE_MSG ("Added task '%s' (%s) to tasklist '%s'", + gtd_task_get_title (task), + gtd_object_get_uid (GTD_OBJECT (task)), + gtd_task_list_get_name (self)); + } + + GTD_EXIT; +} + +static void +on_view_objects_modified_cb (ECalClientView *view, + const GSList *objects, + GtdTaskList *self) +{ + g_autoptr (ECalClient) client = NULL; + GSList *l; + + GTD_ENTRY; + + client = e_cal_client_view_ref_client (view); + + for (l = (GSList*) objects; l; l = l->next) + { + g_autoptr (ECalComponent) component = NULL; + GtdTask *task; + const gchar *uid; + + component = e_cal_component_new_from_icalcomponent (i_cal_component_clone (l->data)); + uid = e_cal_component_get_uid (component); + + task = gtd_task_list_get_task_by_id (self, uid); + + if (!task) + continue; + + gtd_task_eds_set_component (GTD_TASK_EDS (task), component); + + GTD_TRACE_MSG ("Updated task '%s' from tasklist '%s'", + gtd_task_get_title (GTD_TASK (task)), + gtd_task_list_get_name (self)); + } + + GTD_EXIT; +} + +static void +on_view_objects_removed_cb (ECalClientView *view, + const GSList *uids, + GtdTaskList *self) +{ + GSList *l; + + GTD_ENTRY; + + for (l = (GSList*) uids; l; l = l->next) + { + ECalComponentId *id; + GtdTask *task; + + id = l->data; + task = gtd_task_list_get_task_by_id (self, e_cal_component_id_get_uid (id)); + + if (!task) + continue; + + gtd_task_list_remove_task (self, task); + + GTD_TRACE_MSG ("Removed task '%s' from tasklist '%s'", + gtd_task_get_title (task), + gtd_task_list_get_name (self)); + } + + GTD_EXIT; +} + +static void +on_view_completed_cb (ECalClientView *view, + const GError *error, + GtdTaskList *self) +{ + gtd_object_pop_loading (GTD_OBJECT (gtd_manager_get_default ())); + gtd_object_pop_loading (GTD_OBJECT (self)); + + if (error) + { + g_warning ("Error fetching tasks from list: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Error fetching tasks from list"), + error->message, + NULL, + NULL); + return; + } + + maybe_migrate_todo_api_version (GTD_TASK_LIST_EDS (self)); +} + +static void +on_client_view_acquired_cb (GObject *client, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdTaskListEds *self; + + self = GTD_TASK_LIST_EDS (user_data); + + e_cal_client_get_view_finish (E_CAL_CLIENT (client), result, &self->client_view, &error); + + if (error) + { + g_warning ("Error fetching tasks from list: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Error fetching tasks from list"), + error->message, + NULL, + NULL); + return; + } + + g_debug ("ECalClientView for tasklist '%s' successfully acquired", + gtd_task_list_get_name (GTD_TASK_LIST (self))); + + g_signal_connect (self->client_view, "objects-added", G_CALLBACK (on_view_objects_added_cb), self); + g_signal_connect (self->client_view, "objects-removed", G_CALLBACK (on_view_objects_removed_cb), self); + g_signal_connect (self->client_view, "objects-modified", G_CALLBACK (on_view_objects_modified_cb), self); + g_signal_connect (self->client_view, "complete", G_CALLBACK (on_view_completed_cb), self); + + gtd_object_push_loading (GTD_OBJECT (self)); + + e_cal_client_view_start (self->client_view, &error); + + if (error) + { + g_warning ("Error starting view: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Error fetching tasks from list"), + error->message, + NULL, + NULL); + } +} + +static void +on_source_removable_changed_cb (GtdTaskListEds *list) +{ + gtd_task_list_set_is_removable (GTD_TASK_LIST (list), + e_source_get_removable (list->source) || + e_source_get_remote_deletable (list->source)); +} + +static void +on_source_selectable_selected_changed_cb (ESourceSelectable *selectable, + GParamSpec *pspec, + GtdTaskListEds *self) +{ + g_debug ("%s (%s): ESourceSelectable:selected changed, notifying...", + e_source_get_uid (self->source), + e_source_get_display_name (self->source)); + + g_object_notify (G_OBJECT (self), "archived"); +} + +static void +on_save_task_list_finished_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GtdTaskListEds *list; + GError *error; + + list = user_data; + error = NULL; + + gtd_object_pop_loading (GTD_OBJECT (list)); + + e_source_write_finish (E_SOURCE (source), result, &error); + + if (error) + { + g_warning ("%s: %s: %s", + G_STRFUNC, + "Error saving task list", + error->message); + g_clear_error (&error); + } +} + +static void +save_task_list (GtdTaskListEds *list) +{ + if (e_source_get_writable (list->source)) + { + if (!list->cancellable) + list->cancellable = g_cancellable_new (); + + gtd_object_push_loading (GTD_OBJECT (list)); + + e_source_write (list->source, + list->cancellable, + on_save_task_list_finished_cb, + list); + } +} + +static gboolean +color_to_string (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GdkRGBA *color; + gchar *color_str; + + color = g_value_get_boxed (from_value); + color_str = gdk_rgba_to_string (color); + + g_value_set_string (to_value, color_str); + + g_free (color_str); + + return TRUE; +} + +static gboolean +string_to_color (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + GdkRGBA color; + + if (!gdk_rgba_parse (&color, g_value_get_string (from_value))) + gdk_rgba_parse (&color, "#ffffff"); /* calendar default colour */ + + g_value_set_boxed (to_value, &color); + + return TRUE; +} + + +/* + * GtdTaskList overrides + */ + +static gboolean +gtd_task_list_eds_get_archived (GtdTaskList *list) +{ + ESourceSelectable *selectable; + GtdTaskListEds *self; + + self = GTD_TASK_LIST_EDS (list); + selectable = e_source_get_extension (self->source, E_SOURCE_EXTENSION_TASK_LIST); + + return !e_source_selectable_get_selected (selectable); +} + +static void +gtd_task_list_eds_set_archived (GtdTaskList *list, + gboolean archived) +{ + ESourceSelectable *selectable; + GtdTaskListEds *self; + + GTD_ENTRY; + + self = GTD_TASK_LIST_EDS (list); + selectable = e_source_get_extension (self->source, E_SOURCE_EXTENSION_TASK_LIST); + + g_signal_handlers_block_by_func (selectable, on_source_selectable_selected_changed_cb, self); + + e_source_selectable_set_selected (selectable, !archived); + + g_signal_handlers_unblock_by_func (selectable, on_source_selectable_selected_changed_cb, self); + + GTD_EXIT; +} + + +/* + * GtdObject overrides + */ + +static const gchar* +gtd_task_list_eds_get_uid (GtdObject *object) +{ + GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); + + return e_source_get_uid (self->source); +} + +static void +gtd_task_list_eds_set_uid (GtdObject *object, + const gchar *uid) +{ + g_assert_not_reached (); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_eds_finalize (GObject *object) +{ + GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); + + g_cancellable_cancel (self->cancellable); + + g_clear_object (&self->cancellable); + g_clear_object (&self->client); + g_clear_object (&self->client_view); + g_clear_object (&self->source); + + G_OBJECT_CLASS (gtd_task_list_eds_parent_class)->finalize (object); +} + +static void +gtd_task_list_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); + + switch (prop_id) + { + case PROP_CLIENT: + g_value_set_object (value, self->client); + break; + + case PROP_SOURCE: + g_value_set_object (value, self->source); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_eds_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); + + switch (prop_id) + { + case PROP_CLIENT: + if (!g_set_object (&self->client, g_value_get_object (value)) || !self->client) + return; + + e_cal_client_get_view (self->client, + "#t", + self->cancellable, + on_client_view_acquired_cb, + self); + + g_object_notify (object, "client"); + break; + + case PROP_SOURCE: + gtd_task_list_eds_set_source (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_eds_class_init (GtdTaskListEdsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtdObjectClass *gtd_object_class = GTD_OBJECT_CLASS (klass); + GtdTaskListClass *task_list_class = GTD_TASK_LIST_CLASS (klass); + + task_list_class->get_archived = gtd_task_list_eds_get_archived; + task_list_class->set_archived = gtd_task_list_eds_set_archived; + + gtd_object_class->get_uid = gtd_task_list_eds_get_uid; + gtd_object_class->set_uid = gtd_task_list_eds_set_uid; + + object_class->finalize = gtd_task_list_eds_finalize; + object_class->get_property = gtd_task_list_eds_get_property; + object_class->set_property = gtd_task_list_eds_set_property; + + + /** + * GtdTaskListEds::client: + * + * The #ECalClient of this #GtdTaskListEds + */ + g_object_class_install_property (object_class, + PROP_CLIENT, + g_param_spec_object ("client", + "ECalClient of this list", + "The ECalClient of this list", + E_TYPE_CAL_CLIENT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); + + /** + * GtdTaskListEds::source: + * + * The #ESource of this #GtdTaskListEds + */ + g_object_class_install_property (object_class, + PROP_SOURCE, + g_param_spec_object ("source", + "ESource of this list", + "The ESource of this list", + E_TYPE_SOURCE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); +} + +static void +gtd_task_list_eds_init (GtdTaskListEds *self) +{ +} + +void +gtd_task_list_eds_new (GtdProvider *provider, + ESource *source, + ECalClient *client, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + NewTaskListData *data; + + data = g_new (NewTaskListData, 1); + data->provider = g_object_ref (provider); + data->source = g_object_ref (source); + data->client = g_object_ref (client); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_task_data (task, data, new_task_list_data_free); + g_task_set_source_tag (task, gtd_task_list_eds_new); + + g_idle_add (new_task_list_in_idle_cb, g_steal_pointer (&task)); +} + +GtdTaskListEds* +gtd_task_list_eds_new_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_TASK (result), NULL); + g_return_val_if_fail (!error || !*error, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +ESource* +gtd_task_list_eds_get_source (GtdTaskListEds *list) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_EDS (list), NULL); + + return list->source; +} + +void +gtd_task_list_eds_set_source (GtdTaskListEds *self, + ESource *source) +{ + ESourceSelectable *selectable; + ESourceRefresh *refresh; + GdkRGBA color; + gboolean is_inbox; + + g_return_if_fail (GTD_IS_TASK_LIST_EDS (self)); + + if (!g_set_object (&self->source, source)) + return; + + is_inbox = g_str_equal (e_source_get_uid (source), GTD_PROVIDER_EDS_INBOX_ID); + + /* Setup color */ + selectable = E_SOURCE_SELECTABLE (e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST)); + + if (!gdk_rgba_parse (&color, e_source_selectable_get_color (selectable))) + gdk_rgba_parse (&color, "#ffffff"); /* calendar default color */ + + gtd_task_list_set_color (GTD_TASK_LIST (self), &color); + + g_object_bind_property_full (self, + "color", + selectable, + "color", + G_BINDING_BIDIRECTIONAL, + color_to_string, + string_to_color, + self, + NULL); + + g_signal_connect_object (selectable, + "notify::selected", + G_CALLBACK (on_source_selectable_selected_changed_cb), + self, + 0); + + /* Setup tasklist name */ + if (is_inbox) + gtd_task_list_set_name (GTD_TASK_LIST (self), _("Inbox")); + else + gtd_task_list_set_name (GTD_TASK_LIST (self), e_source_get_display_name (source)); + + g_object_bind_property (source, + "display-name", + self, + "name", + G_BINDING_BIDIRECTIONAL); + + /* Save the task list every time something changes */ + g_signal_connect_swapped (source, + "notify", + G_CALLBACK (save_task_list), + self); + + /* Update ::is-removable property */ + gtd_task_list_set_is_removable (GTD_TASK_LIST (self), + e_source_get_removable (source) || + e_source_get_remote_deletable (source)); + + g_signal_connect_swapped (source, + "notify::removable", + G_CALLBACK (on_source_removable_changed_cb), + self); + + g_signal_connect_swapped (source, + "notify::remote-deletable", + G_CALLBACK (on_source_removable_changed_cb), + self); + + /* Refresh timeout */ + refresh = e_source_get_extension (source, E_SOURCE_EXTENSION_REFRESH); + e_source_refresh_set_enabled (refresh, TRUE); + e_source_refresh_set_interval_minutes (refresh, 5); + + e_source_write (source, NULL, NULL, NULL); + + g_object_notify (G_OBJECT (self), "source"); +} + +ECalClient* +gtd_task_list_eds_get_client (GtdTaskListEds *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_EDS (self), NULL); + + return self->client; +} diff --git a/src/plugins/eds/gtd-task-list-eds.h b/src/plugins/eds/gtd-task-list-eds.h new file mode 100644 index 0000000..5a71295 --- /dev/null +++ b/src/plugins/eds/gtd-task-list-eds.h @@ -0,0 +1,53 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_TASK_LIST_EDS_H +#define GTD_TASK_LIST_EDS_H + +#include "endeavour.h" + +#include "gtd-eds.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_EDS (gtd_task_list_eds_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListEds, gtd_task_list_eds, GTD, TASK_LIST_EDS, GtdTaskList) + +void gtd_task_list_eds_new (GtdProvider *provider, + ESource *source, + ECalClient *client, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data); + +GtdTaskListEds* gtd_task_list_eds_new_finish (GAsyncResult *result, + GError **error); + +ESource* gtd_task_list_eds_get_source (GtdTaskListEds *list); + +void gtd_task_list_eds_set_source (GtdTaskListEds *list, + ESource *source); + +ECalClient* gtd_task_list_eds_get_client (GtdTaskListEds *self); + +G_END_DECLS + +#endif /* GTD_TASK_LIST_EDS_H */ diff --git a/src/plugins/eds/meson.build b/src/plugins/eds/meson.build new file mode 100644 index 0000000..50705a3 --- /dev/null +++ b/src/plugins/eds/meson.build @@ -0,0 +1,27 @@ +plugins_ldflags += ['-Wl,--undefined=gtd_plugin_eds_register_types'] + +################ +# Dependencies # +################ + +plugins_deps += [ + dependency('libecal-2.0', version: '>= 3.33.2'), + dependency('libedataserver-1.2', version: '>= 3.32.0'), +] + +plugins_sources += files( + 'e-source-endeavour.c', + 'gtd-plugin-eds.c', + 'gtd-provider-eds.c', + 'gtd-provider-goa.c', + 'gtd-provider-local.c', + 'gtd-task-eds.c', + 'gtd-task-list-eds.c', + 'eds-plugin.c', +) + +plugins_sources += gnome.compile_resources( + 'eds-resources', + 'eds.gresource.xml', + c_name: 'eds_plugin', +) diff --git a/src/plugins/inbox-panel/gtd-inbox-panel.c b/src/plugins/inbox-panel/gtd-inbox-panel.c new file mode 100644 index 0000000..3b1bc9a --- /dev/null +++ b/src/plugins/inbox-panel/gtd-inbox-panel.c @@ -0,0 +1,275 @@ +/* gtd-inbox-panel.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdInboxPanel" + +#include "gtd-inbox-panel.h" + +#include "endeavour.h" + +#include +#include + + +#define GTD_INBOX_PANEL_NAME "inbox-panel" +#define GTD_INBOX_PANEL_PRIORITY 2000 + +struct _GtdInboxPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdInboxPanel, gtd_inbox_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + GtdTask *task; + + task = (GtdTask*) item; + + /* + * The Inbox task list is not explicitly declared here because it's included + * in the filter already and should be filtered out in other lists + */ + return !gtd_task_get_complete (task); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdInboxPanel *self) +{ + guint n_items = g_list_model_get_n_items (model); + + if (self->number_of_tasks == n_items) + return; + + self->number_of_tasks = n_items; + g_object_notify (G_OBJECT (self), "subtitle"); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_inbox_get_panel_name (GtdPanel *panel) +{ + return GTD_INBOX_PANEL_NAME; +} + +static const gchar* +gtd_panel_inbox_get_panel_title (GtdPanel *panel) +{ + return _("Inbox"); +} + +static GList* +gtd_panel_inbox_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_inbox_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_inbox_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_INBOX_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_inbox_get_priority (GtdPanel *panel) +{ + return GTD_INBOX_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_inbox_get_subtitle (GtdPanel *panel) +{ + GtdInboxPanel *self = GTD_INBOX_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_inbox_get_panel_name; + iface->get_panel_title = gtd_panel_inbox_get_panel_title; + iface->get_header_widgets = gtd_panel_inbox_get_header_widgets; + iface->get_menu = gtd_panel_inbox_get_menu; + iface->get_icon = gtd_panel_inbox_get_icon; + iface->get_priority = gtd_panel_inbox_get_priority; + iface->get_subtitle = gtd_panel_inbox_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_inbox_panel_finalize (GObject *object) +{ + GtdInboxPanel *self = (GtdInboxPanel *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + + G_OBJECT_CLASS (gtd_inbox_panel_parent_class)->finalize (object); +} + +static void +gtd_inbox_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdInboxPanel *self = GTD_INBOX_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_INBOX_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_INBOX_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_inbox_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_inbox_panel_class_init (GtdInboxPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_inbox_panel_finalize; + object_class->get_property = gtd_inbox_panel_get_property; + object_class->set_property = gtd_inbox_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_inbox_panel_init (GtdInboxPanel *self) +{ + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *filter; + + self->icon = g_themed_icon_new ("mail-inbox-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->filter_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), FALSE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + g_signal_connect_object (self->filter_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_inbox_panel_new (void) +{ + return g_object_new (GTD_TYPE_INBOX_PANEL, NULL); +} + diff --git a/src/plugins/inbox-panel/gtd-inbox-panel.h b/src/plugins/inbox-panel/gtd-inbox-panel.h new file mode 100644 index 0000000..779e6f2 --- /dev/null +++ b/src/plugins/inbox-panel/gtd-inbox-panel.h @@ -0,0 +1,32 @@ +/* gtd-inbox-panel.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_INBOX_PANEL (gtd_inbox_panel_get_type()) +G_DECLARE_FINAL_TYPE (GtdInboxPanel, gtd_inbox_panel, GTD, INBOX_PANEL, GtkBox) + +GtkWidget* gtd_inbox_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/inbox-panel/inbox-panel-plugin.c b/src/plugins/inbox-panel/inbox-panel-plugin.c new file mode 100644 index 0000000..b5a180c --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel-plugin.c @@ -0,0 +1,32 @@ +/* gtd-plugin-inbox-panel.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdPluginInboxPanel" + +#include "endeavour.h" +#include "gtd-inbox-panel.h" + +G_MODULE_EXPORT void +inbox_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_INBOX_PANEL); +} diff --git a/src/plugins/inbox-panel/inbox-panel.gresource.xml b/src/plugins/inbox-panel/inbox-panel.gresource.xml new file mode 100644 index 0000000..b8e9454 --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel.gresource.xml @@ -0,0 +1,6 @@ + + + + inbox-panel.plugin + + diff --git a/src/plugins/inbox-panel/inbox-panel.plugin b/src/plugins/inbox-panel/inbox-panel.plugin new file mode 100644 index 0000000..1ee0114 --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Inbox +Module = inbox-panel +Description = A panel to show tasks in the inbox +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Hidden = true +Builtin = true +License = GPL +Loader = C +Embedded = inbox_panel_plugin_register_types +Depends = diff --git a/src/plugins/inbox-panel/meson.build b/src/plugins/inbox-panel/meson.build new file mode 100644 index 0000000..06c2463 --- /dev/null +++ b/src/plugins/inbox-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=inbox_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-inbox-panel.c', + 'inbox-panel-plugin.c' +) + +plugins_sources += gnome.compile_resources( + 'inbox-panel-resources', + 'inbox-panel.gresource.xml', + c_name: 'inbox_panel_plugin', +) diff --git a/src/plugins/meson.build b/src/plugins/meson.build new file mode 100644 index 0000000..01b37a2 --- /dev/null +++ b/src/plugins/meson.build @@ -0,0 +1,36 @@ +plugins_incs = [ + incs, + include_directories('..'), +] + +# Ensure enum types header is generated before building plugins +plugins_sources = [ gtd_enum_types[1] ] +plugins_deps = [ endeavour_deps ] +plugins_ldflags = [] +plugins_libs = [] +plugins_confs = [] + +plugins_conf = configuration_data() +plugins_conf.set('VERSION', endeavour_version) + +subdir('all-tasks-panel') +subdir('eds') +subdir('inbox-panel') +subdir('next-week-panel') +subdir('peace') +subdir('scheduled-panel') +subdir('task-lists-workspace') +subdir('today-panel') + +plugins_lib = static_library( + 'plugins', + plugins_sources, + dependencies: plugins_deps, + include_directories: plugins_incs, + link_with: plugins_libs, + link_args: plugins_ldflags, +) + +plugins_dep = declare_dependency( + link_whole: plugins_lib, +) diff --git a/src/plugins/next-week-panel/gtd-next-week-panel.c b/src/plugins/next-week-panel/gtd-next-week-panel.c new file mode 100644 index 0000000..33ea031 --- /dev/null +++ b/src/plugins/next-week-panel/gtd-next-week-panel.c @@ -0,0 +1,573 @@ +/* gtd-next-week-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdNextWeekPanel" + +#include "gtd-next-week-panel.h" + +#include "endeavour.h" + +#include +#include + + +#define GTD_NEXT_WEEK_PANEL_NAME "next-week-panel" +#define GTD_NEXT_WEEK_PANEL_PRIORITY 700 + +struct _GtdNextWeekPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkFilterListModel *incomplete_model; + GtkSortListModel *sort_model; + + GtkCssProvider *css_provider; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdNextWeekPanel, gtd_next_week_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +load_css_provider (GtdNextWeekPanel *self) +{ + g_autoptr (GSettings) settings = NULL; + g_autoptr (GFile) css_file = NULL; + g_autofree gchar *theme_name = NULL; + g_autofree gchar *theme_uri = NULL; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/plugins/next-week-panel/theme", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/next-week-panel/theme/Adwaita.css"); +} + +static gboolean +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); + + return TRUE; +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + /* This case should never happen */ + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < 0) + { + str = g_strdup (_("Overdue")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-top", first_header ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint before_diff, current_diff; + + before_dt = gtd_task_get_due_date (previous_task); + + get_date_offset (before_dt, &before_diff, NULL); + get_date_offset (dt, ¤t_diff, NULL); + + if ((before_diff < 0 && current_diff >= 0) || + (before_diff >= 0 && current_diff >= 0 && before_diff != current_diff)) + { + text = get_string_for_date (dt, &span); + } + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GDateTime *dt1; + GDateTime *dt2; + GtdTask *task1; + GtdTask *task2; + gint retval; + gchar *t1; + gchar *t2; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + /* First, compare by ::due-date. */ + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = compare_by_date (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Third, compare by ::creation-date. */ + dt1 = gtd_task_get_creation_date (task1); + dt2 = gtd_task_get_creation_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Finally, compare by ::title. */ + t1 = t2 = NULL; + + t1 = g_utf8_casefold (gtd_task_get_title (task1), -1); + t2 = g_utf8_casefold (gtd_task_get_title (task2), -1); + + retval = g_strcmp0 (t1, t2); + + g_free (t1); + g_free (t2); + + return retval; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + GtdTask *task; + gboolean complete; + gint days_offset; + + task = (GtdTask*) item; + complete = gtd_task_get_complete (task); + task_dt = gtd_task_get_due_date (task); + + return task_dt != NULL && + get_date_offset (task_dt, &days_offset, NULL) && + days_offset < 7 && + ((days_offset < 0 && !complete) || days_offset >= 0); +} + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + + return !gtd_task_get_complete (task); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdNextWeekPanel *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_next_week_get_panel_name (GtdPanel *panel) +{ + return GTD_NEXT_WEEK_PANEL_NAME; +} + +static const gchar* +gtd_panel_next_week_get_panel_title (GtdPanel *panel) +{ + return _("Next 7 Days"); +} + +static GList* +gtd_panel_next_week_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_next_week_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_next_week_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_NEXT_WEEK_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_next_week_get_priority (GtdPanel *panel) +{ + return GTD_NEXT_WEEK_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_next_week_get_subtitle (GtdPanel *panel) +{ + GtdNextWeekPanel *self = GTD_NEXT_WEEK_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_next_week_get_panel_name; + iface->get_panel_title = gtd_panel_next_week_get_panel_title; + iface->get_header_widgets = gtd_panel_next_week_get_header_widgets; + iface->get_menu = gtd_panel_next_week_get_menu; + iface->get_icon = gtd_panel_next_week_get_icon; + iface->get_priority = gtd_panel_next_week_get_priority; + iface->get_subtitle = gtd_panel_next_week_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_next_week_panel_finalize (GObject *object) +{ + GtdNextWeekPanel *self = (GtdNextWeekPanel *)object; + + g_clear_object (&self->css_provider); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->incomplete_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_next_week_panel_parent_class)->finalize (object); +} + +static void +gtd_next_week_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdNextWeekPanel *self = GTD_NEXT_WEEK_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_NEXT_WEEK_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_NEXT_WEEK_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_next_week_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_next_week_panel_class_init (GtdNextWeekPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_next_week_panel_finalize; + object_class->get_property = gtd_next_week_panel_get_property; + object_class->set_property = gtd_next_week_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_next_week_panel_init (GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) now = g_date_time_new_now_local (); + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *incomplete_filter; + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("view-tasks-week-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + incomplete_filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + self->incomplete_model = gtk_filter_list_model_new (G_LIST_MODEL (self->sort_model), + GTK_FILTER (incomplete_filter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), TRUE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (GTD_TASK_LIST_VIEW (self->view), + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->incomplete_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); + load_css_provider (self); +} + +GtkWidget* +gtd_next_week_panel_new (void) +{ + return g_object_new (GTD_TYPE_NEXT_WEEK_PANEL, NULL); +} diff --git a/src/plugins/next-week-panel/gtd-next-week-panel.h b/src/plugins/next-week-panel/gtd-next-week-panel.h new file mode 100644 index 0000000..cfda880 --- /dev/null +++ b/src/plugins/next-week-panel/gtd-next-week-panel.h @@ -0,0 +1,34 @@ +/* gtd-next-week-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_NEXT_WEEK_PANEL (gtd_next_week_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdNextWeekPanel, gtd_next_week_panel, GTD, NEXT_WEEK_PANEL, GtkBox) + +GtkWidget* gtd_next_week_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/next-week-panel/meson.build b/src/plugins/next-week-panel/meson.build new file mode 100644 index 0000000..618c266 --- /dev/null +++ b/src/plugins/next-week-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=next_week_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-next-week-panel.c', + 'next-week-panel-plugin.c' +) + +plugins_sources += gnome.compile_resources( + 'next-week-panel-resources', + 'next-week-panel.gresource.xml', + c_name: 'next_week_panel_plugin', +) diff --git a/src/plugins/next-week-panel/next-week-panel-plugin.c b/src/plugins/next-week-panel/next-week-panel-plugin.c new file mode 100644 index 0000000..29af419 --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel-plugin.c @@ -0,0 +1,33 @@ +/* gtd-plugin-next-week-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdPluginNextWeekPanel" + +#include "gtd-next-week-panel.h" + +#include "endeavour.h" + +G_MODULE_EXPORT void +next_week_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_NEXT_WEEK_PANEL); +} diff --git a/src/plugins/next-week-panel/next-week-panel.gresource.xml b/src/plugins/next-week-panel/next-week-panel.gresource.xml new file mode 100644 index 0000000..86d42c8 --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel.gresource.xml @@ -0,0 +1,7 @@ + + + + next-week-panel.plugin + theme/Adwaita.css + + diff --git a/src/plugins/next-week-panel/next-week-panel.plugin b/src/plugins/next-week-panel/next-week-panel.plugin new file mode 100644 index 0000000..0f029ad --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = Next 7 days +Module = next-week-panel +Description = A panel to show tasks scheduled for the next 7 days +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = next_week_panel_plugin_register_types +Depends = diff --git a/src/plugins/next-week-panel/theme/Adwaita.css b/src/plugins/next-week-panel/theme/Adwaita.css new file mode 100644 index 0000000..546b582 --- /dev/null +++ b/src/plugins/next-week-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: #4a90d9; + font-size: 16px; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 16px; + font-weight: bold; +} diff --git a/src/plugins/peace/gtd-peace-omni-area-addin.c b/src/plugins/peace/gtd-peace-omni-area-addin.c new file mode 100644 index 0000000..77c4e25 --- /dev/null +++ b/src/plugins/peace/gtd-peace-omni-area-addin.c @@ -0,0 +1,209 @@ +/* gtd-peace-omni-area-addin.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-peace-omni-area-addin.h" + +#include "config.h" + +#include + +#define MESSAGE_ID "peace-message-id" + +#define SWITCH_MESSAGE_TIMEOUT 20 * 60 +#define REMOVE_MESSAGE_TIMEOUT 1 * 60 + +struct _GtdPeaceOmniAreaAddin +{ + GObject parent; + + GtdOmniArea *omni_area; + + guint timeout_id; +}; + +static gboolean switch_message_cb (gpointer user_data); + +static void gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdPeaceOmniAreaAddin, gtd_peace_omni_area_addin, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_OMNI_AREA_ADDIN, gtd_omni_area_addin_iface_init)) + +typedef struct +{ + const gchar *text; + const gchar *icon_name; +} str_pair; + +const str_pair mindful_questions[] = +{ + { N_("Did you drink some water today?"), NULL }, + { N_("What are your goals for today?"), NULL }, + { N_("Can you let your creativity flow?"), NULL }, + { N_("How are you feeling right now?"), NULL }, + { N_("At what point is it good enough?"), NULL }, +}; + +const str_pair reminders[] = +{ + { N_("Remember to breathe. Good. Don't stop."), NULL }, + { N_("Don't forget to drink some water"), NULL }, + { N_("Remember to take some time off"), NULL }, + { N_("Eat fruits if you can 🍐️"), NULL }, + { N_("Take care of yourself"), NULL }, + { N_("Remember to have some fun"), NULL }, + { N_("You're doing great"), NULL }, +}; + +const str_pair inspiring_quotes[] = +{ + { N_("Smile, breathe and go slowly"), NULL }, + { N_("Wherever you go, there you are"), NULL }, + { N_("Working hard is always rewarded"), NULL }, + { N_("Keep calm"), NULL }, + { N_("You can do it"), NULL }, + { N_("Meanwhile, spread the love ♥️"), NULL }, +}; + + +/* + * Callbacks + */ + +static gboolean +remove_message_cb (gpointer user_data) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (user_data); + gint factor = g_random_int_range (2, 6); + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + + self->timeout_id = g_timeout_add_seconds (SWITCH_MESSAGE_TIMEOUT * factor, switch_message_cb, self); + + return G_SOURCE_REMOVE; +} + +static gboolean +switch_message_cb (gpointer user_data) +{ + GtdPeaceOmniAreaAddin *self; + g_autoptr (GIcon) icon = NULL; + const gchar *message; + const gchar *icon_name; + gint source; + + self = GTD_PEACE_OMNI_AREA_ADDIN (user_data); + source = g_random_int_range (0, 3); + + if (source == 0) + { + gint i = g_random_int_range (0, G_N_ELEMENTS (mindful_questions)); + + message = gettext (mindful_questions[i].text); + icon_name = mindful_questions[i].icon_name; + } + else if (source == 1) + { + gint i = g_random_int_range (0, G_N_ELEMENTS (reminders)); + + message = gettext (reminders[i].text); + icon_name = reminders[i].icon_name; + } + else + { + gint i = g_random_int_range (0, G_N_ELEMENTS (inspiring_quotes)); + + message = gettext (inspiring_quotes[i].text); + icon_name = inspiring_quotes[i].icon_name; + } + + if (icon_name) + icon = g_themed_icon_new (icon_name); + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + gtd_omni_area_push_message (self->omni_area, MESSAGE_ID, message, NULL); + + self->timeout_id = g_timeout_add_seconds (REMOVE_MESSAGE_TIMEOUT, remove_message_cb, self); + + return G_SOURCE_REMOVE; +} + + +/* + * GtdOmniAreaAddin iface + */ + +static void +gtd_today_omni_area_addin_omni_area_addin_load (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (addin); + + self->omni_area = omni_area; + + g_clear_handle_id (&self->timeout_id, g_source_remove); + self->timeout_id = g_timeout_add_seconds (SWITCH_MESSAGE_TIMEOUT, switch_message_cb, self); +} + +static void +gtd_today_omni_area_addin_omni_area_addin_unload (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (addin); + + gtd_omni_area_withdraw_message (omni_area, MESSAGE_ID); + + g_clear_handle_id (&self->timeout_id, g_source_remove); + self->omni_area = NULL; +} + +static void +gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface) +{ + iface->load = gtd_today_omni_area_addin_omni_area_addin_load; + iface->unload = gtd_today_omni_area_addin_omni_area_addin_unload; +} + + +/* + * GObject overrides + */ + +static void +gtd_peace_omni_area_addin_finalize (GObject *object) +{ + GtdPeaceOmniAreaAddin *self = (GtdPeaceOmniAreaAddin *)object; + + g_clear_handle_id (&self->timeout_id, g_source_remove); + + G_OBJECT_CLASS (gtd_peace_omni_area_addin_parent_class)->finalize (object); +} + +static void +gtd_peace_omni_area_addin_class_init (GtdPeaceOmniAreaAddinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_peace_omni_area_addin_finalize; +} + +static void +gtd_peace_omni_area_addin_init (GtdPeaceOmniAreaAddin *self) +{ +} diff --git a/src/plugins/peace/gtd-peace-omni-area-addin.h b/src/plugins/peace/gtd-peace-omni-area-addin.h new file mode 100644 index 0000000..8b83653 --- /dev/null +++ b/src/plugins/peace/gtd-peace-omni-area-addin.h @@ -0,0 +1,30 @@ +/* gtd-peace-omni-area-addin.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PEACE_OMNI_AREA_ADDIN (gtd_peace_omni_area_addin_get_type()) +G_DECLARE_FINAL_TYPE (GtdPeaceOmniAreaAddin, gtd_peace_omni_area_addin, GTD, PEACE_OMNI_AREA_ADDIN, GObject) + +G_END_DECLS diff --git a/src/plugins/peace/meson.build b/src/plugins/peace/meson.build new file mode 100644 index 0000000..3cba81b --- /dev/null +++ b/src/plugins/peace/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=peace_plugin_register_types'] + +plugins_sources += files( + 'gtd-peace-omni-area-addin.c', + 'peace-plugin.c', +) + +plugins_sources += gnome.compile_resources( + 'peace-resources', + 'peace.gresource.xml', + c_name: 'peace_plugin', +) diff --git a/src/plugins/peace/peace-plugin.c b/src/plugins/peace/peace-plugin.c new file mode 100644 index 0000000..5ece6fa --- /dev/null +++ b/src/plugins/peace/peace-plugin.c @@ -0,0 +1,31 @@ +/* zen-plugin.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#include "gtd-peace-omni-area-addin.h" + +G_MODULE_EXPORT void +peace_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_OMNI_AREA_ADDIN, + GTD_TYPE_PEACE_OMNI_AREA_ADDIN); +} diff --git a/src/plugins/peace/peace.gresource.xml b/src/plugins/peace/peace.gresource.xml new file mode 100644 index 0000000..13132ed --- /dev/null +++ b/src/plugins/peace/peace.gresource.xml @@ -0,0 +1,6 @@ + + + + peace.plugin + + diff --git a/src/plugins/peace/peace.plugin b/src/plugins/peace/peace.plugin new file mode 100644 index 0000000..d7e85ea --- /dev/null +++ b/src/plugins/peace/peace.plugin @@ -0,0 +1,12 @@ +[Plugin] +Name = Peace +Module = peace +Description = Smile, breathe and go slowly +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = peace_plugin_register_types +Depends = diff --git a/src/plugins/scheduled-panel/gtd-panel-scheduled.c b/src/plugins/scheduled-panel/gtd-panel-scheduled.c new file mode 100644 index 0000000..9b3e57e --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-panel-scheduled.c @@ -0,0 +1,518 @@ +/* gtd-panel-scheduled.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPanelScheduled" + +#include "endeavour.h" + +#include "gtd-panel-scheduled.h" + +#include +#include + +struct _GtdPanelScheduled +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkSortListModel *sort_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_EXTENDED (GtdPanelScheduled, gtd_panel_scheduled, GTK_TYPE_BOX, + 0, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, + gtd_panel_iface_init)) + +#define GTD_PANEL_SCHEDULED_NAME "panel-scheduled" +#define GTD_PANEL_SCHEDULED_PRIORITY 500 + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +static void +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + /* This case should never happen */ + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < -1) + { + /* Translators: This message will never be used with '1 day ago' + * but the singular form is required because some languages do not + * have plurals, some languages reuse the singular form for numbers + * like 21, 31, 41, etc. + */ + str = g_strdup_printf (g_dngettext (NULL, "%d day ago", "%d days ago", -days_diff), -days_diff); + } + else if (days_diff == -1) + { + str = g_strdup (_("Yesterday")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-start", 6, + "margin-bottom", 6, + "margin-top", first_header ? 6 : 18, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdPanelScheduled *panel) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint diff; + + before_dt = gtd_task_get_due_date (previous_task); + diff = compare_by_date (before_dt, dt); + + if (diff != 0) + text = get_string_for_date (dt, &span); + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GDateTime *dt1; + GDateTime *dt2; + GtdTask *task1; + GtdTask *task2; + gint retval; + gchar *t1; + gchar *t2; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + /* First, compare by ::due-date. */ + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = compare_by_date (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Second, compare by ::complete. */ + retval = gtd_task_get_complete (task1) - gtd_task_get_complete (task2); + + if (retval != 0) + return retval; + + /* Third, compare by ::creation-date. */ + dt1 = gtd_task_get_creation_date (task1); + dt2 = gtd_task_get_creation_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Finally, compare by ::title. */ + t1 = t2 = NULL; + + t1 = g_utf8_casefold (gtd_task_get_title (task1), -1); + t2 = g_utf8_casefold (gtd_task_get_title (task2), -1); + + retval = g_strcmp0 (t1, t2); + + g_free (t1); + g_free (t2); + + return retval; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + GtdTask *task; + + task = (GtdTask*) item; + task_dt = gtd_task_get_due_date (task); + + return !gtd_task_get_complete (task) && task_dt != NULL; +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdPanelScheduled *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdPanelScheduled *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + + +/********************** + * GtdPanel iface init + **********************/ +static const gchar* +gtd_panel_scheduled_get_panel_name (GtdPanel *panel) +{ + return GTD_PANEL_SCHEDULED_NAME; +} + +static const gchar* +gtd_panel_scheduled_get_panel_title (GtdPanel *panel) +{ + return _("Scheduled"); +} + +static GList* +gtd_panel_scheduled_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_scheduled_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_scheduled_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_PANEL_SCHEDULED (panel)->icon); +} + +static guint32 +gtd_panel_scheduled_get_priority (GtdPanel *panel) +{ + return GTD_PANEL_SCHEDULED_PRIORITY; +} + +static gchar* +gtd_panel_scheduled_get_subtitle (GtdPanel *panel) +{ + GtdPanelScheduled *self = GTD_PANEL_SCHEDULED (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_scheduled_get_panel_name; + iface->get_panel_title = gtd_panel_scheduled_get_panel_title; + iface->get_header_widgets = gtd_panel_scheduled_get_header_widgets; + iface->get_menu = gtd_panel_scheduled_get_menu; + iface->get_icon = gtd_panel_scheduled_get_icon; + iface->get_priority = gtd_panel_scheduled_get_priority; + iface->get_subtitle = gtd_panel_scheduled_get_subtitle; +} + +static void +gtd_panel_scheduled_finalize (GObject *object) +{ + GtdPanelScheduled *self = (GtdPanelScheduled *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_panel_scheduled_parent_class)->finalize (object); +} + +static void +gtd_panel_scheduled_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdPanelScheduled *self = GTD_PANEL_SCHEDULED (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_PANEL_SCHEDULED_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_PANEL_SCHEDULED_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_panel_scheduled_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_panel_scheduled_class_init (GtdPanelScheduledClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_panel_scheduled_finalize; + object_class->get_property = gtd_panel_scheduled_get_property; + object_class->set_property = gtd_panel_scheduled_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_panel_scheduled_init (GtdPanelScheduled *self) +{ + g_autoptr (GDateTime) now = g_date_time_new_now_local (); + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("alarm-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (self->view, G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (self->view, TRUE); + gtd_task_list_view_set_show_due_date (self->view, FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (self->view, + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->sort_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_panel_scheduled_new (void) +{ + return g_object_new (GTD_TYPE_PANEL_SCHEDULED, NULL); +} + diff --git a/src/plugins/scheduled-panel/gtd-panel-scheduled.h b/src/plugins/scheduled-panel/gtd-panel-scheduled.h new file mode 100644 index 0000000..59af3b5 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-panel-scheduled.h @@ -0,0 +1,35 @@ +/* gtd-panel-scheduled.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PANEL_SCHEDULED_H +#define GTD_PANEL_SCHEDULED_H + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PANEL_SCHEDULED (gtd_panel_scheduled_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPanelScheduled, gtd_panel_scheduled, GTD, PANEL_SCHEDULED, GtkBox) + +GtkWidget* gtd_panel_scheduled_new (void); + +G_END_DECLS + +#endif /* GTD_PANEL_SCHEDULED_H */ diff --git a/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c new file mode 100644 index 0000000..348cfd1 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c @@ -0,0 +1,153 @@ +/* gtd-plugin-scheduled-panel.c + * + * Copyright (C) 2016-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPluginScheduledPanel" + +#include "gtd-panel-scheduled.h" + +#include "gtd-plugin-scheduled-panel.h" + +#include +#include + +struct _GtdPluginScheduledPanel +{ + PeasExtensionBase parent; + + GtkCssProvider *css_provider; +}; + +static void gtd_activatable_iface_init (GtdActivatableInterface *iface); + +G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginScheduledPanel, gtd_plugin_scheduled_panel, PEAS_TYPE_EXTENSION_BASE, + 0, + G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE, + gtd_activatable_iface_init)) + +enum { + PROP_0, + PROP_PREFERENCES_PANEL, + N_PROPS +}; + +/* + * GtdActivatable interface implementation + */ +static void +gtd_plugin_scheduled_panel_activate (GtdActivatable *activatable) +{ + ; +} + +static void +gtd_plugin_scheduled_panel_deactivate (GtdActivatable *activatable) +{ + ; +} + +static GtkWidget* +gtd_plugin_scheduled_panel_get_preferences_panel (GtdActivatable *activatable) +{ + return NULL; +} + +static void +gtd_activatable_iface_init (GtdActivatableInterface *iface) +{ + iface->activate = gtd_plugin_scheduled_panel_activate; + iface->deactivate = gtd_plugin_scheduled_panel_deactivate; + iface->get_preferences_panel = gtd_plugin_scheduled_panel_get_preferences_panel; +} + +static void +gtd_plugin_scheduled_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_PREFERENCES_PANEL: + g_value_set_object (value, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_plugin_scheduled_panel_class_init (GtdPluginScheduledPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gtd_plugin_scheduled_panel_get_property; + + g_object_class_override_property (object_class, + PROP_PREFERENCES_PANEL, + "preferences-panel"); +} + +static void +gtd_plugin_scheduled_panel_init (GtdPluginScheduledPanel *self) +{ + GSettings *settings; + GFile* css_file; + gchar *theme_name; + gchar *theme_uri; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/theme/scheduled-panel", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/scheduled-panel/theme/Adwaita.css"); + + g_object_unref (settings); + g_object_unref (css_file); + g_free (theme_name); + g_free (theme_uri); +} + +static void +gtd_plugin_scheduled_panel_class_finalize (GtdPluginScheduledPanelClass *klass) +{ +} + +G_MODULE_EXPORT void +gtd_plugin_scheduled_panel_register_types (PeasObjectModule *module) +{ + gtd_plugin_scheduled_panel_register_type (G_TYPE_MODULE (module)); + + peas_object_module_register_extension_type (module, + GTD_TYPE_ACTIVATABLE, + GTD_TYPE_PLUGIN_SCHEDULED_PANEL); + + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_PANEL_SCHEDULED); +} diff --git a/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h new file mode 100644 index 0000000..e5391f3 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h @@ -0,0 +1,37 @@ +/* gtd-plugin-scheduled-panel.h + * + * Copyright (C) 2016 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PLUGIN_SCHEDULED_PANEL_H +#define GTD_PLUGIN_SCHEDULED_PANEL_H + +#include "endeavour.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PLUGIN_SCHEDULED_PANEL (gtd_plugin_scheduled_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPluginScheduledPanel, gtd_plugin_scheduled_panel, GTD, PLUGIN_SCHEDULED_PANEL, PeasExtensionBase) + +G_MODULE_EXPORT void gtd_plugin_scheduled_panel_register_types (PeasObjectModule *module); + +G_END_DECLS + +#endif /* GTD_PLUGIN_SCHEDULED_PANEL_H */ + diff --git a/src/plugins/scheduled-panel/meson.build b/src/plugins/scheduled-panel/meson.build new file mode 100644 index 0000000..e720573 --- /dev/null +++ b/src/plugins/scheduled-panel/meson.build @@ -0,0 +1,6 @@ +plugins_ldflags += ['-Wl,--undefined=gtd_plugin_scheduled_panel_register_types'] + +plugins_sources += files( + 'gtd-panel-scheduled.c', + 'gtd-plugin-scheduled-panel.c' +) diff --git a/src/plugins/scheduled-panel/scheduled-panel.gresource.xml b/src/plugins/scheduled-panel/scheduled-panel.gresource.xml new file mode 100644 index 0000000..21b04ae --- /dev/null +++ b/src/plugins/scheduled-panel/scheduled-panel.gresource.xml @@ -0,0 +1,7 @@ + + + + scheduled-panel.plugin + theme/Adwaita.css + + diff --git a/src/plugins/scheduled-panel/scheduled-panel.plugin b/src/plugins/scheduled-panel/scheduled-panel.plugin new file mode 100644 index 0000000..2cb78d8 --- /dev/null +++ b/src/plugins/scheduled-panel/scheduled-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = Scheduled tasks +Module = scheduled-panel +Description = A panel to show scheduled tasks +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = gtd_plugin_scheduled_panel_register_types +Depends = diff --git a/src/plugins/scheduled-panel/theme/Adwaita.css b/src/plugins/scheduled-panel/theme/Adwaita.css new file mode 100644 index 0000000..546b582 --- /dev/null +++ b/src/plugins/scheduled-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: #4a90d9; + font-size: 16px; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 16px; + font-weight: bold; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c new file mode 100644 index 0000000..5919665 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c @@ -0,0 +1,333 @@ +/* gtd-sidebar-list-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebarListRow" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-notification.h" +#include "gtd-provider.h" +#include "gtd-sidebar-list-row.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-utils.h" + +#include +#include + +struct _GtdSidebarListRow +{ + GtkListBoxRow parent; + + GtkImage *color_icon; + GtkLabel *name_label; + GtkLabel *tasks_counter_label; + + GtdTaskList *list; +}; + + +static void on_list_changed_cb (GtdSidebarListRow *self); + +static void on_list_color_changed_cb (GtdTaskList *list, + GParamSpec *pspec, + GtdSidebarListRow *self); + +G_DEFINE_TYPE (GtdSidebarListRow, gtd_sidebar_list_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_LIST, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +update_color_icon (GtdSidebarListRow *self) +{ + g_autoptr (GdkPaintable) paintable = NULL; + g_autoptr (GdkRGBA) color = NULL; + + color = gtd_task_list_get_color (self->list); + paintable = gtd_create_circular_paintable (color, 12); + + gtk_image_set_from_paintable (self->color_icon, paintable); +} + +static void +update_counter_label (GtdSidebarListRow *self) +{ + g_autofree gchar *label = NULL; + GListModel *model; + guint counter = 0; + guint i; + + model = G_LIST_MODEL (self->list); + + for (i = 0; i < g_list_model_get_n_items (model); i++) + counter += !gtd_task_get_complete (g_list_model_get_item (model, i)); + + label = counter > 0 ? g_strdup_printf ("%u", counter) : g_strdup (""); + + gtk_label_set_label (self->tasks_counter_label, label); +} + +static void +set_list (GtdSidebarListRow *self, + GtdTaskList *list) +{ + g_assert (list != NULL); + g_assert (self->list == NULL); + + self->list = g_object_ref (list); + + g_object_bind_property (list, + "name", + self->name_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (gtd_task_list_get_provider (list), + "enabled", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* Always keep the counter label updated */ + g_signal_connect_object (list, "task-added", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (list, "task-updated", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (list, "task-removed", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + + update_counter_label (self); + + /* And also the color icon */ + g_signal_connect_object (list, "notify::color", G_CALLBACK (on_list_color_changed_cb), self, 0); + + update_color_icon (self); +} + + +/* + * Callbacks + */ + +static void +on_import_task_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_task_list_import_task_finish (GTD_TASK_LIST (object), result, &error); + + if (error) + g_warning ("Error updating task: %s", error->message); +} + +static void +on_list_changed_cb (GtdSidebarListRow *self) +{ + update_counter_label (self); +} + +static void +on_list_color_changed_cb (GtdTaskList *list, + GParamSpec *pspec, + GtdSidebarListRow *self) +{ + update_color_icon (self); +} + +static void +on_rename_popover_hidden_cb (GtkPopover *popover, + GtdSidebarListRow *self) +{ + /* + * Remove the relative to, to remove the popover from the widget + * list and avoid parsing any CSS for it. It's a small performance + * improvement. + */ + gtk_widget_set_parent (GTK_WIDGET (popover), NULL); +} + +static gboolean +on_task_drop (GtkDropTarget *target, + const GValue *value, + double x, + double y, + GtdSidebarListRow *self) +{ + GtdTask *task; + + GTD_ENTRY; + + task = g_value_get_object (value); + gtd_task_list_import_task (self->list, + task, + NULL, + on_import_task_cb, + self); + + GTD_RETURN (TRUE); +} + + +static gboolean +on_task_enter_drop (GtkDropTarget *target, + double x, + double y, + GtdSidebarListRow *self) +{ + GTD_ENTRY; + + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_SELECTED , FALSE); + + GTD_RETURN (TRUE); +} + +static gboolean +on_task_leave_drop (GtkDropTarget *target, + double x, + double y, + GtdSidebarListRow *self) +{ + GTD_ENTRY; + + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_SELECTED); + + GTD_RETURN (TRUE); +} + +/* + * GObject overrides + */ + +static void +gtd_sidebar_list_row_finalize (GObject *object) +{ + GtdSidebarListRow *self = (GtdSidebarListRow *)object; + + g_clear_object (&self->list); + + G_OBJECT_CLASS (gtd_sidebar_list_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_list_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarListRow *self = GTD_SIDEBAR_LIST_ROW (object); + + switch (prop_id) + { + case PROP_LIST: + g_value_set_object (value, self->list); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_list_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarListRow *self = GTD_SIDEBAR_LIST_ROW (object); + + switch (prop_id) + { + case PROP_LIST: + set_list (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_list_row_class_init (GtdSidebarListRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_list_row_finalize; + object_class->get_property = gtd_sidebar_list_row_get_property; + object_class->set_property = gtd_sidebar_list_row_set_property; + + properties[PROP_LIST] = g_param_spec_object ("list", + "List", + "The task list this row represents", + GTD_TYPE_TASK_LIST, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-list-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, color_icon); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, name_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, tasks_counter_label); + + gtk_widget_class_bind_template_callback (widget_class, on_rename_popover_hidden_cb); +} + +static void +gtd_sidebar_list_row_init (GtdSidebarListRow *self) +{ + GtkDropTarget *target; + + gtk_widget_init_template (GTK_WIDGET (self)); + + target = gtk_drop_target_new (GTD_TYPE_TASK, GDK_ACTION_MOVE); + gtk_drop_target_set_preload (target, TRUE); + g_signal_connect (target, "drop", G_CALLBACK (on_task_drop), self); + g_signal_connect (target, "enter", G_CALLBACK (on_task_enter_drop), self); + g_signal_connect (target, "leave", G_CALLBACK (on_task_leave_drop), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (target)); +} + +GtkWidget* +gtd_sidebar_list_row_new (GtdTaskList *list) +{ + return g_object_new (GTD_TYPE_SIDEBAR_LIST_ROW, + "list", list, + NULL); +} + +GtdTaskList* +gtd_sidebar_list_row_get_task_list (GtdSidebarListRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_LIST_ROW (self), NULL); + + return self->list; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h new file mode 100644 index 0000000..3660608 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h @@ -0,0 +1,37 @@ +/* gtd-sidebar-list-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_LIST_ROW (gtd_sidebar_list_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarListRow, gtd_sidebar_list_row, GTD, SIDEBAR_LIST_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_list_row_new (GtdTaskList *list); + +GtdTaskList* gtd_sidebar_list_row_get_task_list (GtdSidebarListRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui new file mode 100644 index 0000000..380d1ed --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui @@ -0,0 +1,41 @@ + + + + + diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c new file mode 100644 index 0000000..a379f24 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c @@ -0,0 +1,180 @@ +/* gtd-sidebar-panel-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-panel.h" +#include "gtd-sidebar-panel-row.h" + +struct _GtdSidebarPanelRow +{ + GtkListBoxRow parent; + + GtkWidget *panel_icon; + GtkWidget *subtitle_label; + GtkWidget *title_label; + + GtdPanel *panel; +}; + +G_DEFINE_TYPE (GtdSidebarPanelRow, gtd_sidebar_panel_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_PANEL, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +set_panel (GtdSidebarPanelRow *self, + GtdPanel *panel) +{ + g_assert (panel != NULL); + g_assert (self->panel == NULL); + + self->panel = g_object_ref (panel); + + /* Bind panel properties to the row widgets */ + g_object_bind_property (self->panel, + "icon", + self->panel_icon, + "gicon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->panel, + "title", + self->title_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->panel, + "subtitle", + self->subtitle_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PANEL]); +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_panel_row_finalize (GObject *object) +{ + GtdSidebarPanelRow *self = (GtdSidebarPanelRow *)object; + + g_clear_object (&self->panel); + + G_OBJECT_CLASS (gtd_sidebar_panel_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_panel_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarPanelRow *self = GTD_SIDEBAR_PANEL_ROW (object); + + switch (prop_id) + { + case PROP_PANEL: + g_value_set_object (value, self->panel); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_panel_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarPanelRow *self = GTD_SIDEBAR_PANEL_ROW (object); + + switch (prop_id) + { + case PROP_PANEL: + set_panel (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_panel_row_class_init (GtdSidebarPanelRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_panel_row_finalize; + object_class->get_property = gtd_sidebar_panel_row_get_property; + object_class->set_property = gtd_sidebar_panel_row_set_property; + + properties[PROP_PANEL] = g_param_spec_object ("panel", + "Panel", + "The panel this row represents", + GTD_TYPE_PANEL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, panel_icon); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, subtitle_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, title_label); +} + +static void +gtd_sidebar_panel_row_init (GtdSidebarPanelRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_sidebar_panel_row_new (GtdPanel *panel) +{ + return g_object_new (GTD_TYPE_SIDEBAR_PANEL_ROW, + "panel", panel, + NULL); +} + +GtdPanel* +gtd_sidebar_panel_row_get_panel (GtdSidebarPanelRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_PANEL_ROW (self), NULL); + + return self->panel; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h new file mode 100644 index 0000000..58decde --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h @@ -0,0 +1,37 @@ +/* gtd-sidebar-panel-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_PANEL_ROW (gtd_sidebar_panel_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarPanelRow, gtd_sidebar_panel_row, GTD, SIDEBAR_PANEL_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_panel_row_new (GtdPanel *panel); + +GtdPanel* gtd_sidebar_panel_row_get_panel (GtdSidebarPanelRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui new file mode 100644 index 0000000..5722bd8 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui @@ -0,0 +1,34 @@ + + + + + diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c new file mode 100644 index 0000000..19bb256 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c @@ -0,0 +1,300 @@ +/* gtd-sidebar-provider-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebarProviderRow" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-provider.h" +#include "gtd-sidebar-provider-row.h" + +struct _GtdSidebarProviderRow +{ + GtkListBoxRow parent; + + GtkWidget *loading_label; + GtkLabel *provider_label; + GtkStack *stack; + + GtdProvider *provider; +}; + +static void on_provider_changed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebarProviderRow *self); + +static void on_provider_lists_changed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdSidebarProviderRow *self); + +static void on_provider_notify_loading_cb (GtdProvider *provider, + GParamSpec *pspec, + GtdSidebarProviderRow *self); + +G_DEFINE_TYPE (GtdSidebarProviderRow, gtd_sidebar_provider_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_PROVIDER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +update_provider_label (GtdSidebarProviderRow *self) +{ + g_autoptr (GList) providers = NULL; + GtdManager *manager; + GList *l; + gboolean is_unique; + const gchar *title; + + manager = gtd_manager_get_default (); + providers = gtd_manager_get_providers (manager); + is_unique = TRUE; + + /* + * We need to check if there is another provider with + * the same GType of the provider. + */ + for (l = providers; l; l = l->next) + { + GtdProvider *provider = l->data; + + if (self->provider != provider && + g_str_equal (gtd_provider_get_provider_type (self->provider), gtd_provider_get_provider_type (provider))) + { + is_unique = FALSE; + break; + } + } + + if (is_unique) + title = gtd_provider_get_name (self->provider); + else + title = gtd_provider_get_description (self->provider); + + gtk_label_set_label (self->provider_label, title); +} + +static void +update_loading_state (GtdSidebarProviderRow *self) +{ + g_autoptr (GList) lists = NULL; + gboolean is_loading; + gboolean has_lists; + + g_assert (self->provider != NULL); + + lists = gtd_provider_get_task_lists (self->provider); + is_loading = gtd_object_get_loading (GTD_OBJECT (self->provider)); + has_lists = lists != NULL; + + GTD_TRACE_MSG ("'%s' (%s): is_loading: %d, has_lists: %d", + gtd_provider_get_name (self->provider), + gtd_provider_get_id (self->provider), + is_loading, + has_lists); + + gtk_stack_set_visible_child_name (self->stack, is_loading ? "spinner" : "empty"); + gtk_widget_set_visible (self->loading_label, is_loading && !has_lists); +} + +static void +set_provider (GtdSidebarProviderRow *self, + GtdProvider *provider) +{ + GtdManager *manager; + + g_assert (provider != NULL); + g_assert (self->provider == NULL); + + self->provider = g_object_ref (provider); + + g_object_bind_property (provider, + "enabled", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* Setup the title label */ + manager = gtd_manager_get_default (); + + g_signal_connect_object (manager, "provider-added", G_CALLBACK (on_provider_changed_cb), self, 0); + g_signal_connect_object (manager, "provider-removed", G_CALLBACK (on_provider_changed_cb), self, 0); + + update_provider_label (self); + + /* And the icon */ + g_signal_connect_object (provider, "notify::loading", G_CALLBACK (on_provider_notify_loading_cb), self, 0); + g_signal_connect_object (provider, "list-added", G_CALLBACK (on_provider_lists_changed_cb), self, 0); + g_signal_connect_object (provider, "list-removed", G_CALLBACK (on_provider_lists_changed_cb), self, 0); + + update_loading_state (self); +} + + +/* + * Callbacks + */ + +static void +on_provider_changed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebarProviderRow *self) +{ + update_provider_label (self); +} + +static void +on_provider_lists_changed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdSidebarProviderRow *self) +{ + update_loading_state (self); +} + +static void +on_provider_notify_loading_cb (GtdProvider *provider, + GParamSpec *pspec, + GtdSidebarProviderRow *self) +{ + update_loading_state (self); +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_provider_row_finalize (GObject *object) +{ + GtdSidebarProviderRow *self = (GtdSidebarProviderRow *)object; + + g_clear_object (&self->provider); + + G_OBJECT_CLASS (gtd_sidebar_provider_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_provider_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarProviderRow *self = GTD_SIDEBAR_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + g_value_set_object (value, self->provider); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_provider_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarProviderRow *self = GTD_SIDEBAR_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + set_provider (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +/* + * GtkWidget overrides + */ + +static void +gtd_sidebar_provider_row_class_init (GtdSidebarProviderRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_provider_row_finalize; + object_class->get_property = gtd_sidebar_provider_row_get_property; + object_class->set_property = gtd_sidebar_provider_row_set_property; + + properties[PROP_PROVIDER] = g_param_spec_object ("provider", + "Provider", + "Provider of the row", + GTD_TYPE_PROVIDER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, loading_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, provider_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, stack); +} + +static void +gtd_sidebar_provider_row_init (GtdSidebarProviderRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_sidebar_provider_row_new (GtdProvider *provider) +{ + return g_object_new (GTD_TYPE_SIDEBAR_PROVIDER_ROW, + "provider", provider, + NULL); +} + +GtdProvider* +gtd_sidebar_provider_row_get_provider (GtdSidebarProviderRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_PROVIDER_ROW (self), NULL); + + return self->provider; +} + +void +gtd_sidebar_provider_row_popup_menu (GtdSidebarProviderRow *self) +{ + g_assert (GTD_IS_SIDEBAR_PROVIDER_ROW (self)); + + /* TODO: Implement me */ +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h new file mode 100644 index 0000000..640e6b2 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h @@ -0,0 +1,39 @@ +/* gtd-sidebar-provider-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_PROVIDER_ROW (gtd_sidebar_provider_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarProviderRow, gtd_sidebar_provider_row, GTD, SIDEBAR_PROVIDER_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_provider_row_new (GtdProvider *provider); + +GtdProvider* gtd_sidebar_provider_row_get_provider (GtdSidebarProviderRow *self); + +void gtd_sidebar_provider_row_popup_menu (GtdSidebarProviderRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui new file mode 100644 index 0000000..e97e318 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui @@ -0,0 +1,74 @@ + + + + + diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.c b/src/plugins/task-lists-workspace/gtd-sidebar.c new file mode 100644 index 0000000..0f5760b --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.c @@ -0,0 +1,929 @@ +/* gtd-sidebar.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebar" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-max-size-layout.h" +#include "gtd-notification.h" +#include "gtd-panel.h" +#include "gtd-provider.h" +#include "gtd-sidebar.h" +#include "gtd-sidebar-list-row.h" +#include "gtd-sidebar-panel-row.h" +#include "gtd-sidebar-provider-row.h" +#include "gtd-task-list.h" +#include "gtd-task-list-panel.h" +#include "gtd-utils.h" + +#include + +struct _GtdSidebar +{ + GtdWidget parent; + + GtkListBox *archive_listbox; + GtkListBoxRow *archive_row; + GtkListBox *listbox; + GtkStack *stack; + + GtkStack *panel_stack; + GtdPanel *task_list_panel; + + GSimpleActionGroup *action_group; +}; + +G_DEFINE_TYPE (GtdSidebar, gtd_sidebar, GTD_TYPE_WIDGET) + + +/* + * Auxiliary methods + */ + +static gboolean +activate_row_below (GtdSidebar *self, + GtdSidebarListRow *current_row) +{ + GtkWidget *next_row; + GtkWidget *parent; + GtkWidget *child; + gboolean after_deleted; + + parent = gtk_widget_get_parent (GTK_WIDGET (current_row)); + after_deleted = FALSE; + next_row = NULL; + + for (child = gtk_widget_get_first_child (parent); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (child == (GtkWidget*) current_row) + { + after_deleted = TRUE; + continue; + } + + if (!gtk_widget_get_visible (child) || + !gtk_list_box_row_get_activatable (GTK_LIST_BOX_ROW (child))) + { + continue; + } + + next_row = child; + + if (after_deleted) + break; + } + + if (next_row) + g_signal_emit_by_name (next_row, "activate"); + + return next_row != NULL; +} + +static void +add_task_list (GtdSidebar *self, + GtdTaskList *list) +{ + if (gtd_task_list_is_inbox (list)) + return; + + g_debug ("Adding task list '%s'", gtd_task_list_get_name (list)); + + if (!gtd_task_list_get_archived (list)) + { + gtk_list_box_prepend (self->listbox, gtd_sidebar_list_row_new (list)); + gtk_list_box_invalidate_filter (self->listbox); + } + else + { + gtk_list_box_prepend (self->archive_listbox, gtd_sidebar_list_row_new (list)); + gtk_list_box_invalidate_filter (self->archive_listbox); + } +} + +static void +add_panel (GtdSidebar *self, + GtdPanel *panel) +{ + GtkWidget *row; + + g_debug ("Adding panel '%s'", gtd_panel_get_panel_name (panel)); + + row = gtd_sidebar_panel_row_new (panel); + + gtk_list_box_prepend (self->listbox, row); +} + +static void +add_provider (GtdSidebar *self, + GtdProvider *provider) +{ + g_debug ("Adding provider '%s'", gtd_provider_get_name (provider)); + + gtk_list_box_prepend (self->listbox, gtd_sidebar_provider_row_new (provider)); + gtk_list_box_prepend (self->archive_listbox, gtd_sidebar_provider_row_new (provider)); +} + +static gint +compare_panels (GtdSidebarPanelRow *row_a, + GtdSidebarPanelRow *row_b) +{ + GtdPanel *panel_a; + GtdPanel *panel_b; + + panel_a = gtd_sidebar_panel_row_get_panel (row_a); + panel_b = gtd_sidebar_panel_row_get_panel (row_b); + + return gtd_panel_get_priority (panel_b) - gtd_panel_get_priority (panel_a); +} + +static gint +compare_providers (GtdSidebarProviderRow *row_a, + GtdSidebarProviderRow *row_b) +{ + GtdProvider *provider_a; + GtdProvider *provider_b; + + provider_a = gtd_sidebar_provider_row_get_provider (row_a); + provider_b = gtd_sidebar_provider_row_get_provider (row_b); + + return gtd_provider_compare (provider_a, provider_b); +} + +static gint +compare_lists (GtdSidebarListRow *row_a, + GtdSidebarListRow *row_b) +{ + GtdTaskList *list_a; + GtdTaskList *list_b; + gint result; + + list_a = gtd_sidebar_list_row_get_task_list (row_a); + list_b = gtd_sidebar_list_row_get_task_list (row_b); + + /* First, compare by their providers */ + result = gtd_provider_compare (gtd_task_list_get_provider (list_a), gtd_task_list_get_provider (list_b)); + + if (result != 0) + return result; + + return gtd_collate_compare_strings (gtd_task_list_get_name (list_a), gtd_task_list_get_name (list_b)); +} + +typedef gpointer (*GetDataFunc) (gpointer data); + +static gpointer +get_row_internal (GtdSidebar *self, + GtkListBox *listbox, + GType type, + GetDataFunc get_data_func, + gpointer data) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (g_type_is_a (G_OBJECT_TYPE (child), type) && get_data_func (child) == data) + return child; + } + + return NULL; +} + +static GtkListBoxRow* +get_row_for_panel (GtdSidebar *self, + GtdPanel *panel) +{ + return get_row_internal (self, + self->listbox, + GTD_TYPE_SIDEBAR_PANEL_ROW, + (GetDataFunc) gtd_sidebar_panel_row_get_panel, + panel); +} + +static GtkListBoxRow* +get_row_for_provider (GtdSidebar *self, + GtkListBox *listbox, + GtdProvider *provider) +{ + return get_row_internal (self, + listbox, + GTD_TYPE_SIDEBAR_PROVIDER_ROW, + (GetDataFunc) gtd_sidebar_provider_row_get_provider, + provider); +} + +static GtkListBoxRow* +get_row_for_task_list (GtdSidebar *self, + GtkListBox *listbox, + GtdTaskList *list) +{ + return get_row_internal (self, + listbox, + GTD_TYPE_SIDEBAR_LIST_ROW, + (GetDataFunc) gtd_sidebar_list_row_get_task_list, + list); +} + +static void +activate_appropriate_row (GtdSidebar *self, + GtkListBoxRow *row) +{ + GtkListBoxRow *to_be_activated; + + if (activate_row_below (self, GTD_SIDEBAR_LIST_ROW (row))) + return; + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + FALSE); + + to_be_activated = gtk_list_box_get_row_at_index (self->listbox, 0); + g_signal_emit_by_name (to_be_activated, "activate"); +} + +/* + * Callbacks + */ + +static void +on_action_move_up_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtkListBoxRow *selected_row; + GtkListBoxRow *previous_row; + GtdSidebar *self; + gint selected_row_index; + + GTD_ENTRY; + + self = GTD_SIDEBAR (user_data); + selected_row = gtk_list_box_get_selected_row (self->listbox); + g_assert (selected_row != NULL); + + selected_row_index = gtk_list_box_row_get_index (selected_row); + if (selected_row_index == 0) + return; + + do + { + previous_row = gtk_list_box_get_row_at_index (self->listbox, + --selected_row_index); + } + while (previous_row && + (previous_row == self->archive_row || + !gtk_list_box_row_get_activatable (previous_row))); + + + if (previous_row) + g_signal_emit_by_name (previous_row, "activate"); + + GTD_EXIT; +} + +static void +on_action_move_down_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtkListBoxRow *selected_row; + GtkListBoxRow *next_row; + GtdSidebar *self; + gint selected_row_index; + + GTD_ENTRY; + + self = GTD_SIDEBAR (user_data); + selected_row = gtk_list_box_get_selected_row (self->listbox); + g_assert (selected_row != NULL); + + selected_row_index = gtk_list_box_row_get_index (selected_row); + + do + { + next_row = gtk_list_box_get_row_at_index (self->listbox, + ++selected_row_index); + } + while (next_row && + (next_row == self->archive_row || + !gtk_list_box_row_get_activatable (next_row))); + + + if (next_row) + g_signal_emit_by_name (next_row, "activate"); + + GTD_EXIT; +} + +static void +on_panel_added_cb (GtdManager *manager, + GtdPanel *panel, + GtdSidebar *self) +{ + add_panel (self, panel); +} + +static void +on_panel_removed_cb (GtdManager *manager, + GtdPanel *panel, + GtdSidebar *self) +{ + GtkListBoxRow *row = get_row_for_panel (self, panel); + + g_debug ("Removing panel '%s'", gtd_panel_get_panel_name (panel)); + + if (row) + gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); +} + +static void +on_provider_task_list_removed_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_remove_task_list_finish (GTD_PROVIDER (source), result, &error); +} + +static void +delete_list_cb (GtdNotification *notification, + gpointer user_data) +{ + GtdTaskList *list; + GtdProvider *provider; + + list = GTD_TASK_LIST (user_data); + provider = gtd_task_list_get_provider (list); + + g_assert (provider != NULL); + g_assert (gtd_task_list_is_removable (list)); + + gtd_provider_remove_task_list (provider, + list, + NULL, + on_provider_task_list_removed_cb, + NULL); +} + +static void +undo_delete_list_cb (GtdNotification *notification, + gpointer user_data) +{ + g_assert (GTD_IS_SIDEBAR_LIST_ROW (user_data)); + + gtk_widget_show (GTK_WIDGET (user_data)); +} + +static void +on_task_list_panel_list_deleted_cb (GtdTaskListPanel *panel, + GtdTaskList *list, + GtdSidebar *self) +{ + GtdSidebarListRow *row; + GtdNotification *notification; + g_autofree gchar *title = NULL; + + if (gtd_task_list_get_archived (list)) + row = (GtdSidebarListRow*) get_row_for_task_list (self, self->archive_listbox, list); + else + row = (GtdSidebarListRow*) get_row_for_task_list (self, self->listbox, list); + + g_assert (row != NULL && GTD_IS_SIDEBAR_LIST_ROW (row)); + + GTD_TRACE_MSG ("Removing task list row from sidebar"); + + title = g_strdup_printf (_("Task list %s removed"), gtd_task_list_get_name (list)); + notification = gtd_notification_new (title); + gtd_notification_set_dismissal_action (notification, delete_list_cb, list); + gtd_notification_set_secondary_action (notification, _("Undo"), undo_delete_list_cb, row); + + gtd_manager_send_notification (gtd_manager_get_default (), notification); + + /* + * If the deleted list is selected, go to the next one (or previous, if + * there are no other task list after this one). + */ + if (gtk_list_box_row_is_selected (GTK_LIST_BOX_ROW (row))) + activate_appropriate_row (self, GTK_LIST_BOX_ROW (row)); + + gtk_widget_hide (GTK_WIDGET (row)); +} + +static void +on_listbox_row_activated_cb (GtkListBox *panels_listbox, + GtkListBoxRow *row, + GtdSidebar *self) +{ + if (GTD_IS_SIDEBAR_PANEL_ROW (row)) + { + GtdPanel *panel = gtd_sidebar_panel_row_get_panel (GTD_SIDEBAR_PANEL_ROW (row)); + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.activate-panel", + "(sv)", + gtd_panel_get_panel_name (panel), + g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL)); + } + else if (GTD_IS_SIDEBAR_PROVIDER_ROW (row)) + { + /* Do nothing */ + } + else if (GTD_IS_SIDEBAR_LIST_ROW (row)) + { + GVariantBuilder builder; + GtdProvider *provider; + GtdTaskList *list; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + provider = gtd_task_list_get_provider (list); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", + "provider-id", + g_variant_new_string (gtd_provider_get_id (provider))); + g_variant_builder_add (&builder, "{sv}", + "task-list-id", + g_variant_new_string (gtd_object_get_uid (GTD_OBJECT (list)))); + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.activate-panel", + "(sv)", + "task-list-panel", + g_variant_builder_end (&builder)); + } + else if (row == self->archive_row) + { + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + TRUE); + } + else + { + g_assert_not_reached (); + } +} + +static void +on_panel_stack_visible_child_changed_cb (GtkStack *panel_stack, + GParamSpec *pspec, + GtdSidebar *self) +{ + GtkListBoxRow *panel_row; + GtkListBox *listbox; + GtdPanel *visible_panel; + + GTD_ENTRY; + + g_assert (GTD_IS_PANEL (gtk_stack_get_visible_child (panel_stack))); + + visible_panel = GTD_PANEL (gtk_stack_get_visible_child (panel_stack)); + listbox = self->listbox; + + /* + * If the currently visible panel is the tasklist panel, we + * should choose the tasklist that is visible. Otherwise, + * just select the panel. + */ + if (visible_panel == self->task_list_panel) + { + GtdTaskList *task_list; + + task_list = gtd_task_list_panel_get_task_list (GTD_TASK_LIST_PANEL (self->task_list_panel)); + g_assert (task_list != NULL); + + panel_row = get_row_for_task_list (self, self->listbox, task_list); + + if (!panel_row) + { + panel_row = get_row_for_task_list (self, self->archive_listbox, task_list); + listbox = self->archive_listbox; + } + } + else + { + panel_row = get_row_for_panel (self, visible_panel); + } + + /* Select the row if it's not already selected*/ + if (!gtk_list_box_row_is_selected (panel_row)) + gtk_list_box_select_row (listbox, panel_row); + + GTD_EXIT; +} + +static void +on_provider_added_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebar *self) +{ + add_provider (self, provider); +} + +static void +on_provider_removed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebar *self) +{ + GtkListBoxRow *row; + + g_debug ("Removing provider '%s'", gtd_provider_get_name (provider)); + + row = get_row_for_provider (self, self->listbox, provider); + gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); + + row = get_row_for_provider (self, self->archive_listbox, provider); + gtk_list_box_remove (self->archive_listbox, GTK_WIDGET (row)); +} + + +static void +on_task_list_added_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + add_task_list (self, list); +} + +static void +on_task_list_changed_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + GtkListBoxRow *row; + GtkListBox *listbox; + gboolean archived; + + archived = gtd_task_list_get_archived (list); + listbox = archived ? self->archive_listbox : self->listbox; + row = get_row_for_task_list (self, listbox, list); + + /* + * The task was either archived or unarchived; remove it and add to + * the appropriate listbox. + */ + if (!row) + { + listbox = archived ? self->listbox : self->archive_listbox; + row = get_row_for_task_list (self, listbox, list); + + if (!row) + goto out; + + /* Change to another panel or taklist */ + if (gtk_list_box_row_is_selected (row)) + activate_appropriate_row (self, row); + + /* Destroy the old row */ + gtk_list_box_remove (listbox, GTK_WIDGET (row)); + + /* Add a new row */ + add_task_list (self, list); + } + +out: + gtk_list_box_invalidate_filter (listbox); +} + +static void +on_task_list_removed_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + GtkListBoxRow *row; + GtkListBox *listbox; + + g_debug ("Removing task list '%s'", gtd_task_list_get_name (list)); + + g_assert (!gtd_task_list_is_inbox (list)); + + if (!gtd_task_list_get_archived (list)) + listbox = self->listbox; + else + listbox = self->archive_listbox; + + row = get_row_for_task_list (self, listbox, list); + if (!row) + return; + + gtk_list_box_remove (listbox, GTK_WIDGET (row)); + gtk_list_box_invalidate_filter (listbox); +} + +static gboolean +filter_archive_listbox_cb (GtkListBoxRow *row, + gpointer user_data) +{ + if (GTD_IS_SIDEBAR_LIST_ROW (row)) + { + GtdTaskList *list; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + return gtd_task_list_get_archived (list); + } + else if (GTD_IS_SIDEBAR_PROVIDER_ROW (row)) + { + g_autoptr (GList) lists = NULL; + GtdProvider *provider; + GList *l; + + provider = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row)); + lists = gtd_provider_get_task_lists (provider); + + for (l = lists; l; l = l->next) + { + if (gtd_task_list_get_archived (l->data)) + return TRUE; + } + + return FALSE; + } + else + { + g_assert_not_reached (); + } + + return FALSE; +} + +static gboolean +filter_listbox_cb (GtkListBoxRow *row, + gpointer user_data) +{ + GtdTaskList *list; + + if (!GTD_IS_SIDEBAR_LIST_ROW (row)) + return TRUE; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + return !gtd_task_list_get_archived (list); +} + +static gint +sort_listbox_cb (GtkListBoxRow *row_a, + GtkListBoxRow *row_b, + gpointer user_data) +{ + GtdSidebar *self = GTD_SIDEBAR (user_data); + + /* Special-case the Archive row */ + if (row_a == self->archive_row || row_b == self->archive_row) + { + if (GTD_IS_SIDEBAR_PANEL_ROW (row_b)) + return 1; + else + return -1; + } + + if (G_OBJECT_TYPE (row_a) != G_OBJECT_TYPE (row_b)) + { + gint result; + + /* Panels go above everything else */ + if (GTD_IS_SIDEBAR_PANEL_ROW (row_b) != GTD_IS_SIDEBAR_PANEL_ROW (row_a)) + return GTD_IS_SIDEBAR_PANEL_ROW (row_b) - GTD_IS_SIDEBAR_PANEL_ROW (row_a); + + /* + * At this point, we know that row_a and row_b are either provider rows, or + * tasklist rows. We also know that they're different, i.e. if row_a is a + * provider row, row_b will be a list one, and vice-versa. + */ + if (GTD_IS_SIDEBAR_PROVIDER_ROW (row_a)) + { + GtdProvider *provider_a; + GtdTaskList *list_b; + + provider_a = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row_a)); + list_b = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row_b)); + + /* + * If the providers are different, respect the provider order. If the providers are the + * same, we must put the provider row above the tasklist row. + */ + result = gtd_provider_compare (provider_a, gtd_task_list_get_provider (list_b)); + + if (result != 0) + return result; + + return -1; + } + else + { + GtdTaskList *list_a; + GtdProvider *provider_b; + + list_a = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row_a)); + provider_b = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row_b)); + + /* See comment above */ + result = gtd_provider_compare (gtd_task_list_get_provider (list_a), provider_b); + + if (result != 0) + return result; + + return 1; + } + } + else + { + /* + * We only reach this section of the code if both rows are of the same type, + * so it doesn't matter which one we get the type from. + */ + + if (GTD_IS_SIDEBAR_PANEL_ROW (row_a)) + return compare_panels (GTD_SIDEBAR_PANEL_ROW (row_a), GTD_SIDEBAR_PANEL_ROW (row_b)); + + if (GTD_IS_SIDEBAR_PROVIDER_ROW (row_a)) + return compare_providers (GTD_SIDEBAR_PROVIDER_ROW (row_a), GTD_SIDEBAR_PROVIDER_ROW (row_b)); + + if (GTD_IS_SIDEBAR_LIST_ROW (row_a)) + return compare_lists (GTD_SIDEBAR_LIST_ROW (row_a), GTD_SIDEBAR_LIST_ROW (row_b)); + } + + return 0; +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_constructed (GObject *object) +{ + g_autoptr (GList) providers = NULL; + GListModel *lists; + GtdManager *manager; + GtdSidebar *self; + GList *l; + guint i; + + self = (GtdSidebar *)object; + manager = gtd_manager_get_default (); + + G_OBJECT_CLASS (gtd_sidebar_parent_class)->constructed (object); + + /* Add providers */ + providers = gtd_manager_get_providers (manager); + + for (l = providers; l; l = l->next) + add_provider (self, l->data); + + g_signal_connect (manager, "provider-added", G_CALLBACK (on_provider_added_cb), self); + g_signal_connect (manager, "provider-removed", G_CALLBACK (on_provider_removed_cb), self); + + /* Add task lists */ + lists = gtd_manager_get_task_lists_model (manager); + + for (i = 0; i < g_list_model_get_n_items (lists); i++) + { + g_autoptr (GtdTaskList) list = g_list_model_get_item (lists, i); + + add_task_list (self, list); + } + + g_signal_connect (manager, "list-added", G_CALLBACK (on_task_list_added_cb), self); + g_signal_connect (manager, "list-changed", G_CALLBACK (on_task_list_changed_cb), self); + g_signal_connect (manager, "list-removed", G_CALLBACK (on_task_list_removed_cb), self); +} + +static void +gtd_sidebar_class_init (GtdSidebarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = gtd_sidebar_constructed; + + g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, archive_listbox); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, archive_row); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb); + + gtk_widget_class_set_css_name (widget_class, "sidebar"); +} + +static void +gtd_sidebar_init (GtdSidebar *self) +{ + static const GActionEntry entries[] = { + { "move-up", on_action_move_up_activated_cb }, + { "move-down", on_action_move_down_activated_cb }, + }; + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_sort_func (self->listbox, sort_listbox_cb, self, NULL); + gtk_list_box_set_filter_func (self->listbox, filter_listbox_cb, self, NULL); + + gtk_list_box_set_sort_func (self->archive_listbox, sort_listbox_cb, self, NULL); + gtk_list_box_set_filter_func (self->archive_listbox, filter_archive_listbox_cb, self, NULL); + + self->action_group = g_simple_action_group_new (); + + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + entries, + G_N_ELEMENTS (entries), + self); + + gtk_widget_insert_action_group (GTK_WIDGET (self), + "sidebar", + G_ACTION_GROUP (self->action_group)); +} + +void +gtd_sidebar_set_panel_stack (GtdSidebar *self, + GtkStack *stack) +{ + g_return_if_fail (GTD_IS_SIDEBAR (self)); + g_return_if_fail (GTK_IS_STACK (stack)); + + g_assert (self->panel_stack == NULL); + + self->panel_stack = g_object_ref (stack); + + g_signal_connect_object (stack, + "notify::visible-child", + G_CALLBACK (on_panel_stack_visible_child_changed_cb), + self, + 0); +} + + +void +gtd_sidebar_set_task_list_panel (GtdSidebar *self, + GtdPanel *task_list_panel) +{ + g_return_if_fail (GTD_IS_SIDEBAR (self)); + g_return_if_fail (GTD_IS_PANEL (task_list_panel)); + + g_assert (self->task_list_panel == NULL); + + self->task_list_panel = g_object_ref (task_list_panel); + g_signal_connect_object (self->task_list_panel, + "list-deleted", + G_CALLBACK (on_task_list_panel_list_deleted_cb), + self, + 0); +} + +void +gtd_sidebar_activate (GtdSidebar *self) +{ + GtkListBoxRow *first_row; + + g_assert (GTD_IS_SIDEBAR (self)); + + first_row = gtk_list_box_get_row_at_index (self->listbox, 0); + g_signal_emit_by_name (first_row, "activate"); +} + +void +gtd_sidebar_set_archive_visible (GtdSidebar *self, + gboolean show_archive) +{ + g_assert (GTD_IS_SIDEBAR (self)); + + if (show_archive) + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->archive_listbox)); + else + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->listbox)); +} + +void +gtd_sidebar_connect (GtdSidebar *self, + GtkWidget *workspace) +{ + g_signal_connect (workspace, "panel-added", G_CALLBACK (on_panel_added_cb), self); + g_signal_connect (workspace, "panel-removed", G_CALLBACK (on_panel_removed_cb), self); +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.h b/src/plugins/task-lists-workspace/gtd-sidebar.h new file mode 100644 index 0000000..d85a996 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.h @@ -0,0 +1,44 @@ +/* gtd-sidebar.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR (gtd_sidebar_get_type()) +G_DECLARE_FINAL_TYPE (GtdSidebar, gtd_sidebar, GTD, SIDEBAR, GtdWidget) + +void gtd_sidebar_set_panel_stack (GtdSidebar *self, + GtkStack *stack); + +void gtd_sidebar_set_task_list_panel (GtdSidebar *self, + GtdPanel *task_list_panel); + +void gtd_sidebar_activate (GtdSidebar *self); + +void gtd_sidebar_set_archive_visible (GtdSidebar *self, + gboolean show_archive); + +void gtd_sidebar_connect (GtdSidebar *self, + GtkWidget *workspace); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.ui b/src/plugins/task-lists-workspace/gtd-sidebar.ui new file mode 100644 index 0000000..356eb9c --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.ui @@ -0,0 +1,144 @@ + + + + diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.c b/src/plugins/task-lists-workspace/gtd-task-list-panel.c new file mode 100644 index 0000000..111c777 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.c @@ -0,0 +1,636 @@ +/* gtd-task-list-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskListPanel" + +#include + +#include "gtd-color-button.h" +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-panel.h" +#include "gtd-provider.h" +#include "gtd-task-list.h" +#include "gtd-task-list-panel.h" +#include "gtd-task-list-view.h" +#include "gtd-utils.h" + +struct _GtdTaskListPanel +{ + GtkBox parent; + + GtkButton *archive_button; + GtkFlowBox *colors_flowbox; + GtkPopover *popover; + GtkStack *popover_stack; + GtkWidget *rename_button; + GtkEditable *rename_entry; + GtdTaskListView *task_list_view; + + GtkWidget *previous_color_button; +}; + + +static void on_colors_flowbox_child_activated_cb (GtkFlowBox *colors_flowbox, + GtkFlowBoxChild *child, + GtdTaskListPanel *self); + +static void on_task_list_updated_cb (GObject *source, + GAsyncResult *result, + gpointer user_data); + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + + +G_DEFINE_TYPE_WITH_CODE (GtdTaskListPanel, gtd_task_list_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +enum +{ + LIST_DELETED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = { 0, }; + +/* + * Auxiliary methods + */ + +static const gchar * const colors[] = +{ + "#3584e4", + "#33d17a", + "#f6d32d", + "#ff7800", + "#e01b24", + "#9141ac", + "#986a44", + "#3d3846", + "#ffffff", +}; + +static void +populate_color_grid (GtdTaskListPanel *self) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS (colors); i++) + { + GtkWidget *button; + GdkRGBA color; + + gdk_rgba_parse (&color, colors[i]); + + button = gtd_color_button_new (&color); + gtk_widget_set_size_request (button, -1, 24); + + gtk_flow_box_insert (self->colors_flowbox, button, -1); + } +} + +static void +update_selected_color (GtdTaskListPanel *self) +{ + g_autoptr (GdkRGBA) color = NULL; + GtdTaskList *list; + GtkWidget *button; + guint i; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + color = gtd_task_list_get_color (list); + button = NULL; + + for (i = 0; i < G_N_ELEMENTS (colors); i++) + { + GdkRGBA c; + + gdk_rgba_parse (&c, colors[i]); + + if (gdk_rgba_equal (&c, color)) + { + button = GTK_WIDGET (gtk_flow_box_get_child_at_index (self->colors_flowbox, i)); + break; + } + } + + if (button) + { + g_signal_handlers_block_by_func (button, on_colors_flowbox_child_activated_cb, self); + g_signal_emit_by_name (button, "activate"); + g_signal_handlers_unblock_by_func (button, on_colors_flowbox_child_activated_cb, self); + } + else if (self->previous_color_button) + { + gtk_widget_unset_state_flags (self->previous_color_button, GTK_STATE_FLAG_SELECTED); + self->previous_color_button = NULL; + } +} + +static void +rename_list (GtdTaskListPanel *self) +{ + g_autofree gchar *new_name = NULL; + GtdTaskList *list; + + g_assert (gtk_widget_get_visible (GTK_WIDGET (self->popover))); + g_assert (g_utf8_validate (gtk_editable_get_text (self->rename_entry), -1, NULL)); + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + new_name = g_strdup (gtk_editable_get_text (self->rename_entry)); + new_name = g_strstrip (new_name); + + /* + * Even though the Rename button is insensitive, we may still reach here + * by activating the entry. + */ + if (!new_name || new_name[0] == '\0') + return; + + if (g_strcmp0 (new_name, gtd_task_list_get_name (list)) != 0) + { + gtd_task_list_set_name (list, new_name); + gtd_provider_update_task_list (gtd_task_list_get_provider (list), + list, + NULL, + on_task_list_updated_cb, + self); + } + + gtk_popover_popdown (self->popover); + gtk_editable_set_text (self->rename_entry, ""); +} + +static void +update_archive_button (GtdTaskListPanel *self) +{ + GtdTaskList *list; + gboolean archived; + + GTD_ENTRY; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + archived = gtd_task_list_get_archived (list); + g_object_set (self->archive_button, + "text", archived ? _("Unarchive") : _("Archive"), + NULL); + + GTD_EXIT; +} + + +/* + * Callbacks + */ + +static void +on_task_list_updated_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_update_task_list_finish (GTD_PROVIDER (source), result, &error); + + if (error) + { + g_warning ("Error creating task: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("An error occurred while updating a task"), + error->message, + NULL, + NULL); + } +} + +static void +on_archive_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + GtdProvider *provider; + GtdTaskList *list; + gboolean archived; + + GTD_ENTRY; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + archived = gtd_task_list_get_archived (list); + gtd_task_list_set_archived (list, !archived); + + update_archive_button (self); + + provider = gtd_task_list_get_provider (list); + gtd_provider_update_task_list (provider, + list, + NULL, + on_task_list_updated_cb, + self); + + GTD_EXIT; +} + +static void +on_colors_flowbox_child_activated_cb (GtkFlowBox *colors_flowbox, + GtkFlowBoxChild *child, + GtdTaskListPanel *self) +{ + const GdkRGBA *color; + GtdTaskList *list; + GtkWidget *color_button; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + + g_assert (list != NULL); + + color_button = gtk_flow_box_child_get_child (child); + + if (self->previous_color_button == color_button) + return; + + gtk_widget_set_state_flags (color_button, GTK_STATE_FLAG_SELECTED, FALSE); + + if (self->previous_color_button) + gtk_widget_unset_state_flags (self->previous_color_button, GTK_STATE_FLAG_SELECTED); + + g_debug ("Setting new color for task list '%s'", gtd_task_list_get_name (list)); + + color = gtd_color_button_get_color (GTD_COLOR_BUTTON (color_button)); + gtd_task_list_set_color (list, color); + + gtd_provider_update_task_list (gtd_task_list_get_provider (list), + list, + NULL, + on_task_list_updated_cb, + self); + + self->previous_color_button = color_button; +} + +static void +on_delete_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + GtdTaskList *list; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + GTD_TRACE_MSG ("Emitting GtdTaskListPanel:list-deleted"); + + g_signal_emit (self, signals[LIST_DELETED], 0, list); +} + +static void +on_go_to_rename_page_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + gtk_stack_set_visible_child_name (self->popover_stack, "rename"); +} + +static void +on_popover_hidden_cb (GtkPopover *popover, + GtdTaskListPanel *self) +{ + gtk_editable_set_text (self->rename_entry, ""); + gtk_stack_set_visible_child_name (self->popover_stack, "main"); +} + +static void +on_rename_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + rename_list (self); +} + +static void +on_rename_entry_activated_cb (GtkEntry *entry, + GtdTaskListPanel *self) +{ + rename_list (self); +} + +static void +on_rename_entry_text_changed_cb (GtkEditable *entry, + GParamSpec *pspec, + GtdTaskListPanel *self) +{ + g_autofree gchar *new_name = NULL; + gboolean valid; + + new_name = g_strdup (gtk_editable_get_text (entry)); + new_name = g_strstrip (new_name); + + valid = new_name && new_name[0] != '\0'; + + gtk_widget_set_sensitive (self->rename_button, valid); +} + + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_task_list_panel_get_panel_name (GtdPanel *panel) +{ + return "task-list-panel"; +} + +static const gchar* +gtd_task_list_panel_get_panel_title (GtdPanel *panel) +{ + GtdTaskListPanel *self; + GtdTaskList *list; + + self = GTD_TASK_LIST_PANEL (panel); + list = (GtdTaskList *) gtd_task_list_view_get_model (self->task_list_view); + + return list ? gtd_task_list_get_name (list) : ""; +} + +static GList* +gtd_task_list_panel_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_task_list_panel_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_task_list_panel_get_icon (GtdPanel *panel) +{ + return NULL; +} + +static GtkPopover* +gtd_task_list_panel_get_popover (GtdPanel *panel) +{ + GtdTaskListPanel *self = GTD_TASK_LIST_PANEL (panel); + return self->popover; +} + + +static guint32 +gtd_task_list_panel_get_priority (GtdPanel *panel) +{ + return 0; +} + +static gchar* +gtd_task_list_panel_get_subtitle (GtdPanel *panel) +{ + return NULL; +} + +static void +gtd_task_list_panel_activate (GtdPanel *panel, + GVariant *parameters) +{ + GtdTaskListPanel *self; + GVariantDict dict; + GtdTaskList *list; + GListModel *model; + const gchar *task_list_id; + const gchar *provider_id; + guint i; + + GTD_ENTRY; + + self = GTD_TASK_LIST_PANEL (panel); + + /* + * The task list panel must receive an a{sv} and looks for: + * + * * provider-id: the id of the provider + * * task-list-id: the id of the task list + * + * So it can find the task list from the GtdManager. + */ + + g_variant_dict_init (&dict, parameters); + g_variant_dict_lookup (&dict, "provider-id", "&s", &provider_id); + g_variant_dict_lookup (&dict, "task-list-id", "&s", &task_list_id); + + GTD_TRACE_MSG ("Activating %s with 'provider-id': %s and 'task-list-id': %s", + G_OBJECT_TYPE_NAME (self), + provider_id, + task_list_id); + + model = gtd_manager_get_task_lists_model (gtd_manager_get_default ()); + list = NULL; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + g_autoptr (GtdTaskList) task_list = NULL; + GtdProvider *provider; + + task_list = g_list_model_get_item (model, i); + if (g_strcmp0 (gtd_object_get_uid (GTD_OBJECT (task_list)), task_list_id) != 0) + continue; + + provider = gtd_task_list_get_provider (task_list); + if (g_strcmp0 (gtd_provider_get_id (provider), provider_id) != 0) + return; + + list = task_list; + break; + } + + g_assert (list != NULL); + + gtd_task_list_panel_set_task_list (self, list); + + GTD_EXIT; +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_task_list_panel_get_panel_name; + iface->get_panel_title = gtd_task_list_panel_get_panel_title; + iface->get_header_widgets = gtd_task_list_panel_get_header_widgets; + iface->get_menu = gtd_task_list_panel_get_menu; + iface->get_icon = gtd_task_list_panel_get_icon; + iface->get_popover = gtd_task_list_panel_get_popover; + iface->get_priority = gtd_task_list_panel_get_priority; + iface->get_subtitle = gtd_task_list_panel_get_subtitle; + iface->activate = gtd_task_list_panel_activate; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, NULL); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, "task-list-panel"); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, 0); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, NULL); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (object))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + + +static void +gtd_task_list_panel_class_init (GtdTaskListPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gtd_task_list_panel_get_property; + object_class->set_property = gtd_task_list_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); + + signals[LIST_DELETED] = g_signal_new ("list-deleted", + GTD_TYPE_TASK_LIST_PANEL, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-task-list-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, archive_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, colors_flowbox); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, popover); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, popover_stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_entry); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_entry); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, task_list_view); + + gtk_widget_class_bind_template_callback (widget_class, on_archive_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_colors_flowbox_child_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_delete_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_go_to_rename_page_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_popover_hidden_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_entry_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_entry_text_changed_cb); +} + +static void +gtd_task_list_panel_init (GtdTaskListPanel *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + populate_color_grid (self); +} + +GtkWidget* +gtd_task_list_panel_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LIST_PANEL, NULL); +} + +GtdTaskList* +gtd_task_list_panel_get_task_list (GtdTaskListPanel *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_PANEL (self), NULL); + + return GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); +} + +void +gtd_task_list_panel_set_task_list (GtdTaskListPanel *self, + GtdTaskList *list) +{ + g_return_if_fail (GTD_IS_TASK_LIST_PANEL (self)); + g_return_if_fail (GTD_IS_TASK_LIST (list)); + + gtd_task_list_view_set_model (self->task_list_view, G_LIST_MODEL (list)); + + update_selected_color (self); + update_archive_button (self); + + g_object_notify (G_OBJECT (self), "title"); +} + diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.h b/src/plugins/task-lists-workspace/gtd-task-list-panel.h new file mode 100644 index 0000000..1db7e70 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.h @@ -0,0 +1,40 @@ +/* gtd-task-list-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_PANEL (gtd_task_list_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListPanel, gtd_task_list_panel, GTD, TASK_LIST_PANEL, GtkBox) + +GtkWidget* gtd_task_list_panel_new (void); + +GtdTaskList* gtd_task_list_panel_get_task_list (GtdTaskListPanel *self); + +void gtd_task_list_panel_set_task_list (GtdTaskListPanel *self, + GtdTaskList *list); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.ui b/src/plugins/task-lists-workspace/gtd-task-list-panel.ui new file mode 100644 index 0000000..f5db9d7 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.ui @@ -0,0 +1,113 @@ + + + + + 0 + + + + + + + + + + main + + + vertical + + + true + true + none + 3 + 3 + + + + + + Rename + + + + + + Clear completed tasks… + list.clear-completed-tasks + + + + + + + + Archive + + + + + + Delete + + + + + + + + + + + + rename + + + vertical + 12 + 12 + 12 + 12 + 12 + + + Rename + title + + + + + 12 + + + + + + + + + Rename + + + + + + + + + + + + + + + diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c new file mode 100644 index 0000000..407de89 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c @@ -0,0 +1,711 @@ +/* gtd-task-lists-workspace.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskListsWorkspace" + +#include "gtd-task-lists-workspace.h" + +#include "gtd-debug.h" +#include "task-lists-workspace.h" +#include "gtd-sidebar.h" +#include "gtd-task-list-panel.h" + +#include +#include + +struct _GtdTaskListsWorkspace +{ + GtkBox parent; + + GtkWidget *back_button; + GtkWidget *content_box; + GtkMenuButton *gear_menu_button; + AdwLeaflet *leaflet; + GtkWidget *new_list_button; + GtkBox *panel_box_end; + GtkBox *panel_box_start; + GtkMenuButton *primary_menu_button; + GtkStack *stack; + GtdSidebar *sidebar; + GtkWidget *sidebar_box; + + AdwToastOverlay *sidebar_overlay; + AdwToastOverlay *content_overlay; + + GtdPanel *active_panel; + GtdPanel *task_list_panel; + + GHashTable *notification_list; + + PeasExtensionSet *panels_set; + GSimpleActionGroup *action_group; +}; + +typedef struct +{ + GtdWindow *window; + gchar *primary_text; + gchar *secondary_text; +} ErrorData; + +static void gtd_workspace_iface_init (GtdWorkspaceInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTaskListsWorkspace, gtd_task_lists_workspace, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_WORKSPACE, gtd_workspace_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_TITLE, + N_PROPS +}; + +enum +{ + PANEL_ADDED, + PANEL_REMOVED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + + +/* + * Auxiliary methods + */ + +static void +error_data_free (ErrorData *error_data) +{ + g_free (error_data->primary_text); + g_free (error_data->secondary_text); + g_free (error_data); +} + +static void +add_widgets (GtdTaskListsWorkspace *self, + GList *widgets) +{ + GList *l; + + for (l = widgets; l; l = l->next) + { + switch (gtk_widget_get_halign (l->data)) + { + case GTK_ALIGN_END: + gtk_box_append (self->panel_box_end, l->data); + break; + + case GTK_ALIGN_START: + case GTK_ALIGN_BASELINE: + case GTK_ALIGN_FILL: + default: + gtk_box_append (self->panel_box_start, l->data); + break; + } + } +} + +static void +remove_widgets (GtdTaskListsWorkspace *self, + GList *widgets) +{ + GList *l; + + for (l = widgets; l; l = l->next) + { + GtkBox *box; + + if (gtk_widget_get_halign (l->data) == GTK_ALIGN_END) + box = self->panel_box_end; + else + box = self->panel_box_start; + + g_object_ref (l->data); + gtk_box_remove (box, l->data); + } +} + +static void +update_panel_menu (GtdTaskListsWorkspace *self) +{ + GtkPopover *popover; + const GMenu *menu; + + popover = gtd_panel_get_popover (self->active_panel); + menu = gtd_panel_get_menu (self->active_panel); + + gtk_widget_set_visible (GTK_WIDGET (self->gear_menu_button), popover || menu); + + if (popover) + { + gtk_menu_button_set_popover (self->gear_menu_button, GTK_WIDGET (popover)); + } + else + { + gtk_menu_button_set_popover (self->gear_menu_button, NULL); + gtk_menu_button_set_menu_model (self->gear_menu_button, G_MENU_MODEL (menu)); + } +} + + +/* + * Callbacks + */ + +static void +on_action_activate_panel_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtdTaskListsWorkspace *self; + g_autoptr (GVariant) panel_parameters = NULL; + g_autofree gchar *panel_id = NULL; + GtdPanel *panel; + + self = GTD_TASK_LISTS_WORKSPACE (user_data); + + g_variant_get (parameters, + "(sv)", + &panel_id, + &panel_parameters); + + g_debug ("Activating panel '%s'", panel_id); + + panel = (GtdPanel *) gtk_stack_get_child_by_name (self->stack, panel_id); + g_return_if_fail (panel && GTD_IS_PANEL (panel)); + + gtd_panel_activate (panel, panel_parameters); + + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (panel)); + adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_FORWARD); +} + +static void +on_action_toggle_archive_activated_cb (GSimpleAction *simple, + GVariant *state, + gpointer user_data) +{ + GtdTaskListsWorkspace *self; + gboolean archive_visible; + + self = GTD_TASK_LISTS_WORKSPACE (user_data); + archive_visible = g_variant_get_boolean (state); + + gtk_widget_set_visible (self->new_list_button, !archive_visible); + gtd_sidebar_set_archive_visible (self->sidebar, archive_visible); +} + +static void +on_back_sidebar_button_clicked_cb (GtkButton *button, + GtdTaskListsWorkspace *self) +{ + adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_BACK); +} + +static void +on_back_button_clicked_cb (GtkButton *button, + GtdTaskListsWorkspace *self) +{ + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + FALSE); +} + +static void +on_panel_added_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdPanel *panel, + GtdTaskListsWorkspace *self) +{ + gtk_stack_add_titled (self->stack, + GTK_WIDGET (g_object_ref_sink (panel)), + gtd_panel_get_panel_name (panel), + gtd_panel_get_panel_title (panel)); + + g_signal_emit (self, signals[PANEL_ADDED], 0, panel); +} + +static void +on_panel_removed_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdPanel *panel, + GtdTaskListsWorkspace *self) +{ + g_object_ref (panel); + + gtk_stack_remove (self->stack, GTK_WIDGET (panel)); + g_signal_emit (self, signals[PANEL_REMOVED], 0, panel); + + g_object_unref (panel); +} + +static void +on_panel_menu_changed_cb (GObject *object, + GParamSpec *pspec, + GtdTaskListsWorkspace *self) +{ + if (GTD_PANEL (object) != self->active_panel) + return; + + update_panel_menu (self); +} +static void +on_stack_visible_child_cb (GtdTaskListsWorkspace *self, + GParamSpec *pspec, + GtkStack *stack) +{ + GtkWidget *visible_child; + GtdPanel *panel; + GList *header_widgets; + + GTD_ENTRY; + + visible_child = gtk_stack_get_visible_child (stack); + panel = GTD_PANEL (visible_child); + + /* Remove previous panel's widgets */ + if (self->active_panel) + { + header_widgets = gtd_panel_get_header_widgets (self->active_panel); + + /* Disconnect signals */ + g_signal_handlers_disconnect_by_func (self->active_panel, + on_panel_menu_changed_cb, + self); + + remove_widgets (self, header_widgets); + + g_list_free (header_widgets); + } + + /* Add current panel's header widgets */ + header_widgets = gtd_panel_get_header_widgets (panel); + add_widgets (self, header_widgets); + + g_list_free (header_widgets); + + g_signal_connect (panel, "notify::menu", G_CALLBACK (on_panel_menu_changed_cb), self); + + /* Set panel as the new active panel */ + g_set_object (&self->active_panel, panel); + + /* Setup the panel's menu */ + update_panel_menu (self); + + GTD_EXIT; +} + +void +on_toast_dismissed_cb (AdwToast *self, + gpointer user_data) +{ + GtdNotification *notification = user_data; + + GTD_ENTRY; + + gtd_notification_execute_dismissal_action (notification); + + GTD_EXIT; +} + +void +toast_activated_cb (GtdTaskListsWorkspace *self, + const char *action_name, + GVariant *parameter) +{ + AdwToast *toast; + GtdNotification *notification; + + GTD_ENTRY; + + toast = g_hash_table_lookup (self->notification_list, g_variant_get_string (parameter, NULL)); + + if (toast != NULL) { + notification = g_object_get_data (G_OBJECT (toast), "notification"); + + g_signal_handlers_block_by_func (toast, on_toast_dismissed_cb, notification); + gtd_notification_execute_secondary_action (notification); + } + + GTD_EXIT; +} + +static void +on_show_notification_cb (GtdManager *manager, + GtdNotification *notification, + GtdTaskListsWorkspace *self) +{ + AdwToast *toast; + GValue btn_label = G_VALUE_INIT; + + g_object_get_property (G_OBJECT (notification), "secondary-action-name", &btn_label); + + /* Convert GtdNotification to AdwToast */ + toast = adw_toast_new (gtd_notification_get_text (notification)); + adw_toast_set_button_label (toast, g_value_get_string (&btn_label)); + adw_toast_set_action_name (toast, "toast.activated"); + adw_toast_set_action_target_value (toast, g_variant_new_string (g_value_get_string (&btn_label))); + g_object_set_data (G_OBJECT (toast), "notification", notification); + + g_hash_table_insert (self->notification_list, (char *) g_value_get_string (&btn_label), toast); + g_signal_connect (toast, "dismissed", G_CALLBACK (on_toast_dismissed_cb), notification); + + if (adw_leaflet_get_folded (self->leaflet)) { + if (adw_leaflet_get_visible_child (self->leaflet) == self->content_box) + adw_toast_overlay_add_toast (self->content_overlay, toast); + if (adw_leaflet_get_visible_child (self->leaflet) == self->sidebar_box) + adw_toast_overlay_add_toast (self->sidebar_overlay, toast); + } else { + adw_toast_overlay_add_toast (self->content_overlay, toast); + } +} + +static void +error_message_notification_primary_action (GtdNotification *notification, + gpointer user_data) +{ + error_data_free (user_data); +} + +static void +error_message_notification_secondary_action (GtdNotification *notification, + gpointer user_data) +{ + GtkWidget *dialog; + ErrorData *data; + + data = user_data; + dialog = adw_message_dialog_new (GTK_WINDOW (data->window), + data->primary_text, + NULL); + + adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog), + "%s", + data->secondary_text); + + adw_message_dialog_add_response (ADW_MESSAGE_DIALOG (dialog), + "close", _("Close")); + + g_signal_connect (dialog, + "response", + G_CALLBACK (gtk_window_destroy), + NULL); + + gtk_widget_show (dialog); + + error_data_free (data); +} + +static void +on_show_error_message_cb (GtdManager *manager, + const gchar *primary_text, + const gchar *secondary_text, + GtdNotificationActionFunc function, + gpointer user_data, + GtdTaskListsWorkspace *self) +{ + GtdNotification *notification; + ErrorData *error_data; + + error_data = g_new0 (ErrorData, 1); + notification = gtd_notification_new (primary_text); + + error_data->window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))); + error_data->primary_text = g_strdup (primary_text); + error_data->secondary_text = g_strdup (secondary_text); + + gtd_notification_set_dismissal_action (notification, + error_message_notification_primary_action, + error_data); + + if (!function) + { + gtd_notification_set_secondary_action (notification, + _("Details"), + error_message_notification_secondary_action, + error_data); + } + else + { + gtd_notification_set_secondary_action (notification, secondary_text, function, user_data); + } + + gtd_manager_send_notification (gtd_manager_get_default (), notification); +} + +/* + * GtdWorkspace implementation + */ + +static const gchar* +gtd_task_lists_workspace_get_id (GtdWorkspace *workspace) +{ + return "task-lists"; +} + +static const gchar* +gtd_task_lists_workspace_get_title (GtdWorkspace *workspace) +{ + return _("Task Lists"); +} + +static gint +gtd_task_lists_workspace_get_priority (GtdWorkspace *workspace) +{ + return 1000; +} + +static GIcon* +gtd_task_lists_workspace_get_icon (GtdWorkspace *workspace) +{ + return g_themed_icon_new ("view-list-symbolic"); +} + +static void +gtd_task_lists_workspace_activate (GtdWorkspace *workspace, + GVariant *parameters) +{ + GtdTaskListsWorkspace *self = GTD_TASK_LISTS_WORKSPACE (workspace); + + if (parameters) + { + const gchar *panel_id = g_variant_get_string (parameters, NULL); + + g_debug ("Activating panel '%s'", panel_id); + gtk_stack_set_visible_child_name (self->stack, panel_id); + } + else + { + gtd_sidebar_activate (self->sidebar); + } +} + +static void +gtd_workspace_iface_init (GtdWorkspaceInterface *iface) +{ + iface->get_id = gtd_task_lists_workspace_get_id; + iface->get_title = gtd_task_lists_workspace_get_title; + iface->get_priority = gtd_task_lists_workspace_get_priority; + iface->get_icon = gtd_task_lists_workspace_get_icon; + iface->activate = gtd_task_lists_workspace_activate; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_lists_workspace_dispose (GObject *object) +{ + GtdTaskListsWorkspace *self = (GtdTaskListsWorkspace *)object; + + G_OBJECT_CLASS (gtd_task_lists_workspace_parent_class)->dispose (object); + + g_signal_handlers_disconnect_by_func (self->panels_set, on_panel_added_cb, self); + g_signal_handlers_disconnect_by_func (self->panels_set, on_panel_removed_cb, self); + g_clear_object (&self->panels_set); +} + +static void +gtd_task_lists_workspace_constructed (GObject *object) +{ + GtdManager *manager = gtd_manager_get_default (); + GtdTaskListsWorkspace *self = (GtdTaskListsWorkspace *)object; + + G_OBJECT_CLASS (gtd_task_lists_workspace_parent_class)->constructed (object); + + /* Add loaded panels */ + self->panels_set = peas_extension_set_new (peas_engine_get_default (), + GTD_TYPE_PANEL, + NULL); + + peas_extension_set_foreach (self->panels_set, + (PeasExtensionSetForeachFunc) on_panel_added_cb, + self); + + g_signal_connect (self->panels_set, "extension-added", G_CALLBACK (on_panel_added_cb), self); + g_signal_connect (self->panels_set, "extension-removed", G_CALLBACK (on_panel_removed_cb), self); + g_signal_connect (manager, "show-notification", G_CALLBACK (on_show_notification_cb), self); + g_signal_connect (manager, "show-error-message", G_CALLBACK (on_show_error_message_cb), self); +} + +static void +gtd_task_lists_workspace_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdWorkspace *workspace = GTD_WORKSPACE (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_take_object (value, gtd_workspace_get_icon (workspace)); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_workspace_get_title (workspace)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_lists_workspace_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_task_lists_workspace_class_init (GtdTaskListsWorkspaceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + g_resources_register (task_lists_workspace_get_resource ()); + + object_class->dispose = gtd_task_lists_workspace_dispose; + object_class->constructed = gtd_task_lists_workspace_constructed; + object_class->get_property = gtd_task_lists_workspace_get_property; + object_class->set_property = gtd_task_lists_workspace_set_property; + + + /** + * GtdTaskListsWorkspace::panel-added: + * @manager: a #GtdManager + * @panel: a #GtdPanel + * + * The ::panel-added signal is emmited after a #GtdPanel + * is added. + */ + signals[PANEL_ADDED] = g_signal_new ("panel-added", + GTD_TYPE_TASK_LISTS_WORKSPACE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PANEL); + + /** + * GtdTaskListsWorkspace::panel-removed: + * @manager: a #GtdManager + * @panel: a #GtdPanel + * + * The ::panel-removed signal is emmited after a #GtdPanel + * is removed from the list. + */ + signals[PANEL_REMOVED] = g_signal_new ("panel-removed", + GTD_TYPE_TASK_LISTS_WORKSPACE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PANEL); + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); + + g_type_ensure (GTD_TYPE_PROVIDER_POPOVER); + g_type_ensure (GTD_TYPE_SIDEBAR); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-task-lists-workspace.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, back_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, content_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, gear_menu_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, leaflet); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, new_list_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, panel_box_end); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, panel_box_start); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, primary_menu_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar_overlay); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, content_overlay); + + gtk_widget_class_bind_template_callback (widget_class, on_back_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_back_sidebar_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_stack_visible_child_cb); + + gtk_widget_class_install_action (widget_class, "toast.activated", "s", (GtkWidgetActionActivateFunc) toast_activated_cb); +} + +static void +gtd_task_lists_workspace_init (GtdTaskListsWorkspace *self) +{ + GtkApplication *application; + GMenu *primary_menu; + + static const GActionEntry entries[] = { + { "activate-panel", on_action_activate_panel_activated_cb, "(sv)" }, + { "toggle-archive", on_action_toggle_archive_activated_cb, "b" }, + }; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->notification_list = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + self->action_group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + entries, + G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), + "task-lists-workspace", + G_ACTION_GROUP (self->action_group)); + + gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->back_button), + g_variant_new_boolean (FALSE)); + + /* Task list panel */ + self->task_list_panel = GTD_PANEL (gtd_task_list_panel_new ()); + on_panel_added_cb (NULL, NULL, self->task_list_panel, self); + + gtd_sidebar_connect (self->sidebar, GTK_WIDGET (self)); + gtd_sidebar_set_panel_stack (self->sidebar, self->stack); + gtd_sidebar_set_task_list_panel (self->sidebar, self->task_list_panel); + + /* Fancy primary menu */ + application = GTK_APPLICATION (g_application_get_default ()); + primary_menu = gtk_application_get_menu_by_id (application, "primary-menu"); + gtk_menu_button_set_menu_model (self->primary_menu_button, G_MENU_MODEL (primary_menu)); +} + +GtdWorkspace* +gtd_task_lists_workspace_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LISTS_WORKSPACE, NULL); +} diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h new file mode 100644 index 0000000..0cbc97a --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h @@ -0,0 +1,35 @@ +/* gtd-task-lists-workspace.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LISTS_WORKSPACE (gtd_task_lists_workspace_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListsWorkspace, gtd_task_lists_workspace, GTD, TASK_LISTS_WORKSPACE, GtkBox) + +GtdWorkspace* gtd_task_lists_workspace_new (void); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui new file mode 100644 index 0000000..7d63d1d --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui @@ -0,0 +1,164 @@ + + + + + + bottom + + diff --git a/src/plugins/task-lists-workspace/meson.build b/src/plugins/task-lists-workspace/meson.build new file mode 100644 index 0000000..b80dc50 --- /dev/null +++ b/src/plugins/task-lists-workspace/meson.build @@ -0,0 +1,20 @@ +plugins_ldflags += ['-Wl,--undefined=task_lists_workspace_plugin_register_types'] + +task_lists_panel_sources = files( + 'gtd-sidebar.c', + 'gtd-sidebar-list-row.c', + 'gtd-sidebar-panel-row.c', + 'gtd-sidebar-provider-row.c', + 'gtd-task-list-panel.c', + 'gtd-task-lists-workspace.c', + 'task-lists-workspace-plugin.c', +) + +task_lists_panel_sources += gnome.compile_resources( + 'task-lists-workspace', + 'task-lists-workspace.gresource.xml', + c_name: 'task_lists_workspace', + export: true, +) + +plugins_sources += task_lists_panel_sources diff --git a/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c b/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c new file mode 100644 index 0000000..78d071d --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c @@ -0,0 +1,31 @@ +/* gtd-plugin-task-lists-workspace.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "endeavour.h" + +#include "gtd-task-lists-workspace.h" + +G_MODULE_EXPORT void +task_lists_workspace_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_WORKSPACE, + GTD_TYPE_TASK_LISTS_WORKSPACE); +} diff --git a/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml b/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml new file mode 100644 index 0000000..cf76721 --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml @@ -0,0 +1,12 @@ + + + + gtd-sidebar.ui + gtd-sidebar-list-row.ui + gtd-sidebar-panel-row.ui + gtd-sidebar-provider-row.ui + gtd-task-list-panel.ui + gtd-task-lists-workspace.ui + task-lists-workspace.plugin + + diff --git a/src/plugins/task-lists-workspace/task-lists-workspace.plugin b/src/plugins/task-lists-workspace/task-lists-workspace.plugin new file mode 100644 index 0000000..2b2f090 --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Task Lists Workspace +Module = task-lists-workspace +Description = Plugin implementing the task lists workspace +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = task_lists_workspace_plugin_register_types +Depends = diff --git a/src/plugins/today-panel/gtd-panel-today.c b/src/plugins/today-panel/gtd-panel-today.c new file mode 100644 index 0000000..b6cac7a --- /dev/null +++ b/src/plugins/today-panel/gtd-panel-today.c @@ -0,0 +1,480 @@ +/* gtd-panel-today.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "GtdPanelToday" + +#include +#include "gtd-panel-today.h" + +#include + +struct _GtdPanelToday +{ + GtkBox parent; + + GIcon *icon; + + gint day_change_callback_id; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkFilterListModel *incomplete_model; + GtkSortListModel *sort_model; + + GtkCssProvider *css_provider; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_EXTENDED (GtdPanelToday, gtd_panel_today, GTK_TYPE_BOX, + 0, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, + gtd_panel_iface_init)) + + +#define GTD_PANEL_TODAY_NAME "panel-today" +#define GTD_PANEL_TODAY_PRIORITY 1000 + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +load_css_provider (GtdPanelToday *self) +{ + g_autofree gchar *theme_name = NULL; + g_autofree gchar *theme_uri = NULL; + g_autoptr (GSettings) settings = NULL; + g_autoptr (GFile) css_file = NULL; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/theme/today-panel", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/today-panel/theme/Adwaita.css"); +} + +static gboolean +is_overdue (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) > g_date_time_get_year (today)) + return FALSE; + + if (g_date_time_get_year (dt) < g_date_time_get_year (today)) + return TRUE; + + return g_date_time_get_day_of_year (dt) < g_date_time_get_day_of_year (today); +} + +static gboolean +is_today (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) == g_date_time_get_year (today) && + g_date_time_get_day_of_year (dt) == g_date_time_get_day_of_year (today)) + { + return TRUE; + } + + return FALSE; +} + +static GtkWidget* +create_label (const gchar *text, + gboolean overdue) +{ + GtkWidget *label; + + label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "label", text, + "margin-top", overdue ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, overdue ? "date-overdue" : "date-scheduled"); + + return label; +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + gpointer user_data) +{ + g_autoptr (GDateTime) now = NULL; + g_autoptr (GDateTime) dt = NULL; + GtkWidget *header = NULL; + + now = g_date_time_new_now_local (); + dt = gtd_task_get_due_date (task); + + /* Only show a header if the we have overdue tasks */ + if (!previous_task && is_overdue (now, dt)) + { + header = create_label (_("Overdue"), TRUE); + } + else if (previous_task) + { + g_autoptr (GDateTime) previous_dt = NULL; + + previous_dt = gtd_task_get_due_date (previous_task); + + if (is_today (now, dt) != is_today (now, previous_dt)) + header = create_label (_("Today"), FALSE); + } + + return header; +} + + +/* + * Callbacks + */ + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + g_autoptr (GDateTime) now = NULL; + GtdTask *task; + gboolean complete; + + task = (GtdTask*) item; + now = g_date_time_new_now_local (); + task_dt = gtd_task_get_due_date (task); + + complete = gtd_task_get_complete (task); + + return is_today (now, task_dt) || (!complete && is_overdue (now, task_dt)); +} + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + + return !gtd_task_get_complete (task); +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + g_autoptr (GDateTime) dt1 = NULL; + g_autoptr (GDateTime) dt2 = NULL; + GtdTask *task1; + GtdTask *task2; + GDate dates[2]; + gint result; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + g_date_clear (dates, 2); + + g_date_set_dmy (&dates[0], + g_date_time_get_day_of_month (dt1), + g_date_time_get_month (dt1), + g_date_time_get_year (dt1)); + + g_date_set_dmy (&dates[1], + g_date_time_get_day_of_month (dt2), + g_date_time_get_month (dt2), + g_date_time_get_year (dt2)); + + result = g_date_days_between (&dates[1], &dates[0]); + + if (result != 0) + return result; + + return gtd_task_compare (task1, task2); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdPanelToday *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdPanelToday *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_today_get_panel_name (GtdPanel *panel) +{ + return GTD_PANEL_TODAY_NAME; +} + +static const gchar* +gtd_panel_today_get_panel_title (GtdPanel *panel) +{ + return _("Today"); +} + +static GList* +gtd_panel_today_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_today_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_today_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_PANEL_TODAY (panel)->icon); +} + +static guint32 +gtd_panel_today_get_priority (GtdPanel *panel) +{ + return GTD_PANEL_TODAY_PRIORITY; +} + +static gchar* +gtd_panel_today_get_subtitle (GtdPanel *panel) +{ + GtdPanelToday *self = GTD_PANEL_TODAY (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_today_get_panel_name; + iface->get_panel_title = gtd_panel_today_get_panel_title; + iface->get_header_widgets = gtd_panel_today_get_header_widgets; + iface->get_menu = gtd_panel_today_get_menu; + iface->get_icon = gtd_panel_today_get_icon; + iface->get_priority = gtd_panel_today_get_priority; + iface->get_subtitle = gtd_panel_today_get_subtitle; +} + +static void +gtd_panel_today_finalize (GObject *object) +{ + GtdPanelToday *self = (GtdPanelToday *)object; + + g_clear_object (&self->css_provider); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->incomplete_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_panel_today_parent_class)->finalize (object); +} + +static void +gtd_panel_today_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdPanelToday *self = GTD_PANEL_TODAY (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_PANEL_TODAY_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_PANEL_TODAY_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_panel_today_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_panel_today_class_init (GtdPanelTodayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_panel_today_finalize; + object_class->get_property = gtd_panel_today_get_property; + object_class->set_property = gtd_panel_today_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_panel_today_init (GtdPanelToday *self) +{ + g_autoptr (GDateTime) now = NULL; + GtdManager *manager; + GtkCustomFilter *incomplete_filter; + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + manager = gtd_manager_get_default (); + + self->icon = g_themed_icon_new ("view-tasks-today-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + incomplete_filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + self->incomplete_model = gtk_filter_list_model_new (G_LIST_MODEL (self->sort_model), + GTK_FILTER (incomplete_filter)); + + /* Connect to GtdManager::list-* signals to update the title */ + manager = gtd_manager_get_default (); + now = g_date_time_new_now_local (); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (self->view, G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (self->view, TRUE); + gtd_task_list_view_set_show_due_date (self->view, FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (self->view, header_func, self); + + g_signal_connect_object (self->incomplete_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); + + load_css_provider (self); +} + +GtkWidget* +gtd_panel_today_new (void) +{ + return g_object_new (GTD_TYPE_PANEL_TODAY, NULL); +} diff --git a/src/plugins/today-panel/gtd-panel-today.h b/src/plugins/today-panel/gtd-panel-today.h new file mode 100644 index 0000000..135e67a --- /dev/null +++ b/src/plugins/today-panel/gtd-panel-today.h @@ -0,0 +1,35 @@ +/* gtd-panel-today.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GTD_PANEL_TODAY_H +#define GTD_PANEL_TODAY_H + +#include +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_PANEL_TODAY (gtd_panel_today_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPanelToday, gtd_panel_today, GTD, PANEL_TODAY, GtkBox) + +GtkWidget* gtd_panel_today_new (void); + +G_END_DECLS + +#endif /* GTD_PANEL_TODAY_H */ diff --git a/src/plugins/today-panel/gtd-today-omni-area-addin.c b/src/plugins/today-panel/gtd-today-omni-area-addin.c new file mode 100644 index 0000000..d92979d --- /dev/null +++ b/src/plugins/today-panel/gtd-today-omni-area-addin.c @@ -0,0 +1,304 @@ +/* gtd-today-omni-area-addin.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-today-omni-area-addin.h" + +#include "endeavour.h" +#include "config.h" + +#include + +#define MESSAGE_ID "today-counter-message-id" + +struct _GtdTodayOmniAreaAddin +{ + GObject parent; + + GIcon *icon; + GtkFilterListModel *filter_model; + + GtdOmniArea *omni_area; + guint number_of_tasks; + + gboolean had_tasks; + gboolean finished_tasks; + + guint idle_update_message_timeout_id; +}; + +static void gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTodayOmniAreaAddin, gtd_today_omni_area_addin, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_OMNI_AREA_ADDIN, gtd_omni_area_addin_iface_init)) + +const gchar *end_messages[] = +{ + N_("No more tasks left"), + N_("Nothing else to do here"), + N_("You made it!"), + N_("Looks like there’s nothing else left here") +}; + +static gboolean +can_show_omni_area_message (GtdTodayOmniAreaAddin *self) +{ + GtdWorkspace *current_workspace; + GtdWindow *window; + + window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (self->omni_area))); + current_workspace = gtd_window_get_current_workspace (window); + + if (current_workspace && + g_str_equal (gtd_workspace_get_id (current_workspace), "task-lists")) + { + return TRUE; + } + + return FALSE; +} + +static void +update_omni_area_message (GtdTodayOmniAreaAddin *self) +{ + + g_autofree gchar *message = NULL; + + g_assert (self->omni_area != NULL); + + if (!can_show_omni_area_message (self)) + { + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + return; + } + + if (self->number_of_tasks > 0) + { + message = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, + "%d task for today", + "%d tasks for today", + self->number_of_tasks), + self->number_of_tasks); + } + else + { + if (self->finished_tasks) + { + gint message_index = g_random_int_range (0, G_N_ELEMENTS (end_messages)); + + message = g_strdup (gettext (end_messages[message_index])); + } + else + { + message = g_strdup (_("No tasks scheduled for today")); + } + } + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + gtd_omni_area_push_message (self->omni_area, MESSAGE_ID, message, self->icon); +} + +static gboolean +is_today (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) == g_date_time_get_year (today) && + g_date_time_get_day_of_year (dt) == g_date_time_get_day_of_year (today)) + { + return TRUE; + } + + return FALSE; +} + + +/* + * Callbacks + */ + +static gboolean +idle_update_omni_area_message_cb (gpointer user_data) +{ + GtdTodayOmniAreaAddin *self = GTD_TODAY_OMNI_AREA_ADDIN (user_data); + + update_omni_area_message (self); + + self->idle_update_message_timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + g_autoptr (GDateTime) now = NULL; + GtdTask *task; + + task = (GtdTask*) item; + + if (gtd_task_get_complete (task)) + return FALSE; + + now = g_date_time_new_now_local (); + task_dt = gtd_task_get_due_date (task); + + return is_today (now, task_dt); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdTodayOmniAreaAddin *self) +{ + GtkFilter *filter; + + self->had_tasks = FALSE; + self->finished_tasks = FALSE; + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTodayOmniAreaAddin *self) +{ + guint number_of_tasks = g_list_model_get_n_items (model); + + if (self->number_of_tasks == number_of_tasks) + return; + + self->number_of_tasks = number_of_tasks; + + if (number_of_tasks != 0) + self->had_tasks = number_of_tasks != 0; + + self->finished_tasks = self->had_tasks && number_of_tasks == 0; + + g_clear_handle_id (&self->idle_update_message_timeout_id, g_source_remove); + self->idle_update_message_timeout_id = g_timeout_add_seconds (2, idle_update_omni_area_message_cb, self); +} + +static void +on_window_current_workspace_changed_cb (GtdWindow *window, + GParamSpec *pspec, + GtdTodayOmniAreaAddin *self) +{ + update_omni_area_message (self); +} + + +/* + * GtdOmniAreaAddin iface + */ + +static void +gtd_today_omni_area_addin_omni_area_addin_load (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdTodayOmniAreaAddin *self; + GtdWindow *window; + + self = GTD_TODAY_OMNI_AREA_ADDIN (addin); + window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (omni_area))); + + g_signal_connect_object (window, + "notify::current-workspace", + G_CALLBACK (on_window_current_workspace_changed_cb), + self, + 0); + + self->omni_area = omni_area; + update_omni_area_message (self); +} + +static void +gtd_today_omni_area_addin_omni_area_addin_unload (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdTodayOmniAreaAddin *self = GTD_TODAY_OMNI_AREA_ADDIN (addin); + + gtd_omni_area_withdraw_message (omni_area, MESSAGE_ID); + self->omni_area = NULL; +} + +static void +gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface) +{ + iface->load = gtd_today_omni_area_addin_omni_area_addin_load; + iface->unload = gtd_today_omni_area_addin_omni_area_addin_unload; +} + +/* + * GObject overrides + */ + +static void +gtd_today_omni_area_addin_finalize (GObject *object) +{ + GtdTodayOmniAreaAddin *self = (GtdTodayOmniAreaAddin *)object; + + g_clear_handle_id (&self->idle_update_message_timeout_id, g_source_remove); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + + G_OBJECT_CLASS (gtd_today_omni_area_addin_parent_class)->finalize (object); +} + +static void +gtd_today_omni_area_addin_class_init (GtdTodayOmniAreaAddinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_today_omni_area_addin_finalize; +} + +static void +gtd_today_omni_area_addin_init (GtdTodayOmniAreaAddin *self) +{ + GtkCustomFilter *filter; + GtdManager *manager; + + manager = gtd_manager_get_default (); + + self->icon = g_themed_icon_new ("view-tasks-today-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + g_signal_connect_object (self->filter_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} diff --git a/src/plugins/today-panel/gtd-today-omni-area-addin.h b/src/plugins/today-panel/gtd-today-omni-area-addin.h new file mode 100644 index 0000000..e0fc04a --- /dev/null +++ b/src/plugins/today-panel/gtd-today-omni-area-addin.h @@ -0,0 +1,30 @@ +/* gtd-today-omni-area-addin.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GTD_TYPE_TODAY_OMNI_AREA_ADDIN (gtd_today_omni_area_addin_get_type()) +G_DECLARE_FINAL_TYPE (GtdTodayOmniAreaAddin, gtd_today_omni_area_addin, GTD, TODAY_OMNI_AREA_ADDIN, GObject) + +G_END_DECLS diff --git a/src/plugins/today-panel/meson.build b/src/plugins/today-panel/meson.build new file mode 100644 index 0000000..baf173b --- /dev/null +++ b/src/plugins/today-panel/meson.build @@ -0,0 +1,14 @@ +plugins_ldflags += ['-Wl,--undefined=today_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-panel-today.c', + 'gtd-today-omni-area-addin.c', + 'today-panel-plugin.c' +) + + +plugins_sources += gnome.compile_resources( + 'today-panel-resources', + 'today-panel.gresource.xml', + c_name: 'today_panel_plugin', +) diff --git a/src/plugins/today-panel/theme/Adwaita.css b/src/plugins/today-panel/theme/Adwaita.css new file mode 100644 index 0000000..93b4213 --- /dev/null +++ b/src/plugins/today-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: @theme_selected_bg_color; + font-size: 1.4rem; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 1.4rem; + font-weight: bold; +} diff --git a/src/plugins/today-panel/today-panel-plugin.c b/src/plugins/today-panel/today-panel-plugin.c new file mode 100644 index 0000000..9dbc6ce --- /dev/null +++ b/src/plugins/today-panel/today-panel-plugin.c @@ -0,0 +1,34 @@ +/* gtd-plugin-today-panel.c + * + * Copyright (C) 2016-2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "gtd-panel-today.h" +#include "gtd-today-omni-area-addin.h" + +G_MODULE_EXPORT void +today_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_PANEL_TODAY); + + peas_object_module_register_extension_type (module, + GTD_TYPE_OMNI_AREA_ADDIN, + GTD_TYPE_TODAY_OMNI_AREA_ADDIN); +} diff --git a/src/plugins/today-panel/today-panel.gresource.xml b/src/plugins/today-panel/today-panel.gresource.xml new file mode 100644 index 0000000..1b84499 --- /dev/null +++ b/src/plugins/today-panel/today-panel.gresource.xml @@ -0,0 +1,7 @@ + + + + today-panel.plugin + theme/Adwaita.css + + diff --git a/src/plugins/today-panel/today-panel.plugin b/src/plugins/today-panel/today-panel.plugin new file mode 100644 index 0000000..89640ff --- /dev/null +++ b/src/plugins/today-panel/today-panel.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Today tasks +Module = today-panel +Description = A panel to show tasks scheduled for today +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = today_panel_plugin_register_types +Depends = diff --git a/src/themes/_omniarea.css b/src/themes/_omniarea.css new file mode 100644 index 0000000..1b575d1 --- /dev/null +++ b/src/themes/_omniarea.css @@ -0,0 +1,13 @@ +omniarea entry { + background-color: mix(@theme_bg_color, @content_view_bg, 0.25); + color: @theme_fg_color; +} +omniarea:hover entry, +omniarea:active entry { + background-color: @content_view_bg; + color: @theme_fg_color; +} + +omniarea:backdrop entry { + background-color: @theme_bg_color; +} diff --git a/src/themes/_taskrow.css b/src/themes/_taskrow.css new file mode 100644 index 0000000..f3ccee4 --- /dev/null +++ b/src/themes/_taskrow.css @@ -0,0 +1,38 @@ +taskrow { + transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + border-radius: 12px; +} + +taskrow:hover { + background-color: alpha(@theme_fg_color, 0.1); +} + +taskrow:focus { + background-color: alpha(@theme_fg_color, 0.05); +} + +taskrow.card:hover { + background-color: alpha(@theme_fg_color, 0.01); +} + +taskrow:dir(ltr) { padding-left: 6px; } +taskrow:dir(rtl) { padding-right: 6px; } + +/* task title entry */ +taskrow text { + border: solid 1px transparent; + background: none; + border-radius: 3px; +} + +/* line-through complete tasks */ +taskrow.complete > * { opacity: 0.5; } +taskrow.complete entry.title { text-decoration-line: line-through; } + +/* dnd row */ +taskrow box.dnd { + background: alpha(@theme_fg_color, 0.1); +} + +/* new task row */ +newtaskrow entry { padding: 0 16px 0 14px; } diff --git a/src/themes/_widgets.css b/src/themes/_widgets.css new file mode 100644 index 0000000..0ee041c --- /dev/null +++ b/src/themes/_widgets.css @@ -0,0 +1,27 @@ +/* Star Widget */ + +@keyframes wiggle { + 12.5% { transform: rotate(15deg); } + 37.5% { transform: rotate(-15deg); } + 62.5% { transform: rotate(15deg); } + 87.5% { transform: rotate(-15deg); } +} + +star { + color: alpha(@theme_fg_color, 0.1); + transition: color 400ms; +} + +star:hover { + color: alpha(@theme_fg_color, 0.4); + transition: color 250ms; +} + +star:checked { + color: @theme_selected_bg_color; + transition: color 250ms; + + animation-name: wiggle; + animation-duration: 400ms; + animation-timing-function: linear; +} diff --git a/src/themes/style.css b/src/themes/style.css new file mode 100644 index 0000000..a9096b3 --- /dev/null +++ b/src/themes/style.css @@ -0,0 +1,58 @@ +@import url("resource:///org/gnome/todo/themes/_omniarea.css"); +@import url("resource:///org/gnome/todo/themes/_taskrow.css"); +@import url("resource:///org/gnome/todo/themes/_widgets.css"); + +tasklistview list:drop(active) { + border: none; +} + +row.thumbnail { + border: solid 1px @borders; + box-shadow: 0px 0px 3px alpha(@borders, 0.8); + border-radius: 16px; +} + +grid-item.thumbnail { + padding: 6px; + font-size: 10px; +} + +grid-item image { + margin: 12px 24px; + font-size: 10px; + border: solid 1px @borders; + border-radius: 4px; + box-shadow: 0px 0px 4px alpha(@borders, 0.8); +} + +grid-item { + background-color: transparent; +} + +grid-item.light { + color: #000003; +} + +grid-item.dark { + color: #eeeeec; +} + +arrow-frame { + border: solid 1px; + border-bottom-width: 0px; + border-top-width: 0px; + background-color: @theme_bg_color; + border-color: @borders; +} + +arrow-frame:dir(ltr) { + border-right-width: 0px; +} + +arrow-frame:dir(rtl) { + border-left-width: 0px; +} + +label.main-title { + font-size: 28px; +} diff --git a/src/todo.gresource.xml b/src/todo.gresource.xml new file mode 100644 index 0000000..e9910f3 --- /dev/null +++ b/src/todo.gresource.xml @@ -0,0 +1,9 @@ + + + + themes/style.css + themes/_omniarea.css + themes/_taskrow.css + themes/_widgets.css + + -- cgit v1.2.3