@@ -0,0 +1,743 @@
/* gcal-agenda-view.c
*
* Copyright (C) 2022 Purism SPC
*
* 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 <http://www.gnu.org/licenses/>.
*/
# define G_LOG_DOMAIN "GcalAgendaView"
# include "gcal-agenda-view.h"
# include "gcal-debug.h"
# include "gcal-enums.h"
# include "gcal-event-widget.h"
# include "gcal-range-tree.h"
# include "gcal-timeline-subscriber.h"
# include "gcal-utils.h"
# include "gcal-view.h"
# include <adwaita.h>
# include <glib/gi18n.h>
# include <math.h>
typedef struct
{
GtkWidget * widget ;
GcalEvent * event ;
GcalAgendaView * self ;
} ChildData ;
struct _GcalAgendaView
{
GtkBox parent ;
GtkWidget * list_box ;
GtkWidget * no_events_row ;
GtkWidget * scrolled_window ;
/* property */
GDateTime * date ;
GcalContext * context ;
GcalRangeTree * events ;
guint scroll_grid_timeout_id ;
gulong stack_page_changed_id ;
gint events_on_date ;
gint clicked_cell ;
} ;
static void schedule_position_scroll ( GcalAgendaView * self ) ;
static void gcal_view_interface_init ( GcalViewInterface * iface ) ;
static void gcal_timeline_subscriber_interface_init ( GcalTimelineSubscriberInterface * iface ) ;
enum
{
PROP_0 ,
PROP_DATE ,
PROP_CONTEXT ,
N_PROPS ,
} ;
G_DEFINE_TYPE_WITH_CODE ( GcalAgendaView , gcal_agenda_view , GTK_TYPE_BOX ,
G_IMPLEMENT_INTERFACE ( GCAL_TYPE_VIEW , gcal_view_interface_init )
G_IMPLEMENT_INTERFACE ( GCAL_TYPE_TIMELINE_SUBSCRIBER ,
gcal_timeline_subscriber_interface_init ) ) ;
/*
* Auxiliary methods
*/
static ChildData *
child_data_new ( GtkWidget * widget ,
GcalEvent * event ,
GcalAgendaView * self )
{
ChildData * data ;
data = g_new ( ChildData , 1 ) ;
data - > widget = widget ;
data - > event = g_object_ref ( event ) ;
data - > self = self ;
return data ;
}
static void
child_data_free ( gpointer data )
{
ChildData * child_data = data ;
if ( ! child_data )
return ;
if ( child_data - > widget & & child_data - > self )
{
gtk_list_box_remove ( GTK_LIST_BOX ( child_data - > self - > list_box ) , child_data - > widget ) ;
child_data - > widget = NULL ;
child_data - > self = NULL ;
}
g_clear_object ( & child_data - > event ) ;
g_free ( child_data ) ;
}
static GDateTime *
get_start_date_for_row ( GcalAgendaView * self ,
GtkListBoxRow * row )
{
GtkWidget * widget = gtk_list_box_row_get_child ( row ) ;
if ( GCAL_IS_EVENT_WIDGET ( widget ) )
{
GDateTime * date = gcal_event_widget_get_date_start ( GCAL_EVENT_WIDGET ( widget ) ) ;
/* Pretend events start at least on the selected date */
if ( gcal_date_time_compare_date ( date , self - > date ) > 0 )
return date ;
else
return self - > date ;
}
return self - > date ;
}
static GDateTime *
get_end_date_for_row ( GcalAgendaView * self ,
GtkListBoxRow * row )
{
GtkWidget * widget = gtk_list_box_row_get_child ( row ) ;
if ( GCAL_IS_EVENT_WIDGET ( widget ) )
return gcal_event_widget_get_date_end ( GCAL_EVENT_WIDGET ( widget ) ) ;
return self - > date ;
}
static gboolean
get_row_is_multiday ( GtkListBoxRow * row )
{
GtkWidget * widget = gtk_list_box_row_get_child ( row ) ;
if ( GCAL_IS_EVENT_WIDGET ( widget ) )
{
GcalEvent * event = gcal_event_widget_get_event ( GCAL_EVENT_WIDGET ( widget ) ) ;
return gcal_event_is_multiday ( event ) ;
}
return FALSE ;
}
static gchar *
new_date_header_string ( GDateTime * date )
{
g_autoptr ( GDateTime ) today = NULL ;
g_autoptr ( GDateTime ) tomorrow = NULL ;
g_autoptr ( GDateTime ) yesterday = NULL ;
if ( date = = NULL )
return NULL ;
today = g_date_time_new_now_local ( ) ;
tomorrow = g_date_time_add_days ( today , 1 ) ;
yesterday = g_date_time_add_days ( today , - 1 ) ;
if ( gcal_date_time_compare_date ( date , today ) = = 0 )
return g_strdup ( " Today " ) ;
else if ( gcal_date_time_compare_date ( date , tomorrow ) = = 0 )
return g_strdup ( " Tomorrow " ) ;
else if ( gcal_date_time_compare_date ( date , yesterday ) = = 0 )
return g_strdup ( " Yesterday " ) ;
else
return g_date_time_format ( date , " %A %B %e " ) ;
}
/*
* Callbacks
*/
static void
on_event_activated ( GcalAgendaView * self ,
GtkWidget * widget )
{
g_signal_emit_by_name ( self , " event-activated " , widget ) ;
}
static void
stack_visible_child_changed_cb ( AdwViewStack * stack ,
GParamSpec * pspec ,
GcalAgendaView * self )
{
if ( adw_view_stack_get_visible_child ( stack ) ! = ( GtkWidget * ) self )
return ;
schedule_position_scroll ( self ) ;
g_clear_signal_handler ( & self - > stack_page_changed_id , stack ) ;
}
/* Auxiliary methods */
static void
update_no_events_row ( GcalAgendaView * self )
{
gboolean visible = ! self - > events_on_date ;
gboolean was_visible = gtk_widget_get_visible ( self - > no_events_row ) ;
if ( visible ! = was_visible )
{
gtk_widget_set_visible ( self - > no_events_row , visible ) ;
gtk_list_box_invalidate_headers ( GTK_LIST_BOX ( self - > list_box ) ) ;
}
}
static gboolean
update_grid_scroll_position ( GcalAgendaView * self )
{
g_autoptr ( GDateTime ) week_start = NULL ;
g_autoptr ( GDateTime ) week_end = NULL ;
g_autoptr ( GDateTime ) now = NULL ;
GtkAdjustment * vadjustment ;
gdouble minutes , real_value ;
gdouble max , page , page_increment , value ;
gboolean dummy ;
/* While the scrolled window is not mapped, we keep waiting */
if ( ! gtk_widget_get_realized ( self - > scrolled_window ) | |
! gtk_widget_get_mapped ( self - > scrolled_window ) )
{
if ( self - > stack_page_changed_id = = 0 )
{
GtkWidget * stack = gtk_widget_get_ancestor ( GTK_WIDGET ( self ) , ADW_TYPE_VIEW_STACK ) ;
self - > stack_page_changed_id = g_signal_connect_object ( stack ,
" notify::visible-child " ,
G_CALLBACK ( stack_visible_child_changed_cb ) ,
self ,
0 ) ;
}
self - > scroll_grid_timeout_id = 0 ;
GCAL_RETURN ( G_SOURCE_REMOVE ) ;
}
now = g_date_time_new_now_local ( ) ;
week_start = gcal_date_time_get_start_of_week ( self - > date ) ;
week_end = gcal_date_time_get_end_of_week ( self - > date ) ;
/* Don't animate when not today */
if ( gcal_date_time_compare_date ( now , week_start ) < 0 | | gcal_date_time_compare_date ( now , week_end ) > = 0 )
GCAL_GOTO ( out ) ;
vadjustment = gtk_scrolled_window_get_vadjustment ( GTK_SCROLLED_WINDOW ( self - > scrolled_window ) ) ;
minutes = g_date_time_get_hour ( now ) * 60 + g_date_time_get_minute ( now ) ;
page = gtk_adjustment_get_page_size ( vadjustment ) ;
max = gtk_adjustment_get_upper ( vadjustment ) ;
real_value = max / MINUTES_PER_DAY * minutes - ( page / 2.0 ) ;
page_increment = gtk_adjustment_get_page_increment ( vadjustment ) ;
value = gtk_adjustment_get_value ( vadjustment ) ;
gtk_adjustment_set_page_increment ( vadjustment , real_value - value ) ;
g_signal_emit_by_name ( self - > scrolled_window ,
" scroll-child " ,
GTK_SCROLL_PAGE_FORWARD ,
FALSE ,
& dummy ) ;
gtk_adjustment_set_page_increment ( vadjustment , page_increment ) ;
out :
self - > scroll_grid_timeout_id = 0 ;
GCAL_RETURN ( G_SOURCE_REMOVE ) ;
}
static void
schedule_position_scroll ( GcalAgendaView * self )
{
/* Nothing is scheduled, we should do it now */
if ( self - > scroll_grid_timeout_id > 0 )
g_source_remove ( self - > scroll_grid_timeout_id ) ;
self - > scroll_grid_timeout_id = g_timeout_add ( 200 ,
( GSourceFunc ) update_grid_scroll_position ,
self ) ;
}
static int
sort_func ( GtkListBoxRow * row1 ,
GtkListBoxRow * row2 ,
gpointer user_data )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( user_data ) ;
gboolean multiday1 , multiday2 ;
int result ;
result = gcal_date_time_compare_date ( get_start_date_for_row ( self , row1 ) ,
get_start_date_for_row ( self , row2 ) ) ;
if ( result ! = 0 )
return result ;
multiday1 = get_row_is_multiday ( row1 ) ;
multiday2 = get_row_is_multiday ( row2 ) ;
/* Sort multi day events before multi single day events of the same day. */
if ( multiday1 ! = multiday2 )
return multiday1 ? - 1 : 1 ;
return gcal_date_time_compare_date ( get_end_date_for_row ( self , row1 ) ,
get_end_date_for_row ( self , row2 ) ) ;
}
static void
update_header ( GtkListBoxRow * row ,
GtkListBoxRow * before ,
gpointer user_data )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( user_data ) ;
GDateTime * start = get_start_date_for_row ( self , row ) ;
GDateTime * end = get_end_date_for_row ( self , row ) ;
g_autofree gchar * label = NULL ;
GtkWidget * header ;
g_autoptr ( GDateTime ) today = NULL ;
today = g_date_time_new_now_local ( ) ;
if ( before )
{
GDateTime * before_start = get_start_date_for_row ( self , before ) ;
GDateTime * before_end = get_end_date_for_row ( self , before ) ;
if ( get_row_is_multiday ( row ) & &
get_row_is_multiday ( before ) & &
gcal_date_time_compare_date ( start , before_start ) = = 0 & &
gcal_date_time_compare_date ( start , today ) = = 0 )
{
gtk_list_box_row_set_header ( row , NULL ) ;
return ;
}
if ( gcal_date_time_compare_date ( start , before_start ) = = 0 & &
gcal_date_time_compare_date ( end , before_end ) = = 0 )
{
gtk_list_box_row_set_header ( row , NULL ) ;
return ;
}
}
if ( get_row_is_multiday ( row ) )
{
g_autofree gchar * start_label = NULL ;
g_autofree gchar * end_label = NULL ;
if ( gcal_date_time_compare_date ( start , today ) = = 0 )
{
label = g_strdup ( " On-going " ) ;
}
else
{
start_label = new_date_header_string ( start ) ;
end_label = new_date_header_string ( end ) ;
label = g_strdup_printf ( " %s – %s " , start_label , end_label ) ;
}
}
else
{
label = new_date_header_string ( start ) ;
}
header = g_object_new ( GTK_TYPE_LABEL ,
" label " , label ,
" xalign " , 0 ,
NULL ) ;
gtk_widget_add_css_class ( header , " caption-heading " ) ;
gtk_list_box_row_set_header ( row , header ) ;
}
/*
* GcalView interface
*/
static GDateTime *
gcal_agenda_view_get_date ( GcalView * view )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( view ) ;
return self - > date ;
}
static inline gboolean
count_events_on_data ( GcalRange * range ,
gpointer data ,
gpointer user_data )
{
ChildData * child_data = data ;
GcalAgendaView * self = user_data ;
if ( gcal_date_time_compare_date ( self - > date , gcal_event_get_date_start ( child_data - > event ) ) = = 0 )
self - > events_on_date + + ;
return FALSE ;
}
static void
gcal_agenda_view_set_date ( GcalView * view ,
GDateTime * date )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( view ) ;
GCAL_ENTRY ;
gcal_set_date_time ( & self - > date , date ) ;
schedule_position_scroll ( self ) ;
self - > events_on_date = 0 ;
gcal_range_tree_traverse ( self - > events , G_IN_ORDER , count_events_on_data , self ) ;
update_no_events_row ( self ) ;
gcal_timeline_subscriber_range_changed ( GCAL_TIMELINE_SUBSCRIBER ( view ) ) ;
GCAL_EXIT ;
}
static GList *
gcal_agenda_view_get_children_by_uuid ( GcalView * view ,
GcalRecurrenceModType mod ,
const gchar * uuid )
{
GCAL_ENTRY ;
/* FIXME Not sure what to do here. */
GCAL_RETURN ( NULL ) ;
}
static void
gcal_agenda_view_clear_marks ( GcalView * view )
{
/* FIXME Not sure what to do here. */
}
static GDateTime *
gcal_agenda_view_get_next_date ( GcalView * view )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( view ) ;
g_assert ( self - > date ! = NULL ) ;
return g_date_time_add_weeks ( self - > date , 1 ) ;
}
static GDateTime *
gcal_agenda_view_get_previous_date ( GcalView * view )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( view ) ;
g_assert ( self - > date ! = NULL ) ;
return g_date_time_add_weeks ( self - > date , - 1 ) ;
}
static void
gcal_view_interface_init ( GcalViewInterface * iface )
{
iface - > get_date = gcal_agenda_view_get_date ;
iface - > set_date = gcal_agenda_view_set_date ;
iface - > get_children_by_uuid = gcal_agenda_view_get_children_by_uuid ;
iface - > clear_marks = gcal_agenda_view_clear_marks ;
iface - > get_next_date = gcal_agenda_view_get_next_date ;
iface - > get_previous_date = gcal_agenda_view_get_previous_date ;
}
/*
* GcalTimelineSubscriber iface
*/
static GcalRange *
gcal_agenda_view_get_range ( GcalTimelineSubscriber * subscriber )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( subscriber ) ;
return gcal_range_new_take ( g_date_time_ref ( self - > date ) ,
gcal_date_time_get_end_of_week ( self - > date ) ,
GCAL_RANGE_DEFAULT ) ;
}
static void
gcal_agenda_view_add_event ( GcalTimelineSubscriber * subscriber ,
GcalEvent * event )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( subscriber ) ;
GtkWidget * widget , * row ;
GcalTimestampPolicy timestamp_policy ;
GCAL_ENTRY ;
/* FIXME Check gcal_event_is_multiday (event) and gcal_event_get_all_day (event) */
g_object_ref ( event ) ;
if ( gcal_date_time_compare_date ( self - > date , gcal_event_get_date_start ( event ) ) = = 0 )
{
self - > events_on_date + + ;
update_no_events_row ( self ) ;
}
/* Create and add the new event widget */
if ( gcal_event_get_all_day ( event ) | | gcal_event_is_multiday ( event ) )
timestamp_policy = GCAL_TIMESTAMP_POLICY_END ;
else
timestamp_policy = GCAL_TIMESTAMP_POLICY_START ;
widget = g_object_new ( GCAL_TYPE_EVENT_WIDGET ,
" context " , self - > context ,
" event " , event ,
" orientation " , GTK_ORIENTATION_VERTICAL ,
" timestamp-policy " , timestamp_policy ,
NULL ) ;
row = g_object_new ( GTK_TYPE_LIST_BOX_ROW ,
" activatable " , FALSE ,
" child " , widget ,
NULL ) ;
gcal_range_tree_add_range ( self - > events ,
gcal_event_get_range ( event ) ,
child_data_new ( row , event , self ) ) ;
/* FIXME */
/* g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self); */
gtk_list_box_append ( GTK_LIST_BOX ( self - > list_box ) , row ) ;
gtk_list_box_invalidate_headers ( GTK_LIST_BOX ( self - > list_box ) ) ;
GCAL_EXIT ;
}
static void
gcal_agenda_view_remove_event ( GcalTimelineSubscriber * subscriber ,
GcalEvent * event )
{
GcalAgendaView * self = GCAL_AGENDA_VIEW ( subscriber ) ;
const gchar * uid = gcal_event_get_uid ( event ) ;
GPtrArray * widgets ;
guint i ;
GCAL_ENTRY ;
widgets = gcal_range_tree_get_all_data ( self - > events ) ;
if ( gcal_date_time_compare_date ( self - > date , gcal_event_get_date_start ( event ) ) = = 0 )
{
self - > events_on_date - - ;
update_no_events_row ( self ) ;
}
for ( i = 0 ; widgets & & i < widgets - > len ; i + + )
{
ChildData * data ;
GtkWidget * widget ;
GcalEvent * event ;
data = g_ptr_array_index ( widgets , i ) ;
widget = gtk_list_box_row_get_child ( GTK_LIST_BOX_ROW ( data - > widget ) ) ;
event = gcal_event_widget_get_event ( GCAL_EVENT_WIDGET ( widget ) ) ;
if ( g_strcmp0 ( gcal_event_get_uid ( event ) , uid ) ! = 0 )
continue ;
gcal_range_tree_remove_range ( self - > events , gcal_event_get_range ( event ) , data ) ;
gtk_widget_queue_allocate ( GTK_WIDGET ( self ) ) ;
}
g_clear_pointer ( & widgets , g_ptr_array_unref ) ;
gtk_list_box_invalidate_headers ( GTK_LIST_BOX ( self - > list_box ) ) ;
GCAL_EXIT ;
}
static void
gcal_agenda_view_update_event ( GcalTimelineSubscriber * subscriber ,
GcalEvent * event )
{
GCAL_ENTRY ;
gcal_agenda_view_remove_event ( subscriber , event ) ;
gcal_agenda_view_add_event ( subscriber , event ) ;
GCAL_EXIT ;
}
static void
gcal_timeline_subscriber_interface_init ( GcalTimelineSubscriberInterface * iface )
{
iface - > get_range = gcal_agenda_view_get_range ;
iface - > add_event = gcal_agenda_view_add_event ;
iface - > update_event = gcal_agenda_view_update_event ;
iface - > remove_event = gcal_agenda_view_remove_event ;
}
/*
* GObject overrides
*/
static void
gcal_agenda_view_dispose ( GObject * object )
{
GcalAgendaView * self ;
self = GCAL_AGENDA_VIEW ( object ) ;
g_clear_pointer ( & self - > events , gcal_range_tree_unref ) ;
/* Chain up to parent's dispose() method. */
G_OBJECT_CLASS ( gcal_agenda_view_parent_class ) - > dispose ( object ) ;
}
static void
gcal_agenda_view_finalize ( GObject * object )
{
GcalAgendaView * self ;
self = GCAL_AGENDA_VIEW ( object ) ;
g_clear_pointer ( & self - > date , g_date_time_unref ) ;
g_clear_object ( & self - > context ) ;
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS ( gcal_agenda_view_parent_class ) - > finalize ( object ) ;
}
static void
gcal_agenda_view_set_property ( GObject * object ,
guint property_id ,
const GValue * value ,
GParamSpec * pspec )
{
GcalAgendaView * self = ( GcalAgendaView * ) object ;
switch ( property_id )
{
case PROP_DATE :
gcal_view_set_date ( GCAL_VIEW ( object ) , g_value_get_boxed ( value ) ) ;
break ;
case PROP_CONTEXT :
g_assert ( self - > context = = NULL ) ;
self - > context = g_value_dup_object ( value ) ;
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID ( object , property_id , pspec ) ;
break ;
}
}
static void
gcal_agenda_view_get_property ( GObject * object ,
guint property_id ,
GValue * value ,
GParamSpec * pspec )
{
GcalAgendaView * self ;
g_return_if_fail ( GCAL_IS_AGENDA_VIEW ( object ) ) ;
self = GCAL_AGENDA_VIEW ( object ) ;
switch ( property_id )
{
case PROP_DATE :
g_value_set_boxed ( value , self - > date ) ;
break ;
case PROP_CONTEXT :
g_value_set_object ( value , self - > context ) ;
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID ( object , property_id , pspec ) ;
break ;
}
}
static void
gcal_agenda_view_class_init ( GcalAgendaViewClass * klass )
{
GObjectClass * object_class = G_OBJECT_CLASS ( klass ) ;
GtkWidgetClass * widget_class = GTK_WIDGET_CLASS ( klass ) ;
object_class - > dispose = gcal_agenda_view_dispose ;
object_class - > finalize = gcal_agenda_view_finalize ;
object_class - > set_property = gcal_agenda_view_set_property ;
object_class - > get_property = gcal_agenda_view_get_property ;
g_object_class_override_property ( object_class , PROP_DATE , " active-date " ) ;
g_object_class_override_property ( object_class , PROP_CONTEXT , " context " ) ;
gtk_widget_class_set_template_from_resource ( widget_class , " /org/gnome/calendar/ui/views/gcal-agenda-view.ui " ) ;
gtk_widget_class_bind_template_child ( widget_class , GcalAgendaView , list_box ) ;
gtk_widget_class_bind_template_child ( widget_class , GcalAgendaView , no_events_row ) ;
gtk_widget_class_bind_template_child ( widget_class , GcalAgendaView , scrolled_window ) ;
gtk_widget_class_bind_template_callback ( widget_class , on_event_activated ) ;
gtk_widget_class_set_css_name ( widget_class , " agenda-view " ) ;
}
static void
gcal_agenda_view_init ( GcalAgendaView * self )
{
gtk_widget_init_template ( GTK_WIDGET ( self ) ) ;
self - > events = gcal_range_tree_new_with_free_func ( child_data_free ) ;
gtk_list_box_set_sort_func ( GTK_LIST_BOX ( self - > list_box ) , sort_func , self , NULL ) ;
gtk_list_box_set_header_func ( GTK_LIST_BOX ( self - > list_box ) , update_header , self , NULL ) ;
}