From c485df7554158379bbbd3c60d232a0612f786437 Mon Sep 17 00:00:00 2001 From: Damon Chaplin Date: Sun, 19 Dec 1999 21:13:19 +0000 Subject: added widgets/meeting-time-sel/Makefile 1999-12-19 Damon Chaplin * configure.in (AC_OUTPUT): added widgets/meeting-time-sel/Makefile In widgets directory: 1999-12-19 Damon Chaplin * Makefile.am (SUBDIRS): created SUBDIRS with meeting-time-sel. * meeting-time-sel/Makefile.am: * meeting-time-sel/.cvsignore: * meeting-time-sel/e-meeting-time-sel.h: * meeting-time-sel/e-meeting-time-sel.c: * meeting-time-sel/e-meeting-time-sel-item.h: * meeting-time-sel/e-meeting-time-sel-item.c: * meeting-time-sel/e-meeting-time-sel-list-item.h: * meeting-time-sel/e-meeting-time-sel-list-item.c: * meeting-time-sel/e-meeting-time-sel-mail.xpm: * meeting-time-sel/e-meeting-time-sel-no-mail.xpm: * meeting-time-sel/test-meeting-time-sel.c: new files implementing the meeting time selector. svn path=/trunk/; revision=1505 --- widgets/meeting-time-sel/e-meeting-time-sel.c | 3065 +++++++++++++++++++++++++ 1 file changed, 3065 insertions(+) create mode 100644 widgets/meeting-time-sel/e-meeting-time-sel.c (limited to 'widgets/meeting-time-sel/e-meeting-time-sel.c') diff --git a/widgets/meeting-time-sel/e-meeting-time-sel.c b/widgets/meeting-time-sel/e-meeting-time-sel.c new file mode 100644 index 0000000000..86f81cc80e --- /dev/null +++ b/widgets/meeting-time-sel/e-meeting-time-sel.c @@ -0,0 +1,3065 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +/* + * Author : + * Damon Chaplin + * + * Copyright 1999, Helix Code, Inc. + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "e-meeting-time-sel.h" +#include "e-meeting-time-sel-item.h" +#include "e-meeting-time-sel-list-item.h" + +/* An array of hour strings, "0:00" .. "23:00". */ +const gchar *EMeetingTimeSelectorHours[24] = { + "0:00", "1:00", "2:00", "3:00", "4:00", "5:00", "6:00", "7:00", + "8:00", "9:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", + "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00" +}; + +/* The number of days shown in the entire canvas. */ +#define E_MEETING_TIME_SELECTOR_DAYS_SHOWN 365 + +#define E_MEETING_TIME_SELECTOR_ENTRY_INNER_BORDER 2 + +/* This is the number of pixels between the mouse has to move before the + scroll speed is incremented. */ +#define E_MEETING_TIME_SELECTOR_SCROLL_INCREMENT_WIDTH 10 + +/* This is the maximum scrolling speed. */ +#define E_MEETING_TIME_SELECTOR_MAX_SCROLL_SPEED 4 + + +static void e_meeting_time_selector_class_init (EMeetingTimeSelectorClass * klass); +static void e_meeting_time_selector_init (EMeetingTimeSelector * mts); +static void e_meeting_time_selector_destroy (GtkObject *object); +static void e_meeting_time_selector_alloc_named_color (EMeetingTimeSelector * mts, + const char *name, GdkColor *c); +static void e_meeting_time_selector_add_key_color (EMeetingTimeSelector * mts, + GtkWidget *hbox, + gchar *label_text, + GdkColor *color); +static gint e_meeting_time_selector_expose_key_color (GtkWidget *darea, + GdkEventExpose *event, + GdkColor *color); +static gint e_meeting_time_selector_expose_title_bar (GtkWidget *darea, + GdkEventExpose *event, + gpointer data); +static void e_meeting_time_selector_options_menu_detacher (GtkWidget *widget, + GtkMenu *menu); +static void e_meeting_time_selector_autopick_menu_detacher (GtkWidget *widget, + GtkMenu *menu); +static void e_meeting_time_selector_realize (GtkWidget *widget); +static void e_meeting_time_selector_unrealize (GtkWidget *widget); +static void e_meeting_time_selector_style_set (GtkWidget *widget, + GtkStyle *previous_style); +static gint e_meeting_time_selector_expose_event (GtkWidget *widget, + GdkEventExpose *event); +static void e_meeting_time_selector_hadjustment_changed (GtkAdjustment *adjustment, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_vadjustment_changed (GtkAdjustment *adjustment, + EMeetingTimeSelector *mts); + +static void e_meeting_time_selector_on_canvas_realized (GtkWidget *widget, + EMeetingTimeSelector *mts); + +static gint e_meeting_time_selector_compare_period_starts (const void *arg1, + const void *arg2); +#if 0 +static gint e_meeting_time_selector_compare_periods (const void *arg1, + const void *arg2); +#endif +static gint e_meeting_time_selector_compare_times (EMeetingTimeSelectorTime *time1, + EMeetingTimeSelectorTime *time2); +static void e_meeting_time_selector_on_options_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_options_menu_position_callback (GtkMenu *menu, + gint *x, + gint *y, + gpointer user_data); +static void e_meeting_time_selector_on_zoomed_out_toggled (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_working_hours_toggled (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_invite_others_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_update_free_busy (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_autopick_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_autopick_menu_position_callback (GtkMenu *menu, + gint *x, + gint *y, + gpointer user_data); +static void e_meeting_time_selector_on_autopick_option_toggled (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_prev_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_next_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_autopick (EMeetingTimeSelector *mts, + gboolean forward); +static void e_meeting_time_selector_calculate_time_difference (EMeetingTimeSelectorTime *start, + EMeetingTimeSelectorTime *end, + gint *days, + gint *hours, + gint *minutes); +static void e_meeting_time_selector_find_nearest_interval (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time, + gint days, gint hours, gint mins); +static void e_meeting_time_selector_find_nearest_interval_backward (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time, + gint days, gint hours, gint mins); +static void e_meeting_time_selector_adjust_time (EMeetingTimeSelectorTime *mtstime, + gint days, gint hours, gint minutes); +static EMeetingTimeSelectorPeriod* e_meeting_time_selector_find_time_clash (EMeetingTimeSelector *mts, + EMeetingTimeSelectorAttendee *attendee, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time); + + +static void e_meeting_time_selector_recalc_grid (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_recalc_date_format (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_save_position (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *mtstime); +static void e_meeting_time_selector_restore_position (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *mtstime); +static void e_meeting_time_selector_on_start_time_changed (GtkWidget *widget, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_end_time_changed (GtkWidget *widget, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_update_date_popup_menus (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_attendees_list_size_allocate (GtkWidget *widget, + GtkAllocation *allocation, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_on_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *allocation, + EMeetingTimeSelector *mts); +static void e_meeting_time_selector_update_main_canvas_scroll_region (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_update_attendees_list_scroll_region (EMeetingTimeSelector *mts); +static gboolean e_meeting_time_selector_timeout_handler (gpointer data); +static void e_meeting_time_selector_update_start_date_edit (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_update_end_date_edit (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_ensure_meeting_time_shown (EMeetingTimeSelector *mts); +static void e_meeting_time_selector_update_dates_shown (EMeetingTimeSelector *mts); + +static void e_meeting_time_selector_update_attendees_list_positions (EMeetingTimeSelector *mts); + +static GtkTableClass *e_meeting_time_selector_parent_class; + + +GtkType +e_meeting_time_selector_get_type (void) +{ + static guint e_meeting_time_selector_type = 0; + + if (!e_meeting_time_selector_type) { + GtkTypeInfo e_meeting_time_selector_info = + { + "EMeetingTimeSelector", + sizeof (EMeetingTimeSelector), + sizeof (EMeetingTimeSelectorClass), + (GtkClassInitFunc) e_meeting_time_selector_class_init, + (GtkObjectInitFunc) e_meeting_time_selector_init, + /* reserved_1 */ NULL, + /* reserved_2 */ NULL, + (GtkClassInitFunc) NULL, + }; + + e_meeting_time_selector_type = gtk_type_unique (GTK_TYPE_TABLE, + &e_meeting_time_selector_info); + } + return e_meeting_time_selector_type; +} + + +static void +e_meeting_time_selector_class_init (EMeetingTimeSelectorClass * klass) +{ + GtkObjectClass *object_class; + GtkWidgetClass *widget_class; + + e_meeting_time_selector_parent_class = gtk_type_class (gtk_table_get_type()); + + object_class = (GtkObjectClass *) klass; + widget_class = (GtkWidgetClass *) klass; + + object_class->destroy = e_meeting_time_selector_destroy; + + widget_class->realize = e_meeting_time_selector_realize; + widget_class->unrealize = e_meeting_time_selector_unrealize; + widget_class->style_set = e_meeting_time_selector_style_set; + widget_class->expose_event = e_meeting_time_selector_expose_event; +} + + +static void +e_meeting_time_selector_init (EMeetingTimeSelector * mts) +{ + GtkWidget *hbox, *separator, *button, *label, *table; + GtkWidget *alignment, *child_hbox, *arrow, *menuitem; + GSList *group; + GdkVisual *visual; + GdkColormap *colormap; + guint accel_key; + GtkAccelGroup *menu_accel_group; + time_t meeting_start_time; + struct tm *meeting_start_tm; + guchar stipple_bits[] = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + }; + + mts->accel_group = gtk_accel_group_new (); + + mts->attendees = g_array_new (FALSE, FALSE, + sizeof (EMeetingTimeSelectorAttendee)); + + mts->working_hours_only = TRUE; + mts->day_start_hour = 9; + mts->day_start_minute = 0; + mts->day_end_hour = 18; + mts->day_end_minute = 0; + mts->zoomed_out = FALSE; + mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_NONE; + + /* The default meeting time is the nearest half-hour interval in the + future, in working hours. */ + meeting_start_time = time (NULL); + g_date_clear (&mts->meeting_start_time.date, 1); + g_date_set_time (&mts->meeting_start_time.date, meeting_start_time); + meeting_start_tm = localtime (&meeting_start_time); + mts->meeting_start_time.hour = meeting_start_tm->tm_hour; + mts->meeting_start_time.minute = meeting_start_tm->tm_min; + + e_meeting_time_selector_find_nearest_interval (mts, &mts->meeting_start_time, + &mts->meeting_end_time, + 0, 0, 30); + + e_meeting_time_selector_update_dates_shown (mts); + + mts->meeting_positions_valid = FALSE; + + mts->row_height = 30; + mts->col_width = 50; + mts->day_width = 50 * 24 + 1; + + mts->auto_scroll_timeout_id = 0; + + + mts->attendees_title_bar_vbox = gtk_vbox_new (FALSE, 2); + /* Add some horizontal padding for the shadow around the display. */ + gtk_table_attach (GTK_TABLE (mts), + mts->attendees_title_bar_vbox, + 0, 1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 2, 0); + gtk_widget_show (mts->attendees_title_bar_vbox); + + mts->attendees_title_bar = gtk_drawing_area_new (); + gtk_box_pack_end (GTK_BOX (mts->attendees_title_bar_vbox), + mts->attendees_title_bar, FALSE, FALSE, 0); + gtk_widget_show (mts->attendees_title_bar); + gtk_signal_connect (GTK_OBJECT (mts->attendees_title_bar), + "expose_event", + GTK_SIGNAL_FUNC (e_meeting_time_selector_expose_title_bar), mts); + + mts->attendees_list = gnome_canvas_new (); + /* Add some horizontal padding for the shadow around the display. */ + gtk_table_attach (GTK_TABLE (mts), mts->attendees_list, + 0, 1, 1, 2, + GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 2, 0); + gtk_widget_show (mts->attendees_list); + gtk_signal_connect (GTK_OBJECT (mts->attendees_list), "realize", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_canvas_realized), mts); + gtk_signal_connect (GTK_OBJECT (mts->attendees_list), "size_allocate", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_attendees_list_size_allocate), mts); + + /* Create the item in the list canvas. */ + gnome_canvas_item_new (GNOME_CANVAS_GROUP (GNOME_CANVAS (mts->attendees_list)->root), + e_meeting_time_selector_list_item_get_type (), + "EMeetingTimeSelectorListItem::meeting_time_selector", mts, + NULL); + + mts->display_top = gnome_canvas_new (); + gnome_canvas_set_scroll_region (GNOME_CANVAS (mts->display_top), + 0, 0, + mts->day_width * E_MEETING_TIME_SELECTOR_DAYS_SHOWN, + mts->row_height * 3); + /* Add some horizontal padding for the shadow around the display. */ + gtk_table_attach (GTK_TABLE (mts), mts->display_top, + 1, 4, 0, 1, GTK_EXPAND | GTK_FILL, 0, 2, 0); + gtk_widget_show (mts->display_top); + gtk_signal_connect (GTK_OBJECT (mts->display_top), "realize", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_canvas_realized), mts); + + mts->display_main = gnome_canvas_new (); + e_meeting_time_selector_update_main_canvas_scroll_region (mts); + /* Add some horizontal padding for the shadow around the display. */ + gtk_table_attach (GTK_TABLE (mts), mts->display_main, + 1, 4, 1, 2, + GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 2, 0); + gtk_widget_show (mts->display_main); + gtk_signal_connect (GTK_OBJECT (mts->display_main), "realize", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_canvas_realized), mts); + gtk_signal_connect (GTK_OBJECT (mts->display_main), "size_allocate", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_canvas_size_allocate), mts); + + mts->hscrollbar = gtk_hscrollbar_new (GTK_LAYOUT (mts->display_main)->hadjustment); + gtk_table_attach (GTK_TABLE (mts), mts->hscrollbar, + 1, 4, 2, 3, GTK_EXPAND | GTK_FILL, 0, 0, 0); + gtk_widget_show (mts->hscrollbar); + + mts->vscrollbar = gtk_vscrollbar_new (GTK_LAYOUT (mts->display_main)->vadjustment); + gtk_table_attach (GTK_TABLE (mts), mts->vscrollbar, + 4, 5, 1, 2, 0, GTK_EXPAND | GTK_FILL, 0, 0); + gtk_widget_show (mts->vscrollbar); + + /* Create the item in the top canvas. */ + gnome_canvas_item_new (GNOME_CANVAS_GROUP (GNOME_CANVAS (mts->display_top)->root), + e_meeting_time_selector_item_get_type (), + "EMeetingTimeSelectorItem::meeting_time_selector", mts, + NULL); + + /* Create the item in the main canvas. */ + gnome_canvas_item_new (GNOME_CANVAS_GROUP (GNOME_CANVAS (mts->display_main)->root), + e_meeting_time_selector_item_get_type (), + "EMeetingTimeSelectorItem::meeting_time_selector", mts, + NULL); + + /* Create the hbox containing the color key. */ + hbox = gtk_hbox_new (FALSE, 2); + gtk_table_attach (GTK_TABLE (mts), hbox, + 1, 4, 3, 4, GTK_FILL, 0, 0, 8); + gtk_widget_show (hbox); + + e_meeting_time_selector_add_key_color (mts, hbox, _("Tentative"), &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_TENTATIVE]); + e_meeting_time_selector_add_key_color (mts, hbox, _("Busy"), &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_BUSY]); + e_meeting_time_selector_add_key_color (mts, hbox, _("Out of Office"), &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_OUT_OF_OFFICE]); + e_meeting_time_selector_add_key_color (mts, hbox, _("No Information"), + NULL); + + separator = gtk_hseparator_new (); + gtk_table_attach (GTK_TABLE (mts), separator, + 0, 5, 4, 5, GTK_FILL, 0, 0, 0); + gtk_widget_show (separator); + + /* Create the Invite Others & Options buttons on the left. */ + hbox = gtk_hbox_new (FALSE, 4); + gtk_table_attach (GTK_TABLE (mts), hbox, + 0, 1, 3, 4, GTK_FILL, 0, 0, 0); + gtk_widget_show (hbox); + + button = gtk_button_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (button)->child), + _("_Invite Others...")); + gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0); + gtk_widget_show (button); + gtk_widget_add_accelerator (button, "clicked", mts->accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (button), "clicked", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_invite_others_button_clicked), mts); + + mts->options_button = gtk_button_new (); + gtk_box_pack_start (GTK_BOX (hbox), mts->options_button, TRUE, TRUE, 0); + gtk_widget_show (mts->options_button); + + gtk_signal_connect (GTK_OBJECT (mts->options_button), "clicked", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_options_button_clicked), mts); + + child_hbox = gtk_hbox_new (FALSE, 2); + gtk_container_add (GTK_CONTAINER (mts->options_button), child_hbox); + gtk_widget_show (child_hbox); + + label = gtk_label_new (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (label), _("_Options")); + gtk_box_pack_start (GTK_BOX (child_hbox), label, TRUE, TRUE, 0); + gtk_widget_show (label); + gtk_widget_add_accelerator (mts->options_button, "clicked", mts->accel_group, + accel_key, GDK_MOD1_MASK, 0); + + arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_OUT); + gtk_box_pack_start (GTK_BOX (child_hbox), arrow, FALSE, FALSE, 2); + gtk_widget_show (arrow); + + /* Create the Options menu. */ + mts->options_menu = gtk_menu_new (); + gtk_menu_attach_to_widget (GTK_MENU (mts->options_menu), mts->options_button, + e_meeting_time_selector_options_menu_detacher); + menu_accel_group = gtk_menu_ensure_uline_accel_group (GTK_MENU (mts->options_menu)); + + menuitem = gtk_check_menu_item_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("Show _Only Working Hours")); + gtk_menu_append (GTK_MENU (mts->options_menu), menuitem); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), + mts->working_hours_only); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "toggled", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_working_hours_toggled), mts); + gtk_widget_show (menuitem); + + menuitem = gtk_check_menu_item_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("Show _Zoomed Out")); + gtk_menu_append (GTK_MENU (mts->options_menu), menuitem); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), + mts->zoomed_out); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "toggled", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_zoomed_out_toggled), mts); + gtk_widget_show (menuitem); + + menuitem = gtk_menu_item_new (); + gtk_menu_append (GTK_MENU (mts->options_menu), menuitem); + gtk_widget_set_sensitive (menuitem, FALSE); + gtk_widget_show (menuitem); + + menuitem = gtk_menu_item_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("_Update Free/Busy")); + gtk_menu_append (GTK_MENU (mts->options_menu), menuitem); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "activate", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_update_free_busy), mts); + gtk_widget_show (menuitem); + + /* Create the 3 AutoPick buttons on the left. */ + hbox = gtk_hbox_new (FALSE, 0); + gtk_table_attach (GTK_TABLE (mts), hbox, + 0, 1, 5, 6, GTK_FILL, 0, 0, 0); + gtk_widget_show (hbox); + + button = gtk_button_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (button)->child), + _("_<<")); + gtk_widget_add_accelerator (button, "clicked", mts->accel_group, + accel_key, GDK_MOD1_MASK | GDK_SHIFT_MASK, 0); + gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0); + gtk_widget_show (button); + gtk_signal_connect (GTK_OBJECT (button), "clicked", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_prev_button_clicked), mts); + + mts->autopick_button = gtk_button_new (); + gtk_box_pack_start (GTK_BOX (hbox), mts->autopick_button, TRUE, TRUE, 0); + gtk_widget_show (mts->autopick_button); + + child_hbox = gtk_hbox_new (FALSE, 2); + gtk_container_add (GTK_CONTAINER (mts->autopick_button), child_hbox); + gtk_widget_show (child_hbox); + + label = gtk_label_new (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (label), _("_Autopick")); + gtk_box_pack_start (GTK_BOX (child_hbox), label, TRUE, TRUE, 0); + gtk_widget_show (label); + gtk_widget_add_accelerator (mts->autopick_button, "clicked", mts->accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (mts->autopick_button), "clicked", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_autopick_button_clicked), mts); + + arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_OUT); + gtk_box_pack_start (GTK_BOX (child_hbox), arrow, FALSE, FALSE, 2); + gtk_widget_show (arrow); + + button = gtk_button_new_with_label (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (button)->child), + _(">_>")); + gtk_widget_add_accelerator (button, "clicked", mts->accel_group, + accel_key, GDK_MOD1_MASK | GDK_SHIFT_MASK, 0); + gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0); + gtk_widget_show (button); + gtk_signal_connect (GTK_OBJECT (button), "clicked", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_next_button_clicked), mts); + + /* Create the Autopick menu. */ + mts->autopick_menu = gtk_menu_new (); + gtk_menu_attach_to_widget (GTK_MENU (mts->autopick_menu), mts->autopick_button, + e_meeting_time_selector_autopick_menu_detacher); + menu_accel_group = gtk_menu_ensure_uline_accel_group (GTK_MENU (mts->autopick_menu)); + + menuitem = gtk_radio_menu_item_new_with_label (NULL, ""); + mts->autopick_all_item = menuitem; + group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem)); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("_All People and Resources")); + gtk_menu_append (GTK_MENU (mts->autopick_menu), menuitem); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "toggled", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_autopick_option_toggled), mts); + gtk_widget_show (menuitem); + + menuitem = gtk_radio_menu_item_new_with_label (group, ""); + mts->autopick_all_people_one_resource_item = menuitem; + group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem)); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("All _People and One Resource")); + gtk_menu_append (GTK_MENU (mts->autopick_menu), menuitem); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "toggled", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_autopick_option_toggled), mts); + gtk_widget_show (menuitem); + + menuitem = gtk_radio_menu_item_new_with_label (group, ""); + mts->autopick_required_people_item = menuitem; + group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem)); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("_Required People")); + gtk_menu_append (GTK_MENU (mts->autopick_menu), menuitem); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "activate", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_autopick_option_toggled), mts); + gtk_widget_show (menuitem); + + menuitem = gtk_radio_menu_item_new_with_label (group, ""); + mts->autopick_required_people_one_resource_item = menuitem; + group = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (menuitem)); + accel_key = gtk_label_parse_uline (GTK_LABEL (GTK_BIN (menuitem)->child), _("Required People and _One Resource")); + gtk_menu_append (GTK_MENU (mts->autopick_menu), menuitem); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, 0, 0); + gtk_widget_add_accelerator (menuitem, "activate", menu_accel_group, + accel_key, GDK_MOD1_MASK, 0); + gtk_signal_connect (GTK_OBJECT (menuitem), "activate", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_autopick_option_toggled), mts); + gtk_widget_show (menuitem); + + /* Create the date entry fields on the right. */ + alignment = gtk_alignment_new (0.5, 0.5, 0, 0); + gtk_table_attach (GTK_TABLE (mts), alignment, + 1, 4, 5, 6, GTK_FILL, 0, 0, 0); + gtk_widget_show (alignment); + + table = gtk_table_new (2, 2, FALSE); + gtk_table_set_row_spacings (GTK_TABLE (table), 4); + gtk_container_add (GTK_CONTAINER (alignment), table); + gtk_widget_show (table); + + label = gtk_label_new (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (label), + _("Meeting _start time:")); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_table_attach (GTK_TABLE (table), label, + 0, 1, 0, 1, GTK_FILL, 0, 4, 0); + gtk_widget_show (label); + + mts->start_date_edit = gnome_date_edit_new (0, TRUE, TRUE); + /* I don't like the 'Calendar' label. */ + gtk_widget_hide (GNOME_DATE_EDIT (mts->start_date_edit)->cal_label); + gtk_table_attach (GTK_TABLE (table), mts->start_date_edit, + 1, 2, 0, 1, GTK_FILL, 0, 0, 0); + gtk_widget_show (mts->start_date_edit); + gtk_signal_connect (GTK_OBJECT (mts->start_date_edit), "date_changed", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_start_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (mts->start_date_edit), "time_changed", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_start_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (GNOME_DATE_EDIT (mts->start_date_edit)->date_entry), "activate", GTK_SIGNAL_FUNC (e_meeting_time_selector_on_start_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (GNOME_DATE_EDIT (mts->start_date_edit)->time_entry), "activate", GTK_SIGNAL_FUNC (e_meeting_time_selector_on_start_time_changed), mts); + gtk_widget_add_accelerator (GNOME_DATE_EDIT (mts->start_date_edit)->date_entry, + "grab_focus", mts->accel_group, + accel_key, GDK_MOD1_MASK, 0); + + label = gtk_label_new (""); + accel_key = gtk_label_parse_uline (GTK_LABEL (label), + _("Meeting _end time:")); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_table_attach (GTK_TABLE (table), label, + 0, 1, 1, 2, GTK_FILL, 0, 4, 0); + gtk_widget_show (label); + + mts->end_date_edit = gnome_date_edit_new (0, TRUE, TRUE); + gtk_widget_hide (GNOME_DATE_EDIT (mts->end_date_edit)->cal_label); + gtk_table_attach (GTK_TABLE (table), mts->end_date_edit, + 1, 2, 1, 2, GTK_FILL, 0, 0, 0); + gtk_widget_show (mts->end_date_edit); + gtk_signal_connect (GTK_OBJECT (mts->end_date_edit), "date_changed", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_end_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (mts->end_date_edit), "time_changed", + GTK_SIGNAL_FUNC (e_meeting_time_selector_on_end_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (GNOME_DATE_EDIT (mts->end_date_edit)->date_entry), "activate", GTK_SIGNAL_FUNC (e_meeting_time_selector_on_end_time_changed), mts); + gtk_signal_connect (GTK_OBJECT (GNOME_DATE_EDIT (mts->end_date_edit)->time_entry), "activate", GTK_SIGNAL_FUNC (e_meeting_time_selector_on_end_time_changed), mts); + gtk_widget_add_accelerator (GNOME_DATE_EDIT (mts->end_date_edit)->date_entry, + "grab_focus", mts->accel_group, + accel_key, GDK_MOD1_MASK, 0); + + gtk_table_set_col_spacing (GTK_TABLE (mts), 0, 4); + gtk_table_set_row_spacing (GTK_TABLE (mts), 4, 12); + + /* Allocate the colors. */ + visual = gtk_widget_get_visual (GTK_WIDGET (mts)); + colormap = gtk_widget_get_colormap (GTK_WIDGET (mts)); + mts->color_context = gdk_color_context_new (visual, colormap); + e_meeting_time_selector_alloc_named_color (mts, "gray75", &mts->bg_color); + e_meeting_time_selector_alloc_named_color (mts, "gray50", &mts->all_attendees_bg_color); + gdk_color_black (colormap, &mts->grid_color); + gdk_color_white (colormap, &mts->grid_shadow_color); + e_meeting_time_selector_alloc_named_color (mts, "gray50", &mts->grid_unused_color); + gdk_color_white (colormap, &mts->meeting_time_bg_color); + gdk_color_white (colormap, &mts->stipple_bg_color); + gdk_color_white (colormap, &mts->attendee_list_bg_color); + + e_meeting_time_selector_alloc_named_color (mts, "LightSkyBlue2", &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_TENTATIVE]); + e_meeting_time_selector_alloc_named_color (mts, "blue", &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_BUSY]); + e_meeting_time_selector_alloc_named_color (mts, "HotPink3", &mts->busy_colors[E_MEETING_TIME_SELECTOR_BUSY_OUT_OF_OFFICE]); + + /* Create the stipple, for attendees with no data. */ + mts->stipple = gdk_bitmap_create_from_data (NULL, (gchar*)stipple_bits, + 8, 8); + + /* Connect handlers to the adjustments in the main canvas, so we can + scroll the other 2 canvases. */ + gtk_signal_connect (GTK_OBJECT (GTK_LAYOUT (mts->display_main)->hadjustment), "value_changed", GTK_SIGNAL_FUNC (e_meeting_time_selector_hadjustment_changed), mts); + gtk_signal_connect (GTK_OBJECT (GTK_LAYOUT (mts->display_main)->vadjustment), "value_changed", GTK_SIGNAL_FUNC (e_meeting_time_selector_vadjustment_changed), mts); + + e_meeting_time_selector_recalc_grid (mts); + e_meeting_time_selector_ensure_meeting_time_shown (mts); + e_meeting_time_selector_update_start_date_edit (mts); + e_meeting_time_selector_update_end_date_edit (mts); + e_meeting_time_selector_update_date_popup_menus (mts); +} + + +/* This adds a color to the color key beneath the main display. If color is + NULL, it displays the No Info stipple instead. */ +static void +e_meeting_time_selector_add_key_color (EMeetingTimeSelector * mts, + GtkWidget *hbox, + gchar *label_text, GdkColor *color) +{ + GtkWidget *child_hbox, *darea, *label; + + child_hbox = gtk_hbox_new (FALSE, 4); + gtk_box_pack_start (GTK_BOX (hbox), child_hbox, TRUE, TRUE, 0); + gtk_widget_show (child_hbox); + + darea = gtk_drawing_area_new (); + gtk_box_pack_start (GTK_BOX (child_hbox), darea, FALSE, FALSE, 0); + gtk_object_set_user_data (GTK_OBJECT (darea), mts); + gtk_widget_set_usize (darea, 14, 14); + gtk_widget_show (darea); + + label = gtk_label_new (label_text); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (child_hbox), label, TRUE, TRUE, 0); + gtk_widget_show (label); + + gtk_signal_connect (GTK_OBJECT (darea), "expose_event", + GTK_SIGNAL_FUNC (e_meeting_time_selector_expose_key_color), + color); +} + + +static gint +e_meeting_time_selector_expose_title_bar (GtkWidget *widget, + GdkEventExpose *event, + gpointer data) +{ + EMeetingTimeSelector * mts; + GdkFont *font; + + mts = E_MEETING_TIME_SELECTOR (data); + + gtk_draw_shadow (widget->style, widget->window, GTK_STATE_NORMAL, + GTK_SHADOW_OUT, 0, 0, + E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH + 1, + widget->allocation.height); + gtk_draw_shadow (widget->style, widget->window, GTK_STATE_NORMAL, + GTK_SHADOW_OUT, + E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH + 1, 0, + widget->allocation.width - E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH - 1, + widget->allocation.height); + + font = widget->style->font; + gdk_draw_string (widget->window, font, + widget->style->fg_gc[GTK_STATE_NORMAL], + E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH + 4, + 4 + font->ascent, _("All Attendees")); + + return FALSE; +} + + +static gint +e_meeting_time_selector_expose_key_color (GtkWidget *darea, + GdkEventExpose *event, + GdkColor *color) +{ + EMeetingTimeSelector * mts; + GdkGC *gc; + gint width, height; + + mts = gtk_object_get_user_data (GTK_OBJECT (darea)); + gc = mts->color_key_gc; + width = darea->allocation.width; + height = darea->allocation.height; + + gtk_draw_shadow (darea->style, darea->window, GTK_STATE_NORMAL, + GTK_SHADOW_IN, 0, 0, width, height); + + if (color) { + gdk_gc_set_foreground (gc, color); + gdk_draw_rectangle (darea->window, gc, TRUE, 1, 1, + width - 2, height - 2); + } else { + gdk_gc_set_foreground (gc, &mts->grid_color); + gdk_gc_set_background (gc, &mts->stipple_bg_color); + gdk_gc_set_stipple (gc, mts->stipple); + gdk_gc_set_fill (gc, GDK_OPAQUE_STIPPLED); + gdk_draw_rectangle (darea->window, gc, TRUE, 1, 1, + width - 2, height - 2); + gdk_gc_set_fill (gc, GDK_SOLID); + } + + return TRUE; +} + + +static void +e_meeting_time_selector_alloc_named_color (EMeetingTimeSelector * mts, + const char *name, GdkColor *c) +{ + int failed; + + g_return_if_fail (name != NULL); + g_return_if_fail (c != NULL); + + gdk_color_parse (name, c); + c->pixel = 0; + c->pixel = gdk_color_context_get_pixel (mts->color_context, + c->red, c->green, c->blue, + &failed); + if (failed) + g_warning ("Failed to allocate color: %s\n", name); +} + + +static void +e_meeting_time_selector_options_menu_detacher (GtkWidget *widget, + GtkMenu *menu) +{ + EMeetingTimeSelector *mts; + + g_return_if_fail (widget != NULL); + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (widget)); + + mts = E_MEETING_TIME_SELECTOR (widget); + g_return_if_fail (mts->options_menu == (GtkWidget*) menu); + + mts->options_menu = NULL; +} + + +static void +e_meeting_time_selector_autopick_menu_detacher (GtkWidget *widget, + GtkMenu *menu) +{ + EMeetingTimeSelector *mts; + + g_return_if_fail (widget != NULL); + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (widget)); + + mts = E_MEETING_TIME_SELECTOR (widget); + g_return_if_fail (mts->autopick_menu == (GtkWidget*) menu); + + mts->autopick_menu = NULL; +} + + +GtkWidget * +e_meeting_time_selector_new (void) +{ + GtkWidget *mts; + + mts = GTK_WIDGET (gtk_type_new (e_meeting_time_selector_get_type ())); + + return mts; +} + + +static void +e_meeting_time_selector_destroy (GtkObject *object) +{ + EMeetingTimeSelector *mts; + EMeetingTimeSelectorAttendee *attendee; + gint row; + + mts = E_MEETING_TIME_SELECTOR (object); + + e_meeting_time_selector_remove_timeout (mts); + + gdk_color_context_free (mts->color_context); + gdk_bitmap_unref (mts->stipple); + + for (row = 0; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + g_free (attendee->name); + g_array_free (attendee->busy_periods, TRUE); + } + + g_array_free (mts->attendees, TRUE); + + if (GTK_OBJECT_CLASS (e_meeting_time_selector_parent_class)->destroy) + (*GTK_OBJECT_CLASS (e_meeting_time_selector_parent_class)->destroy)(object); +} + + +static void +e_meeting_time_selector_realize (GtkWidget *widget) +{ + EMeetingTimeSelector *mts; + + if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->realize) + (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->realize)(widget); + + mts = E_MEETING_TIME_SELECTOR (widget); + + mts->color_key_gc = gdk_gc_new (widget->window); +} + + +static void +e_meeting_time_selector_unrealize (GtkWidget *widget) +{ + EMeetingTimeSelector *mts; + + mts = E_MEETING_TIME_SELECTOR (widget); + + gdk_gc_unref (mts->color_key_gc); + mts->color_key_gc = NULL; + + if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->unrealize) + (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->unrealize)(widget); +} + + +static void +e_meeting_time_selector_style_set (GtkWidget *widget, + GtkStyle *previous_style) +{ + EMeetingTimeSelector *mts; + EMeetingTimeSelectorTime saved_time; + GdkFont *font; + gint hour, max_hour_width; + + if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->style_set) + (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->style_set)(widget, previous_style); + + mts = E_MEETING_TIME_SELECTOR (widget); + font = widget->style->font; + + /* Calculate the widths of the hour strings in the style's font. */ + max_hour_width = 0; + for (hour = 0; hour < 24; hour++) { + mts->hour_widths[hour] = gdk_string_width (font, EMeetingTimeSelectorHours[hour]); + max_hour_width = MAX (max_hour_width, mts->hour_widths[hour]); + } + + /* The row height really depends on the requested height of the + GtkEntry widgets in the list on the left, so we really need to + call size_request on them, AFTER their style has been set. */ + /* FIXME: This uses the default style ythickness of 2, though it won't + be needed when we switch to Miguel's new editable GtkCList widget + so I won't worry about it. */ + mts->row_height = font->ascent + font->descent + + E_MEETING_TIME_SELECTOR_ENTRY_INNER_BORDER * 2 + + 2 * 2; + mts->col_width = max_hour_width + 4; + + e_meeting_time_selector_save_position (mts, &saved_time); + e_meeting_time_selector_recalc_grid (mts); + e_meeting_time_selector_restore_position (mts, &saved_time); + + gtk_widget_set_usize (mts->display_top, -1, mts->row_height * 3); + gtk_widget_set_usize (mts->attendees_title_bar, -1, mts->row_height); + + GTK_LAYOUT (mts->display_main)->hadjustment->step_increment = mts->col_width; + GTK_LAYOUT (mts->display_main)->vadjustment->step_increment = mts->row_height; +} + + +/* This draws a shadow around the top display and main display. */ +static gint +e_meeting_time_selector_expose_event (GtkWidget *widget, + GdkEventExpose *event) +{ + EMeetingTimeSelector *mts; + gint x, y, w, h; + + mts = E_MEETING_TIME_SELECTOR (widget); + + /* Draw the shadow around the attendees title bar and list. */ + x = mts->attendees_title_bar->allocation.x - 2; + y = mts->attendees_title_bar->allocation.y - 2; + w = mts->attendees_title_bar->allocation.width + 4; + h = mts->attendees_title_bar->allocation.height + mts->attendees_list->allocation.height + 4; + + gtk_draw_shadow (widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_IN, x, y, w, h); + + /* Draw the shadow around the graphical displays. */ + x = mts->display_top->allocation.x - 2; + y = mts->display_top->allocation.y - 2; + w = mts->display_top->allocation.width + 4; + h = mts->display_top->allocation.height + mts->display_main->allocation.height + 4; + + gtk_draw_shadow (widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_IN, x, y, w, h); + + if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->expose_event) + (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->expose_event)(widget, event); + + return FALSE; +} + + +/* When the main canvas scrolls, we scroll the other canvases. */ +static void +e_meeting_time_selector_hadjustment_changed (GtkAdjustment *adjustment, + EMeetingTimeSelector *mts) +{ + GtkAdjustment *adj; + + adj = GTK_LAYOUT (mts->display_top)->hadjustment; + if (adj->value != adjustment->value) { + adj->value = adjustment->value; + gtk_signal_emit_by_name (GTK_OBJECT (adj), "value_changed"); + } +} + + +static void +e_meeting_time_selector_vadjustment_changed (GtkAdjustment *adjustment, + EMeetingTimeSelector *mts) +{ + GtkAdjustment *adj; + + adj = GTK_LAYOUT (mts->attendees_list)->vadjustment; + if (adj->value != adjustment->value) { + adj->value = adjustment->value; + gtk_signal_emit_by_name (GTK_OBJECT (adj), "value_changed"); + } +} + + +void +e_meeting_time_selector_get_meeting_time (EMeetingTimeSelector *mts, + gint *start_year, + gint *start_month, + gint *start_day, + gint *start_hour, + gint *start_minute, + gint *end_year, + gint *end_month, + gint *end_day, + gint *end_hour, + gint *end_minute) +{ + *start_year = g_date_year (&mts->meeting_start_time.date); + *start_month = g_date_month (&mts->meeting_start_time.date); + *start_day = g_date_day (&mts->meeting_start_time.date); + *start_hour = mts->meeting_start_time.hour; + *start_minute = mts->meeting_start_time.minute; + + *end_year = g_date_year (&mts->meeting_end_time.date); + *end_month = g_date_month (&mts->meeting_end_time.date); + *end_day = g_date_day (&mts->meeting_end_time.date); + *end_hour = mts->meeting_end_time.hour; + *end_minute = mts->meeting_end_time.minute; +} + + +gboolean +e_meeting_time_selector_set_meeting_time (EMeetingTimeSelector *mts, + gint start_year, + gint start_month, + gint start_day, + gint start_hour, + gint start_minute, + gint end_year, + gint end_month, + gint end_day, + gint end_hour, + gint end_minute) +{ + g_return_val_if_fail (IS_E_MEETING_TIME_SELECTOR (mts), FALSE); + + /* Check the dates are valid. */ + if (!g_date_valid_dmy (start_day, start_month, start_year) + || !g_date_valid_dmy (end_day, end_month, end_year) + || start_hour < 0 || start_hour > 23 + || end_hour < 0 || end_hour > 23 + || start_minute < 0 || start_minute > 59 + || end_minute < 0 || end_minute > 59) + return FALSE; + + g_date_set_dmy (&mts->meeting_start_time.date, start_day, start_month, + start_year); + mts->meeting_start_time.hour = start_hour; + mts->meeting_start_time.minute = start_minute; + g_date_set_dmy (&mts->meeting_end_time.date, end_day, end_month, + end_year); + mts->meeting_end_time.hour = end_hour; + mts->meeting_end_time.minute = end_minute; + + mts->meeting_positions_valid = FALSE; + + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + + /* Set the times in the GnomeDateEdit widgets. */ + e_meeting_time_selector_update_start_date_edit (mts); + e_meeting_time_selector_update_end_date_edit (mts); + + return TRUE; +} + + +void +e_meeting_time_selector_set_working_hours_only (EMeetingTimeSelector *mts, + gboolean working_hours_only) +{ + EMeetingTimeSelectorTime saved_time; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + + if (mts->working_hours_only == working_hours_only) + return; + + mts->working_hours_only = working_hours_only; + + e_meeting_time_selector_save_position (mts, &saved_time); + e_meeting_time_selector_recalc_grid (mts); + e_meeting_time_selector_restore_position (mts, &saved_time); + + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + e_meeting_time_selector_update_date_popup_menus (mts); +} + + +void +e_meeting_time_selector_set_working_hours (EMeetingTimeSelector *mts, + gint day_start_hour, + gint day_start_minute, + gint day_end_hour, + gint day_end_minute) +{ + EMeetingTimeSelectorTime saved_time; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + + if (mts->day_start_hour == day_start_hour + && mts->day_start_minute == day_start_minute + && mts->day_end_hour == day_end_hour + && mts->day_end_minute == day_end_minute) + return; + + mts->day_start_hour = day_start_hour; + mts->day_start_minute = day_start_minute; + mts->day_end_hour = day_end_hour; + mts->day_end_minute = day_end_minute; + + e_meeting_time_selector_save_position (mts, &saved_time); + e_meeting_time_selector_recalc_grid (mts); + e_meeting_time_selector_restore_position (mts, &saved_time); + + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + e_meeting_time_selector_update_date_popup_menus (mts); +} + + +void +e_meeting_time_selector_set_zoomed_out (EMeetingTimeSelector *mts, + gboolean zoomed_out) +{ + EMeetingTimeSelectorTime saved_time; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + + if (mts->zoomed_out == zoomed_out) + return; + + mts->zoomed_out = zoomed_out; + + e_meeting_time_selector_save_position (mts, &saved_time); + e_meeting_time_selector_recalc_grid (mts); + e_meeting_time_selector_restore_position (mts, &saved_time); + + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); +} + + +EMeetingTimeSelectorAutopickOption +e_meeting_time_selector_get_autopick_option (EMeetingTimeSelector *mts) +{ + if (GTK_CHECK_MENU_ITEM (mts->autopick_all_item)->active) + return E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_RESOURCES; + if (GTK_CHECK_MENU_ITEM (mts->autopick_all_people_one_resource_item)->active) + return E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_ONE_RESOURCE; + if (GTK_CHECK_MENU_ITEM (mts->autopick_required_people_item)->active) + return E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE; + return E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE_AND_ONE_RESOURCE; +} + + +void +e_meeting_time_selector_set_autopick_option (EMeetingTimeSelector *mts, + EMeetingTimeSelectorAutopickOption autopick_option) +{ + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + + switch (autopick_option) { + case E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_RESOURCES: + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (mts->autopick_all_item), TRUE); + break; + case E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_ONE_RESOURCE: + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (mts->autopick_all_people_one_resource_item), TRUE); + break; + case E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE: + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (mts->autopick_required_people_item), TRUE); + break; + case E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE_AND_ONE_RESOURCE: + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (mts->autopick_required_people_one_resource_item), TRUE); + break; + } +} + + +/* Adds an attendee to the list, returning the row. The data is meant for + something like an address book id, though if the user edits the name this + will become invalid. We'll probably have to handle address book lookup + ourself. */ +gint +e_meeting_time_selector_attendee_add (EMeetingTimeSelector *mts, + gchar *attendee_name, + gpointer data) +{ + EMeetingTimeSelectorAttendee attendee; + GtkWidget *entry; + gint list_width, item_width; + + g_return_val_if_fail (IS_E_MEETING_TIME_SELECTOR (mts), -1); + g_return_val_if_fail (attendee_name != NULL, -1); + + attendee.name = g_strdup (attendee_name); + attendee.type = E_MEETING_TIME_SELECTOR_REQUIRED_PERSON; + attendee.has_calendar_info = FALSE; + attendee.send_meeting_to = TRUE; + attendee.busy_periods = g_array_new (FALSE, FALSE, + sizeof (EMeetingTimeSelectorPeriod)); + attendee.busy_periods_sorted = TRUE; + attendee.longest_period_in_days = 0; + attendee.data = data; + + /* Add to the list on the left. */ + entry = gtk_entry_new (); + gtk_entry_set_text (GTK_ENTRY (entry), attendee_name); + gtk_widget_show (entry); + list_width = GTK_WIDGET (mts->attendees_list)->allocation.width; + item_width = MAX (1, list_width - E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH); + attendee.text_item = gnome_canvas_item_new (GNOME_CANVAS_GROUP (GNOME_CANVAS (mts->attendees_list)->root), + gnome_canvas_widget_get_type (), + "GnomeCanvasWidget::widget", entry, + "GnomeCanvasWidget::size_pixels", TRUE, + "GnomeCanvasWidget::x", (gdouble) E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH, + "GnomeCanvasWidget::y", (gdouble) (mts->attendees->len * mts->row_height), + "GnomeCanvasWidget::width", (gdouble) item_width, + "GnomeCanvasWidget::height", (gdouble) mts->row_height, + NULL); + gnome_canvas_item_hide (attendee.text_item); + + g_array_append_val (mts->attendees, attendee); + + /* Update the scroll region. */ + e_meeting_time_selector_update_attendees_list_scroll_region (mts); + e_meeting_time_selector_update_main_canvas_scroll_region (mts); + + /* Redraw the canvases. */ + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + + + return mts->attendees->len - 1; +} + + +gint +e_meeting_time_selector_attendee_find_by_name (EMeetingTimeSelector *mts, + gchar *attendee_name, + gint start_row) +{ + EMeetingTimeSelectorAttendee *attendee; + gint row; + + g_return_val_if_fail (IS_E_MEETING_TIME_SELECTOR (mts), -1); + g_return_val_if_fail (start_row >= 0, -1); + g_return_val_if_fail (start_row < mts->attendees->len, -1); + + for (row = start_row; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + if (!strcmp (attendee->name, attendee_name)) + return row; + } + + return -1; +} + + +gint +e_meeting_time_selector_attendee_find_by_data (EMeetingTimeSelector *mts, + gpointer data, + gint start_row) +{ + EMeetingTimeSelectorAttendee *attendee; + gint row; + + g_return_val_if_fail (IS_E_MEETING_TIME_SELECTOR (mts), -1); + g_return_val_if_fail (start_row >= 0, -1); + g_return_val_if_fail (start_row < mts->attendees->len, -1); + + for (row = start_row; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + if (attendee->data == data) + return row; + } + + return -1; +} + + +void +e_meeting_time_selector_attendee_remove (EMeetingTimeSelector *mts, + gint row) +{ + EMeetingTimeSelectorAttendee *attendee; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + g_return_if_fail (row >= 0); + g_return_if_fail (row < mts->attendees->len); + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + + g_free (attendee->name); + g_array_free (attendee->busy_periods, TRUE); + + /* Destroy the GtkEntry in the list. */ + gtk_object_destroy (GTK_OBJECT (attendee->text_item)); + + g_array_remove_index (mts->attendees, row); + + /* Update the positions of all the other GtkEntry widgets. */ + e_meeting_time_selector_update_attendees_list_positions (mts); + + e_meeting_time_selector_update_attendees_list_scroll_region (mts); + e_meeting_time_selector_update_main_canvas_scroll_region (mts); + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); +} + + +void +e_meeting_time_selector_attendee_set_type (EMeetingTimeSelector *mts, + gint row, + EMeetingTimeSelectorAttendeeType type) +{ + EMeetingTimeSelectorAttendee *attendee; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + g_return_if_fail (row >= 0); + g_return_if_fail (row < mts->attendees->len); + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + attendee->type = type; +} + + +void +e_meeting_time_selector_attendee_set_has_calendar_info (EMeetingTimeSelector *mts, + gint row, + gboolean has_calendar_info) +{ + EMeetingTimeSelectorAttendee *attendee; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + g_return_if_fail (row >= 0); + g_return_if_fail (row < mts->attendees->len); + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + attendee->has_calendar_info = has_calendar_info; +} + + +void +e_meeting_time_selector_attendee_set_send_meeting_to (EMeetingTimeSelector *mts, + gint row, + gboolean send_meeting_to) +{ + EMeetingTimeSelectorAttendee *attendee; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + g_return_if_fail (row >= 0); + g_return_if_fail (row < mts->attendees->len); + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + attendee->send_meeting_to = send_meeting_to; +} + + +/* Clears all busy times for the given attendee. */ +void +e_meeting_time_selector_attendee_clear_busy_periods (EMeetingTimeSelector *mts, + gint row) +{ + EMeetingTimeSelectorAttendee *attendee; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + g_return_if_fail (row >= 0); + g_return_if_fail (row < mts->attendees->len); + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + + g_array_set_size (attendee->busy_periods, 0); + attendee->busy_periods_sorted = TRUE; + attendee->longest_period_in_days = 0; +} + + +/* Adds one busy time for the given attendee. It returns FALSE if the date + or time is invalid. Months and days count from 1. */ +gboolean +e_meeting_time_selector_attendee_add_busy_period (EMeetingTimeSelector *mts, + gint row, + gint start_year, + gint start_month, + gint start_day, + gint start_hour, + gint start_minute, + gint end_year, + gint end_month, + gint end_day, + gint end_hour, + gint end_minute, + EMeetingTimeSelectorBusyType busy_type) +{ + EMeetingTimeSelectorAttendee *attendee; + EMeetingTimeSelectorPeriod period; + gint period_in_days; + + g_return_val_if_fail (IS_E_MEETING_TIME_SELECTOR (mts), FALSE); + g_return_val_if_fail (row >= 0, FALSE); + g_return_val_if_fail (row < mts->attendees->len, FALSE); + g_return_val_if_fail (busy_type >= 0, FALSE); + g_return_val_if_fail (busy_type < E_MEETING_TIME_SELECTOR_BUSY_LAST, FALSE); + + /* Check the dates are valid. */ + if (!g_date_valid_dmy (start_day, start_month, start_year)) + return FALSE; + if (!g_date_valid_dmy (end_day, end_month, end_year)) + return FALSE; + if (start_hour < 0 || start_hour > 23) + return FALSE; + if (end_hour < 0 || end_hour > 23) + return FALSE; + if (start_minute < 0 || start_minute > 59) + return FALSE; + if (end_minute < 0 || end_minute > 59) + return FALSE; + + g_date_clear (&period.start.date, 1); + g_date_clear (&period.end.date, 1); + g_date_set_dmy (&period.start.date, start_day, start_month, start_year); + g_date_set_dmy (&period.end.date, end_day, end_month, end_year); + period.start.hour = start_hour; + period.start.minute = start_minute; + period.end.hour = end_hour; + period.end.minute = end_minute; + period.busy_type = busy_type; + + /* Check that the start time is before or equal to the end time. */ + if (e_meeting_time_selector_compare_times (&period.start, &period.end) > 0) + return FALSE; + + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + g_array_append_val (attendee->busy_periods, period); + attendee->has_calendar_info = TRUE; + attendee->busy_periods_sorted = FALSE; + + period_in_days = g_date_julian (&period.end.date) - g_date_julian (&period.start.date) + 1; + attendee->longest_period_in_days = MAX (attendee->longest_period_in_days, period_in_days); + + return TRUE; +} + + +void +e_meeting_time_selector_attendee_ensure_periods_sorted (EMeetingTimeSelector *mts, + EMeetingTimeSelectorAttendee *attendee) +{ + if (attendee->busy_periods_sorted) + return; + + qsort (attendee->busy_periods->data, attendee->busy_periods->len, + sizeof (EMeetingTimeSelectorPeriod), + e_meeting_time_selector_compare_period_starts); + attendee->busy_periods_sorted = TRUE; +} + + +/* This compares two time periods, using their end times. */ +static gint +e_meeting_time_selector_compare_period_starts (const void *arg1, + const void *arg2) +{ + EMeetingTimeSelectorPeriod *period1, *period2; + + period1 = (EMeetingTimeSelectorPeriod *) arg1; + period2 = (EMeetingTimeSelectorPeriod *) arg2; + + return e_meeting_time_selector_compare_times (&period1->start, + &period2->start); +} + + +/* This compares two time periods, using start and end times, mainly to see if + they overlap at all. If they overlap it returns 0. Or -1 if arg1 < arg2. + Or 1 if arg1 > arg2. */ +/* Currently unused. */ +#if 0 +static gint +e_meeting_time_selector_compare_periods (const void *arg1, + const void *arg2) +{ + EMeetingTimeSelectorPeriod *period1, *period2; + + period1 = (EMeetingTimeSelectorPeriod *) arg1; + period2 = (EMeetingTimeSelectorPeriod *) arg2; + + /* If period 2 starts after period 1 ends, return 1. */ + if (e_meeting_time_selector_compare_times (&period2->start, &period1->end) >= 0) + return 1; + + /* If period 1 starts after period 2 ends, return -1. */ + if (e_meeting_time_selector_compare_times (&period1->start, &period2->end) >= 0) + return -1; + + /* They must overlap so return 0. */ + return 0; +} +#endif + + +static gint +e_meeting_time_selector_compare_times (EMeetingTimeSelectorTime *time1, + EMeetingTimeSelectorTime *time2) +{ + gint day_comparison; + + day_comparison = g_date_compare (&time1->date, + &time2->date); + if (day_comparison != 0) + return day_comparison; + + if (time1->hour < time2->hour) + return -1; + if (time1->hour > time2->hour) + return 1; + + if (time1->minute < time2->minute) + return -1; + if (time1->minute > time2->minute) + return 1; + + /* The start times are exactly the same. */ + return 0; +} + + +/* + * DEBUGGING ROUTINES - functions to output various bits of data. + */ + +#ifdef E_MEETING_TIME_SELECTOR_DEBUG + +/* Debugging function to dump information on all attendees. */ +void +e_meeting_time_selector_dump (EMeetingTimeSelector *mts) +{ + EMeetingTimeSelectorAttendee *attendee; + EMeetingTimeSelectorPeriod *period; + gint row, period_num; + gchar buffer[128]; + + g_return_if_fail (IS_E_MEETING_TIME_SELECTOR (mts)); + + g_print ("\n\nAttendee Information:\n"); + + for (row = 0; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + g_print ("Attendee: %s\n", attendee->name); + g_print (" Longest Busy Period: %i days\n", + attendee->longest_period_in_days); + + e_meeting_time_selector_attendee_ensure_periods_sorted (mts, attendee); +#if 1 + for (period_num = 0; + period_num < attendee->busy_periods->len; + period_num++) { + period = &g_array_index (attendee->busy_periods, + EMeetingTimeSelectorPeriod, + period_num); + + g_date_strftime (buffer, 128, "%A, %B %d, %Y", + &period->start.date); + g_print (" Start: %s %i:%02i\n", buffer, + period->start.hour, period->start.minute); + + g_date_strftime (buffer, 128, "%A, %B %d, %Y", + &period->end.date); + g_print (" End : %s %i:%02i\n", buffer, + period->end.hour, period->end.minute); + } +#endif + } + +} + + +/* This formats a EMeetingTimeSelectorTime in a string and returns it. + Note that it uses a static buffer. */ +gchar* +e_meeting_time_selector_dump_time (EMeetingTimeSelectorTime *mtstime) +{ + static gchar buffer[128]; + + gchar buffer2[128]; + + g_date_strftime (buffer, 128, "%A, %B %d, %Y", &mtstime->date); + sprintf (buffer2, " at %i:%02i", (gint) mtstime->hour, + (gint) mtstime->minute); + strcat (buffer, buffer2); + + return buffer; +} + + +/* This formats a GDate in a string and returns it. + Note that it uses a static buffer. */ +gchar* +e_meeting_time_selector_dump_date (GDate *date) +{ + static gchar buffer[128]; + + g_date_strftime (buffer, 128, "%A, %B %d, %Y", date); + return buffer; +} + +#endif /* E_MEETING_TIME_SELECTOR_DEBUG */ + + +static void +e_meeting_time_selector_on_invite_others_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + + +} + + +static void +e_meeting_time_selector_on_options_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + gtk_menu_popup (GTK_MENU (mts->options_menu), NULL, NULL, + e_meeting_time_selector_options_menu_position_callback, + mts, 1, GDK_CURRENT_TIME); +} + + +static void +e_meeting_time_selector_options_menu_position_callback (GtkMenu *menu, + gint *x, + gint *y, + gpointer user_data) +{ + EMeetingTimeSelector *mts; + GtkRequisition menu_requisition; + gint max_x, max_y; + + mts = E_MEETING_TIME_SELECTOR (user_data); + + /* Calculate our preferred position. */ + gdk_window_get_origin (mts->options_button->window, x, y); + *y += mts->options_button->allocation.height; + + /* Now make sure we are on the screen. */ + gtk_widget_size_request (mts->options_menu, &menu_requisition); + max_x = MAX (0, gdk_screen_width () - menu_requisition.width); + max_y = MAX (0, gdk_screen_height () - menu_requisition.height); + *x = CLAMP (*x, 0, max_x); + *y = CLAMP (*y, 0, max_y); +} + + +static void +e_meeting_time_selector_on_update_free_busy (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + + /* Make sure the menu pops down, which doesn't happen by default if + keyboard accelerators are used. */ + if (GTK_WIDGET_VISIBLE (mts->options_menu)) + gtk_menu_popdown (GTK_MENU (mts->options_menu)); +} + + +static void +e_meeting_time_selector_on_autopick_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + gtk_menu_popup (GTK_MENU (mts->autopick_menu), NULL, NULL, + e_meeting_time_selector_autopick_menu_position_callback, + mts, 1, GDK_CURRENT_TIME); +} + + +static void +e_meeting_time_selector_autopick_menu_position_callback (GtkMenu *menu, + gint *x, + gint *y, + gpointer user_data) +{ + EMeetingTimeSelector *mts; + GtkRequisition menu_requisition; + gint max_x, max_y; + + mts = E_MEETING_TIME_SELECTOR (user_data); + + /* Calculate our preferred position. */ + gdk_window_get_origin (mts->autopick_button->window, x, y); + *y += mts->autopick_button->allocation.height; + + /* Now make sure we are on the screen. */ + gtk_widget_size_request (mts->autopick_menu, &menu_requisition); + max_x = MAX (0, gdk_screen_width () - menu_requisition.width); + max_y = MAX (0, gdk_screen_height () - menu_requisition.height); + *x = CLAMP (*x, 0, max_x); + *y = CLAMP (*y, 0, max_y); +} + + +static void +e_meeting_time_selector_on_autopick_option_toggled (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + /* Make sure the menu pops down, which doesn't happen by default if + keyboard accelerators are used. */ + if (GTK_WIDGET_VISIBLE (mts->autopick_menu)) + gtk_menu_popdown (GTK_MENU (mts->autopick_menu)); +} + + +static void +e_meeting_time_selector_on_prev_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + e_meeting_time_selector_autopick (mts, FALSE); +} + + +static void +e_meeting_time_selector_on_next_button_clicked (GtkWidget *button, + EMeetingTimeSelector *mts) +{ + e_meeting_time_selector_autopick (mts, TRUE); +} + + +/* This tries to find the previous or next meeting time for which all + attendees will be available. */ +static void +e_meeting_time_selector_autopick (EMeetingTimeSelector *mts, + gboolean forward) +{ + EMeetingTimeSelectorTime start_time, end_time, *resource_free; + EMeetingTimeSelectorAttendee *attendee; + EMeetingTimeSelectorPeriod *period; + EMeetingTimeSelectorAutopickOption autopick_option; + gint duration_days, duration_hours, duration_minutes, row; + gboolean meeting_time_ok, skip_optional = FALSE; + gboolean need_one_resource = FALSE, found_resource; + + /* Get the current meeting duration in days + hours + minutes. */ + e_meeting_time_selector_calculate_time_difference (&mts->meeting_start_time, &mts->meeting_end_time, &duration_days, &duration_hours, &duration_minutes); + + /* Find the first appropriate start time. */ + start_time = mts->meeting_start_time; + if (forward) + e_meeting_time_selector_find_nearest_interval (mts, &start_time, &end_time, duration_days, duration_hours, duration_minutes); + else + e_meeting_time_selector_find_nearest_interval_backward (mts, &start_time, &end_time, duration_days, duration_hours, duration_minutes); + + /* Determine if we can skip optional people and if we only need one + resource based on the autopick option. */ + autopick_option = e_meeting_time_selector_get_autopick_option (mts); + if (autopick_option == E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE + || autopick_option == E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE_AND_ONE_RESOURCE) + skip_optional = TRUE; + if (autopick_option == E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_ONE_RESOURCE + || autopick_option == E_MEETING_TIME_SELECTOR_REQUIRED_PEOPLE_AND_ONE_RESOURCE) + need_one_resource = TRUE; + + /* Keep moving forward or backward until we find a possible meeting + time. */ + for (;;) { + meeting_time_ok = TRUE; + found_resource = FALSE; + resource_free = NULL; + + /* Step through each attendee, checking if the meeting time + intersects one of the attendees busy periods. */ + for (row = 0; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, + row); + + /* Skip optional people if they don't matter. */ + if (skip_optional && attendee->type == E_MEETING_TIME_SELECTOR_OPTIONAL_PERSON) + continue; + + period = e_meeting_time_selector_find_time_clash (mts, attendee, &start_time, &end_time); + + if (need_one_resource && attendee->type == E_MEETING_TIME_SELECTOR_RESOURCE) { + if (period) { + /* We want to remember the closest + prev/next time that one resource is + available, in case we don't find any + free resources. */ + if (forward) { + if (!resource_free || e_meeting_time_selector_compare_times (resource_free, &period->end) > 0) + resource_free = &period->end; + } else { + if (!resource_free || e_meeting_time_selector_compare_times (resource_free, &period->start) < 0) + resource_free = &period->start; + } + + } else { + found_resource = TRUE; + } + } else if (period) { + /* Skip the period which clashed. */ + if (forward) { + start_time = period->end; + } else { + start_time = period->start; + e_meeting_time_selector_adjust_time (&start_time, -duration_days, -duration_hours, -duration_minutes); + } + meeting_time_ok = FALSE; + break; + } + } + + /* Check that we found one resource if necessary. If not, skip + to the closest time that a resource is free. Note that if + there are no resources, resource_free will never get set, + so we assume the meeting time is OK. */ + if (meeting_time_ok && need_one_resource && !found_resource + && resource_free) { + if (forward) { + start_time = *resource_free; + } else { + start_time = *resource_free; + e_meeting_time_selector_adjust_time (&start_time, -duration_days, -duration_hours, -duration_minutes); + } + meeting_time_ok = FALSE; + } + + if (meeting_time_ok) { + mts->meeting_start_time = start_time; + mts->meeting_end_time = end_time; + mts->meeting_positions_valid = FALSE; + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + + /* Make sure the time is shown. */ + e_meeting_time_selector_ensure_meeting_time_shown (mts); + + /* Set the times in the GnomeDateEdit widgets. */ + e_meeting_time_selector_update_start_date_edit (mts); + e_meeting_time_selector_update_end_date_edit (mts); + return; + } + + /* Move forward to the next possible interval. */ + if (forward) + e_meeting_time_selector_find_nearest_interval (mts, &start_time, &end_time, duration_days, duration_hours, duration_minutes); + else + e_meeting_time_selector_find_nearest_interval_backward (mts, &start_time, &end_time, duration_days, duration_hours, duration_minutes); + } +} + + +static void +e_meeting_time_selector_calculate_time_difference (EMeetingTimeSelectorTime *start, + EMeetingTimeSelectorTime *end, + gint *days, + gint *hours, + gint *minutes) +{ + *days = g_date_julian (&end->date) - g_date_julian (&start->date); + *hours = end->hour - start->hour; + *minutes = end->minute - start->minute; + if (*minutes < 0) { + *minutes += 60; + *hours = *hours - 1; + } + if (*hours < 0) { + *hours += 24; + *days = *days - 1; + } +} + + +/* This moves the given time forward to the next suitable start of a meeting. + If zoomed_out is set, this means every hour. If not every half-hour. */ +static void +e_meeting_time_selector_find_nearest_interval (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time, + gint days, gint hours, gint mins) +{ + gint minutes_shown; + gboolean set_to_start_of_working_day = FALSE; + + if (mts->zoomed_out) { + start_time->hour++; + start_time->minute = 0; + } else { + start_time->minute += 30; + start_time->minute -= start_time->minute % 30; + } + e_meeting_time_selector_fix_time_overflows (start_time); + + *end_time = *start_time; + e_meeting_time_selector_adjust_time (end_time, days, hours, mins); + + /* Check if the interval is less than a day as seen in the display. + If it isn't we don't worry about the working day. */ + if (!mts->working_hours_only || days > 0) + return; + minutes_shown = (mts->day_end_hour - mts->day_start_hour) * 60; + minutes_shown += mts->day_end_minute - mts->day_start_minute; + if (hours * 60 + mins > minutes_shown) + return; + + /* If the meeting time finishes past the end of the working day, move + onto the start of the next working day. If the meeting time starts + before the working day, move it on as well. */ + if (start_time->hour > mts->day_end_hour + || (start_time->hour == mts->day_end_hour + && start_time->minute > mts->day_end_minute) + || end_time->hour > mts->day_end_hour + || (end_time->hour == mts->day_end_hour + && end_time->minute > mts->day_end_minute)) { + g_date_add_days (&start_time->date, 1); + set_to_start_of_working_day = TRUE; + } else if (start_time->hour < mts->day_start_hour + || (start_time->hour == mts->day_start_hour + && start_time->minute < mts->day_start_minute)) { + set_to_start_of_working_day = TRUE; + } + + if (set_to_start_of_working_day) { + start_time->hour = mts->day_start_hour; + start_time->minute = mts->day_start_minute; + + if (mts->zoomed_out) { + if (start_time->minute > 0) { + start_time->hour++; + start_time->minute = 0; + } + } else { + start_time->minute += 29; + start_time->minute -= start_time->minute % 30; + } + e_meeting_time_selector_fix_time_overflows (start_time); + + *end_time = *start_time; + e_meeting_time_selector_adjust_time (end_time, days, hours, mins); + } +} + + +/* This moves the given time backward to the next suitable start of a meeting. + If zoomed_out is set, this means every hour. If not every half-hour. */ +static void +e_meeting_time_selector_find_nearest_interval_backward (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time, + gint days, gint hours, gint mins) +{ + gint new_hour, minutes_shown; + gboolean set_to_end_of_working_day = FALSE; + + new_hour = start_time->hour; + if (mts->zoomed_out) { + if (start_time->minute == 0) + new_hour--; + start_time->minute = 0; + } else { + if (start_time->minute == 0) { + start_time->minute = 30; + new_hour--; + } else if (start_time->minute <= 30) + start_time->minute = 0; + else + start_time->minute = 30; + } + if (new_hour < 0) { + new_hour += 24; + g_date_subtract_days (&start_time->date, 1); + } + start_time->hour = new_hour; + + *end_time = *start_time; + e_meeting_time_selector_adjust_time (end_time, days, hours, mins); + + /* Check if the interval is less than a day as seen in the display. + If it isn't we don't worry about the working day. */ + if (!mts->working_hours_only || days > 0) + return; + minutes_shown = (mts->day_end_hour - mts->day_start_hour) * 60; + minutes_shown += mts->day_end_minute - mts->day_start_minute; + if (hours * 60 + mins > minutes_shown) + return; + + /* If the meeting time finishes past the end of the working day, move + back to the end of the working day. If the meeting time starts + before the working day, move it back to the end of the previous + working day. */ + if (start_time->hour > mts->day_end_hour + || (start_time->hour == mts->day_end_hour + && start_time->minute > mts->day_end_minute) + || end_time->hour > mts->day_end_hour + || (end_time->hour == mts->day_end_hour + && end_time->minute > mts->day_end_minute)) { + set_to_end_of_working_day = TRUE; + } else if (start_time->hour < mts->day_start_hour + || (start_time->hour == mts->day_start_hour + && start_time->minute < mts->day_start_minute)) { + g_date_subtract_days (&end_time->date, 1); + set_to_end_of_working_day = TRUE; + } + + if (set_to_end_of_working_day) { + end_time->hour = mts->day_end_hour; + end_time->minute = mts->day_end_minute; + *start_time = *end_time; + e_meeting_time_selector_adjust_time (start_time, -days, -hours, -mins); + + if (mts->zoomed_out) { + start_time->minute = 0; + } else { + start_time->minute -= start_time->minute % 30; + } + + *end_time = *start_time; + e_meeting_time_selector_adjust_time (end_time, days, hours, mins); + } +} + + +/* This adds on the given days, hours & minutes to a EMeetingTimeSelectorTime. + It is used to calculate the end of a period given a start & duration. + Days, hours & minutes can be negative, to move backwards, but they should + be within normal ranges, e.g. hours should be between -23 and 23. */ +static void +e_meeting_time_selector_adjust_time (EMeetingTimeSelectorTime *mtstime, + gint days, gint hours, gint minutes) +{ + gint new_hours, new_minutes; + + /* We have to handle negative values for hous and minutes here, since + EMeetingTimeSelectorTime uses guint8s to store them. */ + new_minutes = mtstime->minute + minutes; + if (new_minutes < 0) { + new_minutes += 60; + hours -= 1; + } + + new_hours = mtstime->hour + hours; + if (new_hours < 0) { + new_hours += 24; + days -= 1; + } + + g_date_add_days (&mtstime->date, days); + mtstime->hour = new_hours; + mtstime->minute = new_minutes; + + e_meeting_time_selector_fix_time_overflows (mtstime); +} + + +/* This looks for any busy period of the given attendee which clashes with + the start and end time. It uses a binary search. */ +static EMeetingTimeSelectorPeriod* +e_meeting_time_selector_find_time_clash (EMeetingTimeSelector *mts, + EMeetingTimeSelectorAttendee *attendee, + EMeetingTimeSelectorTime *start_time, + EMeetingTimeSelectorTime *end_time) +{ + EMeetingTimeSelectorPeriod *period; + gint period_num; + + period_num = e_meeting_time_selector_find_first_busy_period (mts, attendee, &start_time->date); + + if (period_num == -1) + return NULL; + + /* Step forward through the busy periods until we find a clash or we + go past the end_time. */ + while (period_num < attendee->busy_periods->len) { + period = &g_array_index (attendee->busy_periods, + EMeetingTimeSelectorPeriod, + period_num); + + /* If the period starts at or after the end time, there is no + clash and we are finished. The busy periods are sorted by + their start times, so all the rest will be later. */ + if (e_meeting_time_selector_compare_times (&period->start, + end_time) >= 0) + return NULL; + + /* If the period ends after the start time, we have found a + clash. From the above test we already know the busy period + isn't completely after the meeting time. */ + if (e_meeting_time_selector_compare_times (&period->end, + start_time) > 0) { + return period; + } + + period_num++; + } + + return NULL; +} + + +/* This subtracts the attendees longest_period_in_days from the given date, + and does a binary search of the attendee's busy periods array to find the + first one which could possible end on the given day or later. + If none are found it returns -1. */ +gint +e_meeting_time_selector_find_first_busy_period (EMeetingTimeSelector *mts, + EMeetingTimeSelectorAttendee *attendee, + GDate *date) +{ + EMeetingTimeSelectorPeriod *period; + gint lower, upper, middle, cmp = 0; + GDate tmp_date; + + /* Make sure the busy periods have been sorted. */ + e_meeting_time_selector_attendee_ensure_periods_sorted (mts, attendee); + + /* Calculate the first day which could have a busy period which + continues onto our given date. */ + tmp_date = *date; + g_date_subtract_days (&tmp_date, attendee->longest_period_in_days); + + /* We want the first busy period which starts on tmp_date. */ + lower = 0; + upper = attendee->busy_periods->len; + + if (upper == 0) + return -1; + + while (lower < upper) { + middle = (lower + upper) >> 1; + + period = &g_array_index (attendee->busy_periods, + EMeetingTimeSelectorPeriod, middle); + + cmp = g_date_compare (&tmp_date, &period->start.date); + + if (cmp == 0) + break; + else if (cmp < 0) + upper = middle; + else + lower = middle + 1; + } + + /* There may be several busy periods on the same day so we step + backwards to the first one. */ + if (cmp == 0) { + while (middle > 0) { + period = &g_array_index (attendee->busy_periods, + EMeetingTimeSelectorPeriod, middle - 1); + if (g_date_compare (&tmp_date, &period->start.date) != 0) + break; + middle--; + } + } else if (cmp > 0) { + /* This means we couldn't find a period on the given day, and + the last one we looked at was before it, so if there are + any more periods after this one we return it. */ + middle++; + if (attendee->busy_periods->len <= middle) + return -1; + } + + return middle; +} + + +static void +e_meeting_time_selector_on_zoomed_out_toggled (GtkWidget *menuitem, + EMeetingTimeSelector *mts) +{ + /* Make sure the menu pops down, which doesn't happen by default if + keyboard accelerators are used. */ + if (GTK_WIDGET_VISIBLE (mts->options_menu)) + gtk_menu_popdown (GTK_MENU (mts->options_menu)); + + e_meeting_time_selector_set_zoomed_out (mts, GTK_CHECK_MENU_ITEM (menuitem)->active); +} + + +static void +e_meeting_time_selector_on_working_hours_toggled (GtkWidget *menuitem, + EMeetingTimeSelector *mts) +{ + /* Make sure the menu pops down, which doesn't happen by default if + keyboard accelerators are used. */ + if (GTK_WIDGET_VISIBLE (mts->options_menu)) + gtk_menu_popdown (GTK_MENU (mts->options_menu)); + + e_meeting_time_selector_set_working_hours_only (mts, GTK_CHECK_MENU_ITEM (menuitem)->active); +} + + +/* This recalculates day_width, first_hour_shown and last_hour_shown. */ +static void +e_meeting_time_selector_recalc_grid (EMeetingTimeSelector *mts) +{ + if (mts->working_hours_only) { + mts->first_hour_shown = mts->day_start_hour; + mts->last_hour_shown = mts->day_end_hour; + if (mts->day_end_minute != 0) + mts->last_hour_shown += 1; + } else { + mts->first_hour_shown = 0; + mts->last_hour_shown = 24; + } + + /* In the brief view we use the nearest hours divisible by 3. */ + if (mts->zoomed_out) { + mts->first_hour_shown -= mts->first_hour_shown % 3; + mts->last_hour_shown += 2; + mts->last_hour_shown -= mts->last_hour_shown % 3; + } + + mts->day_width = mts->col_width * (mts->last_hour_shown - mts->first_hour_shown); + if (mts->zoomed_out) + mts->day_width /= 3; + + /* Add one pixel for the extra vertical grid line. */ + mts->day_width++; + + gnome_canvas_set_scroll_region (GNOME_CANVAS (mts->display_top), + 0, 0, + mts->day_width * E_MEETING_TIME_SELECTOR_DAYS_SHOWN, + mts->row_height * 3); + e_meeting_time_selector_update_main_canvas_scroll_region (mts); + + e_meeting_time_selector_recalc_date_format (mts); + mts->meeting_positions_valid = FALSE; +} + + +/* This saves the first visible time in the given EMeetingTimeSelectorTime. */ +static void +e_meeting_time_selector_save_position (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *mtstime) +{ + gint scroll_x, scroll_y; + + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (mts->display_main), + &scroll_x, &scroll_y); + e_meeting_time_selector_calculate_time (mts, scroll_x, mtstime); +} + + +/* This restores a saved position. */ +static void +e_meeting_time_selector_restore_position (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *mtstime) +{ + gint scroll_x, scroll_y, new_scroll_x; + + new_scroll_x = e_meeting_time_selector_calculate_time_position (mts, + mtstime); + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (mts->display_main), + &scroll_x, &scroll_y); + gnome_canvas_scroll_to (GNOME_CANVAS (mts->display_main), + new_scroll_x, scroll_y); +} + + +/* This returns the x pixel coords of the meeting time in the entire scroll + region. It recalculates them if they have been marked as invalid. + If it returns FALSE then no meeting time is set or the meeting time is + not visible in the current scroll area. */ +gboolean +e_meeting_time_selector_get_meeting_time_positions (EMeetingTimeSelector *mts, + gint *start_x, + gint *end_x) +{ + if (mts->meeting_positions_valid) { + if (mts->meeting_positions_in_scroll_area) { + *start_x = mts->meeting_start_x; + *end_x = mts->meeting_end_x; + return TRUE; + } else { + return FALSE; + } + } + + mts->meeting_positions_valid = TRUE; + + /* Check if the days aren't in our current range. */ + if (g_date_compare (&mts->meeting_start_time.date, &mts->last_date_shown) > 0 + || g_date_compare (&mts->meeting_end_time.date, &mts->first_date_shown) < 0) { + mts->meeting_positions_in_scroll_area = FALSE; + return FALSE; + } + + mts->meeting_positions_in_scroll_area = TRUE; + *start_x = mts->meeting_start_x = e_meeting_time_selector_calculate_time_position (mts, &mts->meeting_start_time); + *end_x = mts->meeting_end_x = e_meeting_time_selector_calculate_time_position (mts, &mts->meeting_end_time); + + return TRUE; +} + + +/* This recalculates the date format to used, by computing the width of the + longest date strings in the widget's font and seeing if they fit. */ +static void +e_meeting_time_selector_recalc_date_format (EMeetingTimeSelector *mts) +{ + GDate date; + gint max_date_width, base_width, max_weekday_width, max_month_width; + gint weekday, month; + gchar buffer[128]; + GdkFont *font; + + font = GTK_WIDGET (mts)->style->font; + + /* Check if we will be able to display the full date strings. */ + mts->date_format = E_MEETING_TIME_SELECTOR_DATE_SHORT; + g_date_clear (&date, 1); + g_date_set_dmy (&date, 20, 1, 2000); + + /* Calculate the maximum date width we can fit into the display. */ + max_date_width = mts->day_width - 2; + + /* First compute the width of the date string without the day or + month names. */ + g_date_strftime (buffer, 128, ", %d, %Y", &date); + base_width = gdk_string_width (font, buffer); + + /* If that doesn't fit just return. We have to use the short format. + If that doesn't fit it will just be clipped. */ + if (gdk_string_width (font, buffer) > max_date_width) + return; + + /* Now find the biggest weekday name. We start on any day and just + go through seven days. */ + max_weekday_width = 0; + for (weekday = 1; weekday <= 7; weekday++) { + g_date_strftime (buffer, 128, "%A", &date); + max_weekday_width = MAX (max_weekday_width, + gdk_string_width (font, buffer)); + g_date_add_days (&date, 1); + } + + /* Now find the biggest month name. */ + max_month_width = 0; + for (month = 1; month <= 12; month++) { + g_date_set_month (&date, month); + g_date_strftime (buffer, 128, "%B", &date); + max_month_width = MAX (max_month_width, + gdk_string_width (font, buffer)); + } + + /* See if we can use the full date. */ + if (base_width + max_month_width + max_weekday_width <= max_date_width) { + mts->date_format = E_MEETING_TIME_SELECTOR_DATE_FULL; + return; + } + + /* Now try it with abbreviated weekday names. */ + g_date_strftime (buffer, 128, " %x", &date); + base_width = gdk_string_width (font, buffer); + + max_weekday_width = 0; + for (weekday = 1; weekday <= 7; weekday++) { + g_date_strftime (buffer, 128, "%a", &date); + max_weekday_width = MAX (max_weekday_width, + gdk_string_width (font, buffer)); + g_date_add_days (&date, 1); + } + + if (base_width + max_weekday_width <= max_date_width) + mts->date_format = E_MEETING_TIME_SELECTOR_DATE_ABBREVIATED_DAY; +} + + +/* Turn off the background of the canvas windows. This reduces flicker + considerably when scrolling. (Why isn't it in GnomeCanvas?). */ +static void +e_meeting_time_selector_on_canvas_realized (GtkWidget *widget, + EMeetingTimeSelector *mts) +{ + gdk_window_set_back_pixmap (GTK_LAYOUT (widget)->bin_window, + NULL, FALSE); +} + + +/* This is called when the meeting start time GnomeDateEdit is changed, + either via the "date_changed". "time_changed" or "activate" signals on one + of the GtkEntry widgets. So don't use the widget parameter since it may be + one of the child GtkEntry widgets. */ +static void +e_meeting_time_selector_on_start_time_changed (GtkWidget *widget, + EMeetingTimeSelector *mts) +{ + gint duration_days, duration_hours, duration_minutes; + EMeetingTimeSelectorTime mtstime; + time_t newtime; + struct tm *newtime_tm; + + newtime = gnome_date_edit_get_date (GNOME_DATE_EDIT (mts->start_date_edit)); + newtime_tm = localtime (&newtime); + g_date_clear (&mtstime.date, 1); + g_date_set_time (&mtstime.date, newtime); + mtstime.hour = newtime_tm->tm_hour; + mtstime.minute = newtime_tm->tm_min; + + /* If the time hasn't changed, just return. */ + if (e_meeting_time_selector_compare_times (&mtstime, &mts->meeting_start_time) == 0) + return; + + /* Calculate the current meeting duration. */ + e_meeting_time_selector_calculate_time_difference (&mts->meeting_start_time, &mts->meeting_end_time, &duration_days, &duration_hours, &duration_minutes); + + /* Set the new start time. */ + mts->meeting_start_time = mtstime; + + /* Update the end time so the meeting duration stays the same. */ + mts->meeting_end_time = mts->meeting_start_time; + e_meeting_time_selector_adjust_time (&mts->meeting_end_time, duration_days, duration_hours, duration_minutes); + e_meeting_time_selector_update_end_date_edit (mts); + + mts->meeting_positions_valid = FALSE; + e_meeting_time_selector_ensure_meeting_time_shown (mts); + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); +} + + +/* This is called when the meeting end time GnomeDateEdit is changed, + either via the "date_changed", "time_changed" or "activate" signals on one + of the GtkEntry widgets. So don't use the widget parameter since it may be + one of the child GtkEntry widgets. */ +static void +e_meeting_time_selector_on_end_time_changed (GtkWidget *widget, + EMeetingTimeSelector *mts) +{ + EMeetingTimeSelectorTime mtstime; + time_t newtime; + struct tm *newtime_tm; + + newtime = gnome_date_edit_get_date (GNOME_DATE_EDIT (mts->end_date_edit)); + newtime_tm = localtime (&newtime); + g_date_clear (&mtstime.date, 1); + g_date_set_time (&mtstime.date, newtime); + mtstime.hour = newtime_tm->tm_hour; + mtstime.minute = newtime_tm->tm_min; + + /* If the time hasn't changed, just return. */ + if (e_meeting_time_selector_compare_times (&mtstime, &mts->meeting_end_time) == 0) + return; + + /* Set the new end time. */ + mts->meeting_end_time = mtstime; + + /* If the start time is after the end time, set it to the same time. */ + if (e_meeting_time_selector_compare_times (&mtstime, &mts->meeting_start_time) < 0) { + /* We set it first, before updating the widget, so the signal + handler will just return. */ + mts->meeting_start_time = mtstime; + e_meeting_time_selector_update_start_date_edit (mts); + } + + mts->meeting_positions_valid = FALSE; + e_meeting_time_selector_ensure_meeting_time_shown (mts); + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); +} + + +/* This updates the ranges shown in the GnomeDateEdit popup menus, according + to working_hours_only etc. */ +static void +e_meeting_time_selector_update_date_popup_menus (EMeetingTimeSelector *mts) +{ + GnomeDateEdit *start_edit, *end_edit; + gint low_hour, high_hour; + + start_edit = GNOME_DATE_EDIT (mts->start_date_edit); + end_edit = GNOME_DATE_EDIT (mts->end_date_edit); + + if (mts->working_hours_only) { + low_hour = mts->day_start_hour; + high_hour = mts->day_end_hour; + } else { + low_hour = 0; + high_hour = 23; + } + + gnome_date_edit_set_popup_range (start_edit, low_hour, high_hour); + gnome_date_edit_set_popup_range (end_edit, low_hour, high_hour); +} + + +static void +e_meeting_time_selector_on_canvas_size_allocate (GtkWidget *widget, + GtkAllocation *allocation, + EMeetingTimeSelector *mts) +{ + e_meeting_time_selector_update_main_canvas_scroll_region (mts); + + e_meeting_time_selector_ensure_meeting_time_shown (mts); +} + + +static void +e_meeting_time_selector_on_attendees_list_size_allocate (GtkWidget *widget, + GtkAllocation *allocation, + EMeetingTimeSelector *mts) +{ + e_meeting_time_selector_update_attendees_list_scroll_region (mts); + e_meeting_time_selector_update_attendees_list_positions (mts); +} + + +/* This updates the list canvas scroll region according to the number of + attendees. If the total height needed is less than the height of the canvas, + we must use the height of the canvas, or it causes problems. */ +static void +e_meeting_time_selector_update_attendees_list_scroll_region (EMeetingTimeSelector *mts) +{ + gint height, canvas_width, canvas_height; + + height = mts->row_height * mts->attendees->len; + canvas_width = GTK_WIDGET (mts->attendees_list)->allocation.width; + canvas_height = GTK_WIDGET (mts->attendees_list)->allocation.height; + + height = MAX (height, canvas_height); + + gnome_canvas_set_scroll_region (GNOME_CANVAS (mts->attendees_list), + 0, 0, + canvas_width, + height); +} + + +/* This updates the canvas scroll regions according to the number of attendees. + If the total height needed is less than the height of the canvas, we must + use the height of the canvas, or it causes problems. */ +static void +e_meeting_time_selector_update_main_canvas_scroll_region (EMeetingTimeSelector *mts) +{ + gint height, canvas_height, list_width; + + height = mts->row_height * mts->attendees->len; + canvas_height = GTK_WIDGET (mts->display_main)->allocation.height; + list_width = GTK_WIDGET (mts->attendees_list)->allocation.width; + + height = MAX (height, canvas_height); + + gnome_canvas_set_scroll_region (GNOME_CANVAS (mts->attendees_list), + 0, 0, + list_width, + height); + gnome_canvas_set_scroll_region (GNOME_CANVAS (mts->display_main), + 0, 0, + mts->day_width * E_MEETING_TIME_SELECTOR_DAYS_SHOWN, + height); +} + + +/* This changes the meeting time based on the given x coordinate and whether + we are dragging the start or end bar. It returns the new position, which + will be swapped if the start bar is dragged past the end bar or vice versa. + It make sure the meeting time is never dragged outside the visible canvas + area. */ +void +e_meeting_time_selector_drag_meeting_time (EMeetingTimeSelector *mts, + gint x) +{ + EMeetingTimeSelectorTime first_time, last_time, drag_time, *time_to_set; + gint scroll_x, scroll_y, canvas_width; + gboolean set_both_times = FALSE; + + /* Get the x coords of visible part of the canvas. */ + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (mts->display_main), + &scroll_x, &scroll_y); + canvas_width = mts->display_main->allocation.width; + + /* Save the x coordinate for the timeout handler. */ + mts->last_drag_x = (x < scroll_x) ? x - scroll_x + : x - scroll_x - canvas_width + 1; + + /* Check if the mouse is off the edge of the canvas. */ + if (x < scroll_x || x > scroll_x + canvas_width) { + /* If we haven't added a timeout function, add one. */ + if (mts->auto_scroll_timeout_id == 0) { + mts->auto_scroll_timeout_id = g_timeout_add (60, e_meeting_time_selector_timeout_handler, mts); + mts->scroll_count = 0; + + /* Call the handler to start scrolling now. */ + e_meeting_time_selector_timeout_handler (mts); + return; + } + } else { + e_meeting_time_selector_remove_timeout (mts); + } + + /* Calculate the minimum & maximum times we can use, based on the + scroll offsets and whether zoomed_out is set. */ + e_meeting_time_selector_calculate_time (mts, scroll_x, &first_time); + e_meeting_time_selector_calculate_time (mts, scroll_x + canvas_width - 1, + &last_time); + if (mts->zoomed_out) { + if (first_time.minute > 30) + first_time.hour++; + first_time.minute = 0; + last_time.minute = 0; + } else { + first_time.minute += 15; + first_time.minute -= first_time.minute % 30; + last_time.minute -= last_time.minute % 30; + } + e_meeting_time_selector_fix_time_overflows (&first_time); + e_meeting_time_selector_fix_time_overflows (&last_time); + + /* Calculate the time from x coordinate. */ + e_meeting_time_selector_calculate_time (mts, x, &drag_time); + + /* Calculate the nearest half-hour or hour, depending on whether + zoomed_out is set. */ + if (mts->zoomed_out) { + if (drag_time.minute > 30) + drag_time.hour++; + drag_time.minute = 0; + } else { + drag_time.minute += 15; + drag_time.minute -= drag_time.minute % 30; + } + e_meeting_time_selector_fix_time_overflows (&drag_time); + + /* Now make sure we are between first_time & last_time. */ + if (e_meeting_time_selector_compare_times (&drag_time, &first_time) < 0) + drag_time = first_time; + if (e_meeting_time_selector_compare_times (&drag_time, &last_time) > 0) + drag_time = last_time; + + /* Set the meeting start or end time to drag_time. */ + if (mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + time_to_set = &mts->meeting_start_time; + else + time_to_set = &mts->meeting_end_time; + + /* If the time is unchanged, just return. */ + if (e_meeting_time_selector_compare_times (time_to_set, &drag_time) == 0) + return; + + *time_to_set = drag_time; + + /* Check if the start time and end time need to be switched. */ + if (e_meeting_time_selector_compare_times (&mts->meeting_start_time, + &mts->meeting_end_time) > 0) { + drag_time = mts->meeting_start_time; + mts->meeting_start_time = mts->meeting_end_time; + mts->meeting_end_time = drag_time; + + if (mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_END; + else + mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_START; + + set_both_times = TRUE; + } + + /* Mark the calculated positions as invalid. */ + mts->meeting_positions_valid = FALSE; + + /* Redraw the canvases. */ + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + + /* Set the times in the GnomeDateEdit widgets. */ + if (set_both_times + || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + e_meeting_time_selector_update_start_date_edit (mts); + + if (set_both_times + || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END) + e_meeting_time_selector_update_end_date_edit (mts); +} + + +/* This is the timeout function which handles auto-scrolling when the user is + dragging one of the meeting time vertical bars outside the left or right + edge of the canvas. */ +static gboolean +e_meeting_time_selector_timeout_handler (gpointer data) +{ + EMeetingTimeSelector *mts; + EMeetingTimeSelectorTime drag_time, *time_to_set; + gint scroll_x, max_scroll_x, scroll_y, canvas_width; + gint scroll_speed, scroll_offset; + gboolean set_both_times = FALSE; + + mts = E_MEETING_TIME_SELECTOR (data); + + GDK_THREADS_ENTER (); + + /* Return if we don't need to scroll yet. */ + if (mts->scroll_count-- > 0) { + GDK_THREADS_LEAVE (); + return TRUE; + } + + /* Get the x coords of visible part of the canvas. */ + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (mts->display_main), + &scroll_x, &scroll_y); + canvas_width = mts->display_main->allocation.width; + + /* Calculate the scroll delay, between 0 and MAX_SCROLL_SPEED. */ + scroll_speed = abs (mts->last_drag_x / E_MEETING_TIME_SELECTOR_SCROLL_INCREMENT_WIDTH); + scroll_speed = MIN (scroll_speed, + E_MEETING_TIME_SELECTOR_MAX_SCROLL_SPEED); + + /* Reset the scroll count. */ + mts->scroll_count = E_MEETING_TIME_SELECTOR_MAX_SCROLL_SPEED - scroll_speed; + + /* Calculate how much we need to scroll. */ + if (mts->last_drag_x >= 0) + scroll_offset = mts->col_width; + else + scroll_offset = -mts->col_width; + + scroll_x += scroll_offset; + max_scroll_x = (mts->day_width * E_MEETING_TIME_SELECTOR_DAYS_SHOWN) + - canvas_width; + scroll_x = CLAMP (scroll_x, 0, max_scroll_x); + + /* Calculate the minimum or maximum visible time in the canvas, which + we will now set the dragged time to. */ + if (scroll_offset > 0) { + e_meeting_time_selector_calculate_time (mts, + scroll_x + canvas_width - 1, + &drag_time); + if (mts->zoomed_out) { + drag_time.minute = 0; + } else { + drag_time.minute -= drag_time.minute % 30; + } + } else { + e_meeting_time_selector_calculate_time (mts, scroll_x, + &drag_time); + if (mts->zoomed_out) { + if (drag_time.minute > 30) + drag_time.hour++; + drag_time.minute = 0; + } else { + drag_time.minute += 15; + drag_time.minute -= drag_time.minute % 30; + } + } + e_meeting_time_selector_fix_time_overflows (&drag_time); + + /* Set the meeting start or end time to drag_time. */ + if (mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + time_to_set = &mts->meeting_start_time; + else + time_to_set = &mts->meeting_end_time; + + /* If the time is unchanged, just return. */ + if (e_meeting_time_selector_compare_times (time_to_set, &drag_time) == 0) { + GDK_THREADS_LEAVE (); + return TRUE; + } + + *time_to_set = drag_time; + + /* Check if the start time and end time need to be switched. */ + if (e_meeting_time_selector_compare_times (&mts->meeting_start_time, &mts->meeting_end_time) > 0) { + drag_time = mts->meeting_start_time; + mts->meeting_start_time = mts->meeting_end_time; + mts->meeting_end_time = drag_time; + + if (mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_END; + else + mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_START; + + set_both_times = TRUE; + } + + /* Mark the calculated positions as invalid. */ + mts->meeting_positions_valid = FALSE; + + /* Set the times in the GnomeDateEdit widgets. */ + if (set_both_times + || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) + e_meeting_time_selector_update_start_date_edit (mts); + + if (set_both_times + || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END) + e_meeting_time_selector_update_end_date_edit (mts); + + /* Redraw the canvases. We freeze and thaw the layouts so that they + get redrawn completely. Otherwise the pixels get scrolled left or + right which is not good for us (since our vertical bars have been + moved) and causes flicker. */ + gtk_layout_freeze (GTK_LAYOUT (mts->display_main)); + gtk_layout_freeze (GTK_LAYOUT (mts->display_top)); + gnome_canvas_scroll_to (GNOME_CANVAS (mts->display_main), + scroll_x, scroll_y); + gnome_canvas_scroll_to (GNOME_CANVAS (mts->display_top), + scroll_x, scroll_y); + gtk_layout_thaw (GTK_LAYOUT (mts->display_main)); + gtk_layout_thaw (GTK_LAYOUT (mts->display_top)); + + GDK_THREADS_LEAVE (); + return TRUE; +} + + +/* This removes our auto-scroll timeout function, if we have one installed. */ +void +e_meeting_time_selector_remove_timeout (EMeetingTimeSelector *mts) +{ + if (mts->auto_scroll_timeout_id) { + g_source_remove (mts->auto_scroll_timeout_id); + mts->auto_scroll_timeout_id = 0; + } +} + + +/* This updates the GnomeDateEdit widget displaying the meeting start time. */ +static void +e_meeting_time_selector_update_start_date_edit (EMeetingTimeSelector *mts) +{ + struct tm start_tm; + time_t start_time_t; + + g_date_to_struct_tm (&mts->meeting_start_time.date, &start_tm); + start_tm.tm_hour = mts->meeting_start_time.hour; + start_tm.tm_min = mts->meeting_start_time.minute; + start_time_t = mktime (&start_tm); + gnome_date_edit_set_time (GNOME_DATE_EDIT (mts->start_date_edit), + start_time_t); +} + + +/* This updates the GnomeDateEdit widget displaying the meeting end time. */ +static void +e_meeting_time_selector_update_end_date_edit (EMeetingTimeSelector *mts) +{ + struct tm end_tm; + time_t end_time_t; + + g_date_to_struct_tm (&mts->meeting_end_time.date, &end_tm); + end_tm.tm_hour = mts->meeting_end_time.hour; + end_tm.tm_min = mts->meeting_end_time.minute; + end_time_t = mktime (&end_tm); + gnome_date_edit_set_time (GNOME_DATE_EDIT (mts->end_date_edit), + end_time_t); +} + + +/* This ensures that the meeting time is shown on screen, by scrolling the + canvas and possibly by changing the range of dates shown in the canvas. */ +static void +e_meeting_time_selector_ensure_meeting_time_shown (EMeetingTimeSelector *mts) +{ + gint start_x, end_x, scroll_x, scroll_y, canvas_width; + gint new_scroll_x; + gboolean fits_in_canvas; + + /* Check if we need to change the range of dates shown. */ + if (g_date_compare (&mts->meeting_start_time.date, + &mts->first_date_shown) < 0 + || g_date_compare (&mts->meeting_end_time.date, + &mts->last_date_shown) > 0) { + e_meeting_time_selector_update_dates_shown (mts); + gtk_widget_queue_draw (mts->display_top); + gtk_widget_queue_draw (mts->display_main); + } + + /* If all of the meeting time is visible, just return. */ + e_meeting_time_selector_get_meeting_time_positions (mts, &start_x, + &end_x); + gnome_canvas_get_scroll_offsets (GNOME_CANVAS (mts->display_main), + &scroll_x, &scroll_y); + canvas_width = mts->display_main->allocation.width; + if (start_x > scroll_x && end_x <= scroll_x + canvas_width) + return; + + fits_in_canvas = end_x - start_x < canvas_width ? TRUE : FALSE; + + /* If the meeting is not entirely visible, either center it if it is + smaller than the canvas, or show the start of it if it is big. */ + if (fits_in_canvas) { + new_scroll_x = (start_x + end_x - canvas_width) / 2; + } else { + new_scroll_x = start_x; + } + gnome_canvas_scroll_to (GNOME_CANVAS (mts->display_main), + new_scroll_x, scroll_y); +} + + +/* This updates the range of dates shown in the canvas, to make sure that the + currently selected meeting time is in the range. */ +static void +e_meeting_time_selector_update_dates_shown (EMeetingTimeSelector *mts) +{ + mts->first_date_shown = mts->meeting_start_time.date; + g_date_subtract_days (&mts->first_date_shown, 60); + + mts->last_date_shown = mts->first_date_shown; + g_date_add_days (&mts->last_date_shown, E_MEETING_TIME_SELECTOR_DAYS_SHOWN - 1); +} + + +/* This checks if the time's hour is over 24 or its minute is over 60 and if + so it updates the day/hour appropriately. Note that hours and minutes are + stored in guint8's so they can't overflow by much. */ +void +e_meeting_time_selector_fix_time_overflows (EMeetingTimeSelectorTime *mtstime) +{ + gint hours_to_add, days_to_add; + + hours_to_add = mtstime->minute / 60; + if (hours_to_add > 0) { + mtstime->minute -= hours_to_add * 60; + mtstime->hour += hours_to_add; + } + + days_to_add = mtstime->hour / 24; + if (days_to_add > 0) { + mtstime->hour -= days_to_add * 24; + g_date_add_days (&mtstime->date, days_to_add); + } +} + + +static void +e_meeting_time_selector_update_attendees_list_positions (EMeetingTimeSelector *mts) +{ + EMeetingTimeSelectorAttendee *attendee; + gint list_width, item_width; + gint row; + + list_width = GTK_WIDGET (mts->attendees_list)->allocation.width; + item_width = MAX (1, list_width - E_MEETING_TIME_SELECTOR_ICON_COLUMN_WIDTH); + for (row = 0; row < mts->attendees->len; row++) { + attendee = &g_array_index (mts->attendees, + EMeetingTimeSelectorAttendee, row); + + gnome_canvas_item_set (attendee->text_item, + "GnomeCanvasWidget::y", (gdouble) (row * mts->row_height), + "GnomeCanvasWidget::width", (gdouble) item_width, + "GnomeCanvasWidget::height", (gdouble) (mts->row_height), + NULL); + + gnome_canvas_item_show (attendee->text_item); + } + +} + + +/* + * CONVERSION ROUTINES - functions to convert between different coordinate + * spaces and dates. + */ + +/* This takes an x pixel coordinate within the entire canvas scroll region and + returns the date in which it falls. If day_position is not NULL it also + returns the x coordinate within the date, relative to the visible part of + the canvas. It is used when painting the days in the item_draw function. + Note that it must handle negative x coordinates in case we are dragging off + the edge of the canvas. */ +void +e_meeting_time_selector_calculate_day_and_position (EMeetingTimeSelector *mts, + gint x, + GDate *date, + gint *day_position) +{ + gint days_from_first_shown; + + *date = mts->first_date_shown; + + if (x >= 0) { + days_from_first_shown = x / mts->day_width; + g_date_add_days (date, days_from_first_shown); + if (day_position) + *day_position = - x % mts->day_width; + } else { + days_from_first_shown = -x / mts->day_width + 1; + g_date_subtract_days (date, days_from_first_shown); + if (day_position) + *day_position = -mts->day_width - x % mts->day_width; + } +} + + +/* This takes an x pixel coordinate within a day, and converts it to hours + and minutes, depending on working_hours_only and zoomed_out. */ +void +e_meeting_time_selector_convert_day_position_to_hours_and_mins (EMeetingTimeSelector *mts, gint day_position, guint8 *hours, guint8 *minutes) +{ + if (mts->zoomed_out) + day_position *= 3; + + /* Calculate the hours & minutes from the first displayed. */ + *hours = day_position / mts->col_width; + *minutes = (day_position % mts->col_width) * 60 / mts->col_width; + + /* Now add on the first hour shown. */ + *hours += mts->first_hour_shown; +} + + +/* This takes an x pixel coordinate within the entire canvas scroll region and + returns the time in which it falls. Note that it won't be extremely + accurate since hours may only be a few pixels wide in the display. + With zoomed_out set each pixel may represent 5 minutes or more, depending + on how small the font is. */ +void +e_meeting_time_selector_calculate_time (EMeetingTimeSelector *mts, + gint x, + EMeetingTimeSelectorTime *time) +{ + gint day_position; + + /* First get the day and the x position within the day. */ + e_meeting_time_selector_calculate_day_and_position (mts, x, &time->date, + NULL); + + /* Now convert the day_position into an hour and minute. */ + if (x >= 0) + day_position = x % mts->day_width; + else + day_position = mts->day_width + x % mts->day_width; + + e_meeting_time_selector_convert_day_position_to_hours_and_mins (mts, day_position, &time->hour, &time->minute); +} + + +/* This takes a EMeetingTimeSelectorTime and calculates the x pixel coordinate + within the entire canvas scroll region. It is used to draw the selected + meeting time and all the busy periods. */ +gint +e_meeting_time_selector_calculate_time_position (EMeetingTimeSelector *mts, + EMeetingTimeSelectorTime *mtstime) +{ + gint x, date_offset, day_offset; + + /* Calculate the number of days since the first date shown in the + entire canvas scroll region. */ + date_offset = g_date_julian (&mtstime->date) - g_date_julian (&mts->first_date_shown); + + /* Calculate the x pixel coordinate of the start of the day. */ + x = date_offset * mts->day_width; + + /* Add on the hours and minutes, depending on whether zoomed_out and + working_hours_only are set. */ + day_offset = (mtstime->hour - mts->first_hour_shown) * 60 + + mtstime->minute; + /* The day width includes an extra vertical grid line so subtract 1. */ + day_offset *= (mts->day_width - 1); + day_offset /= (mts->last_hour_shown - mts->first_hour_shown) * 60; + + /* Clamp the day_offset in case the time isn't actually visible. */ + x += CLAMP (day_offset, 0, mts->day_width); + + return x; +} + + -- cgit v1.2.3